// Replay — historical playback. Big timeline scrubber as centerpiece.
const { Card, Pill, fmtTs, fmtNum, fmtPct } = window.UI;
const RC2 = window.Recharts;
// adapt a backend apiEvent to the page-shape `{id, at, kind, payload}`.
// payload is delivered as a JSON RawMessage; parse it once so the row
// renderer can read `payload.account`, `.coin`, `.score`, etc.
function adaptReplayEvent(e) {
let payload = {};
try {
if (typeof e.payload === 'string') payload = JSON.parse(e.payload);
else if (e.payload && typeof e.payload === 'object') payload = e.payload;
} catch (_) { payload = {}; }
return {
id: e.id,
at: e.at_ms,
kind: e.kind,
payload,
};
}
function PageReplay({ app }) {
const accounts = app.accounts || [];
const [accountId, setAccountId] = React.useState(app.account);
React.useEffect(() => { setAccountId(app.account); }, [app.account]);
const acct = accounts.find(a => a.id === accountId) || null;
const accountID = acct && acct.accountID;
const { data: rawEvents, error, loading, refresh } = window.API.useAPI(
() => accountID
? window.API.get('/api/v1/replay/events', { account_id: accountID, limit: 2000 })
: Promise.resolve([]),
[accountID],
);
// No mock fallback: when the event log is empty (fresh DB / no
// strategy run yet) the page renders an empty-state card.
const events = (rawEvents && Array.isArray(rawEvents))
? rawEvents.map(adaptReplayEvent)
: [];
const haveEvents = events.length >= 2;
if (!haveEvents) {
return (
replay
deterministic playback of recorded event log · {' '}
{loading && loading…}
{error && api error}
{!loading && !error && no events recorded yet}
no events yet for {acct ? acct.id : 'this account'}.
the event log fills as the strategy + dispatcher loop emits decisions, order placements, and risk verdicts (M11.2). once that ships, every decision is replayable here at adjustable speed.
);
}
const tMin = events[0].at;
const tMax = events[events.length-1].at;
const span = Math.max(1, tMax - tMin);
const [cursor, setCursor] = React.useState(tMin + span * 0.65);
const [playing, setPlaying] = React.useState(false);
const [speed, setSpeed] = React.useState(4);
const [kindFilter, setKindFilter] = React.useState('all');
React.useEffect(() => {
if (!playing) return;
const id = setInterval(() => {
setCursor(c => {
const next = c + speed * 1000 * 0.5;
if (next > tMax) { setPlaying(false); return tMax; }
return next;
});
}, 80);
return () => clearInterval(id);
}, [playing, speed, tMax]);
const buckets = React.useMemo(() => {
const N = 80;
const bs = new Array(N).fill(0);
events.forEach(e => {
const idx = Math.min(N-1, Math.floor(((e.at - tMin) / span) * N));
bs[idx]++;
});
const max = Math.max(1, ...bs);
return bs.map(v => v / max);
}, [events, tMin, span]);
const allKinds = React.useMemo(() => Array.from(new Set(events.map(e => e.kind))), [events]);
const streamed = events.filter(e => e.at <= cursor && (kindFilter === 'all' || e.kind === kindFilter)).slice(-20).reverse();
// Simulated state derived from events (M11.2 will reconstruct from
// event_log replay). For now we use the last `trade_completed` event
// before the cursor; equity sim shows a constant line until those
// events are emitted.
const lastTradeEv = [...events].reverse().find(e => e.at <= cursor && (e.kind === 'trade_completed' || e.kind === 'jump_confirmed'));
const simCoin = (lastTradeEv && lastTradeEv.payload && lastTradeEv.payload.to_coin) || (acct && acct.current_coin) || '—';
const simEquity = []; // empty until event_log carries equity ticks
const simEquityNow = 0;
const simEquityStart = 0;
return (
replay
deterministic playback of recorded event log · simulated state shown · {' '}
{loading && loading…}
{error && api error}
{!loading && !error && live}