SPB Git

spb/findata

Public
9.2 KB · 157 lines tsx
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
Files · b31ccfb82046b1137ca27720f0467b178d4cebd6
Menu

Theme