Files
Duriin-API/public/admin/assets/js/ops/overview.js
T
ImBenjiandClaude Opus 5 0a5b30ab63 feat: new ops console for the admin surface
The old admin was five separate html pages, so every navigation was a full
reload and the operational picture was scattered across all of them. Worth
saying: the api was never the problem, every endpoint answers in under 200ms.
It felt slow because of the architecture, not the backend.

This is a single page console. React and htm from a cdn, no bundler and no
babel-in-the-browser, because a runtime transpiler on every load is exactly the
slowness we are trying to get rid of. Hash routing, polling that keeps the last
good payload on screen instead of flashing a spinner, and stale responses are
dropped so a slow request cannot overwrite a newer one.

/admin/api/ops/overview answers the whole dashboard in one call rather than
making the browser fan out and stitch. It carries the things that actually
matter and were not visible anywhere before: when live evidence matures, why a
cohort does or does not clear the trade gate, which stage of the pipeline has
gone quiet, and what is sitting in dead letters.

Controls, all of which change production and all of which ask twice:
- requeue dead letters, which only ever moves dead_letter back to pending
- execution mode and a kill switch, now read from autonomy_settings on every
  poll instead of only from AUTONOMY_EXECUTION_MODE, so halting no longer needs
  a redeploy first
- run the reaction analysis and read its output

The d3 graph is framed rather than ported. It works, and rewriting it would risk
something valuable for nothing the operator can see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
2026-09-01 20:17:36 +01:00

217 lines
9.4 KiB
JavaScript

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`
<div class="row spread">
<span class="row" style="gap:9px">
<${Dot} tone=${tone} live />
<span>${label}</span>
</span>
<span class="row" style="gap:10px">
${note && html`<span class="muted small nowrap">${note}</span>`}
<span class="mono small nowrap">${detail}</span>
</span>
</div>`;
}
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`
<div class="grid" style="gap:12px">
<div class="row spread">
<span class="stat">${num(liveResolved)}</span>
<span class="muted small">matured of ${num(liveOpen + liveResolved)} live</span>
</div>
<${Bar} value=${liveResolved} max=${liveOpen + liveResolved}
tone=${liveResolved > 0 ? 'ok' : 'warn'} />
<div class="muted small">
${liveResolved > 0
? 'Live evidence is accumulating.'
: html`Nothing has matured yet. First outcome ${html`<strong>${whenDate(next)}</strong>`}.`}
</div>
<div class="table-scroll">
<table>
<thead><tr><th>horizon</th><th class="num">open</th><th>first matures</th></tr></thead>
<tbody>
${(maturity || []).map((row) => html`
<tr key=${row.horizon_days}>
<td class="mono">${row.horizon_days}d</td>
<td class="num">${num(row.n)}</td>
<td class="mono small">${whenDate(row.first_matures)}</td>
</tr>`)}
</tbody>
</table>
</div>
</div>`;
}
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`
<div class="grid" style="gap:14px">
<div class="row spread">
<span class="stat" style=${qualifying.length ? 'color:var(--ok)' : null}>${qualifying.length}</span>
<span class="muted small">live cohorts clear the gate</span>
</div>
${!live.length
? html`<${Empty}>No live calibration exists yet, so every decision abstains. Offline cohorts cannot authorise orders.<//>`
: html`
<div class="gate">
${rows.map((row) => {
const passed = row.invert ? row.value <= row.need : row.value >= row.need;
return html`
<div class="gate-row" key=${row.label}>
<span class="gate-label">${row.label}</span>
<span class="mono small" style=${`color:var(--${passed ? 'ok' : 'ink-dim'})`}>
${row.fmt(row.value)} ${passed ? '✓' : `/ ${row.fmt(row.need)}`}
</span>
<div style="grid-column:1/-1">
<${Bar} value=${row.invert ? Math.max(0, 1 - row.value) : row.value}
max=${row.invert ? 1 : row.need} tone=${passed ? 'ok' : 'warn'} />
</div>
</div>`;
})}
</div>
<div class="muted small">Best-populated live cohort shown.</div>`}
</div>`;
}
export function Overview({ notify }) {
const { data, error, loading, at } = usePoll('/admin/api/ops/overview', 5000);
if (loading && !data) {
return html`<div class="grid cols-3">
${[0, 1, 2].map((i) => html`<section class="card" key=${i}><${Skeleton} rows=${2} /></section>`)}
</div>`;
}
if (error && !data) return html`<${Card} title="Overview"><div class="muted">${error}</div><//>`;
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`
<div class="grid" style="gap:14px">
${error && html`<div class="banner warn">
<${Dot} tone="warn" /><span>Live updates interrupted: ${error}. Showing the last good snapshot.</span>
</div>`}
${deadTotal > 0 && html`
<div class="banner">
<${Dot} tone="bad" />
<span><strong>${num(deadTotal)}</strong> dead-lettered jobs are not being retried.</span>
<span class="grow"></span>
<a class="nav-item" href="#/controls" style="padding:4px 10px">Go to controls →</a>
</div>`}
<div class="grid cols-3">
<${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)}`} />
</div>
<div class="grid cols-2">
<${Card} title="Pipeline">
<div class="grid" style="gap:9px">
<${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`} />
</div>
<//>
<${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`<span class="muted small">only live counts as a track record</span>`}>
${!(data.byOrigin || []).length
? html`<${Empty}>No scored outcomes yet.<//>`
: html`
<div class="table-scroll">
<table>
<thead><tr><th>origin</th><th class="num">n</th><th class="num">accuracy</th><th class="num">mean excess</th></tr></thead>
<tbody>
${data.byOrigin.map((row) => {
const acc = Number(row.total) ? Number(row.correct) / Number(row.total) : null;
return html`
<tr key=${row.origin}>
<td>
<div class="row" style="gap:7px">
<${Pill} tone=${row.origin === 'live' ? 'ok' : ''}>${row.origin}<//>
</div>
<div class="muted small" style="margin-top:3px">${ORIGIN_NOTE[row.origin] || ''}</div>
</td>
<td class="num">${num(row.total)}</td>
<td class="num">${pct(acc)}</td>
<td class="num" style=${`color:var(--${Number(row.mean_excess) >= 0 ? 'ok' : 'bad'})`}>
${pct(row.mean_excess, 2)}
</td>
</tr>`;
})}
</tbody>
</table>
</div>`}
<//>
</div>
<div class="muted small mono">
${abstain ? `${num(abstain.n)} decisions, all ABSTAIN · ` : ''}
updated ${at ? new Date(at).toLocaleTimeString() : '—'}
</div>
</div>`;
}