1"use client";2import Link from "next/link";3import { RefreshCw } from "lucide-react";4import { api } from "@/lib/api";5import { useAsync, useInterval } from "@/lib/hooks";6import { fmtBytes, fmtCompact, fmtGB, fmtInt, fmtRelative } from "@/lib/format";7import { Card, ErrorState, KpiTile, PageHeader, Skeleton, StatusBadge } from "@/components/ui";8import { DataTable } from "@/components/DataTable";910export interface PublicOverviewData {11 generated_at: string;12 status: string | null;13 totals: { files: number; rows: number; bytes: number; datasets_active: number; dataset_versions: number; raw_archives: number; raw_bytes: number };14 families: { family: string; datasets: number; rows: number; bytes: number }[];15 datasets: { id: string; name: string; kind: string; asset_class: string | null; interval: string | null; adjustment: string | null; status: string; current_version: number; row_count: number | null; bytes: number | null; file_count: number | null }[];16 coverage: { dataset_id: string; symbols: number; first_session: string | null; last_session: string | null; bars: number | null }[];17 options: { underlyings: number | null; first_day: string | null; last_day: string | null; rows: number | null };18 fundamentals: { entities: number; facts: number; first_period: string | null; last_period: string | null; earnings_events: number };19 instruments: { n: number; active: number; delisted: number };20 freshness: { vendor_last_update: string | null; last_successful_job: string | null; jobs_24h: Record<string, number> };21 storage: { hot_free_gb: number | null; cold_free_gb: number | null; curated_gb: number | null; raw_gb: number | null };22 backup: { last: string | null; verified: boolean | null };23 license: string;24}2526const FAMILY_LABEL: Record<string, string> = {27 "market_bars.stock": "US stocks",28 "market_bars.etf": "US ETFs",29 "options.eod": "Options (EOD chains)",30 "market_bars.futures": "Futures (continuous)",31 "market_bars.futures_contracts": "Futures (contracts)",32 "market_bars.fx": "FX",33 "market_bars.crypto": "Crypto",34 "market_bars.index": "Indices",35};3637export function PublicOverview({ signedIn }: { signedIn: boolean }) {38 const { data, error, loading, reload } = useAsync((signal) => api.get<PublicOverviewData>("/public/overview", undefined, signal), []);39 useInterval(reload, 120_000);40 const stocks = data?.coverage.find((c) => c.dataset_id === "market_bars.stock.1m.raw");41 const etfs = data?.coverage.find((c) => c.dataset_id === "market_bars.etf.1m.raw");42 const indices = data?.coverage.find((c) => c.dataset_id === "market_bars.index.1m.raw");43 const jobs = data?.freshness.jobs_24h ?? {};4445 return (46 <>47 <PageHeader48 title="FinData OS — the lake at a glance"49 subtitle={data ? <>Catalog figures refreshed {fmtRelative(data.generated_at)} · public metrics only, data access requires sign-in</> : "Research-grade market data lakehouse (FirstRate Data, private use)"}50 actions={51 <>52 {data?.status && <StatusBadge status={data.status} label={data.status === "ok" ? "all systems ok" : data.status} />}53 <button className="btn btn-sm" onClick={reload} disabled={loading}>54 <RefreshCw className={loading ? "h-3.5 w-3.5 animate-spin" : "h-3.5 w-3.5"} /> Refresh55 </button>56 {!signedIn && (57 <Link href="/login/" className="btn btn-primary btn-sm">58 Sign in59 </Link>60 )}61 </>62 }63 />6465 {error && <ErrorState error={error} retry={reload} />}66 {!data && !error && <Skeleton rows={6} />}6768 {data && (69 <>70 <div className="mb-4 grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-6">71 <KpiTile label="Rows in the lake" value={fmtCompact(data.totals.rows)} sub={fmtInt(data.totals.rows)} />72 <KpiTile label="Curated Parquet" value={fmtBytes(data.totals.bytes, 0)} sub={`${fmtInt(data.totals.files)} files · ${fmtInt(data.totals.datasets_active)} active datasets`} href={signedIn ? "/datasets/" : undefined} />73 <KpiTile label="Stocks (1-min)" value={fmtInt(stocks?.symbols)} sub={stocks ? `${stocks.first_session} → ${stocks.last_session}` : "—"} href={signedIn ? "/symbols/" : undefined} />74 <KpiTile label="ETFs (1-min)" value={fmtInt(etfs?.symbols)} sub={etfs ? `${etfs.first_session} → ${etfs.last_session}` : "—"} />75 <KpiTile label="Option underlyings" value={fmtInt(data.options.underlyings)} sub={data.options.first_day ? `${fmtCompact(data.options.rows)} rows · ${data.options.first_day} → ${data.options.last_day}` : "—"} />76 <KpiTile label="Fundamentals" value={fmtInt(data.fundamentals.entities)} sub={`${fmtCompact(data.fundamentals.facts)} PIT facts · ${fmtCompact(data.fundamentals.earnings_events)} earnings`} href={signedIn ? "/fundamentals/" : undefined} />77 <KpiTile label="Indices (1-min)" value={fmtInt(indices?.symbols)} sub={indices ? `${indices.first_session} → ${indices.last_session}` : "—"} />78 <KpiTile label="Instruments" value={fmtInt(data.instruments.n)} sub={`${fmtInt(data.instruments.active)} active · ${fmtInt(data.instruments.delisted)} delisted`} />79 <KpiTile label="Vendor last update" value={data.freshness.vendor_last_update ?? "—"} sub={data.freshness.last_successful_job ? `last job ${fmtRelative(data.freshness.last_successful_job)}` : undefined} />80 <KpiTile label="Jobs (24 h)" value={`${fmtInt(jobs.succeeded ?? 0)} ok`} sub={`${fmtInt(jobs.queued ?? 0)} queued · ${fmtInt(jobs.running ?? 0)} running · ${fmtInt(jobs.dead ?? 0)} dead`} tone={jobs.dead ? "crit" : undefined} />81 <KpiTile label="Raw archives kept" value={fmtBytes(data.totals.raw_bytes, 0)} sub={`${fmtInt(data.totals.raw_archives)} vendor archives (sha256 recorded)`} />82 <KpiTile83 label="Free space"84 value={data.storage.hot_free_gb !== null ? fmtGB(data.storage.hot_free_gb, 0) : "—"}85 sub={data.storage.cold_free_gb !== null ? `internal · external ${fmtGB(data.storage.cold_free_gb, 0)}` : "internal SSD"}86 />87 </div>8889 <div className="grid gap-4 lg:grid-cols-[1fr_minmax(280px,0.7fr)]">90 <Card title="Data families">91 <DataTable92 rows={data.families}93 rowKey={(r) => r.family}94 mobile="scroll"95 columns={[96 { key: "family", header: "Family", render: (r) => <span className="font-medium">{FAMILY_LABEL[r.family] ?? r.family}</span> },97 { key: "datasets", header: "Datasets", render: (r) => <span className="tnum">{fmtInt(r.datasets)}</span> },98 { key: "rows", header: "Rows", render: (r) => <span className="tnum">{fmtInt(r.rows)}</span> },99 { key: "bytes", header: "Size", render: (r) => <span className="tnum">{fmtBytes(r.bytes, 1)}</span> },100 ]}101 />102 </Card>103 <Card title="Platform">104 <ul className="flex flex-col gap-2 text-sm">105 <li className="flex items-center justify-between gap-2">106 <span>Status</span>107 <StatusBadge status={data.status} label={data.status ?? "unknown"} />108 </li>109 <li className="flex items-center justify-between gap-2">110 <span>Dataset versions published</span>111 <span className="tnum">{fmtInt(data.totals.dataset_versions)}</span>112 </li>113 <li className="flex items-center justify-between gap-2">114 <span>Fundamentals periods</span>115 <span className="tnum text-xs">{data.fundamentals.first_period ?? "—"} → {data.fundamentals.last_period ?? "—"}</span>116 </li>117 <li className="flex items-center justify-between gap-2">118 <span>Last backup</span>119 <span className="text-right text-xs">120 {data.backup.last ? (121 <>122 <StatusBadge status={data.backup.verified ? "verified" : "warning"} label={data.backup.verified ? "verified" : "unverified"} />{" "}123 <span className="text-ink-2">{fmtRelative(data.backup.last)}</span>124 </>125 ) : (126 <StatusBadge status="warning" label="none yet" />127 )}128 </span>129 </li>130 </ul>131 <p className="mt-4 text-xs text-ink-2">{data.license}</p>132 <p className="mt-2 text-xs text-ink-2">133 API reference: <a className="text-accent-ink hover:underline" href="/api/docs">/api/docs</a> · <Link className="text-accent-ink hover:underline" href="/docs/">Documentation</Link>134 </p>135 </Card>136 </div>137138 <Card title="Datasets" className="mt-4">139 <DataTable140 rows={data.datasets.filter((d) => d.status === "active" && (d.row_count ?? 0) > 0)}141 rowKey={(r) => r.id}142 mobile="scroll"143 columns={[144 { key: "id", header: "Dataset", mono: true, render: (r) => (signedIn ? <Link href={`/datasets/detail/?id=${encodeURIComponent(r.id)}`} className="text-accent-ink hover:underline">{r.id}</Link> : r.id) },145 { key: "asset_class", header: "Asset", hideSm: true, render: (r) => r.asset_class ?? "—" },146 { key: "interval", header: "Interval", hideSm: true, render: (r) => r.interval ?? "—" },147 { key: "adjustment", header: "Adjustment", hideSm: true, render: (r) => r.adjustment ?? "—" },148 { key: "row_count", header: "Rows", render: (r) => <span className="tnum">{fmtInt(r.row_count)}</span> },149 { key: "bytes", header: "Size", render: (r) => <span className="tnum">{fmtBytes(r.bytes, 1)}</span> },150 { key: "current_version", header: "Version", hideSm: true, render: (r) => <span className="tnum">v{r.current_version}</span> },151 ]}152 />153 </Card>154 </>155 )}156 </>157 );158}159