Clarify executor controls and alert history
All checks were successful
deploy / deploy (push) Successful in 36s
All checks were successful
deploy / deploy (push) Successful in 36s
Proof: Dashboard system controls and alert history stay operator-legible under runtime health flapping without implying nonexistent arm behavior. Assumptions: Manual executor arming remains intentionally absent from the dashboard for this turn, so resume should mean intake resume only. Still fake: Ops-sentinel still emits raw runtime transition churn underneath; this change collapses it in the dashboard instead of changing runtime alert hysteresis.
This commit is contained in:
parent
69be378784
commit
3c1ad1dde4
3 changed files with 151 additions and 8 deletions
|
|
@ -130,8 +130,8 @@ const CONTROL_DEFINITIONS = [
|
|||
action: 'pause',
|
||||
method: 'POST',
|
||||
path: '/pause',
|
||||
label: 'Pause Executor',
|
||||
description: 'Pause trade-executor command consumption without moving funds.',
|
||||
label: 'Pause Executor Intake',
|
||||
description: 'Pause trade-executor command consumption without changing armed state.',
|
||||
page: 'system',
|
||||
risk_class: 'safe',
|
||||
},
|
||||
|
|
@ -140,8 +140,8 @@ const CONTROL_DEFINITIONS = [
|
|||
action: 'resume',
|
||||
method: 'POST',
|
||||
path: '/resume',
|
||||
label: 'Resume Executor',
|
||||
description: 'Resume trade-executor command consumption.',
|
||||
label: 'Resume Executor Intake',
|
||||
description: 'Resume trade-executor command consumption without changing armed state.',
|
||||
page: 'system',
|
||||
risk_class: 'safe',
|
||||
},
|
||||
|
|
@ -329,11 +329,11 @@ export function buildDashboardBootstrap({
|
|||
const activeAlerts = normalizeAlertList(
|
||||
servicesByName['ops-sentinel']?.state?.active_alerts || [],
|
||||
);
|
||||
const recentAlerts = normalizeAlertList(
|
||||
const recentAlerts = summarizeRecentAlertTransitions(normalizeAlertList(
|
||||
servicesByName['ops-sentinel']?.state?.recent_transitions
|
||||
|| recentAlertTransitions?.map((entry) => entry.payload)
|
||||
|| [],
|
||||
);
|
||||
));
|
||||
const profitability = buildProfitabilitySummary({
|
||||
metric: portfolioMetric,
|
||||
successfulTradeSummary,
|
||||
|
|
@ -1072,9 +1072,39 @@ function normalizeAlert(alert) {
|
|||
cleared_at: alert.cleared_at || null,
|
||||
last_evaluated_at: alert.last_evaluated_at || null,
|
||||
details: alert.details || {},
|
||||
transition_count: Number(alert.transition_count || 1),
|
||||
raised_count: Number(alert.raised_count || (alert.status === 'raised' ? 1 : 0)),
|
||||
cleared_count: Number(alert.cleared_count || (alert.status === 'cleared' ? 1 : 0)),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeRecentAlertTransitions(alerts) {
|
||||
const summaries = new Map();
|
||||
|
||||
for (const alert of alerts || []) {
|
||||
const key = buildAlertKey(alert);
|
||||
const existing = summaries.get(key);
|
||||
if (!existing) {
|
||||
summaries.set(key, {
|
||||
...alert,
|
||||
transition_count: alert.transition_count || 1,
|
||||
raised_count: alert.status === 'raised' ? 1 : 0,
|
||||
cleared_count: alert.status === 'cleared' ? 1 : 0,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
existing.transition_count += 1;
|
||||
existing.raised_count += alert.status === 'raised' ? 1 : 0;
|
||||
existing.cleared_count += alert.status === 'cleared' ? 1 : 0;
|
||||
}
|
||||
|
||||
return [...summaries.values()].sort((left, right) => sortTimestamps(
|
||||
right.cleared_at || right.raised_at || right.last_evaluated_at,
|
||||
left.cleared_at || left.raised_at || left.last_evaluated_at,
|
||||
));
|
||||
}
|
||||
|
||||
function appendUniqueRecentQuote(quotes, nextQuote, limit) {
|
||||
const deduped = [nextQuote, ...quotes.filter((quote) => quote.quote_id !== nextQuote.quote_id)];
|
||||
return deduped.slice(0, limit);
|
||||
|
|
|
|||
|
|
@ -13,15 +13,31 @@ export default function AlertsGrid({ items, emptyMessage = 'No alerts are active
|
|||
<div className="service-card" key={`${item.alert_code}:${item.raised_at || item.cleared_at || index}`}>
|
||||
<div className="service-head">
|
||||
<strong>{item.alert_code}</strong>
|
||||
<div className="pills">
|
||||
<Pill label={item.status || 'unknown'} stateLabel={item.status || 'unknown'} />
|
||||
<Pill label={item.severity} stateLabel={item.severity} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="service-detail">
|
||||
<div>{item.reason}</div>
|
||||
<div>{`Scope ${item.service_scope}`}</div>
|
||||
<div>{formatTimestamp(item.raised_at || item.cleared_at || item.last_evaluated_at)}</div>
|
||||
<div>{formatTransitionTimestamp(item)}</div>
|
||||
{item.transition_count > 1 ? (
|
||||
<div>{`Transitions ${item.transition_count} (raised ${item.raised_count}, cleared ${item.cleared_count})`}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTransitionTimestamp(item) {
|
||||
if (item.status === 'cleared' && item.cleared_at) {
|
||||
return `Cleared ${formatTimestamp(item.cleared_at)}`;
|
||||
}
|
||||
if (item.raised_at) {
|
||||
return `Raised ${formatTimestamp(item.raised_at)}`;
|
||||
}
|
||||
return formatTimestamp(item.last_evaluated_at);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ test('control routing only resolves the allowlisted safe dashboard actions', ()
|
|||
service: 'liquidity-manager',
|
||||
action: 'refresh',
|
||||
});
|
||||
const resumeExecutor = resolveDashboardControl({
|
||||
service: 'trade-executor',
|
||||
action: 'resume',
|
||||
});
|
||||
const risky = resolveDashboardControl({
|
||||
service: 'strategy-engine',
|
||||
action: 'arm',
|
||||
|
|
@ -112,6 +116,8 @@ test('control routing only resolves the allowlisted safe dashboard actions', ()
|
|||
|
||||
assert.equal(refresh?.path, '/refresh');
|
||||
assert.equal(refresh?.risk_class, 'safe');
|
||||
assert.equal(resumeExecutor?.path, '/resume');
|
||||
assert.equal(resumeExecutor?.label, 'Resume Executor Intake');
|
||||
assert.equal(risky, null);
|
||||
});
|
||||
|
||||
|
|
@ -557,6 +563,97 @@ test('ingest disconnected still renders as a critical transport failure', () =>
|
|||
assert.match(ingest.health_reasons.join(' '), /websocket disconnected/);
|
||||
});
|
||||
|
||||
test('recent alert history collapses repeated flapping transitions into one readable entry', () => {
|
||||
const config = buildConfig();
|
||||
const bootstrap = buildDashboardBootstrap({
|
||||
config,
|
||||
auth: {
|
||||
authenticated: true,
|
||||
subject: 'local-operator',
|
||||
mode: 'stub',
|
||||
roles: ['operator'],
|
||||
},
|
||||
portfolioMetric: null,
|
||||
inventorySnapshot: null,
|
||||
marketPrice: null,
|
||||
recentQuotes: [],
|
||||
successfulTrades: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0,
|
||||
total_pages: 1,
|
||||
items: [],
|
||||
},
|
||||
successfulTradeSummary: {
|
||||
total: 0,
|
||||
last_successful_trade_at: null,
|
||||
},
|
||||
fundingObservations: [],
|
||||
recentTradeDecisions: [],
|
||||
recentAlertTransitions: [],
|
||||
serviceSnapshots: [
|
||||
{
|
||||
service: 'ops-sentinel',
|
||||
label: 'Ops Sentinel',
|
||||
base_url: 'http://ops-sentinel',
|
||||
reachable: true,
|
||||
health: { ok: true },
|
||||
state: {
|
||||
active_alerts: [],
|
||||
recent_transitions: [
|
||||
{
|
||||
alert_code: 'near_intents_quotes_stale',
|
||||
status: 'raised',
|
||||
severity: 'critical',
|
||||
reason: 'quote truth stale',
|
||||
service_scope: 'near-intents-ingest',
|
||||
pair: config.activePair,
|
||||
raised_at: '2026-04-04T09:33:00.000Z',
|
||||
first_raised_at: '2026-04-04T09:30:00.000Z',
|
||||
cleared_at: null,
|
||||
last_evaluated_at: '2026-04-04T09:33:00.000Z',
|
||||
details: {},
|
||||
},
|
||||
{
|
||||
alert_code: 'near_intents_quotes_stale',
|
||||
status: 'cleared',
|
||||
severity: 'critical',
|
||||
reason: 'quote truth stale',
|
||||
service_scope: 'near-intents-ingest',
|
||||
pair: config.activePair,
|
||||
raised_at: '2026-04-04T09:31:00.000Z',
|
||||
first_raised_at: '2026-04-04T09:30:00.000Z',
|
||||
cleared_at: '2026-04-04T09:32:00.000Z',
|
||||
last_evaluated_at: '2026-04-04T09:32:00.000Z',
|
||||
details: {},
|
||||
},
|
||||
{
|
||||
alert_code: 'near_intents_quotes_stale',
|
||||
status: 'raised',
|
||||
severity: 'critical',
|
||||
reason: 'quote truth stale',
|
||||
service_scope: 'near-intents-ingest',
|
||||
pair: config.activePair,
|
||||
raised_at: '2026-04-04T09:31:00.000Z',
|
||||
first_raised_at: '2026-04-04T09:30:00.000Z',
|
||||
cleared_at: null,
|
||||
last_evaluated_at: '2026-04-04T09:31:00.000Z',
|
||||
details: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(bootstrap.system.alerts.recent.length, 1);
|
||||
assert.equal(bootstrap.system.alerts.recent[0].alert_code, 'near_intents_quotes_stale');
|
||||
assert.equal(bootstrap.system.alerts.recent[0].status, 'raised');
|
||||
assert.equal(bootstrap.system.alerts.recent[0].transition_count, 3);
|
||||
assert.equal(bootstrap.system.alerts.recent[0].raised_count, 2);
|
||||
assert.equal(bootstrap.system.alerts.recent[0].cleared_count, 1);
|
||||
});
|
||||
|
||||
test('funding summary includes credited bridge deposits without observer-backed funding observations', () => {
|
||||
const config = buildConfig();
|
||||
const bootstrap = buildDashboardBootstrap({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue