Python 51.6%
TypeScript 46.7%
CSS 1.7%
1import type { Metadata } from "next";2import { apiSafe } from "@/lib/api";3import { Badge, Card, Empty, SectionHeader } from "@/components/ui";4import { dateTimeFr, relTime } from "@/lib/format";56export const metadata: Metadata = { title: "Statut des données", description: "État en temps réel des connecteurs, sources et travaux d'ingestion de QC26." };7export const revalidate = 30;89interface Status { components: Record<string, string>; sources: { id: string; name: string; url: string; tier: number; kind: string; status: string; lastCheckedAt: string | null; lastOkAt: string | null; detail: string | null }[]; jobs: { id: number; kind: string; status: string; startedAt: string; finishedAt: string | null; items: number; message: string | null }[]; latestRun: { id: number; completedAt: string | null } | null; dbLatencyMs: number; sseClients: number; serverTime: string }1011const LABELS: Record<string, string> = { pollWatcher: "Poll watcher", forecastEngine: "Forecast engine", electionFeed: "Election feed", documentWatcher: "Document watcher", officialData: "Données officielles", signals: "Signaux (marchés, presse, attention)" };1213export default async function Page() {14 const st = await apiSafe<Status>("/api/status", { revalidate: 30 });15 if (!st) return <Empty title="Moteur injoignable" />;16 const tone = (s: string) => (s === "operational" ? "ok" : s === "ready" ? "accent" : "warn");17 return (18 <div className="space-y-8">19 <div><div className="eyebrow eyebrow-accent">QC26 Data Status</div><h1 className="display text-[40px] md:text-[56px] mt-3">Statut des données</h1><p className="text-ink-2 mt-2 text-[15px] num">Heure serveur {dateTimeFr(st.serverTime)} · latence base {st.dbLatencyMs} ms · {st.sseClients} clients SSE</p></div>20 <div className="grid grid-cols-2 lg:grid-cols-6 gap-2 sm:gap-3">{Object.entries(st.components).map(([k, v]) => <Card key={k}><div className="text-[13px] font-semibold">{LABELS[k] ?? k}</div><div className="mt-2"><Badge tone={tone(v)}>● {v}</Badge></div></Card>)}</div>21 <section><SectionHeader eyebrow="Sources" title="Connecteurs et sources" description="Tier 1 = officiel (Élections Québec, partis), 2 = firmes de sondage, 3 = signaux (marchés, presse, attention), 5 = index tiers (amorçage uniquement)." /><Card pad={false} className="overflow-x-auto"><table className="data-table min-w-[720px]"><thead><tr><th>Source</th><th>Tier</th><th>Statut</th><th>Vérifié</th><th>Dernier succès</th><th>Détail</th></tr></thead><tbody>{st.sources.sort((a, b) => a.tier - b.tier).map((s) => <tr key={s.id}><td><a href={s.url} target="_blank" rel="noopener noreferrer" className="font-medium hover:underline">{s.name}</a></td><td className="num">{s.tier}</td><td><Badge tone={tone(s.status)}>{s.status}</Badge></td><td className="num text-ink-2">{s.lastCheckedAt ? relTime(s.lastCheckedAt) : "—"}</td><td className="num text-ink-2">{s.lastOkAt ? relTime(s.lastOkAt) : "—"}</td><td className="text-[12px] text-ink-3">{s.detail}</td></tr>)}</tbody></table></Card></section>22 <section><SectionHeader eyebrow="Travaux" title="Derniers travaux d'ingestion" /><Card pad={false} className="overflow-x-auto"><table className="data-table min-w-[720px]"><thead><tr><th>Travail</th><th>Statut</th><th>Début</th><th>Fin</th><th className="r">Éléments</th><th>Message</th></tr></thead><tbody>{st.jobs.map((j) => <tr key={j.id}><td className="font-medium">{j.kind}</td><td><Badge tone={j.status === "ok" ? "ok" : j.status === "running" ? "accent" : "warn"}>{j.status}</Badge></td><td className="num">{dateTimeFr(j.startedAt)}</td><td className="num">{dateTimeFr(j.finishedAt)}</td><td className="r num">{j.items}</td><td className="text-[12px] text-ink-3">{j.message}</td></tr>)}</tbody></table></Card></section>23 </div>24 );25}26