SPB Git

spb/findata

Public
9.5 KB · 168 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";11import { PublicOverview } from "@/components/PublicOverview";1213export default function OverviewPage() {14  const { isOwner, me } = useAuth();15  const signedIn = !!me && me.mfa_passed;16  return (17    <>18      <PublicOverview signedIn={signedIn} />19      {signedIn && <OperatorOverview isOwner={isOwner} />}20    </>21  );22}2324function OperatorOverview({ isOwner }: { isOwner: boolean }) {25  const { data, error, loading, reload } = useAsync((signal) => api.get<HealthSummary>("/health/summary", undefined, signal), []);26  useInterval(reload, 60_000);2728  const h = data?.health;29  const lake = h?.components.lake;30  const jobs = data?.jobs ?? {};31  const anomalies = data?.anomalies_7d ?? {};32  const anomaliesTotal = Object.values(anomalies).reduce((a, b) => a + b, 0);33  const primaryDs = h?.datasets?.find((d) => d.dataset_id === "market_bars.stock.1m.raw") ?? h?.datasets?.[0];34  const dead = jobs.dead ?? 0;35  const freeTone = lake ? (lake.free_gb < lake.hard_min_free_gb ? "crit" : lake.free_gb < lake.hard_min_free_gb * 2 ? "warn" : undefined) : undefined;3637  return (38    <div className="mt-6">39      <PageHeader40        title="Operations"41        subtitle={h ? <>Health checked {fmtRelative(h.checked_at)} · auto-refresh every 60 s</> : "Lakehouse status at a glance"}42        actions={43          <>44            {h && <StatusBadge status={h.status} label={h.status === "ok" ? "all systems ok" : h.status} />}45            <button className="btn btn-sm" onClick={reload} disabled={loading}>46              <RefreshCw className={loading ? "h-3.5 w-3.5 animate-spin" : "h-3.5 w-3.5"} /> Refresh47            </button>48          </>49        }50      />5152      <Banner tone="warn" className="mb-4">53        <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.54      </Banner>5556      {error && <ErrorState error={error} retry={reload} />}57      {!data && !error && <Skeleton rows={6} />}5859      {data && (60        <>61          {h && h.alerts.length > 0 && (62            <Card title={`Alerts (${h.alerts.length})`} className="mb-4">63              <ul className="flex flex-col gap-1.5">64                {h.alerts.map((a, i) => (65                  <li key={i} className="flex flex-wrap items-center gap-2 text-sm">66                    <SeverityBadge severity={a.severity} />67                    <code className="text-xs text-ink-3">{a.code}</code>68                    <span>{a.message}</span>69                  </li>70                ))}71              </ul>72            </Card>73          )}7475          <div className="mb-4 grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5">76            <KpiTile label="1-min symbols" value={fmtInt(data.bars_1m.symbols)} sub={`${fmtInt(data.instruments.active)} active instruments`} href="/symbols/" />77            <KpiTile label="Sessions" value={fmtInt(data.bars_1m.sessions)} sub="with 1-min bars" href="/coverage/" />78            <KpiTile label="1-min bars" value={fmtCompact(data.bars_1m.bars)} sub={data.bars_1m.bars ? fmtInt(data.bars_1m.bars) : undefined} href="/bars/" />79            <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} />80            <KpiTile81              label="Vendor lag"82              value={primaryDs?.vendor_lag_days === null || primaryDs?.vendor_lag_days === undefined ? "—" : `${primaryDs.vendor_lag_days} d`}83              sub={primaryDs?.vendor_last_update ? `vendor ${primaryDs.vendor_last_update}` : "no vendor checkpoint"}84              tone={primaryDs?.vendor_lag_days ? "warn" : undefined}85            />86            <KpiTile87              label="Lake free"88              value={lake ? fmtGB(lake.free_gb, 0) : "—"}89              sub={lake ? `curated ${fmtGB(lake.curated_gb, 2)} / budget ${lake.soft_limit_gb} GB · min free ${lake.hard_min_free_gb} GB` : undefined}90              tone={freeTone}91              href={isOwner ? "/storage/" : undefined}92            />93            <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} />94            <KpiTile label="Fundamentals" value={fmtInt(data.fundamentals.entities)} sub={`${fmtCompact(data.fundamentals.facts)} facts`} href="/fundamentals/" />95            <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/" />96            <KpiTile label="Datasets" value={fmtInt(data.datasets.active)} sub={`${data.datasets.planned} planned · ${data.datasets.pending} pending · ${data.datasets.unsupported} unsupported`} href="/datasets/" />97          </div>9899          <div className="grid gap-4 lg:grid-cols-[1fr_minmax(280px,0.6fr)]">100            <Card title="Recent jobs" actions={isOwner && <Link href="/ingestion/" className="text-xs text-accent-ink hover:underline">All jobs →</Link>}>101              <DataTable<Job>102                rows={data.recent_jobs}103                rowKey={(r) => r.id}104                mobile="scroll"105                columns={[106                  { key: "kind", header: "Kind", render: (r) => <span className="font-medium">{r.kind}</span> },107                  { key: "dataset_id", header: "Dataset", mono: true, hideSm: true },108                  { key: "status", header: "Status", render: (r) => <StatusBadge status={r.status} /> },109                  { key: "started_at", header: "Started", render: (r) => <span className="tnum text-xs">{fmtTs(r.started_at)}</span>, hideSm: true },110                  { key: "finished_at", header: "Finished", render: (r) => <span className="tnum text-xs">{fmtTs(r.finished_at)}</span>, hideSm: true },111                  { 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>) },112                ]}113              />114            </Card>115            <Card title="Components">116              <ul className="flex flex-col gap-2 text-sm">117                <li className="flex items-center justify-between gap-2">118                  <span>Postgres</span>119                  <StatusBadge status={h?.components.postgres?.startsWith("error") ? "error" : h?.components.postgres} label={h?.components.postgres} />120                </li>121                <li className="flex items-center justify-between gap-2">122                  <span>Ingest worker</span>123                  <StatusBadge status={h?.components.ingest_worker === "running" ? "ok" : "error"} label={h?.components.ingest_worker ?? "unknown"} />124                </li>125                <li className="flex items-center justify-between gap-2">126                  <span>Backup</span>127                  <span className="text-right text-xs">128                    {h?.components.backup?.last ? (129                      <>130                        <StatusBadge status={h.components.backup.verified ? "verified" : "warning"} label={h.components.backup.verified ? "verified" : "unverified"} />{" "}131                        <span className="text-ink-2">{fmtRelative(h.components.backup.last)}</span>132                      </>133                    ) : (134                      <StatusBadge status="warning" label="none yet" />135                    )}136                  </span>137                </li>138                <li className="flex items-center justify-between gap-2">139                  <span>SSD SMART</span>140                  <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"} />141                </li>142              </ul>143              {h?.datasets && h.datasets.length > 0 && (144                <div className="mt-4">145                  <div className="mb-1 text-xs font-semibold text-ink-2">Dataset freshness</div>146                  <ul className="flex flex-col gap-1 text-xs">147                    {h.datasets.map((d) => (148                      <li key={d.dataset_id} className="flex flex-wrap items-center justify-between gap-1">149                        <Link href={`/datasets/detail/?id=${encodeURIComponent(d.dataset_id)}`} className="font-mono text-accent-ink hover:underline">150                          {d.dataset_id}151                        </Link>152                        <span className="tnum text-ink-2">153                          v{d.version ?? "—"} · {d.last_session ?? "no sessions"}154                          {d.vendor_lag_days ? <span className="ml-1 text-warn-ink">· lag {d.vendor_lag_days} d</span> : null}155                        </span>156                      </li>157                    ))}158                  </ul>159                </div>160              )}161            </Card>162          </div>163        </>164      )}165    </div>166  );167}168
Files · main
Menu

Theme