1"use client";2import Link from "next/link";3import { RefreshCw } from "lucide-react";4import { api, type HealthSummary } from "@/lib/api";5import { useAsync, useInterval } from "@/lib/hooks";6import { fmtCompact, fmtGB, fmtInt, fmtRelative, fmtTs } from "@/lib/format";7import { Banner, Card, ErrorState, KpiTile, PageHeader, SeverityBadge, Skeleton, StatusBadge } from "@/components/ui";8import { DataTable } from "@/components/DataTable";9import { Job } from "@/lib/api";10import { useAuth } from "@/lib/auth";1112export default function OverviewPage() {13 const { isOwner } = useAuth();14 const { data, error, loading, reload } = useAsync((signal) => api.get<HealthSummary>("/health/summary", undefined, signal), []);15 useInterval(reload, 60_000);1617 const h = data?.health;18 const lake = h?.components.lake;19 const jobs = data?.jobs ?? {};20 const anomalies = data?.anomalies_7d ?? {};21 const anomaliesTotal = Object.values(anomalies).reduce((a, b) => a + b, 0);22 const primaryDs = h?.datasets?.find((d) => d.dataset_id === "market_bars.stock.1m.raw") ?? h?.datasets?.[0];23 const dead = jobs.dead ?? 0;24 const freeTone = lake ? (lake.free_gb < lake.hard_min_free_gb ? "crit" : lake.free_gb < lake.hard_min_free_gb * 2 ? "warn" : undefined) : undefined;2526 return (27 <>28 <PageHeader29 title="Overview"30 subtitle={h ? <>Health checked {fmtRelative(h.checked_at)} · auto-refresh every 60 s</> : "Lakehouse status at a glance"}31 actions={32 <>33 {h && <StatusBadge status={h.status} label={h.status === "ok" ? "all systems ok" : h.status} />}34 <button className="btn btn-sm" onClick={reload} disabled={loading}>35 <RefreshCw className={loading ? "h-3.5 w-3.5 animate-spin" : "h-3.5 w-3.5"} /> Refresh36 </button>37 </>38 }39 />4041 <Banner tone="warn" className="mb-4">42 <strong>FirstRate Data — private use only.</strong> Raw bars and tables may not be redistributed (no public API, CSV or tables). Derived results may be published with attribution.43 </Banner>4445 {error && <ErrorState error={error} retry={reload} />}46 {!data && !error && <Skeleton rows={6} />}4748 {data && (49 <>50 {h && h.alerts.length > 0 && (51 <Card title={`Alerts (${h.alerts.length})`} className="mb-4">52 <ul className="flex flex-col gap-1.5">53 {h.alerts.map((a, i) => (54 <li key={i} className="flex flex-wrap items-center gap-2 text-sm">55 <SeverityBadge severity={a.severity} />56 <code className="text-xs text-ink-3">{a.code}</code>57 <span>{a.message}</span>58 </li>59 ))}60 </ul>61 </Card>62 )}6364 <div className="mb-4 grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5">65 <KpiTile label="1-min symbols" value={fmtInt(data.bars_1m.symbols)} sub={`${fmtInt(data.instruments.active)} active instruments`} href="/symbols/" />66 <KpiTile label="Sessions" value={fmtInt(data.bars_1m.sessions)} sub="with 1-min bars" href="/coverage/" />67 <KpiTile label="1-min bars" value={fmtCompact(data.bars_1m.bars)} sub={data.bars_1m.bars ? fmtInt(data.bars_1m.bars) : undefined} href="/bars/" />68 <KpiTile label="Last session" value={data.bars_1m.last ?? "—"} sub={primaryDs?.days_since_last_session !== null && primaryDs?.days_since_last_session !== undefined ? `${primaryDs.days_since_last_session} d ago` : undefined} />69 <KpiTile70 label="Vendor lag"71 value={primaryDs?.vendor_lag_days === null || primaryDs?.vendor_lag_days === undefined ? "—" : `${primaryDs.vendor_lag_days} d`}72 sub={primaryDs?.vendor_last_update ? `vendor ${primaryDs.vendor_last_update}` : "no vendor checkpoint"}73 tone={primaryDs?.vendor_lag_days ? "warn" : undefined}74 />75 <KpiTile76 label="Lake free"77 value={lake ? fmtGB(lake.free_gb, 0) : "—"}78 sub={lake ? `curated ${fmtGB(lake.curated_gb, 2)} / budget ${lake.soft_limit_gb} GB · min free ${lake.hard_min_free_gb} GB` : undefined}79 tone={freeTone}80 href={isOwner ? "/storage/" : undefined}81 />82 <KpiTile label="Jobs" value={`${fmtInt(jobs.queued ?? 0)} / ${fmtInt(jobs.running ?? 0)} / ${fmtInt(dead)}`} sub="queued / running / dead" tone={dead ? "crit" : undefined} href={isOwner ? "/ingestion/" : undefined} />83 <KpiTile label="Fundamentals" value={fmtInt(data.fundamentals.entities)} sub={`${fmtCompact(data.fundamentals.facts)} facts`} href="/fundamentals/" />84 <KpiTile label="Anomalies (7 d)" value={fmtInt(anomaliesTotal)} sub={Object.entries(anomalies).map(([k, v]) => `${k} ${v}`).join(" · ") || "none"} tone={anomalies.error ? "crit" : anomaliesTotal ? "warn" : undefined} href="/quality/" />85 <KpiTile label="Datasets" value={fmtInt(data.datasets.active)} sub={`${data.datasets.planned} planned · ${data.datasets.pending} pending · ${data.datasets.unsupported} unsupported`} href="/datasets/" />86 </div>8788 <div className="grid gap-4 lg:grid-cols-[1fr_minmax(280px,0.6fr)]">89 <Card title="Recent jobs" actions={isOwner && <Link href="/ingestion/" className="text-xs text-accent-ink hover:underline">All jobs →</Link>}>90 <DataTable<Job>91 rows={data.recent_jobs}92 rowKey={(r) => r.id}93 mobile="scroll"94 columns={[95 { key: "kind", header: "Kind", render: (r) => <span className="font-medium">{r.kind}</span> },96 { key: "dataset_id", header: "Dataset", mono: true, hideSm: true },97 { key: "status", header: "Status", render: (r) => <StatusBadge status={r.status} /> },98 { key: "started_at", header: "Started", render: (r) => <span className="tnum text-xs">{fmtTs(r.started_at)}</span>, hideSm: true },99 { key: "finished_at", header: "Finished", render: (r) => <span className="tnum text-xs">{fmtTs(r.finished_at)}</span>, hideSm: true },100 { key: "last_error", header: "Error", render: (r) => (r.last_error ? <span className="text-xs text-crit-ink break-words">{r.last_error}</span> : <span className="text-ink-3">—</span>) },101 ]}102 />103 </Card>104 <Card title="Components">105 <ul className="flex flex-col gap-2 text-sm">106 <li className="flex items-center justify-between gap-2">107 <span>Postgres</span>108 <StatusBadge status={h?.components.postgres?.startsWith("error") ? "error" : h?.components.postgres} label={h?.components.postgres} />109 </li>110 <li className="flex items-center justify-between gap-2">111 <span>Ingest worker</span>112 <StatusBadge status={h?.components.ingest_worker === "running" ? "ok" : "error"} label={h?.components.ingest_worker ?? "unknown"} />113 </li>114 <li className="flex items-center justify-between gap-2">115 <span>Backup</span>116 <span className="text-right text-xs">117 {h?.components.backup?.last ? (118 <>119 <StatusBadge status={h.components.backup.verified ? "verified" : "warning"} label={h.components.backup.verified ? "verified" : "unverified"} />{" "}120 <span className="text-ink-2">{fmtRelative(h.components.backup.last)}</span>121 </>122 ) : (123 <StatusBadge status="warning" label="none yet" />124 )}125 </span>126 </li>127 <li className="flex items-center justify-between gap-2">128 <span>SSD SMART</span>129 <StatusBadge status={h?.components.ssd_smart === "Verified" ? "ok" : h?.components.ssd_smart === "unavailable" || h?.components.ssd_smart === "Not Supported" ? "neutral" : "error"} label={h?.components.ssd_smart ?? "unknown"} />130 </li>131 </ul>132 {h?.datasets && h.datasets.length > 0 && (133 <div className="mt-4">134 <div className="mb-1 text-xs font-semibold text-ink-2">Dataset freshness</div>135 <ul className="flex flex-col gap-1 text-xs">136 {h.datasets.map((d) => (137 <li key={d.dataset_id} className="flex flex-wrap items-center justify-between gap-1">138 <Link href={`/datasets/detail/?id=${encodeURIComponent(d.dataset_id)}`} className="font-mono text-accent-ink hover:underline">139 {d.dataset_id}140 </Link>141 <span className="tnum text-ink-2">142 v{d.version ?? "—"} · {d.last_session ?? "no sessions"}143 {d.vendor_lag_days ? <span className="ml-1 text-warn-ink">· lag {d.vendor_lag_days} d</span> : null}144 </span>145 </li>146 ))}147 </ul>148 </div>149 )}150 </Card>151 </div>152 </>153 )}154 </>155 );156}157