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)}`}.`}
${(maturity || []).map((row) => html` `)}
horizonopenfirst matures
${row.horizon_days}d ${num(row.n)} ${whenDate(row.first_matures)}
`; } 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``} ${deadTotal > 0 && html` `}
<${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`
${data.byOrigin.map((row) => { const acc = Number(row.total) ? Number(row.correct) / Number(row.total) : null; return html` `; })}
originnaccuracymean excess
<${Pill} tone=${row.origin === 'live' ? 'ok' : ''}>${row.origin}
${ORIGIN_NOTE[row.origin] || ''}
${num(row.total)} ${pct(acc)} = 0 ? 'ok' : 'bad'})`}> ${pct(row.mean_excess, 2)}
`}
${abstain ? `${num(abstain.n)} decisions, all ABSTAIN · ` : ''} updated ${at ? new Date(at).toLocaleTimeString() : '—'}
`; }