SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
4.1 KB · 80 lines tsx
Raw Blame History
1'use client';23import { useState } from 'react';4import { SeriesChart } from '@/components/charts/SeriesChart';5import { adminFetch } from '@/lib/admin-fetch';6import { fmt } from '@/lib/format';7import { COMPONENT_LABEL, COMPONENT_ORDER } from '@/lib/pressure';8import type { AdminConfig, AdminReplay } from '@/lib/types';9import { AdminPage, ErrorNote, Field, Panel, btnPrimary, inputCls, useAdmin } from './shared';1011export function Replay() {12  const { data: cfg } = useAdmin<AdminConfig>('/config');13  const [from, setFrom] = useState(() => new Date(Date.now() - 86400_000).toISOString().slice(0, 16));14  const [to, setTo] = useState(() => new Date().toISOString().slice(0, 16));15  const [weights, setWeights] = useState<Record<string, number> | null>(null);16  const [res, setRes] = useState<AdminReplay | null>(null);17  const [err, setErr] = useState<string | null>(null);18  const [busy, setBusy] = useState(false);19  const w = weights ?? cfg?.pressure_weights ?? Object.fromEntries(COMPONENT_ORDER.map((c) => [c, 0]));20  const sum = Object.values(w).reduce((a, b) => a + Number(b || 0), 0);21  const valid = Math.abs(sum - 1) <= 0.001;2223  const run = async () => {24    setBusy(true);25    setErr(null);26    try {27      setRes(await adminFetch<AdminReplay>('/replay', { method: 'POST', body: { from: new Date(from + 'Z').toISOString(), to: new Date(to + 'Z').toISOString(), weights: w } }));28    } catch (e) {29      setErr(String(e));30    } finally {31      setBusy(false);32    }33  };3435  const diffs = res ? res.points.map((p) => p.pressure_replayed - p.pressure_original) : [];36  const maxDiff = diffs.length ? Math.max(...diffs.map(Math.abs)) : 0;3738  return (39    <AdminPage title="Replay" desc="Recompute the index over a past window with alternative weights, from stored signal features. Validates scoring changes against real incidents before applying them.">40      <div className="grid grid-cols-[minmax(0,1fr)] gap-3 lg:grid-cols-[320px_minmax(0,1fr)]">41        <Panel title="Window & weights" right={<span className="num" style={{ color: valid ? 'var(--ok)' : 'var(--bad)' }}>Σ {fmt(sum, 3)}</span>}>42          <div className="grid grid-cols-2 gap-3">43            <Field label="from (UTC)">44              <input type="datetime-local" value={from} onChange={(e) => setFrom(e.target.value)} className={`${inputCls} num w-full`} />45            </Field>46            <Field label="to (UTC)">47              <input type="datetime-local" value={to} onChange={(e) => setTo(e.target.value)} className={`${inputCls} num w-full`} />48            </Field>49          </div>50          <ul className="mt-3 space-y-2">51            {Object.entries(w).map(([id, v]) => (52              <li key={id} className="grid grid-cols-[minmax(0,1fr)_80px] items-center gap-3 text-[12.5px]">53                <span className="text-ink">{COMPONENT_LABEL[id as keyof typeof COMPONENT_LABEL] ?? id}</span>54                <input type="number" step="0.01" min={0} max={1} value={v} onChange={(e) => setWeights({ ...w, [id]: Number(e.target.value) })} className={`${inputCls} num w-full`} aria-label={`${id} weight`} />55              </li>56            ))}57          </ul>58          <button type="button" className={`${btnPrimary} mt-3 w-full`} disabled={!valid || busy} onClick={() => void run()}>59            {busy ? 'replaying…' : 'Replay'}60          </button>61          <ErrorNote err={err} />62        </Panel>63        <Panel title="Original vs replayed" right={res ? <span className="num">step {res.step_seconds} s · max |Δ| {fmt(maxDiff)}</span> : undefined}>64          {res ? (65            <SeriesChart66              lines={[67                { name: 'original', color: '#8B98A5', points: res.points.map((p) => ({ ts: p.ts, value: p.pressure_original })), area: true },68                { name: 'replayed', color: '#5B8DEF', points: res.points.map((p) => ({ ts: p.ts, value: p.pressure_replayed })), width: 1.5 },69              ]}70              height={340}71            />72          ) : (73            <p className="py-10 text-center text-[12px] text-ink-3">Choose a window and weights, then replay.</p>74          )}75        </Panel>76      </div>77    </AdminPage>78  );79}80