import { html, usePoll, num, compact, pct, ago, whenDate, health } from './core.js';
import { Card, Stat, Dot, Pill, Bar, Skeleton, Empty } from './ui.js';
const GATE = { minSample: 30, minInstruments: 5, maxConcentration: 0.5 };
const ORIGIN_NOTE = {
live: 'genuine real-time work, the only thing that can authorise an order',
historical: 'coordinator backfill, training evidence only',
replay: 'walk-forward replay, training evidence only',
};
function PipelineRow({ label, tone, detail, note }) {
return html`
<${Dot} tone=${tone} live />
${label}
${note && html`${note}`}
${detail}
`;
}
function LiveEvidence({ maturity, liveOpen, liveResolved }) {
const total = (maturity || []).reduce((sum, row) => sum + Number(row.n || 0), 0);
const next = (maturity || [])
.map((row) => row.first_matures).filter(Boolean).sort()[0];
if (!total && !liveResolved) {
return html`<${Empty}>No live predictions yet./>`;
}
return html`
${num(liveResolved)}
matured of ${num(liveOpen + liveResolved)} live
<${Bar} value=${liveResolved} max=${liveOpen + liveResolved}
tone=${liveResolved > 0 ? 'ok' : 'warn'} />
${liveResolved > 0
? 'Live evidence is accumulating.'
: html`Nothing has matured yet. First outcome ${html`${whenDate(next)}`}.`}
`;
}
function GateCard({ cohorts }) {
const live = (cohorts || []).filter((c) => c.source === 'live');
const best = live.slice().sort((a, b) => Number(b.sample_size) - Number(a.sample_size))[0];
const qualifying = live.filter((c) => Number(c.sample_size) >= GATE.minSample
&& Number(c.distinct_instruments) >= GATE.minInstruments
&& Number(c.top_instrument_share) <= GATE.maxConcentration);
const rows = [
{ label: 'samples', value: best ? Number(best.sample_size) : 0, need: GATE.minSample,
fmt: (v) => num(v) },
{ label: 'distinct tickers', value: best ? Number(best.distinct_instruments) : 0, need: GATE.minInstruments,
fmt: (v) => num(v) },
{ label: 'top ticker share', value: best ? Number(best.top_instrument_share) : 1,
need: GATE.maxConcentration, invert: true, fmt: (v) => pct(v, 0) },
];
return html`
${qualifying.length}
live cohorts clear the gate
${!live.length
? html`<${Empty}>No live calibration exists yet, so every decision abstains. Offline cohorts cannot authorise orders./>`
: html`
${rows.map((row) => {
const passed = row.invert ? row.value <= row.need : row.value >= row.need;
return html`
${row.label}
${row.fmt(row.value)} ${passed ? '✓' : `/ ${row.fmt(row.need)}`}
<${Bar} value=${row.invert ? Math.max(0, 1 - row.value) : row.value}
max=${row.invert ? 1 : row.need} tone=${passed ? 'ok' : 'warn'} />
`;
})}
Best-populated live cohort shown.
`}
`;
}
export function Overview({ notify }) {
const { data, error, loading, at } = usePoll('/admin/api/ops/overview', 5000);
if (loading && !data) {
return html`
${[0, 1, 2].map((i) => html`<${Skeleton} rows=${2} />`)}
`;
}
if (error && !data) return html`<${Card} title="Overview">${error}
/>`;
const p = data.predictions || {};
const f = data.freshness || {};
const thresholds = f.thresholds || {};
const deadTotal = (data.deadLetters || []).reduce((s, r) => s + Number(r.n || 0), 0);
const abstain = (data.decisions || []).find((d) => d.action === 'ABSTAIN');
const acting = (data.decisions || []).filter((d) => d.action === 'BUY' || d.action === 'SELL')
.reduce((s, d) => s + Number(d.n || 0), 0);
return html`
${error && html`
<${Dot} tone="warn" />Live updates interrupted: ${error}. Showing the last good snapshot.
`}
${deadTotal > 0 && html`
<${Dot} tone="bad" />
${num(deadTotal)} dead-lettered jobs are not being retried.
Go to controls →
`}
<${Stat} label="Live predictions open" value=${num(p.live_open)}
sub=${`${num(p.live_resolved)} matured · ${num(p.total)} total`} />
<${Stat} label="Order intents" value=${num(data.orderIntents)}
tone=${Number(data.orderIntents) > 0 ? 'warn' : null}
sub=${acting ? `${num(acting)} actionable decisions` : 'every decision has abstained'} />
<${Stat} label="Archive" value=${compact(data.archive && data.archive.max_id)}
sub=${`last ingest ${ago(f.ingestMinutes)}`} />
<${Card} title="Pipeline">
<${PipelineRow} label="Ingest" note="articles"
tone=${health(f.ingestMinutes, thresholds.ingest)}
detail=${ago(f.ingestMinutes)} />
<${PipelineRow} label="Coordinator" note="predictions"
tone=${health(f.predictionMinutes, thresholds.prediction)}
detail=${ago(f.predictionMinutes)} />
<${PipelineRow} label="Outcomes" note="scored"
tone=${health(f.outcomeMinutes, thresholds.outcome)}
detail=${ago(f.outcomeMinutes)} />
<${PipelineRow} label="Execution"
note=${data.controls ? (data.controls.killSwitch ? 'kill switch on' : data.controls.mode) : ''}
tone=${data.controls && data.controls.killSwitch ? 'bad' : 'ok'}
detail=${`${num(data.orderIntents)} intents`} />
/>
<${Card} title="Can it trade yet?">
<${GateCard} cohorts=${data.cohorts} />
/>
<${Card} title="Live evidence">
<${LiveEvidence} maturity=${data.maturity}
liveOpen=${Number(p.live_open || 0)} liveResolved=${Number(p.live_resolved || 0)} />
/>
<${Card} title="Accuracy by origin"
right=${html`
only live counts as a track record`}>
${!(data.byOrigin || []).length
? html`<${Empty}>No scored outcomes yet./>`
: html`
`}
/>
${abstain ? `${num(abstain.n)} decisions, all ABSTAIN · ` : ''}
updated ${at ? new Date(at).toLocaleTimeString() : '—'}
`;
}