import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, ScatterChart, Scatter, CartesianGrid, LineChart, Line, } from 'recharts'; import { useStats } from '../hooks/useStats'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { api } from '../api'; import type { Phd2Log } from '../api/types'; import { useRef, useState } from 'react'; const FILTER_COLORS: Record = { sv220: '#9b59b6', c2: '#4d9de0', sv260: '#e8832a', uvir: '#3dba72', }; function PHD2Section() { const qc = useQueryClient(); const inputRef = useRef(null); const [uploading, setUploading] = useState(false); const [uploadResult, setUploadResult] = useState(null); const [deleting, setDeleting] = useState(null); const { data } = useQuery({ queryKey: ['phd2'], queryFn: () => api.phd2.list(), }); const items = data?.items ?? []; const handleFile = async (file: File) => { setUploading(true); setUploadResult(null); const fd = new FormData(); fd.append('file', file); try { const res = await api.phd2.upload(fd) as any; if (res.duplicate) { setUploadResult('⚠ Duplicate session - not imported'); } else { const analysis = res.analysis; const details: string[] = []; details.push(`RMS ${analysis.rms_total_arcsec.toFixed(2)}″`); if (analysis.duration_min) details.push(`${analysis.duration_min}m`); if (analysis.camera_name) details.push(analysis.camera_name); setUploadResult(`✓ Uploaded: ${details.join(' · ')}`); qc.invalidateQueries({ queryKey: ['phd2'] }); qc.invalidateQueries({ queryKey: ['stats'] }); } } catch { setUploadResult('✗ Upload failed'); } setUploading(false); }; const handleDelete = async (id: number) => { if (!confirm('Delete this PHD2 session?')) return; setDeleting(id); try { await api.phd2.delete(id); qc.invalidateQueries({ queryKey: ['phd2'] }); qc.invalidateQueries({ queryKey: ['stats'] }); } catch (e) { alert(`Failed to delete: ${e instanceof Error ? e.message : 'Unknown error'}`); } setDeleting(null); }; return (
PHD2 Guiding Sessions
!uploading && inputRef.current?.click()} style={{ padding: '4px 12px', border: '1px solid var(--border)', borderRadius: 3, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-mid)', cursor: uploading ? 'default' : 'pointer', background: 'var(--bg-deep)', }} > {uploading ? 'Parsing…' : '↑ Upload .log'}
e.target.files?.[0] && handleFile(e.target.files[0])} />
{uploadResult && (
{uploadResult}
)} {items.length === 0 ? (
No PHD2 logs imported yet. Upload a .log file to analyze guiding performance.
) : ( {['Date', 'File', 'Duration', 'RMS Total', 'RMS RA', 'RMS Dec', 'Peak', 'Lost', 'SNR', ''].map(h => ( ))} {items.map((log: Phd2Log) => { const rms = log.rms_total ?? 0; const rmsColor = rms < 0.7 ? 'var(--good)' : rms < 1.2 ? 'var(--warn)' : 'var(--danger)'; return ( ); })}
{h}
{log.session_date} {log.filename} {log.duration_min ? `${log.duration_min}m` : '—'} {log.rms_total ? `${log.rms_total.toFixed(2)}″` : '—'} {log.rms_ra ? `${log.rms_ra.toFixed(2)}″` : '—'} {log.rms_dec ? `${log.rms_dec.toFixed(2)}″` : '—'} 2 ? 'var(--warn)' : 'var(--text-mid)', whiteSpace: 'nowrap' }}> {log.peak_error ? `${log.peak_error.toFixed(2)}″` : '—'} 5 ? 'var(--danger)' : 'var(--text-mid)', whiteSpace: 'nowrap' }}> {log.star_lost_count ?? 0} {log.guide_star_snr ? log.guide_star_snr.toFixed(1) : '—'}
)}
); } export default function Stats() { const { data: stats, isLoading } = useStats(); if (isLoading || !stats) { return (
Loading statistics…
); } const totalH = (stats.total_integration_min / 60).toFixed(1); return (

Statistics

{/* Header stat cards */}
{[ { label: 'Total Sessions', value: stats.total_sessions }, { label: 'Integration Time', value: `${totalH}h` }, { label: 'Objects Imaged', value: stats.objects_with_keeper }, { label: 'Filter Types Used', value: stats.filter_usage.length }, ].map(({ label, value }) => (
{label}
{value}
))}
{/* Charts */}
{/* Integration per month */}
Integration per Month
`${Math.round(v/60)}h`} /> [`${(v / 60).toFixed(1)}h`, 'Integration']} />
{/* Filter usage pie */}
Filter Usage
{stats.filter_usage.map((entry) => ( ))} `${(v / 60).toFixed(1)}h`} contentStyle={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', fontSize: 11 }} />
{stats.filter_usage.map(f => (
{f.filter_id.toUpperCase()} — {(f.total_min / 60).toFixed(1)}h
))}
{/* Object type breakdown */}
By Object Type
`${Math.round(v/60)}h`} /> `${(v / 60).toFixed(1)}h`} />
{/* Guiding RMS over time */}
Guiding RMS over Time
{stats.guiding.length === 0 ? (
No PHD2 logs imported yet.
) : ( ({ ...g, rms_total: g.rms_total ?? null, rms_ra: g.rms_ra ?? null, rms_dec: g.rms_dec ?? null }))}> `${v}″`} /> `${v.toFixed(2)}″`} /> )}
{/* Top targets */}
Most Integrated Targets
{['Name', 'Type', 'Sessions', 'Integration'].map(h => ( ))} {stats.top_targets.map(t => ( ))}
{h}
{t.common_name ?? t.name} {t.common_name && {t.name}} {t.obj_type} {t.sessions} {(t.total_min / 60).toFixed(1)}h
{/* Quality breakdown */} {stats.quality && stats.quality.length > 0 && (
Session Quality Breakdown
{stats.quality.map(q => { const colorMap: Record = { keeper: 'var(--good)', needs_more: 'var(--blue)', rejected: 'var(--danger)', pending: 'var(--muted)', }; const color = colorMap[q.quality] ?? 'var(--text-mid)'; const total = stats.quality.reduce((s, x) => s + x.count, 0); return (
{q.quality.replace('_', ' ')}
{q.count}
{total > 0 ? Math.round((q.count / total) * 100) : 0}%
); })}
)}
); }