Files
Astronome/frontend/src/pages/Dashboard.tsx
T
2026-04-10 00:09:42 +02:00

236 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { useTonight } from '../hooks/useTonight';
import { useWeather, useForecast } from '../hooks/useWeather';
import { useTargets } from '../hooks/useTargets';
import { useStats } from '../hooks/useStats';
import GoNogo from '../components/weather/GoNogo';
import DewAlert from '../components/weather/DewAlert';
import MoonPhaseIcon from '../components/sky/MoonPhaseIcon';
import DetailDrawer from '../components/targets/DetailDrawer';
import type { Target } from '../api/types';
const FILTER_LABELS: Record<string, string> = {
sv220: 'HaOIII', c2: 'SII/OIII', sv260: 'LP', uvir: 'UV/IR',
};
const CC_LABELS: Record<number, string> = {
1: 'Clear', 2: 'Clear', 3: 'Mostly clear', 4: 'Partly cloudy',
5: 'Partly cloudy', 6: 'Cloudy', 7: 'Mostly cloudy', 8: 'Overcast', 9: 'Overcast',
};
const CC_COLOR = (n: number) => n <= 2 ? 'var(--good)' : n <= 4 ? 'var(--teal)' : n <= 6 ? 'var(--warn)' : 'var(--danger)';
function fmtTime(utc?: string): string {
if (!utc) return '—';
return new Date(utc).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris' });
}
function fmtDuration(min?: number): string {
if (!min) return '—';
const h = Math.floor(min / 60);
const m = min % 60;
return `${h}h ${m < 10 ? '0' : ''}${m}m`;
}
function fmtIntTotal(min: number): string {
if (min < 60) return `${min} min`;
const h = (min / 60).toFixed(1);
return `${h} h`;
}
export default function Dashboard() {
const { data: tonight } = useTonight();
const { data: weather } = useWeather();
const { data: forecast } = useForecast();
const { data: targets } = useTargets({ tonight: true, limit: 5 });
const { data: stats } = useStats();
const [expandedTarget, setExpandedTarget] = useState<Target | null>(null);
const moonPct = tonight?.moon_illumination != null
? `${Math.round(tonight.moon_illumination * 100)}%`
: '—';
// Next 4 forecast slots for mini weather bar (3h each = 12h ahead)
const slots = (forecast as { dataseries?: { cloudcover?: number; seeing?: number; timepoint?: number }[] })?.dataseries?.slice(0, 8) ?? [];
return (
<div style={{ padding: '24px 28px' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginBottom: 20 }}>
<h1 style={{ fontFamily: 'var(--font-display)', fontSize: 20, fontWeight: 700, color: 'var(--text-hi)', letterSpacing: '0.04em' }}>
Dashboard
</h1>
{tonight?.date && (
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-lo)' }}>
{tonight.date}
</span>
)}
</div>
{/* Dew alert banner */}
{weather?.dew_alert && (
<div style={{ marginBottom: 16 }}>
<DewAlert level={weather.dew_alert} temp={weather.temp_c} dewPoint={weather.dew_point_c} />
</div>
)}
{/* Stat cards row */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 20 }}>
{/* Go/No-go */}
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>Tonight</div>
<GoNogo status={weather?.go_nogo} />
{weather?.temp_c != null && (
<div style={{ marginTop: 8, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-mid)' }}>
{weather.temp_c.toFixed(1)}°C · {weather.humidity_pct?.toFixed(0)}% RH
</div>
)}
</div>
{/* Moon */}
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>Moon</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{tonight?.moon_illumination != null && <MoonPhaseIcon illumination={tonight.moon_illumination} size={32} />}
<div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 600, color: 'var(--text-hi)', lineHeight: 1 }}>{moonPct}</div>
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 3 }}>{tonight?.moon_phase_name ?? '—'}</div>
</div>
</div>
</div>
{/* True dark */}
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>True Dark</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 20, fontWeight: 600, color: 'var(--teal)', lineHeight: 1 }}>
{fmtDuration(tonight?.true_dark_minutes)}
</div>
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 4 }}>
{tonight?.true_dark_start_utc
? `${fmtTime(tonight.true_dark_start_utc)} ${fmtTime(tonight.true_dark_end_utc)}`
: 'No full dark tonight'}
</div>
</div>
{/* Stats summary */}
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>Logbook</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 20, fontWeight: 600, color: 'var(--amber)', lineHeight: 1 }}>
{stats ? fmtIntTotal(stats.total_integration_min) : '—'}
</div>
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 4 }}>
{stats ? `${stats.total_sessions} sessions · ${stats.objects_with_keeper} keepers` : 'No data yet'}
</div>
</div>
</div>
{/* Tonight timing + top targets + forecast */}
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr 1fr', gap: 16, marginBottom: 20 }}>
{/* Tonight timing */}
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 10 }}>Tonight's Window</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tbody>
{[
['Dusk', fmtTime(tonight?.astro_dusk_utc)],
['Dawn', fmtTime(tonight?.astro_dawn_utc)],
['Moon rise', fmtTime(tonight?.moon_rise_utc)],
['Moon set', fmtTime(tonight?.moon_set_utc)],
['Dark start', fmtTime(tonight?.true_dark_start_utc)],
['Dark end', fmtTime(tonight?.true_dark_end_utc)],
].map(([label, value]) => (
<tr key={label}>
<td style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 11, paddingBottom: 4, paddingRight: 8 }}>{label}</td>
<td style={{ color: 'var(--text-hi)', fontFamily: 'var(--font-mono)', fontSize: 11, textAlign: 'right' }}>{value}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Top targets */}
<div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 10 }}>Top Targets Tonight</div>
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, overflow: 'hidden' }}>
{!targets?.items?.length && (
<div style={{ padding: 16, color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>
Catalog loading…
</div>
)}
{targets?.items?.map((t, i) => (
<div key={t.id}>
<div
onClick={() => setExpandedTarget(expandedTarget?.id === t.id ? null : t)}
style={{
display: 'flex',
alignItems: 'center',
padding: '8px 14px',
borderBottom: '1px solid var(--border)',
gap: 10,
cursor: 'pointer',
background: expandedTarget?.id === t.id ? 'var(--bg-hover)' : 'transparent',
transition: 'background 0.1s',
}}
>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', width: 14 }}>{i + 1}</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--amber)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{t.common_name ?? t.name}
</div>
<div style={{ fontSize: 11, color: 'var(--text-lo)' }}>
{t.name} · {t.usable_min ? `${t.usable_min}min` : ''}
</div>
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600, color: (t.max_alt_deg ?? 0) >= 30 ? 'var(--good)' : 'var(--warn)' }}>
{t.max_alt_deg?.toFixed(0)}°
</span>
{t.recommended_filter && (
<span className={`filter-pill ${t.recommended_filter}`} style={{ fontSize: 9 }}>
{FILTER_LABELS[t.recommended_filter] ?? t.recommended_filter.toUpperCase()}
</span>
)}
</div>
{expandedTarget?.id === t.id && <DetailDrawer target={t} />}
</div>
))}
</div>
</div>
{/* Forecast mini bars */}
<div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 10 }}>24h Forecast</div>
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '12px 14px' }}>
{slots.length === 0 && (
<div style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 11 }}>No forecast data</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{slots.map((slot, i) => {
const cc = slot.cloudcover ?? 5;
const seeing = slot.seeing ?? 5;
const hoursAhead = (i + 1) * 3;
const label = `+${hoursAhead}h`;
return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', width: 28 }}>{label}</span>
<div style={{ flex: 1, background: 'var(--bg-deep)', borderRadius: 2, height: 6, overflow: 'hidden' }}>
<div style={{
height: '100%',
width: `${((9 - cc) / 8) * 100}%`,
background: CC_COLOR(cc),
borderRadius: 2,
transition: 'width 0.3s',
}} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: CC_COLOR(cc), width: 80 }}>
{CC_LABELS[cc] ?? ''}
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: seeing <= 3 ? 'var(--good)' : seeing <= 5 ? 'var(--warn)' : 'var(--danger)', width: 24, textAlign: 'right' }}>
{['', '0.5', '0.75', '1.0', '1.25', '1.5', '2.0', '2.5', '>3'][seeing] ?? ''}
</span>
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
);
}