import { html, usePoll, api, useState, num } from './core.js'; import { Card, Dot, Pill, Confirm, Empty, Skeleton } from './ui.js'; export function Controls({ notify }) { const { data, error, loading, refresh } = usePoll('/admin/api/ops/overview', 8000); const [busy, setBusy] = useState(null); const [analysis, setAnalysis] = useState(null); const act = async (key, run, describe) => { setBusy(key); try { const result = await run(); notify({ tone: 'ok', message: describe(result) }); await refresh(); return result; } catch (err) { console.error('[ops] control failed:', key, err.message); notify({ tone: 'bad', message: `${key} failed: ${err.message}`, sticky: true }); return null; } finally { setBusy(null); } }; if (loading && !data) return html`
<${Skeleton} rows=${4} />
`; const controls = (data && data.controls) || {}; const dead = (data && data.deadLetters) || []; const deadTotal = dead.reduce((sum, row) => sum + Number(row.n || 0), 0); const killed = Boolean(controls.killSwitch); return html`
${error && html``} <${Card} title="Execution" right=${html`<${Pill} tone=${killed ? 'bad' : (controls.mode === 'paper' ? 'warn' : 'ok')}> ${killed ? 'halted' : (controls.mode || 'unknown')} `}>
<${Dot} tone=${killed ? 'bad' : 'ok'} /> Kill switch ${killed ? 'engaged' : 'released'}
While engaged no order intents are created. Broker reconciliation keeps running so the account stays visible. Takes effect within one poll, no redeploy.
<${Confirm} label=${killed ? 'release' : 'engage kill switch'} confirmLabel=${killed ? 'yes, release' : 'yes, halt trading'} danger=${!killed} busy=${busy === 'kill'} onConfirm=${() => act('kill', () => api('/admin/api/ops/settings', { method: 'POST', body: { killSwitch: !killed } }), (r) => `Kill switch ${r.controls.killSwitch ? 'engaged' : 'released'}`)} />
Mode
shadow records intents without touching the broker. paper submits them to Alpaca paper. Currently from ${controls.modeSource === 'settings' ? 'this dashboard' : 'the environment'}.
<${Confirm} label=${controls.mode === 'paper' ? 'switch to shadow' : 'switch to paper'} confirmLabel=${controls.mode === 'paper' ? 'yes, shadow' : 'yes, submit to broker'} danger=${controls.mode !== 'paper'} busy=${busy === 'mode'} onConfirm=${() => act('mode', () => api('/admin/api/ops/settings', { method: 'POST', body: { mode: controls.mode === 'paper' ? 'shadow' : 'paper' }, }), (r) => `Execution mode is now ${r.controls.mode}`)} />
<${Card} title="Dead letters" right=${deadTotal > 0 && html`<${Pill} tone="bad">${num(deadTotal)} stuck`}> ${!deadTotal ? html`<${Empty}>Nothing dead-lettered. Good.` : html`
Requeue moves rows from dead_letter back to pending. It never deletes and never edits a payload, so the worst case is repeated work.
${dead.map((row) => html` `)}
joblanenlast error
${row.job_type} <${Pill}>${row.lane} ${num(row.n)} ${row.sample_error || '—'} <${Confirm} label="requeue" busy=${busy === `dl:${row.job_type}:${row.lane}`} confirmLabel=${`requeue ${num(row.n)}`} onConfirm=${() => act(`dl:${row.job_type}:${row.lane}`, () => api('/admin/api/ops/dead-letters/requeue', { method: 'POST', body: { jobType: row.job_type, lane: row.lane }, }), (r) => `Requeued ${num(r.requeued)} ${row.job_type} jobs`)} />
`} <${Card} title="Analyses">
Reaction conditioning
Tests whether the initial market reaction predicts anything, on every matured outcome. Read only, takes a few minutes, caches price history.
${analysis && html`
${analysis.error && html`
${analysis.error}
`}
${analysis.output || '(no output)'}
`}
`; }