SPB Git

spb/findata

Public
10.0 KB · 159 lines tsx
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
Files · f85ef07ccaee074d0f21ee15a0887b71bc6ad458
Menu

Theme