Files
signal-platform/frontend/src/pages/RegimePage.tsx
T
dennisthiessenandClaude Opus 5 83c0555e52
Deploy / lint (push) Failing after 8s
Deploy / test (push) Skipped
Deploy / deploy (push) Skipped
Event study: report its own statistical limits
The v3 cutover run scored 2/4 corrections warned against v2's 3/4, which reads
like a regression and is not one. Only 4 of the 11 detected corrections fall in
the holdout, so recall is one event from a different headline -- and the event
that flips is decided by threshold placement, not by what the score saw. "v3
without the credit sensor" catches 2025-02-21 at a *higher* threshold (35.5)
than shipped v3 misses it at (32.3), because the alarm rule needs a rising edge
and a lower threshold can fire outside the horizon then never reset below.

Two caveats are now computed and surfaced rather than left for the reader to
infer:

- Holdout event count against MIN_EVENTS_FOR_CONFIDENCE. The summary sentence
  states how many of the detected corrections actually fall in the test period.
- Warning-sensor coverage across the split. The score renormalises over what is
  available, so a training window predating a sensor's history freezes the
  threshold on a different construct than the holdout is measured against. At
  the cutover that is 39% of training sessions with all three sensors versus
  100% of the test period, credit history beginning 2023-07-25.

Restricting the threshold to sensor-matched training sessions was tested and
rejected: those sessions are a calm recent stretch, so the threshold falls from
32.3 to 22.5 and false alarms rise from 3.3 to 8.6/yr. It swaps a coverage bias
for a regime-selection bias. The report states its limits instead.

_warning_series now returns per-session sensor counts alongside the scores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:12:53 +02:00

511 lines
27 KiB
TypeScript

