Skip to content

Commit d1bef62

Browse files
authored
Merge pull request #207 from chvvkumar/snd
AstroBin Multi-Session CSV Copy
2 parents 6354059 + 0a00e69 commit d1bef62

1 file changed

Lines changed: 131 additions & 22 deletions

File tree

frontend/src/pages/TargetDetailPage.tsx

Lines changed: 131 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,104 @@ const TargetDetailPage: Component = () => {
311311

312312
const selectNoDates = () => setSelectedChartDates([]);
313313

314+
const [csvCopied, setCsvCopied] = createSignal(false);
315+
const [csvLoading, setCsvLoading] = createSignal(false);
316+
317+
const copyMultiSessionAstrobinCsv = async () => {
318+
const dates = selectedChartDates();
319+
if (dates.length === 0) return;
320+
321+
const cache = sessionCache();
322+
const missingDates = dates.filter((d) => !cache[d]);
323+
324+
if (missingDates.length > 0) {
325+
setCsvLoading(true);
326+
try {
327+
const results = await Promise.all(
328+
missingDates.map((d) => api.getSessionDetail(params.targetId, d))
329+
);
330+
const newCache = { ...sessionCache() };
331+
missingDates.forEach((d, i) => {
332+
newCache[d] = results[i];
333+
});
334+
setSessionCache(newCache);
335+
} catch (e: any) {
336+
showToast(e?.message ?? "Failed to load session details", "error");
337+
setCsvLoading(false);
338+
return;
339+
}
340+
setCsvLoading(false);
341+
}
342+
343+
const aliasMap = ctx.filterAliasMap();
344+
const abFilterIds = ctx.settings()?.general.astrobin_filter_ids ?? {};
345+
const bortle = ctx.settings()?.general.astrobin_bortle ?? "";
346+
347+
const lookupAbId = (filterName: string) => {
348+
const canonical = aliasMap[filterName] ?? filterName;
349+
return abFilterIds[canonical] ?? abFilterIds[filterName] ?? "";
350+
};
351+
352+
const median = (vals: number[]) => {
353+
if (vals.length === 0) return null;
354+
const s = [...vals].sort((a, b) => a - b);
355+
const mid = Math.floor(s.length / 2);
356+
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
357+
};
358+
359+
const header =
360+
"date,filter,number,duration,binning,gain,sensorCooling,fNumber,bortle,meanSqm,meanFwhm,temperature";
361+
const allRows: string[] = [];
362+
const updatedCache = sessionCache();
363+
364+
for (const date of dates) {
365+
const d = updatedCache[date];
366+
if (!d) continue;
367+
368+
const buildRows = (
369+
filterDetails: typeof d.filter_details,
370+
gain: number | null,
371+
sensorTemp: number | null,
372+
fwhm: number | null,
373+
ambientTemp: number | null
374+
) => {
375+
for (const f of filterDetails) {
376+
const filterId = lookupAbId(f.filter_name);
377+
const g = gain !== null ? gain : "";
378+
const cooling = sensorTemp !== null ? Math.round(sensorTemp) : "";
379+
const mFwhm = fwhm !== null ? fwhm.toFixed(2) : "";
380+
const temp = ambientTemp !== null ? ambientTemp.toFixed(2) : "";
381+
allRows.push(
382+
`${d.session_date},${filterId},${f.frame_count},${f.exposure_time ?? ""},,${g},${cooling},,${bortle},,${mFwhm},${temp}`
383+
);
384+
}
385+
};
386+
387+
if (d.rigs.length > 1) {
388+
for (const rig of d.rigs) {
389+
const temps = rig.frames
390+
.map((f) => f.sensor_temp)
391+
.filter((t): t is number => t !== null);
392+
const fwhms = rig.frames
393+
.map((f) => f.fwhm)
394+
.filter((v): v is number => v !== null);
395+
const ambTemps = rig.frames
396+
.map((f) => f.ambient_temp)
397+
.filter((v): v is number => v !== null);
398+
buildRows(rig.filter_details, rig.gain, median(temps), median(fwhms), median(ambTemps));
399+
}
400+
} else {
401+
buildRows(d.filter_details, d.gain, d.sensor_temp, d.median_fwhm, d.median_ambient_temp);
402+
}
403+
}
404+
405+
const csv = [header, ...allRows].join("\n");
406+
navigator.clipboard.writeText(csv).then(() => {
407+
setCsvCopied(true);
408+
setTimeout(() => setCsvCopied(false), 2000);
409+
});
410+
};
411+
314412
const pendingLoads = new Set<string>();
315413
const MAX_CONCURRENT_LOADS = 2;
316414
const loadQueue: string[] = [];
@@ -879,32 +977,43 @@ const TargetDetailPage: Component = () => {
879977
Each card is one imaging session on this target. Expand a card to see per-frame details (exposure, filter, camera temperature, HFR, guiding). Example: compare two sessions of the same target to pick the better one for stacking.
880978
</p>
881979
</HelpPopover>
980+
<Show when={selectedChartDates().length > 0}>
981+
<button
982+
class="ml-auto text-tiny px-2 py-0.5 border border-theme-border rounded text-theme-text-tertiary hover:text-theme-text-primary hover:border-theme-accent transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
983+
onClick={copyMultiSessionAstrobinCsv}
984+
disabled={csvLoading()}
985+
>
986+
{csvCopied()
987+
? "Copied!"
988+
: csvLoading()
989+
? "Loading..."
990+
: `AstroBin CSV (${selectedChartDates().length})`}
991+
</button>
992+
</Show>
882993
</div>
883994
<div class="overflow-x-auto">
884995
<table class="w-full min-w-[600px]" style={{ "border-collapse": "separate", "border-spacing": "0 10px" }}>
885996
<thead>
886997
<tr class="text-caption text-theme-text-tertiary uppercase tracking-wider">
887-
<Show when={targetChartExpanded()}>
888-
<th class="py-2 pl-4 pr-1 w-8">
889-
<input
890-
type="checkbox"
891-
checked={selectedChartDates().length === detail().sessions.length}
892-
ref={(el) => {
893-
createEffect(() => {
894-
const len = selectedChartDates().length;
895-
const total = detail().sessions.length;
896-
el.indeterminate = len > 0 && len < total;
897-
});
898-
}}
899-
onChange={(e) => {
900-
if (e.currentTarget.checked) selectAllDates();
901-
else selectNoDates();
902-
}}
903-
class="w-3.5 h-3.5 rounded border-theme-border cursor-pointer"
904-
title="Select all / none"
905-
/>
906-
</th>
907-
</Show>
998+
<th class="py-2 pl-4 pr-1 w-8">
999+
<input
1000+
type="checkbox"
1001+
checked={selectedChartDates().length === detail().sessions.length}
1002+
ref={(el) => {
1003+
createEffect(() => {
1004+
const len = selectedChartDates().length;
1005+
const total = detail().sessions.length;
1006+
el.indeterminate = len > 0 && len < total;
1007+
});
1008+
}}
1009+
onChange={(e) => {
1010+
if (e.currentTarget.checked) selectAllDates();
1011+
else selectNoDates();
1012+
}}
1013+
class="w-3.5 h-3.5 rounded border-theme-border cursor-pointer"
1014+
title="Select all / none"
1015+
/>
1016+
</th>
9081017
<th class="py-2 px-4 text-left font-medium">Date ({tzLabel()})</th>
9091018
<th class="py-2 px-2 text-right font-medium"></th>
9101019
<th class="py-2 px-2 text-right font-medium">Frames</th>
@@ -947,7 +1056,7 @@ const TargetDetailPage: Component = () => {
9471056
detected_stars: visible("quality", "detected_stars"),
9481057
guiding_rms: visible("guiding", "rms_total"),
9491058
}}
950-
showCheckbox={targetChartExpanded()}
1059+
showCheckbox={true}
9511060
checked={selectedChartDates().includes(session.session_date)}
9521061
onCheckChange={() => toggleChartDate(session.session_date)}
9531062
targetId={params.targetId}

0 commit comments

Comments
 (0)