Initial Commit

This commit is contained in:
2026-04-09 23:23:31 +02:00
commit 66b1c6777d
94 changed files with 15173 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
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>
);
}