import { lazy, Suspense, useState, type ReactNode } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { PageHeader } from '../components/ui/PageHeader';
import { Callout } from '../components/ui/Callout';
import { Disclosure } from '../components/ui/Disclosure';
import { Badge } from '../components/ui/Badge';
import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton';
import { useAuthStore } from '../stores/authStore';
import {
getEventStudy,
getRegimeConfig,
getRegimeFundamentals,
getRegimeMonitor,
refreshRegimeFundamentals,
updateRegimeConfig,
updateRegimeFundamentals,
} from '../api/regime';
import type {
CapexState,
EventStudyReport,
GoodNewsReaction,
RegimeBand,
RegimeConfig,
RegimeFundamentalOverlay,
RegimeFundamentals,
RegimeFundamentalsUpdate,
RegimeReading,
} from '../lib/types';
const ScoreHistoryChart = lazy(() => import('../components/regime/ScoreHistoryChart'));
const RegimeQuadrant = lazy(() => import('../components/regime/RegimeQuadrant'));
const BAND_STYLES: Record<RegimeBand, { text: string; bar: string; ring: string; label: string }> = {
stable: { text: 'text-emerald-400', bar: 'bg-emerald-400', ring: 'border-emerald-400/30', label: 'Stable' },
watch: { text: 'text-amber-400', bar: 'bg-amber-400', ring: 'border-amber-400/30', label: 'Watch' },
elevated: { text: 'text-orange-400', bar: 'bg-orange-400', ring: 'border-orange-400/30', label: 'Elevated' },
breaking: { text: 'text-red-400', bar: 'bg-red-400', ring: 'border-red-400/30', label: 'High stress' },
};
function TrendChip({ label, delta }: { label: string; delta: number | null | undefined }) {
if (delta == null) {
return <span className="rounded-lg bg-white/[0.04] px-2.5 py-1 text-xs text-gray-500">{label}: n/a</span>;
}
const color = delta === 0 ? 'text-gray-400' : delta > 0 ? 'text-red-400' : 'text-emerald-400';
const arrow = delta === 0 ? '→' : delta > 0 ? '↑' : '↓';
return (
<span className="rounded-lg bg-white/[0.04] px-2.5 py-1 text-xs text-gray-400">
{label}: <span className={`font-medium ${color}`}>{arrow} {delta > 0 ? '+' : ''}{delta}</span>
</span>
);
}
function ScoreGauge({
label,
reading,
divider,
footnote,
}: {
label: string;
reading: RegimeReading | undefined;
divider?: number;
footnote: ReactNode;
}) {
const score = reading?.score;
const complete = reading?.band != null;
const style = complete ? BAND_STYLES[reading.band as RegimeBand] : null;
const position = Math.min(100, Math.max(0, score ?? 0));
const bands = reading?.bands;
const ticks = bands ? [bands.watch, bands.elevated, bands.breaking] : [30, 60, 80];
return (
<div className={`glass border p-6 ${style?.ring ?? 'border-white/[0.06]'}`}>
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<div className="text-[11px] uppercase tracking-wider text-gray-500">{label}</div>
<div className="mt-1 flex items-baseline gap-2">
<span className={`font-display text-6xl font-bold ${style?.text ?? 'text-gray-500'}`}>
{score == null ? '—' : Math.round(score)}
</span>
{score != null && <span className="text-sm text-gray-500">/ 100</span>}
</div>
<div className="mt-1 flex flex-wrap items-center gap-2">
<span className={`text-sm font-medium ${style?.text ?? 'text-gray-500'}`}>
{style?.label ?? 'Incomplete'}
</span>
<span className="text-xs text-gray-600">coverage {Math.round(reading?.coverage ?? 0)}%</span>
</div>
</div>
<div className="flex gap-2">
<TrendChip label="7d" delta={reading?.trend?.delta_7} />
<TrendChip label="30d" delta={reading?.trend?.delta_30} />
</div>
</div>
{score != null && (
<>
<div className="relative mt-5 h-2 rounded-full bg-gradient-to-r from-emerald-500/30 via-amber-500/30 to-red-500/40">
{divider != null && (
<div className="absolute -top-1 h-4 w-0.5 bg-gray-300/70" style={{ left: `${divider}%` }} />
)}
<div
className={`absolute -top-1.5 h-5 w-5 -translate-x-1/2 rounded-full border-2 border-white/70 ${style?.bar ?? 'bg-gray-500'}`}
style={{ left: `${position}%` }}
/>
</div>
{/* Thresholds come from the reading: the two axes no longer share them. */}
<div className="relative mt-1.5 h-4 text-[10px] uppercase tracking-wider text-gray-600">
<span className="absolute left-0">0</span>
{ticks.map((tick) => (
<span key={tick} className="absolute -translate-x-1/2 num" style={{ left: `${tick}%` }}>
{tick}
</span>
))}
<span className="absolute right-0">100</span>
</div>
</>
)}
<p className="mt-4 text-xs leading-relaxed text-gray-500">{footnote}</p>
</div>
);
}
const CAPEX_TONE: Record<CapexState, string> = {
raising: 'text-emerald-400',
holding: 'text-amber-400',
cutting: 'text-red-400',
unknown: 'text-gray-500',
};
function FundamentalOverlayCard({ overlay }: { overlay: RegimeFundamentalOverlay }) {
const capex = overlay.capex ?? {};
const reaction = overlay.good_news_stock_down;
return (
<div className="glass border border-white/[0.06] p-5">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<div className="text-[11px] uppercase tracking-wider text-gray-500">
Fundamental overlay · context, not scored
</div>
<div className="flex flex-wrap items-center gap-2 text-[11px] text-gray-500">
{overlay.source && <span>{overlay.source}</span>}
{overlay.effective_date && <span>· effective {overlay.effective_date}</span>}
{overlay.pending && <Badge label="pending" variant="manual" />}
{overlay.stale && <Badge label="stale" variant="manual" />}
</div>
</div>
{overlay.pending ? (
<p className="mt-3 text-xs leading-relaxed text-amber-400/90">
A newer observation was collected but is not effective until {overlay.effective_date ?? 'the next session'}.
Observations are never backdated, so the reading below appears from that session onward.
</p>
) : (
<>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<div>
<div className="mb-2 flex items-baseline justify-between text-xs">
<span className="font-medium text-gray-300">Hyperscaler capex guidance</span>
<span className="num text-gray-500">{overlay.capex_stress ?? 'n/a'}</span>
</div>
<div className="space-y-1">
{Object.entries(capex).map(([symbol, state]) => (
<div key={symbol} className="flex items-center justify-between text-xs">
<span className="font-mono text-gray-400">{symbol}</span>
<span className={CAPEX_TONE[state] ?? 'text-gray-500'}>{state}</span>
</div>
))}
</div>
</div>
<div>
<div className="mb-2 flex items-baseline justify-between text-xs">
<span className="font-medium text-gray-300">Good news, stock down</span>
<span className="num text-gray-500">{overlay.earnings_stress ?? 'n/a'}</span>
</div>
<div className={`text-sm font-medium ${reaction === 'yes' ? 'text-red-400' : reaction === 'no' ? 'text-emerald-400' : 'text-gray-500'}`}>
{reaction === 'yes' ? 'Yes — beats sold into' : reaction === 'no' ? 'No — ordinary reactions' : 'Mixed'}
</div>
</div>
</div>
{overlay.reasoning && (
<p className="mt-4 text-xs leading-relaxed text-gray-400">{overlay.reasoning}</p>
)}
</>
)}
<p className="mt-4 text-[11px] leading-relaxed text-gray-600">
These observations are qualitative, refreshed roughly quarterly, and deliberately excluded from State and
Warning. In v2 they carried 20 of 100 Warning points not enough to cross the study's alarm threshold even
when both were pegged — so they are reported here rather than diluted into a daily score.
</p>
</div>
);
}
function PillarBreakdown({ title, reading }: { title: string; reading: RegimeReading }) {
return (
<Disclosure summary={`${title} pillars · ${Math.round(reading.coverage)}% coverage`}>
<div className="overflow-x-auto rounded-lg border border-white/[0.06]">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/[0.06] text-left text-xs uppercase tracking-wider text-gray-500">
<th className="px-4 py-3 font-medium">Pillar / sensor</th>
<th className="px-4 py-3 text-right font-medium">Score</th>
<th className="px-4 py-3 text-right font-medium">Weight</th>
<th className="px-4 py-3 text-right font-medium">Contribution</th>
</tr>
</thead>
<tbody>
{reading.pillars.map((pillar) => (
<tr key={pillar.id} className="border-b border-white/[0.04] align-top last:border-0">
<td className="px-4 py-3">
<div className="font-medium text-gray-200">{pillar.label}</div>
<div className="mt-1 space-y-0.5">
{pillar.sensors.map((sensor) => (
<div key={sensor.id} className="text-xs text-gray-500">
<span className="font-mono text-gray-600">{sensor.id}</span> {sensor.label}:{' '}
<span className="num text-gray-400">{sensor.score == null ? 'n/a' : sensor.score}</span>
</div>
))}
</div>
</td>
<td className="px-4 py-3 text-right num text-gray-300">{pillar.score ?? ''}</td>
<td className="px-4 py-3 text-right num text-gray-400">{pillar.weight}</td>
<td className="px-4 py-3 text-right num text-gray-300">{pillar.available ? pillar.contribution.toFixed(1) : ''}</td>
</tr>
))}
</tbody>
</table>
</div>
</Disclosure>
);
}
function EventStudyBody({ report }: { report: EventStudyReport }) {
const metrics = report.metrics;
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
<Badge label={report.evaluation ?? 'exploratory'} variant={report.evaluation === 'holdout' ? 'auto' : 'manual'} />
{report.generated_at && <span className="text-xs text-gray-500">generated {new Date(report.generated_at).toLocaleDateString()}</span>}
{report.sample && <span className="text-xs text-gray-500">test {report.sample.test_start} → {report.sample.end}</span>}
</div>
<p className="text-sm leading-relaxed text-gray-300">{report.summary}</p>
{metrics && (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{[
['Warned', `${metrics.events_warned}/${metrics.events}`],
['Missed', metrics.events_missed],
['False alarms/year', metrics.false_alarms_per_year.toFixed(1)],
['Median lead', metrics.median_lead_days == null ? '' : `${metrics.median_lead_days}d`],
].map(([label, value]) => (
<div key={String(label)} className="rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2">
<div className="text-[11px] text-gray-500">{label}</div>
<div className="mt-0.5 text-lg font-semibold text-gray-200">{value}</div>
</div>
))}
</div>
)}
{report.events && report.events.length > 0 && (
<div className="overflow-x-auto rounded-lg border border-white/[0.06]">
<table className="w-full text-xs">
<thead><tr className="border-b border-white/[0.06] text-left text-gray-500">
<th className="px-3 py-2 font-medium">Correction</th>
<th className="px-3 py-2 text-right font-medium">Warned</th>
<th className="px-3 py-2 text-right font-medium">Lead</th>
</tr></thead>
<tbody>{report.events.map((event) => (
<tr key={event.date} className="border-b border-white/[0.03] last:border-0">
<td className="px-3 py-2 num text-gray-300">{event.date}</td>
<td className={`px-3 py-2 text-right ${event.warned ? 'text-emerald-400' : 'text-gray-500'}`}>{event.warned ? 'yes' : 'no'}</td>
<td className="px-3 py-2 text-right num text-gray-300">{event.lead_days == null ? '' : `${event.lead_days}d`}</td>
</tr>
))}</tbody>
</table>
</div>
)}
{report.reliability && (report.reliability.underpowered || report.reliability.sensor_coverage_mismatch) && (
<Callout variant="warning">
<div className="space-y-1.5">
{report.reliability.underpowered && (
<p>
<strong>Underpowered.</strong> Only {report.reliability.events_in_holdout} of{' '}
{report.reliability.events_detected} detected corrections fall in the test period (
{report.reliability.minimum_events}+ needed). Recall is one event away from a materially
different headline, and which events flip is usually decided by where the frozen threshold
lands rather than by what the score saw. Read the direction, not the ratio.
</p>
)}
{report.reliability.sensor_coverage_mismatch && (
<p>
<strong>Sensor coverage differs across the split.</strong>{' '}
{report.reliability.train_full_sensor_share}% of training sessions had all{' '}
{report.reliability.sensors_expected} Warning sensors versus{' '}
{report.reliability.holdout_full_sensor_share}% of test sessions
{report.params?.credit_sensor_from && ` — credit history begins ${report.params.credit_sensor_from}`}
. The score renormalises over what is available, so the threshold was frozen on a partly
different construct than it is measured against.
</p>
)}
</div>
</Callout>
)}
<p className="text-[11px] leading-relaxed text-gray-600">
The threshold is frozen on the training period and measured on the chronological test period. Reconstructed
pre-freeze basket history remains exploratory.
</p>
</div>
);
}
function EventStudyPanel() {
const study = useQuery({ queryKey: ['regime', 'event-study'], queryFn: getEventStudy });
return (
<Disclosure summary="Warning study · chronological correction alarms">
{study.isLoading && <SkeletonCard className="h-24" />}
{study.data === null && <Callout variant="empty">Not run yet — trigger “Event Study” in Admin → Jobs.</Callout>}
{study.data && !study.data.available && <Callout variant="warning">{study.data.reason ?? 'No data'}</Callout>}
{study.data?.available && <EventStudyBody report={study.data} />}
</Disclosure>
);
}
const SELECT_CLASS = 'rounded-md border border-white/[0.08] bg-white/[0.03] px-2 py-1.5 text-xs text-gray-200';
const CAPEX_OPTIONS: { value: CapexState; label: string }[] = [
{ value: 'raising', label: 'Raising' },
{ value: 'holding', label: 'Holding' },
{ value: 'cutting', label: 'Cutting' },
{ value: 'unknown', label: 'Unknown' },
];
function FundamentalsEditor({
data,
onSave,
onRefresh,
saving,
refreshing,
}: {
data: RegimeFundamentals;
onSave: (body: RegimeFundamentalsUpdate) => void;
onRefresh: () => void;
saving: boolean;
refreshing: boolean;
}) {
const [capex, setCapex] = useState<Record<string, CapexState>>(() => ({ ...data.capex }));
const [reaction, setReaction] = useState<GoodNewsReaction>(data.good_news_stock_down);
const knownCapex = Object.values(capex).filter((state) => state !== 'unknown');
// Mirrors _CAPEX_STATE_SCORES: raising 0, holding 50, cutting 100. Holding is
// the deceleration case and used to score identically to raising.
const capexPoints = knownCapex.reduce((sum, state) => sum + (state === 'cutting' ? 100 : state === 'holding' ? 50 : 0), 0);
const derivedF1 = knownCapex.length >= 3 ? Math.round((capexPoints / knownCapex.length) * 10) / 10 : null;
const derivedF3 = reaction === 'yes' ? 100 : reaction === 'no' ? 0 : null;
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-500">
<span>Source: {data.source}</span>
{data.fetched_at && <span>· fetched {new Date(data.fetched_at).toLocaleDateString()}</span>}
{data.effective_date && <span>· effective {data.effective_date}</span>}
{data.locked && <Badge label="locked" variant="manual" />}
</div>
{data.reasoning && <p className="text-xs leading-relaxed text-gray-400">{data.reasoning}</p>}
<div>
<div className="mb-2 flex items-center justify-between gap-3 text-xs">
<span className="font-medium text-gray-300">F1 · Capex guidance by hyperscaler</span>
<span className="num text-gray-500">score {derivedF1 ?? 'n/a'}</span>
</div>
<div className="grid grid-cols-2 gap-2">
{Object.entries(capex).map(([symbol, state]) => (
<label key={symbol} className="flex items-center justify-between gap-2 rounded-md bg-white/[0.02] px-2.5 py-2 text-xs text-gray-400">
<span className="font-mono text-gray-300">{symbol}</span>
<select
className={SELECT_CLASS}
value={state}
onChange={(event) => setCapex((current) => ({ ...current, [symbol]: event.target.value as CapexState }))}
>
{CAPEX_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
</label>
))}
</div>
<p className="mt-1.5 text-[11px] text-gray-600">Raising = 0, holding = 50, cutting = 100; at least three known names required. Display only — this does not enter Warning.</p>
</div>
<label className="flex items-center justify-between gap-3 text-xs text-gray-400">
<span>
<span className="font-medium text-gray-300">F3 · Good news, stock down</span>
<span className="ml-2 num text-gray-600">score {derivedF3 ?? 'n/a'}</span>
</span>
<select className={SELECT_CLASS} value={reaction} onChange={(event) => setReaction(event.target.value as GoodNewsReaction)}>
<option value="yes">Yes · stress</option>
<option value="no">No · ordinary</option>
<option value="mixed">Mixed · unavailable</option>
</select>
</label>
<div className="flex flex-wrap gap-2">
<button className="btn-primary px-3 py-1.5 text-sm disabled:opacity-50" disabled={saving} onClick={() => onSave({ capex, good_news_stock_down: reaction, locked: true })}>Save observations</button>
<button className="rounded-lg px-3 py-1.5 text-sm text-gray-400 hover:bg-white/[0.04] disabled:opacity-50" disabled={refreshing} onClick={onRefresh}>{refreshing ? 'Refreshing' : 'Refresh via LLM'}</button>
{data.locked && <button className="rounded-lg px-3 py-1.5 text-sm text-gray-400 hover:bg-white/[0.04]" onClick={() => onSave({ locked: false })}>Unlock</button>}
</div>
</div>
);
}
function ConfigEditor({ data, onSave, saving }: { data: RegimeConfig; onSave: (updates: Partial<RegimeConfig>) => void; saving: boolean }) {
const [basket, setBasket] = useState(data.breadth_basket.join(', '));
const [staleness, setStaleness] = useState(data.fundamental_staleness_days);
const symbols = basket.split(/[\s,]+/).map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
return (
<div className="space-y-3">
<label className="block text-xs text-gray-400">
<span>Fixed breadth basket · {symbols.length} symbols</span>
<textarea value={basket} onChange={(event) => setBasket(event.target.value)} rows={5} className="mt-1 w-full rounded-lg border border-white/[0.08] bg-white/[0.03] p-2 font-mono text-xs text-gray-200" />
</label>
<label className="flex items-center gap-2 text-xs text-gray-400">
<span>Fundamental staleness</span>
<input type="number" min={30} max={180} value={staleness} onChange={(event) => setStaleness(Number(event.target.value))} className="w-20 rounded-md border border-white/[0.08] bg-white/[0.03] px-2 py-1 text-right num text-gray-200" />
<span>days</span>
</label>
<p className="text-[11px] text-gray-600">Changing the basket resets its freeze date and silently reseeds quadrant alerts.</p>
<button className="btn-primary px-3 py-1.5 text-sm disabled:opacity-50" disabled={saving || symbols.length < 20} onClick={() => onSave({ breadth_basket: symbols, fundamental_staleness_days: staleness })}>Save basket &amp; freshness</button>
</div>
);
}
function AdminControls() {
const queryClient = useQueryClient();
const fundamentals = useQuery({ queryKey: ['regime', 'fundamentals'], queryFn: getRegimeFundamentals });
const config = useQuery({ queryKey: ['regime', 'config'], queryFn: getRegimeConfig });
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['regime'] });
const refresh = useMutation({ mutationFn: refreshRegimeFundamentals, onSuccess: invalidate });
const saveFundamentals = useMutation({ mutationFn: updateRegimeFundamentals, onSuccess: invalidate });
const saveConfig = useMutation({ mutationFn: updateRegimeConfig, onSuccess: invalidate });
return (
<Disclosure summary="Admin · Monitor settings">
<div className="grid gap-5 xl:grid-cols-2 xl:gap-6">
<section className="border-b border-white/[0.06] pb-5 xl:border-b-0 xl:border-r xl:pb-0 xl:pr-6">
<div className="mb-3 text-[11px] uppercase tracking-wider text-gray-500">Fundamental observations</div>
{fundamentals.isLoading && <SkeletonCard className="h-36" />}
{fundamentals.data && <FundamentalsEditor key={fundamentals.dataUpdatedAt} data={fundamentals.data} onSave={(body) => saveFundamentals.mutate(body)} onRefresh={() => refresh.mutate()} saving={saveFundamentals.isPending} refreshing={refresh.isPending} />}
{refresh.isError && <Callout variant="error">Refresh failed: {(refresh.error as Error).message}</Callout>}
</section>
<section>
<div className="mb-3 text-[11px] uppercase tracking-wider text-gray-500">Fixed basket &amp; freshness</div>
{config.isLoading && <SkeletonCard className="h-36" />}
{config.data && <ConfigEditor key={config.dataUpdatedAt} data={config.data} onSave={(updates) => saveConfig.mutate(updates)} saving={saveConfig.isPending} />}
{saveConfig.isError && <Callout variant="error">Save failed: {(saveConfig.error as Error).message}</Callout>}
</section>
</div>
</Disclosure>
);
}
export default function RegimePage() {
const isAdmin = useAuthStore((state) => state.role) === 'admin';
const monitor = useQuery({ queryKey: ['regime', 'monitor'], queryFn: getRegimeMonitor });
const data = monitor.data;
return (
<div className="space-y-6 animate-slide-up">
<PageHeader title="Regime Monitor" subtitle="AI/Tech risk thermometer · State and Warning · feeds no trades" />
<Callout variant="info"><strong>Risk thermometer — not an entry, exit, or sizing signal.</strong> State measures current stress; Warning measures deterioration and divergence.</Callout>
{monitor.isLoading && <><SkeletonCard className="h-44" /><SkeletonTable rows={6} cols={4} /></>}
{monitor.isError && <Callout variant="error" onRetry={() => monitor.refetch()}>Failed to load: {(monitor.error as Error).message}</Callout>}
{data && !data.available && <Callout variant="empty">V2 is not computed yet — run “Regime Monitor” from Admin → Jobs or wait for the daily pipeline.</Callout>}
{data?.available && data.state && data.warning && (
<>
{(!data.data_quality?.is_fresh || data.state.band == null || data.warning.band == null) && (
<Callout variant="warning">
Reading is incomplete or stale. State coverage {Math.round(data.state.coverage)}%, Warning coverage {Math.round(data.warning.coverage)}%
{data.data_quality?.stale_inputs?.length ? ` · stale: ${data.data_quality.stale_inputs.join(', ')}` : ''}.
</Callout>
)}
<div className="grid gap-4 lg:grid-cols-2">
<ScoreGauge
label="State · current structural stress"
reading={data.state}
divider={data.quadrant_config?.state_divider}
footnote={<>One capped price vote plus fixed-basket breadth, HY credit, and volatility. As of {data.date}. VIX {data.inputs?.vix ?? ''} · HY OAS {data.inputs?.hy_oas ?? ''}.</>}
/>
<ScoreGauge
label="Warning · deterioration & divergence"
reading={data.warning}
divider={data.quadrant_config?.warning_divider}
footnote={<>Breadth divergence, SMH/SPY rollover, and HY credit impulse. Breadth loss counts fully when price masks it and partially when price confirms it. Missing sensors reduce coverage; they never default to 50.</>}
/>
</div>
{data.fundamental_context && <FundamentalOverlayCard overlay={data.fundamental_context} />}
<p className="text-xs text-gray-600">
Data quality · oldest market input:{' '}
{data.data_quality?.oldest_market_input_age_days == null
? 'unavailable'
: `${data.data_quality.oldest_market_input_age_days}d`}
</p>
<Suspense fallback={<SkeletonCard className="h-80" />}><RegimeQuadrant /></Suspense>
<Suspense fallback={<SkeletonCard className="h-72" />}><ScoreHistoryChart /></Suspense>
<div className="grid gap-3 lg:grid-cols-2">
<PillarBreakdown title="State" reading={data.state} />
<PillarBreakdown title="Warning" reading={data.warning} />
</div>
{data.basket && (
<p className="text-xs leading-relaxed text-gray-600">
Fixed basket {data.basket.members_available ?? '—'}/{data.basket.members_expected} available · hash {data.basket.hash} · frozen {data.basket.basket_asof}. History reconstructed before the freeze date is retrospective/exploratory; readings after it form the trustworthy forward series.
</p>
)}
</>
)}
<EventStudyPanel />
{isAdmin && <AdminControls />}
</div>
);
}