From bf6c5fa15cb3ca2c3280a8622b56b757e3dbc488 Mon Sep 17 00:00:00 2001 From: aurph Date: Fri, 3 Jul 2026 13:16:48 -0400 Subject: [PATCH] Real gauges: replace the synthetic indices with measured numbers (owner-directed) The three headline gauges - AI Power Demand (sentiment formula around a fixed 72 baseline), Nuclear Power Index (never-rebalanced basket where VST compounded to ~43% weight and ~91% of daily variance), and Grid Stress (r 0.96 co-movement with NPI) - are gone from the dashboard. Owner's words: the 2024 starting point is arbitrary; make the numbers real. Replacements, each a direct computation over sourced data with provenance on the card: - TRACKED AI DC POWER: 18.8 GW operational + construction across the verified >=400 MW facility dataset (announced projects excluded from the headline - press releases are not steel - and said so on card). - COST OF AI COMPUTE: fleet-average GPU rental $/hr with real 1Y change, from the GPU price index (priciest/cheapest rows). - GRID HEADROOM: tightest reserve margin among AI-load RTOs (MISO 13.4%), NERC LTRA 2025, three tightest listed. The 'Gauges Since Jan 2024' card becomes TRACKED BUILDOUT OVER TIME: a real cumulative-capacity series from facility open dates - solid step line for operational history, dashed for the construction pipeline at planned dates, undated sites counted honestly. Header strip shows tracked GW + GW building instead of the NPI. Consistency: the Power map's GW headline now uses the same definition (announced excluded, noted in the title attr); RTO reserve margins extracted to a shared data/rto-config consumed by both surfaces. No server changes: gauges compute client-side from existing endpoints (/api/datacenters, /api/gpu-prices/metrics). /api/kpis and /api/index-history keep serving and recording for transparency; nothing on the dashboard consumes them anymore. New pure module lib/real-gauges.ts, 100% covered (suite 180 tests). --- client/src/data/rto-config.ts | 22 + client/src/lib/__tests__/real-gauges.test.ts | 165 +++++ client/src/lib/real-gauges.ts | 174 +++++ client/src/pages/PowerMap.tsx | 26 +- client/src/pages/TiltOverview.tsx | 714 +++++++------------ 5 files changed, 612 insertions(+), 489 deletions(-) create mode 100644 client/src/data/rto-config.ts create mode 100644 client/src/lib/__tests__/real-gauges.test.ts create mode 100644 client/src/lib/real-gauges.ts diff --git a/client/src/data/rto-config.ts b/client/src/data/rto-config.ts new file mode 100644 index 0000000..7007dff --- /dev/null +++ b/client/src/data/rto-config.ts @@ -0,0 +1,22 @@ +/** + * RTO/ISO reserve margins and AI-load signals - NERC LTRA 2025 (2026 + * projections). Single source of truth: the Power map, its operator table, + * and the overview's Grid Headroom gauge all read from here. + */ +export interface RTOConfig { + label: string; + reserveMargin: number; + aiSignal: "Critical" | "Elevated" | "Moderate" | "Low"; +} + +export const RTO_CONFIG: Record = { + PJM: { label: "PJM", reserveMargin: 17.5, aiSignal: "Elevated" }, + MISO: { label: "MISO", reserveMargin: 13.4, aiSignal: "Critical" }, + ERCOT: { label: "ERCOT", reserveMargin: 15.8, aiSignal: "Critical" }, + WECC: { label: "WECC", reserveMargin: 24.6, aiSignal: "Moderate" }, + SERC: { label: "SERC", reserveMargin: 23.1, aiSignal: "Moderate" }, + SPP: { label: "SPP", reserveMargin: 27.8, aiSignal: "Low" }, + NPCC: { label: "NPCC", reserveMargin: 26.4, aiSignal: "Low" }, +}; + +export const RTO_SOURCE_NOTE = "NERC LTRA 2025 (2026 projections)"; diff --git a/client/src/lib/__tests__/real-gauges.test.ts b/client/src/lib/__tests__/real-gauges.test.ts new file mode 100644 index 0000000..dbe76d6 --- /dev/null +++ b/client/src/lib/__tests__/real-gauges.test.ts @@ -0,0 +1,165 @@ +/** + * Real gauges: the numbers replacing the synthetic indices. A silent bug + * here puts a wrong headline on the front page - full coverage. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + buildBuildoutHistory, + computeTrackedPower, + filterTrackedFacilities, + fmtGW, + parseOpenDate, + tightestRTO, + type FacilityLite, +} from "../real-gauges"; + +const F = (status: string, powerMW: number | null, openDate?: string, name?: string): FacilityLite => ({ + status, + powerMW, + openDate, + name, +}); + +describe("filterTrackedFacilities", () => { + it("applies the same >=400 MW floor the Power map advertises", () => { + const kept = filterTrackedFacilities([F("operational", 400), F("operational", 399), F("operational", null)]); + assert.equal(kept.length, 1); + assert.equal(kept[0].powerMW, 400); + }); + it("empty and missing input yield empty output", () => { + assert.deepEqual(filterTrackedFacilities([]), []); + assert.deepEqual(filterTrackedFacilities(undefined as never), []); + }); +}); + +describe("computeTrackedPower", () => { + it("buckets MW and counts by status; tracked = operational + construction", () => { + const t = computeTrackedPower([ + F("operational", 1000), + F("operational", 500), + F("construction", 700), + F("announced", 9000), + ]); + assert.equal(t.operationalMW, 1500); + assert.equal(t.constructionMW, 700); + assert.equal(t.announcedMW, 9000); + assert.equal(t.trackedMW, 2200); + assert.equal(t.operationalCount, 2); + assert.equal(t.constructionCount, 1); + assert.equal(t.announcedCount, 1); + }); + it("announced is never in the headline number", () => { + const t = computeTrackedPower([F("announced", 5000)]); + assert.equal(t.trackedMW, 0); + }); + it("null/negative/NaN MW counts the facility but adds zero", () => { + const t = computeTrackedPower([F("operational", null), F("operational", -50), F("operational", NaN)]); + assert.equal(t.operationalMW, 0); + assert.equal(t.operationalCount, 3); + }); + it("unknown statuses are ignored; empty input is all zeros", () => { + assert.equal(computeTrackedPower([F("retired", 100)]).trackedMW, 0); + assert.equal(computeTrackedPower([]).trackedMW, 0); + assert.equal(computeTrackedPower(undefined as never).trackedMW, 0); + }); +}); + +describe("fmtGW", () => { + it("formats MW as GW", () => { + assert.equal(fmtGW(23600), "23.6 GW"); + assert.equal(fmtGW(500), "0.5 GW"); + assert.equal(fmtGW(30200, 2), "30.20 GW"); + }); +}); + +describe("parseOpenDate", () => { + it("bare year -> Jan 1", () => { + assert.equal(parseOpenDate("2023"), Date.UTC(2023, 0, 1)); + }); + it("year + quarter -> quarter start", () => { + assert.equal(parseOpenDate("2026 Q1"), Date.UTC(2026, 0, 1)); + assert.equal(parseOpenDate("2026 Q2"), Date.UTC(2026, 3, 1)); + assert.equal(parseOpenDate("2026 Q4"), Date.UTC(2026, 9, 1)); + }); + it("tolerates spacing", () => { + assert.equal(parseOpenDate(" 2025 Q3 "), Date.UTC(2025, 6, 1)); + }); + it("garbage, empty, and null are excluded, not guessed", () => { + for (const bad of [null, undefined, "", "soon", "Q3 2026", "2026 Q5", "2026-03"]) { + assert.equal(parseOpenDate(bad as never), null, String(bad)); + } + }); +}); + +describe("buildBuildoutHistory", () => { + it("cumulative operational series ordered by open date", () => { + const h = buildBuildoutHistory([ + F("operational", 500, "2024", "B"), + F("operational", 1000, "2022", "A"), + F("construction", 700, "2026 Q3", "C"), + ]); + assert.deepEqual(h.online.map((p) => p.cumMW), [1000, 1500]); + assert.deepEqual(h.online.map((p) => p.addedMW), [1000, 500]); + assert.ok(h.online[0].t < h.online[1].t); + }); + it("pipeline continues cumulatively from the operational total", () => { + const h = buildBuildoutHistory([ + F("operational", 1000, "2022"), + F("construction", 700, "2026 Q3"), + F("construction", 300, "2027"), + ]); + assert.deepEqual(h.pipeline.map((p) => p.cumMW), [1700, 2000]); + }); + it("same-date facilities merge into one step and drop the single-name label", () => { + const h = buildBuildoutHistory([ + F("operational", 100, "2023", "X"), + F("operational", 200, "2023", "Y"), + ]); + assert.equal(h.online.length, 1); + assert.equal(h.online[0].addedMW, 300); + assert.equal(h.online[0].name, undefined); + }); + it("single facility at a date keeps its name", () => { + const h = buildBuildoutHistory([F("operational", 100, "2023", "Solo")]); + assert.equal(h.online[0].name, "Solo"); + }); + it("undated facilities are excluded and counted honestly", () => { + const h = buildBuildoutHistory([ + F("operational", 100, "2023"), + F("operational", 200, undefined), + F("construction", 300, "soon"), + ]); + assert.equal(h.online.length, 1); + assert.equal(h.undatedCount, 2); + }); + it("announced facilities never enter either series", () => { + const h = buildBuildoutHistory([F("announced", 5000, "2028")]); + assert.deepEqual(h.online, []); + assert.deepEqual(h.pipeline, []); + }); + it("empty input yields empty series", () => { + const h = buildBuildoutHistory([]); + assert.deepEqual(h.online, []); + assert.deepEqual(h.pipeline, []); + assert.equal(h.undatedCount, 0); + }); +}); + +describe("tightestRTO", () => { + it("picks the minimum reserve margin", () => { + const t = tightestRTO({ + ERCOT: { label: "ERCOT", reserveMargin: 15.8, aiSignal: "Critical" }, + MISO: { label: "MISO", reserveMargin: 13.4, aiSignal: "Critical" }, + SPP: { label: "SPP", reserveMargin: 27.8, aiSignal: "Low" }, + }); + assert.ok(t); + assert.equal(t.rto, "MISO"); + assert.equal(t.reserveMarginPct, 13.4); + }); + it("skips non-finite margins; null on empty", () => { + const t = tightestRTO({ X: { label: "X", reserveMargin: NaN, aiSignal: "Low" } }); + assert.equal(t, null); + assert.equal(tightestRTO({}), null); + }); +}); diff --git a/client/src/lib/real-gauges.ts b/client/src/lib/real-gauges.ts new file mode 100644 index 0000000..281e57e --- /dev/null +++ b/client/src/lib/real-gauges.ts @@ -0,0 +1,174 @@ +/** + * Real headline gauges (owner-directed replacement for the synthetic + * AI Power Demand / NPI / Grid Stress indices). Every number here is a + * direct computation over sourced data - no baselines, no rebasing, no + * sentiment formulas. Pure module, 100% covered in Lake-8 style. + * + * Sources: + * - Tracked AI DC power: the verified facility dataset (/api/datacenters) + * - Cost of AI compute: the GPU rental index (/api/gpu-prices/metrics) + * - Grid headroom: NERC LTRA reserve margins (data/rto-config) + */ +import type { RTOConfig } from "@/data/rto-config"; + +/** + * Same hyperscale floor the Power map advertises: only >=400 MW sites are + * "tracked". The two surfaces must agree or the headline contradicts its + * own drill-down. + */ +export const MIN_TRACKED_MW = 400; + +export function filterTrackedFacilities(facilities: T[]): T[] { + return (facilities ?? []).filter( + (f) => typeof f.powerMW === "number" && Number.isFinite(f.powerMW) && f.powerMW >= MIN_TRACKED_MW, + ); +} + +export interface FacilityLite { + powerMW: number | null | undefined; + status: string; + openDate?: string | null; + name?: string; +} + +// ─── Tracked AI DC power ──────────────────────────────────────────────────── + +export interface TrackedPower { + operationalMW: number; + constructionMW: number; + announcedMW: number; + /** headline: operational + construction (committed steel, not press releases) */ + trackedMW: number; + operationalCount: number; + constructionCount: number; + announcedCount: number; +} + +export function computeTrackedPower(facilities: FacilityLite[]): TrackedPower { + const t: TrackedPower = { + operationalMW: 0, + constructionMW: 0, + announcedMW: 0, + trackedMW: 0, + operationalCount: 0, + constructionCount: 0, + announcedCount: 0, + }; + for (const f of facilities ?? []) { + const mw = typeof f.powerMW === "number" && Number.isFinite(f.powerMW) && f.powerMW > 0 ? f.powerMW : 0; + if (f.status === "operational") { + t.operationalMW += mw; + t.operationalCount++; + } else if (f.status === "construction") { + t.constructionMW += mw; + t.constructionCount++; + } else if (f.status === "announced") { + t.announcedMW += mw; + t.announcedCount++; + } + } + t.trackedMW = t.operationalMW + t.constructionMW; + return t; +} + +export function fmtGW(mw: number, digits = 1): string { + return `${(mw / 1000).toFixed(digits)} GW`; +} + +// ─── Buildout history (real series from facility open dates) ──────────────── + +/** + * "2023" -> Jan 1 2023; "2026 Q3" -> first day of that quarter. Null on + * anything else - unparseable dates are excluded, never guessed. + */ +export function parseOpenDate(openDate: string | null | undefined): number | null { + if (!openDate) return null; + const m = /^(\d{4})(?:\s*Q([1-4]))?$/.exec(openDate.trim()); + if (!m) return null; + const year = Number(m[1]); + const quarter = m[2] ? Number(m[2]) : null; + const month = quarter ? (quarter - 1) * 3 : 0; + const t = Date.UTC(year, month, 1); + return Number.isFinite(t) ? t : null; +} + +export interface BuildoutPoint { + t: number; + /** cumulative MW online (or committed, for the pipeline series) at t */ + cumMW: number; + addedMW: number; + name?: string; +} + +export interface BuildoutHistory { + /** operational facilities by open date, cumulative - observed history */ + online: BuildoutPoint[]; + /** construction facilities by planned open date, cumulative ON TOP of the + * operational total - the committed pipeline, rendered dashed */ + pipeline: BuildoutPoint[]; + /** facilities excluded because their open date could not be parsed */ + undatedCount: number; +} + +/** + * Cumulative tracked capacity over time. Facilities at the same date + * aggregate into one step. Announced facilities are excluded entirely - + * press releases are not steel. + */ +export function buildBuildoutHistory(facilities: FacilityLite[]): BuildoutHistory { + let undatedCount = 0; + const collect = (status: string) => { + const byT = new Map(); + for (const f of facilities ?? []) { + if (f.status !== status) continue; + const mw = typeof f.powerMW === "number" && Number.isFinite(f.powerMW) && f.powerMW > 0 ? f.powerMW : 0; + const t = parseOpenDate(f.openDate); + if (t === null) { + undatedCount++; + continue; + } + const cur = byT.get(t) ?? { mw: 0, names: [] }; + cur.mw += mw; + if (f.name) cur.names.push(f.name); + byT.set(t, cur); + } + return Array.from(byT.entries()).sort((a, b) => a[0] - b[0]); + }; + + const online: BuildoutPoint[] = []; + let cum = 0; + for (const [t, { mw, names }] of collect("operational")) { + cum += mw; + online.push({ t, cumMW: cum, addedMW: mw, name: names.length === 1 ? names[0] : undefined }); + } + + const pipeline: BuildoutPoint[] = []; + let pcum = cum; // pipeline continues from the operational total + for (const [t, { mw, names }] of collect("construction")) { + pcum += mw; + pipeline.push({ t, cumMW: pcum, addedMW: mw, name: names.length === 1 ? names[0] : undefined }); + } + + return { online, pipeline, undatedCount }; +} + +// ─── Grid headroom ────────────────────────────────────────────────────────── + +export interface GridHeadroom { + rto: string; + label: string; + reserveMarginPct: number; + aiSignal: RTOConfig["aiSignal"]; +} + +/** The tightest reserve margin among tracked RTOs - the binding constraint. */ +export function tightestRTO(config: Record): GridHeadroom | null { + let best: GridHeadroom | null = null; + for (const [rto, c] of Object.entries(config ?? {})) { + if (!Number.isFinite(c.reserveMargin)) continue; + if (!best || c.reserveMargin < best.reserveMarginPct) { + best = { rto, label: c.label, reserveMarginPct: c.reserveMargin, aiSignal: c.aiSignal }; + } + } + return best; +} diff --git a/client/src/pages/PowerMap.tsx b/client/src/pages/PowerMap.tsx index 639fd21..b4c0f19 100644 --- a/client/src/pages/PowerMap.tsx +++ b/client/src/pages/PowerMap.tsx @@ -55,21 +55,7 @@ function filterTracked(list: DataCenter[]): DataCenter[] { return list.filter((d) => d.powerMW >= MIN_TRACKED_MW); } -interface RTOConfig { - label: string; - reserveMargin: number; - aiSignal: "Critical" | "Elevated" | "Moderate" | "Low"; -} - -const RTO_CONFIG: Record = { - PJM: { label: "PJM", reserveMargin: 17.5, aiSignal: "Elevated" }, - MISO: { label: "MISO", reserveMargin: 13.4, aiSignal: "Critical" }, - ERCOT: { label: "ERCOT", reserveMargin: 15.8, aiSignal: "Critical" }, - WECC: { label: "WECC", reserveMargin: 24.6, aiSignal: "Moderate" }, - SERC: { label: "SERC", reserveMargin: 23.1, aiSignal: "Moderate" }, - SPP: { label: "SPP", reserveMargin: 27.8, aiSignal: "Low" }, - NPCC: { label: "NPCC", reserveMargin: 26.4, aiSignal: "Low" }, -}; +import { RTO_CONFIG, type RTOConfig } from "@/data/rto-config"; /** * ONE stress ramp for the whole page: region fills (stress view), the map @@ -707,7 +693,11 @@ export default function PowerMap() { const announced = dataCenters.filter((d) => d.status === "announced"); - const totalMW = dataCenters.reduce((s, d) => s + d.powerMW, 0); + // Headline matches the overview's Tracked AI Power gauge: operational + + // construction only. Announced projects are press releases, not steel - + // they render on the map but stay out of the GW headline. + const totalMW = dataCenters.reduce((s, d) => s + (d.status === "announced" ? 0 : d.powerMW), 0); + const announcedMW = dataCenters.reduce((s, d) => s + (d.status === "announced" ? d.powerMW : 0), 0); const totalTWh = (dataCenters.reduce((s, d) => s + d.annualMWh, 0) / 1_000_000).toFixed(1); const opCount = dataCenters.filter((d) => d.status === "operational").length; const conCount = dataCenters.filter((d) => d.status === "construction").length; @@ -910,7 +900,7 @@ export default function PowerMap() {
{dataCenters.length} facilities | - {(totalMW / 1000).toFixed(1)} GW + {(totalMW / 1000).toFixed(1)} GW | {totalTWh} TWh/yr | @@ -958,7 +948,7 @@ export default function PowerMap() {
- {(totalMW / 1000).toFixed(1)} GW + {(totalMW / 1000).toFixed(1)} GW | {totalTWh} TWh/yr | diff --git a/client/src/pages/TiltOverview.tsx b/client/src/pages/TiltOverview.tsx index 2f32865..50c66e8 100644 --- a/client/src/pages/TiltOverview.tsx +++ b/client/src/pages/TiltOverview.tsx @@ -27,6 +27,11 @@ import { BRAND, CATEGORY_COLORS as TOKEN_CATEGORY_COLORS, CHART_CHROME, DATA_QUALITY, INK, SEMANTIC, SERIES, } from "@/lib/tokens"; import { axisProps, gridProps, timeTicks, tooltipContentStyle, tooltipItemStyle, tooltipLabelStyle } from "@/lib/chart-theme"; +import { RTO_CONFIG, RTO_SOURCE_NOTE } from "@/data/rto-config"; +import { + buildBuildoutHistory, computeTrackedPower, filterTrackedFacilities, fmtGW, tightestRTO, + type BuildoutHistory, type FacilityLite, type TrackedPower, +} from "@/lib/real-gauges"; import { fmtDate } from "@/lib/gpu-series"; import { heatColor, heatTextColor } from "@/lib/stack-transforms"; @@ -75,23 +80,15 @@ const annotations = [ { year: "2024", label: "TMI restart + SMR deal", color: alpha(BRAND.secondary, 0.5) }, ]; -interface KpiData { - aiPowerIndex: number; - npiValue: number; - gridStress: number; - smrPolicyScore: number; - npiBaseDate: string; - constituents: { - // AI Power Index constituents (intraday % change signals) - nvdaChange: number; tsmChange: number; eqixChange: number; muChange: number; - // NPI constituents (price performance since Jan 1, 2024 base) - cegPerf: number; vstPerf: number; ccjPerf: number; nlrPerf: number; - uPerf: number; policyPerf: number; npiPolicyMultiplier: number; npiMomentum: number; - // Grid Stress signals - vstChange: number; cegChange: number; - }; +/** The slice of /api/gpu-prices/metrics the gauges read. */ +interface GpuFleetLite { + fleetAvg: number; + fleetAvg1yChange: number | null; + modelCount: number; + rows: Array<{ model: string; current: number }>; } + interface TopMover { ticker: string; name: string; @@ -160,187 +157,6 @@ const CustomTooltip = ({ active, payload, label }: any) => { return null; }; -function ConstituentRow({ label, value }: { label: string; value: number }) { - const isUp = value >= 0; - return ( -
- {label} - - {isUp ? "+" : ""}{value.toFixed(2)}% - -
- ); -} - -function PerfRow({ label, perf, base }: { label: string; perf: number; base?: string }) { - const pct = ((perf - 1) * 100).toFixed(1); - const isUp = perf >= 1; - return ( -
- {label} -
- {base && {base}} - - {isUp ? "+" : ""}{pct}% - -
-
- ); -} - -function KpiCard({ - icon: Icon, - title, - value, - unit, - subtitle, - color, - methodology, - constituents, - isLoading, -}: { - icon: any; - title: string; - value: number | null; - unit: string; - subtitle?: string; - color: "neutral" | "amber" | "red"; - methodology: string; - constituents?: React.ReactNode; - isLoading: boolean; -}) { - const colorMap = { - neutral: { - icon: "text-muted-foreground", - bg: "bg-muted/25", - border: "border-card-border", - value: "text-foreground", - }, - amber: { - icon: "text-brand-2", - bg: "bg-brand-2/10", - border: "border-brand-2/25", - value: "text-brand-2", - }, - red: { - icon: "text-negative", - bg: "bg-negative-deep/10", - border: "border-negative-deep/25", - value: "text-negative", - }, - }; - const c = colorMap[color]; - - return ( - -
-
- -
- - - - - -

{methodology}

- {constituents &&
{constituents}
} -
-
-
- -
-

{title}

- {isLoading ? ( - <> - - - - - ) : ( - <> -
- - {value !== null ? value.toFixed(1) : "--"} - - {unit} -
-

- {subtitle ?? "Live market signals. Tap info for methodology."} -

- - )} -
-
- ); -} - -function TiltStatusBar({ aiPower, gridStress, npi }: { aiPower: number | null; gridStress: number | null; npi: number | null }) { - if (aiPower === null || gridStress === null || npi === null) return null; - - const isElevated = aiPower > 78 && gridStress > 70 && npi > 130; - const isEasing = aiPower < 68 && gridStress < 55; - const status = isElevated ? "elevated" : isEasing ? "easing" : "tracking baseline"; - const statusColor = isElevated ? BRAND.primary : isEasing ? INK.muted : BRAND.secondary; - const statusBg = isElevated ? "bg-brand/10 border-brand/25" : isEasing ? "bg-muted/20 border-card-border" : "bg-brand-2/10 border-brand-2/20"; - const description = isElevated - ? `All three gauges elevated. The market is pricing the AI-power complex aggressively today; grid-equity momentum at ${gridStress.toFixed(0)}/100. These read market positioning, not physical grid conditions.` - : isEasing - ? `Gauges have pulled back from peaks. Could be sector rotation or cooling sentiment. Watch hyperscaler capex guidance.` - : `Tracking baseline. Market gauges near their fixed baselines. NPI at ${npi.toFixed(0)} reflects nuclear-complex performance since the Jan 2024 base.`; - - const numbers = [ - { label: "AI Demand", val: aiPower }, - { label: "NPI", val: npi }, - { label: "Grid Stress", val: gridStress }, - ] as const; - - return ( - -
- {/* Status + numbers row on mobile, status only on desktop */} -
-
-

Tilt Status

-
-
- {status} -
-
- {/* Mini numbers shown beside status on mobile */} -
- {numbers.map(({ label, val }) => ( -
-

{label}

-

{val.toFixed(0)}

-
- ))} -
-
- - {/* Vertical divider - desktop only */} -
- - {/* Description */} -

{description}

- - {/* Mini numbers - desktop only (shown inline on mobile) */} -
- {numbers.map(({ label, val }) => ( -
-

{label}

-

{val.toFixed(0)}

-
- ))} -
-
- - ); -} - -// Mover/sector tags draw from the stable category palette so each sector -// keeps one color across the whole app. const SECTOR_COLORS: Record = { compute: TOKEN_CATEGORY_COLORS.compute, nuclear: TOKEN_CATEGORY_COLORS.nuclear, @@ -453,135 +269,156 @@ function TopMoversSection({ topMovers, pulse, isLoading, isError, updatedAt, onR ); } -interface IndexHistoryDay { - date: string; - aiDemand: number | null; - gridStress: number | null; - npiEquityLegs?: number | null; - npi?: number | null; -} - -const MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; -function gaugeTickLabel(date: string): string { - const [y, m] = date.split("-"); - return `${MONTH_ABBR[Number(m) - 1]} '${y.slice(2)}`; -} - -// Replaces the old Sector Pulse card (its sector averages now live as chips -// inside Top Movers). Plots the daily gauge history that the validation -// study reconstructed and the server now records: one consistent series, -// our own data, time depth instead of a second "what moved today" list. -// Gauge baselines mirror server/indices.ts (AI_INDEX.BASELINE, GRID_STRESS.BASELINE). -const GAUGE_BASELINES = { ai: 72, gs: 68 } as const; - -type GaugeRange = "3M" | "6M" | "1Y" | "ALL"; -const GAUGE_RANGES: GaugeRange[] = ["3M", "6M", "1Y", "ALL"]; - -function gaugeRangeStart(range: GaugeRange, now: number): number | null { - const d = new Date(now); - if (range === "3M") return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - 3, d.getUTCDate()); - if (range === "6M") return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - 6, d.getUTCDate()); - if (range === "1Y") return Date.UTC(d.getUTCFullYear() - 1, d.getUTCMonth(), d.getUTCDate()); - return null; +/** One real gauge: a measured number, its provenance, and where it links. */ +function RealGaugeCard({ + icon: Icon, + title, + value, + delta, + deltaColor, + subtitle, + methodology, + rows, + isLoading, + href, + updatedAt, +}: { + icon: typeof Zap; + title: string; + value: string | null; + delta: string | null; + deltaColor?: string; + subtitle: string; + methodology: string; + rows: Array<{ label: string; value: string }>; + isLoading: boolean; + href: string; + updatedAt?: number; +}) { + return ( + +
+
+ +
+ + + + + +

{methodology}

+
+
+
+

{title}

+ {isLoading || value === null ? ( + + ) : ( +
+ {value} + {delta && ( + {delta} + )} +
+ )} +

{subtitle}

+ {rows.length > 0 && ( +
+ {rows.map((r) => ( +
+ {r.label} + {r.value} +
+ ))} +
+ )} +
+ + Full data + + {updatedAt !== undefined && } +
+
+ ); } -function GaugeHistoryCard({ live }: { live: { npi: number; ai: number; gs: number } | null }) { - const { data, isLoading, isError, refetch, dataUpdatedAt } = useQuery<{ days: IndexHistoryDay[] }>({ - queryKey: ["/api/index-history"], - refetchInterval: 30 * 60_000, - }); - const [range, setRange] = useState("ALL"); - - // True time axis: date strings -> UTC ms so gaps in the recorder render as - // gaps in time, not as one category step. - const series = useMemo( - () => - (data?.days ?? []) - .filter((d) => d.npiEquityLegs != null || d.aiDemand != null) - .map((d) => ({ t: Date.parse(`${d.date}T00:00:00Z`), npi: d.npiEquityLegs ?? null, ai: d.aiDemand, gs: d.gridStress })) - .filter((d) => Number.isFinite(d.t)), - [data], - ); - const now = series.length ? series[series.length - 1].t : Date.now(); - const start = gaugeRangeStart(range, now); - const windowed = useMemo(() => (start === null ? series : series.filter((d) => d.t >= start)), [series, start]); +/** + * Real buildout history: cumulative tracked capacity from facility open + * dates. Solid = operational (observed history), dashed = construction + * pipeline by planned open date (committed, not yet online). + */ +function BuildoutHistoryCard({ + buildout, + tracked, + isLoading, +}: { + buildout: BuildoutHistory | null; + tracked: TrackedPower | null; + isLoading: boolean; +}) { + const series = useMemo(() => { + if (!buildout) return []; + const pts: Array<{ t: number; online: number | null; pipeline: number | null; addedMW: number; name?: string }> = []; + for (const p of buildout.online) pts.push({ t: p.t, online: p.cumMW, pipeline: null, addedMW: p.addedMW, name: p.name }); + // bridge point so the dashed pipeline continues from the last online step + const last = buildout.online[buildout.online.length - 1]; + if (last && buildout.pipeline.length) pts.push({ t: last.t, online: null, pipeline: last.cumMW, addedMW: 0 }); + for (const p of buildout.pipeline) pts.push({ t: p.t, online: null, pipeline: p.cumMW, addedMW: p.addedMW, name: p.name }); + return pts.sort((a, b) => a.t - b.t); + }, [buildout]); const ticks = useMemo(() => { - if (windowed.length < 2) return []; - return timeTicks(windowed[0].t, windowed[windowed.length - 1].t, 560).map((d) => +d); - }, [windowed]); + if (series.length < 2) return []; + return timeTicks(series[0].t, series[series.length - 1].t, 560).map((d) => +d); + }, [series]); return ( - // flex-1: fills the left column so it ends flush with the right column. - +
-

NPI Gauge History

+

Tracked Buildout Over Time

- Daily series reconstructed from public prices with the shipped formulas, then recorded live going - forward (one methodology end to end; reproduce with npm run backtest:indices). The NPI line is the - equity legs with uranium and policy at par; the headline number is the full live NPI including both. - Sparklines show the two sentiment gauges against their fixed formula baselines (dashed). + Cumulative rated power of the verified facility dataset by each facility's open date. Solid = operational + today (observed history). Dashed = under-construction capacity at its planned open date (committed + pipeline, not yet online). Announced projects are excluded entirely.

-
- {GAUGE_RANGES.map((r) => ( - - ))} - {live && ( - - NPI {live.npi.toFixed(1)} - - )} -
-
-
-

- line: NPI equity legs · Jan 1 2024 = 100 · headline: full live NPI -

- + {tracked && ( + + {fmtGW(tracked.trackedMW)} tracked + + )}
+

+ solid: operational · dashed: construction pipeline · announced excluded + {buildout && buildout.undatedCount > 0 ? ` · ${buildout.undatedCount} undated sites excluded` : ""} +

- {isError ? ( -
- refetch()} /> -
- ) : isLoading ? ( - <> + {isLoading || series.length === 0 ? ( + isLoading ? (
-
- - + ) : ( +
+ Facility data unavailable.
- - ) : windowed.length === 0 ? ( -
- No recorded days in this window. -
+ ) ) : ( <> -
+
- + - + @@ -598,86 +435,55 @@ function GaugeHistoryCard({ live }: { live: { npi: number; ai: number; gs: numbe `${(mw / 1000).toFixed(0)} GW`} /> fmtDate(t, true)} - formatter={(value, name) => - value == null ? ["n/a", name] : [Number(value).toFixed(1), name] - } + labelFormatter={(t: number) => fmtDate(t, false)} + formatter={(value: number, name: string, entry: any) => { + const added = entry?.payload?.addedMW; + const nm = entry?.payload?.name; + const detail = added ? ` (+${fmtGW(added)}${nm ? ` · ${nm}` : ""})` : ""; + return [`${fmtGW(value)}${detail}`, name === "online" ? "Operational" : "Pipeline"]; + }} /> - +
d.npi != null) - .map((d) => [fmtDate(d.t, true), (d.npi as number).toFixed(1)])} + caption="Tracked AI data center buildout over time" + columns={["Date", "Cumulative GW", "Series"]} + rows={series.map((p) => [ + fmtDate(p.t, false), + ((p.online ?? p.pipeline ?? 0) / 1000).toFixed(2), + p.online !== null ? "operational" : "pipeline", + ])} /> - -
- {( - [ - { key: "ai", label: "AI Demand", color: BRAND.secondary, value: live?.ai ?? null }, - { key: "gs", label: "Grid Stress", color: SEMANTIC.negativeDeep, value: live?.gs ?? null }, - ] as const - ).map((g) => { - // Honest domain: the window's own values UNION the formula - // baseline, padded - so distance from baseline is real, and a - // one-point wiggle can't fill the full height. - const vals = windowed.map((d) => d[g.key]).filter((v): v is number => v != null); - const base = GAUGE_BASELINES[g.key]; - const lo = Math.min(...vals, base); - const hi = Math.max(...vals, base); - const pad = Math.max((hi - lo) * 0.15, 1); - return ( -
-
- {g.label} - - {g.value != null ? g.value.toFixed(1) : "–"} - -
-
- - - - - - - - -
-

baseline {base}

-
- ); - })} -
)} @@ -1047,10 +853,34 @@ function ModuleGrid() { } export default function TiltOverview() { - const { data: kpiData, isLoading, isError: kpiError, dataUpdatedAt: kpiUpdatedAt, refetch: refetchKpis } = useQuery({ - queryKey: ["/api/kpis"], + // Real gauge sources: the facility dataset (shared with the Power map) and + // the GPU price index (shared with GPU Prices) - react-query dedupes both. + const { data: facilities, isLoading: dcLoading, dataUpdatedAt: dcUpdatedAt } = useQuery({ + queryKey: ["/api/datacenters"], + refetchInterval: 900000, + }); + const { data: gpuData, isLoading: gpuLoading, dataUpdatedAt: gpuUpdatedAt } = useQuery({ + queryKey: ["/api/gpu-prices/metrics"], refetchInterval: 900000, }); + // Same >=400 MW floor as the Power map, so headline and drill-down agree. + const trackedFacilities = useMemo(() => (facilities ? filterTrackedFacilities(facilities) : null), [facilities]); + const tracked = useMemo(() => (trackedFacilities ? computeTrackedPower(trackedFacilities) : null), [trackedFacilities]); + const buildout = useMemo(() => (trackedFacilities ? buildBuildoutHistory(trackedFacilities) : null), [trackedFacilities]); + const headroom = useMemo(() => tightestRTO(RTO_CONFIG), []); + const headroomRows = useMemo(() => { + return Object.values(RTO_CONFIG) + .sort((a, b) => a.reserveMargin - b.reserveMargin) + .slice(0, 3) + .map((r) => ({ label: r.label, value: `${r.reserveMargin.toFixed(1)}% · ${r.aiSignal}` })); + }, []); + const gpuTopRows = useMemo(() => { + const rows = [...(gpuData?.rows ?? [])].sort((a, b) => b.current - a.current); + return [ + ...(rows.length ? [{ label: rows[0].model, value: `$${rows[0].current.toFixed(2)}/hr · priciest` }] : []), + ...(rows.length > 1 ? [{ label: rows[rows.length - 1].model, value: `$${rows[rows.length - 1].current.toFixed(2)}/hr · cheapest` }] : []), + ]; + }, [gpuData]); const { data: topMovers, isLoading: topMoversLoading, isError: topMoversError, dataUpdatedAt: topMoversUpdatedAt, refetch: refetchTopMovers } = useQuery({ queryKey: ["/api/top-movers"], @@ -1062,26 +892,6 @@ export default function TiltOverview() { refetchInterval: 900000, }); - // Shares the cache with GaugeHistoryCard (same key) - powers the header's - // NPI change, labeled by its TRUE span: the recorder has gaps, and calling - // a 22-day move "1D" would lie. - const { data: historyData } = useQuery<{ days: IndexHistoryDay[] }>({ - queryKey: ["/api/index-history"], - refetchInterval: 30 * 60_000, - }); - const npiDelta = useMemo(() => { - const days = (historyData?.days ?? []).filter((d) => d.npi != null); - if (days.length < 2 || !kpiData) return null; - const prev = days[days.length - 2]; - const latest = days[days.length - 1]; - const prevT = Date.parse(`${prev.date}T00:00:00Z`); - const latestT = Date.parse(`${latest.date}T00:00:00Z`); - const gapDays = Math.round((latestT - prevT) / 86_400_000); - const pct = ((latest.npi! - prev.npi!) / prev.npi!) * 100; - return { pct, label: gapDays <= 4 ? "1D" : `vs ${fmtDate(prevT, true)}` }; - }, [historyData, kpiData]); - - const c = kpiData?.constituents; return (
@@ -1090,20 +900,18 @@ export default function TiltOverview() {

Tilt Overview

- {kpiData && ( - - NPI - {kpiData.npiValue.toFixed(1)} - {npiDelta && ( - = 0 ? "text-positive" : "text-negative"}`}> - {npiDelta.pct >= 0 ? "+" : ""}{npiDelta.pct.toFixed(1)}% {npiDelta.label} - - )} + {tracked && ( + + Tracked AI Power + {fmtGW(tracked.trackedMW)} + + +{fmtGW(tracked.constructionMW)} building + )}
- Updated {relativeTime(kpiUpdatedAt)} + Updated {relativeTime(dcUpdatedAt)}
@@ -1121,9 +929,7 @@ export default function TiltOverview() { updatedAt={topMoversUpdatedAt} onRetry={() => refetchTopMovers()} /> - +
@@ -1300,90 +1106,56 @@ export default function TiltOverview() {
- {/* Market gauges — research depth, intentionally demoted from headline. - Labels follow the published backtest (docs/INDEX_VALIDATION.md): - the momentum gauges showed no correlation with physical output, - so they are presented as market sentiment, not measurements. */} + {/* Real gauges (owner-directed): direct measurements over sourced + data. The synthetic sentiment indices this replaces are archived + in docs/INDEX_VALIDATION.md. */}
- market gauges · methodology and validation in each card + the buildout, measured · source on every card
- - - - - - - ) : undefined} - isLoading={isLoading} - /> - - - - - - - - ) : undefined} - isLoading={isLoading} + title="Tracked AI DC Power" + value={tracked ? fmtGW(tracked.trackedMW) : null} + delta={tracked ? `${tracked.operationalCount + tracked.constructionCount} facilities` : null} + subtitle="Operational + under construction, verified facilities" + methodology={`Sum of rated power across the verified US AI data center dataset (facilities >=400 MW). Operational plus under-construction only - announced projects (${tracked ? fmtGW(tracked.announcedMW) : "-"}) are press releases, not steel, and are excluded from the headline. Same dataset as the Power map.`} + isLoading={dcLoading} + rows={tracked ? [ + { label: "Operational", value: `${fmtGW(tracked.operationalMW)} · ${tracked.operationalCount} sites` }, + { label: "Construction", value: `${fmtGW(tracked.constructionMW)} · ${tracked.constructionCount} sites` }, + { label: "Announced (excl.)", value: `${fmtGW(tracked.announcedMW)} · ${tracked.announcedCount} sites` }, + ] : []} + href="/power-map" + updatedAt={dcUpdatedAt} /> - 0 ? "+" : ""}${gpuData.fleetAvg1yChange.toFixed(1)}% 1Y` : null} + deltaColor={gpuData?.fleetAvg1yChange != null ? (gpuData.fleetAvg1yChange > 0 ? SEMANTIC.negative : SEMANTIC.positive) : undefined} + subtitle={`Fleet-average GPU rental, ${gpuData?.modelCount ?? "-"} models`} + methodology="Mean on-demand rental price across the tracked GPU fleet, blended from public neocloud and marketplace listings (sourced estimates, flagged per model on the GPU Prices page). Falling prices mean compute supply is catching demand." + isLoading={gpuLoading} + rows={gpuData ? gpuTopRows : []} + href="/neocloud-intel" + updatedAt={gpuUpdatedAt} + /> + - - - - - ) : undefined} - isLoading={isLoading} + title="Grid Headroom" + value={headroom ? `${headroom.reserveMarginPct.toFixed(1)}%` : null} + delta={headroom ? `${headroom.label} · tightest RTO` : null} + deltaColor={headroom ? SEMANTIC.negative : undefined} + subtitle="Lowest reserve margin among AI-load RTOs" + methodology={`Projected reserve margins from ${RTO_SOURCE_NOTE}. The headline is the tightest region - the binding constraint on new AI load. NERC's reference margin level is ~15%; below it, new interconnection gets hard.`} + isLoading={false} + rows={headroomRows} + href="/power-map" />
- - {!isLoading && kpiData && ( -
- -
- )} - - {kpiError && ( - - refetchKpis()} - className="py-4" - /> - - )}
{/* 4-column stat strip */}