/* global React, ReactDOM */ const { useState: useS, useEffect: useE, useRef: useR } = React; function useLang() { const [lang, setLang] = useS(() => localStorage.getItem("penumbra_lang") || "fr"); useE(() => { localStorage.setItem("penumbra_lang", lang); document.documentElement.lang = lang; }, [lang]); return [lang, setLang]; } function useHashRoute() { return "predict"; } function todayStr() { return new Date().toISOString().slice(0, 10); } function App() { const [lang, setLang] = useLang(); const route = useHashRoute(); const t = (k) => (window.I18N[lang] && window.I18N[lang][k]) || k; return ( <>
{ if (route === "predict") { e.preventDefault(); } window.location.hash = ""; }}> Penumbra
); } function PredictPage({ t, lang }) { const [depIata, setDepIata] = useS("CDG"); const [arrIata, setArrIata] = useS("JFK"); const [date, setDate] = useS(todayStr()); const [departureTime, setDepartureTime] = useS("10:00"); const [error, setError] = useS(""); const [loading, setLoading] = useS(false); const [trajectory, setTrajectory] = useS(null); const [meta, setMeta] = useS(null); const [solarEvent, setSolarEvent] = useS(null); const onCalculate = async () => { setError(""); setTrajectory(null); setSolarEvent(null); const dep = depIata.trim().toUpperCase(); const arr = arrIata.trim().toUpperCase(); if (!dep || !arr || dep.length < 3 || arr.length < 3) { setError(t("err_iata_required")); return; } setLoading(true); try { const { trajectory: traj, meta: m, solarEvent: se } = await window.buildTrajectory({ depIata: dep, arrIata: arr, date, departureTime, }); setTrajectory(traj); setMeta(m); setSolarEvent(se); } catch (e) { setError(e.message || t("err_unknown_flight")); } finally { setLoading(false); } }; const onSubmit = (e) => { e.preventDefault(); onCalculate(); }; return ( <>

{t("hero_title")}

{t("hero_lead")}

{t("hero_badge")}
{!trajectory && (
{t("form_section_kicker")}

{t("form_section_title")}

setDate(e.target.value)} />
setDepartureTime(e.target.value)} />
{loading && (
Calcul en cours via le modèle XGBoost…
)} {error &&
{error}
}

{t("form_note")}

)} {trajectory && ( <> {solarEvent && ( )} { setTrajectory(null); setMeta(null); setSolarEvent(null); }} /> )}
} title={t("exp1_title")} text={t("exp1_text")} /> } title={t("exp2_title")} text={t("exp2_text")} /> } title={t("exp3_title")} text={t("exp3_text")} />
); } function AirportInput({ label, value, onChange, placeholder }) { const [query, setQuery] = useS(value || ""); const [suggestions, setSuggestions] = useS([]); const [open, setOpen] = useS(false); const timer = useR(null); const fetch_ = (q) => { clearTimeout(timer.current); if (q.length < 2) { setSuggestions([]); return; } timer.current = setTimeout(async () => { try { const r = await fetch(`/airports/search?q=${encodeURIComponent(q)}`); if (r.ok) setSuggestions(await r.json()); } catch (_) {} }, 180); }; const onInput = (e) => { const v = e.target.value; setQuery(v); setOpen(true); fetch_(v); }; const onSelect = (ap) => { const display = `${ap.iata} — ${ap.city}`; setQuery(display); onChange(ap.iata); setSuggestions([]); setOpen(false); }; const onBlur = () => setTimeout(() => setOpen(false), 160); const onFocus = () => { if (query.length >= 2) setOpen(true); }; return (
{open && suggestions.length > 0 && ( )}
); } function SolarEventAlert({ event, t }) { const stormColor = { g1: "#e6b800", g2: "#e68600", g3: "#e65200", g4: "#c42b00", g5: "#8b0000" }; const scale = event.scale || ""; const badgeColor = stormColor[scale.toLowerCase()] || "#c42b00"; return (
{t("solar_alert_title")} {scale && (
{t("solar_alert_storm")} {scale} — Kp max {event.kp_max}
)} {event.flare_class && (
{t("solar_alert_flare")} {event.flare_class}
)}
{t("solar_alert_note")}
); } function ExpCard({ icon, title, text }) { return (

{title}

{text}

); } function ResultPanel({ trajectory, meta, t, lang, onReset }) { const dose = trajectory.totalDose; const low = trajectory.lowDose; const high = trajectory.highDose; const riskKey = { low: "risk_low", mod: "risk_mod", high: "risk_high" }[trajectory.totalRisk]; const chestXrays = dose / 100; const annualPct = (dose / 1000) * 100; const groundHours = dose / 0.27; const groundDays = groundHours / 24; const fmtDose = window.fmtNumber(dose, 0); const fmtLow = window.fmtNumber(low, 0); const fmtHigh = window.fmtNumber(high, 0); const dateLocale = lang === "fr" ? "fr-FR" : "en-US"; const dateFmt = new Date(meta.date).toLocaleDateString(dateLocale, { day: "numeric", month: "long", year: "numeric" }); const durationStr = (() => { const h = Math.floor(trajectory.durationHours); const m = Math.round((trajectory.durationHours - h) * 60); return `${h} h ${String(m).padStart(2, "0")}`; })(); const xrayValTxt = window.fmtNumber(chestXrays, chestXrays < 1 ? 2 : 1) + " " + (chestXrays >= 2 ? t("cmp_xray_v_many") : t("cmp_xray_v_one")); const groundValTxt = groundDays >= 1 ? window.fmtNumber(groundDays, groundDays < 2 ? 1 : 0) + " " + (groundDays >= 2 ? t("ground_unit_d_many") : t("ground_unit_d_one")) : window.fmtNumber(groundHours, 0) + " " + t("ground_unit_h"); return (
{t("res_label")}
{fmtDose} µSv
{t("res_ci")} · {fmtLow} – {fmtHigh} µSv
{t(riskKey)}
{t("cmp_xray_k")} {xrayValTxt}
{t("cmp_annual_k")} {window.fmtNumber(annualPct, annualPct < 1 ? 2 : 1)} %
{t("cmp_ground_k")} {groundValTxt}
{t("meta_route")} {meta.fromAp.city} ({meta.fromAp.iata}) → {meta.toAp.city} ({meta.toAp.iata})
{t("meta_date")} {dateFmt}
{t("meta_duration")} {durationStr}
{t("meta_distance")} {window.fmtNumber(trajectory.distanceKm, 0)} km
{t("meta_alt")} FL{Math.round(meta.cruiseFt / 100)}
{meta.kpUsed !== undefined && (
Kp ({meta.dataSource}) {typeof meta.kpUsed === "number" ? meta.kpUsed.toFixed(1) : meta.kpUsed}
)}
); } ReactDOM.createRoot(document.getElementById("root")).render();