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`
<${Dot} tone="warn" />${error}
`}
<${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.
`}
/>
<${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.
{
setAnalysis(null);
const result = await act('analysis',
() => api('/admin/api/ops/analysis/reaction', { method: 'POST' }),
(r) => (r.ok ? 'Analysis finished' : 'Analysis exited with an error'));
if (result) setAnalysis(result);
}}>
${busy === 'analysis' ? 'running…' : 'run'}
${analysis && html`
${analysis.error && html`
${analysis.error}
`}
${analysis.output || '(no output)'}
`}
/>
`;
}