Files
ai-exchange/src/web/app.js
T
2026-05-12 01:17:52 +08:00

1383 lines
56 KiB
JavaScript

const healthStatus = document.getElementById('healthStatus');
const refreshBtn = document.getElementById('refreshBtn');
const instrumentIdInput = document.getElementById('instrumentId');
const timeframeInput = document.getElementById('timeframe');
const limitInput = document.getElementById('limit');
const autoRefreshSecondsInput = document.getElementById('autoRefreshSeconds');
const executionStatusFilterInput = document.getElementById('executionStatusFilter');
const sessionFilterInput = document.getElementById('sessionFilter');
const searchTextInput = document.getElementById('searchText');
const trustClassFilterInput = document.getElementById('trustClassFilter');
const lastUpdated = document.getElementById('lastUpdated');
const activeFilters = document.getElementById('activeFilters');
const overviewCards = document.getElementById('overviewCards');
const marketStatePanel = document.getElementById('marketStatePanel');
const runtimeStatusPanel = document.getElementById('runtimeStatusPanel');
const decisionPacketPanel = document.getElementById('decisionPacketPanel');
const trustScorePanel = document.getElementById('trustScorePanel');
const liveReadinessPanel = document.getElementById('liveReadinessPanel');
const launchReadinessPanel = document.getElementById('launchReadinessPanel');
const sessionFocusPanel = document.getElementById('sessionFocusPanel');
const preMarketPlanPanel = document.getElementById('preMarketPlanPanel');
const tradeCandidatesPanel = document.getElementById('tradeCandidatesPanel');
const executionTicketsPanel = document.getElementById('executionTicketsPanel');
const alertHistoryPanel = document.getElementById('alertHistoryPanel');
const alertQualityPanel = document.getElementById('alertQualityPanel');
const visualReviewsPanel = document.getElementById('visualReviewsPanel');
const positionManagementPanel = document.getElementById('positionManagementPanel');
const operatorConsolePanel = document.getElementById('operatorConsolePanel');
const postTradeReviewPanel = document.getElementById('postTradeReviewPanel');
const canvas = document.getElementById('candlesChart');
const ctx = canvas.getContext('2d');
let autoRefreshTimer = null;
async function fetchJson(url, options = {}) {
const fetchOptions = { ...options };
if (fetchOptions.body) {
fetchOptions.headers = {
'Content-Type': 'application/json',
...(fetchOptions.headers || {}),
};
}
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`${response.status} ${url}`);
}
if (response.status === 204) {
return null;
}
return response.json();
}
async function postJson(url, body = {}) {
return fetchJson(url, {
method: 'POST',
body: JSON.stringify(body),
});
}
function setText(element, value) {
if (!element) return;
element.textContent = value == null ? '' : String(value);
}
function clearNode(node) {
if (!node) return;
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
function createElement(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text != null) setText(element, text);
return element;
}
function formatNumber(value, digits = 2) {
if (value == null || value === '') return 'n/a';
const number = Number(value);
if (!Number.isFinite(number)) return 'n/a';
return number.toFixed(digits);
}
function formatPercent(value) {
if (value == null) return 'n/a';
const number = Number(value);
if (!Number.isFinite(number)) return 'n/a';
return `${(number * 100).toFixed(1)}%`;
}
function formatTimestamp(value) {
if (!value) return 'n/a';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
return date.toLocaleString();
}
function formatList(values, emptyText = 'n/a') {
if (!Array.isArray(values) || values.length === 0) return emptyText;
return values.map((value) => String(value)).join(', ');
}
function formatJsonInline(value, emptyText = 'n/a') {
if (value == null) return emptyText;
if (Array.isArray(value) && value.length === 0) return emptyText;
if (!Array.isArray(value) && typeof value === 'object' && Object.keys(value).length === 0) return emptyText;
return JSON.stringify(value);
}
function statusClass(status) {
const raw = String(status || 'unknown').toLowerCase().replace(/[^a-z0-9]+/g, '_');
return `status-pill status-${raw || 'unknown'}`;
}
function buildStatusPill(status) {
return createElement('span', statusClass(status), status || 'unknown');
}
function buildMetricCard(label, value, detail = '') {
const card = createElement('div', 'card');
card.appendChild(createElement('div', 'metric', label));
card.appendChild(createElement('div', 'value-small', value));
if (detail) card.appendChild(createElement('div', 'metric', detail));
return card;
}
function buildInfoGrid(entries) {
const grid = createElement('div', 'info-grid');
entries.forEach(([label, value]) => {
const row = createElement('div', 'info-row');
row.appendChild(createElement('div', 'metric', label));
row.appendChild(createElement('div', 'info-value', value == null || value === '' ? 'n/a' : String(value)));
grid.appendChild(row);
});
return grid;
}
function buildActionBar(actions) {
const bar = createElement('div', 'actions');
actions
.filter((action) => action && typeof action.onClick === 'function')
.forEach((action) => {
const button = createElement('button', '', action.label);
button.type = 'button';
button.addEventListener('click', action.onClick);
bar.appendChild(button);
});
return bar;
}
function buildItemCard(title, status, entries, actions = [], detail = '') {
const card = createElement('div', 'card');
const header = createElement('div', 'card-header');
header.appendChild(createElement('strong', '', title));
header.appendChild(buildStatusPill(status));
card.appendChild(header);
card.appendChild(buildInfoGrid(entries));
if (detail) card.appendChild(createElement('div', 'metric card-detail', detail));
if (actions.length) card.appendChild(buildActionBar(actions));
return card;
}
function getSearchText() {
return String(searchTextInput.value || '').trim().toLowerCase();
}
function updateActiveFilters() {
const active = [];
const executionStatus = executionStatusFilterInput.value || 'all';
const session = sessionFilterInput.value || 'all';
const search = getSearchText();
const trustClass = trustClassFilterInput.value || 'all';
if (executionStatus !== 'all') active.push(`execution=${executionStatus}`);
if (session !== 'all') active.push(`session=${session}`);
if (trustClass !== 'all') active.push(`trust=${trustClass}`);
if (search) active.push(`search=${search}`);
setText(activeFilters, `Filters: ${active.length ? active.join(', ') : 'none'}`);
}
function filterBySearch(items, projector) {
const search = getSearchText();
if (!search) return items;
return items.filter((item) => String(projector(item)).toLowerCase().includes(search));
}
function filterExecutions(items) {
const statusFilter = executionStatusFilterInput.value || 'all';
const sessionFilter = sessionFilterInput.value || 'all';
return filterBySearch(
items.filter((item) => (statusFilter === 'all' || item.execution_status === statusFilter)
&& (sessionFilter === 'all' || item.session_code === sessionFilter)),
(item) => [
item.execution_id,
item.execution_status,
item.side,
item.session_code,
item.close_reason,
item.ai_explanation,
...(item.error_tags || []),
].join(' ')
);
}
function filterBySession(items, accessor) {
const sessionFilter = sessionFilterInput.value || 'all';
if (sessionFilter === 'all') return items;
return items.filter((item) => String(accessor(item) || '') === sessionFilter);
}
function queryString(params) {
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value == null || value === '') return;
search.set(key, String(value));
});
return search.toString();
}
async function runAction(url, body = {}) {
try {
await postJson(url, body);
await refreshDashboard();
} catch (error) {
showErrorPanel(String(error));
}
}
function renderHealth(health) {
setText(
healthStatus,
[
health?.app || 'ai-ict',
health?.database_configured ? 'db ready' : 'db missing',
health?.session_timezone || 'timezone unknown',
].join(' | ')
);
}
function renderOverview(weeklyReview, candidates, tickets, executions) {
clearNode(overviewCards);
const openExecutions = executions.filter((item) => item.execution_status === 'open');
const confirmedTickets = tickets.filter((item) => item.status === 'confirmed');
const executableCandidates = candidates.filter((item) => item.executable);
const cards = [
buildMetricCard('Executable Candidates', String(executableCandidates.length), 'ready to ticket'),
buildMetricCard('Confirmed Tickets', String(confirmedTickets.length), 'human confirmed'),
buildMetricCard('Open Positions', String(openExecutions.length), 'live management load'),
buildMetricCard('Framework Rate', formatPercent(weeklyReview?.framework_execution_rate), 'weekly execution discipline'),
buildMetricCard('Out-of-Model Rate', formatPercent(weeklyReview?.out_of_model_trade_rate), 'model drift'),
buildMetricCard('Risk Violations', String(weeklyReview?.risk_violation_count ?? 0), 'weekly count'),
];
cards.forEach((card) => overviewCards.appendChild(card));
}
function renderMarketState(payload, executions) {
clearNode(marketStatePanel);
const marketState = payload?.market_state || {};
const freshness = marketState.freshness || {};
const session = marketState.session || {};
const newsWindow = marketState.news_window || {};
const riskDecision = payload?.risk_decision || {};
const brokerState = payload?.broker_state || {};
const brokerAccount = brokerState.account || {};
const brokerAlerts = brokerState.alerts || [];
const panel = buildInfoGrid([
['symbol', String(instrumentIdInput.value || '1')],
['session', session.session_code || 'OFF_HOURS'],
['data freshness', freshness.code || 'unknown'],
['news state', newsWindow.code || 'unknown'],
['broker state', brokerState.status || 'unknown'],
['broker equity', formatNumber(brokerAccount.equity, 2)],
['broker available balance', formatNumber(brokerAccount.available_balance, 2)],
['broker open positions', String((brokerState.positions || []).length)],
['broker pending orders', String((brokerState.orders || []).length)],
['broker alerts', formatList(brokerAlerts)],
['account risk state', formatJsonInline(riskDecision.reject_codes || [])],
['open position', String(executions.filter((item) => item.execution_status === 'open').length)],
['kill switch', (riskDecision.reject_codes || []).includes('execution_kill_switch_enabled') ? 'active' : 'off'],
]);
marketStatePanel.appendChild(panel);
}
function renderRuntimeStatus(runtimeStatus) {
clearNode(runtimeStatusPanel);
const tasks = runtimeStatus?.tasks || {};
const blockers = runtimeStatus?.current_blockers || [];
const incidents = runtimeStatus?.incidents || [];
const watchdog = runtimeStatus?.runtime_watchdog || {};
const stalledTasks = watchdog?.stalled_tasks || [];
const stalledMap = new Map(stalledTasks.map((item) => [item.name, item]));
runtimeStatusPanel.appendChild(
buildInfoGrid([
['scheduler state', runtimeStatus?.scheduler_state || 'unknown'],
['supervisor state', runtimeStatus?.supervisor_state || 'unknown'],
['enabled', String(Boolean(runtimeStatus?.enabled))],
['degraded', String(Boolean(runtimeStatus?.degraded_mode_state?.is_degraded))],
['symbols', formatList(runtimeStatus?.symbols || [])],
['blockers', formatList(blockers)],
['stalled tasks', formatList(watchdog?.stalled_task_names || [])],
['watchdog checked', formatTimestamp(watchdog?.checked_ts)],
['last incident', incidents.length ? `${incidents[incidents.length - 1].code} @ ${formatTimestamp(incidents[incidents.length - 1].observed_ts)}` : 'none'],
])
);
const grid = createElement('div', 'cards');
Object.entries(tasks).forEach(([name, task]) => {
const stalled = stalledMap.get(name) || null;
grid.appendChild(
buildItemCard(
name,
stalled ? 'warning' : (task.last_status || 'idle'),
[
['last success', formatTimestamp(task.last_success_ts)],
['next run', formatTimestamp(task.next_run_ts)],
['run count', String(task.run_count ?? 0)],
['success / failure', `${task.success_count ?? 0} / ${task.failure_count ?? 0}`],
['stalled', stalled ? `${stalled.reason} (${formatNumber(stalled.age_seconds, 1)}s > ${stalled.threshold_seconds}s)` : 'no'],
['last error', task.last_error || 'none'],
['details', formatJsonInline(task.details || {})],
]
)
);
});
runtimeStatusPanel.appendChild(grid);
}
function renderDecisionPacket(packet, alertRows) {
clearNode(decisionPacketPanel);
if (!packet || packet.status === 'unavailable') {
decisionPacketPanel.appendChild(createElement('div', 'empty-state', 'Decision packet unavailable.'));
return;
}
decisionPacketPanel.appendChild(
buildInfoGrid([
['status', packet.status || 'unknown'],
['recommendation', packet.action_recommendation || 'monitor'],
['operator mode', packet.operator_mode || 'unknown'],
['trust classification', packet.trust_classification || 'unknown'],
['bias', packet.bias || 'unknown'],
['setup', packet.setup_type || 'n/a'],
['candidate', packet.candidate_id || 'n/a'],
['ticket', packet.execution_ticket_id || 'n/a'],
['risk result', formatJsonInline(packet.risk_result?.reasons || [])],
['why allowed', formatList(packet.why_allowed || [])],
['why blocked', formatList(packet.why_blocked || [])],
['ai explanation', packet.ai_explanation || 'n/a'],
['council summary', packet.research_council?.research_summary || 'n/a'],
['council confidence', packet.research_council?.committee_confidence != null ? formatNumber(packet.research_council.committee_confidence, 2) : 'n/a'],
['council follow-up', formatList(packet.research_council?.follow_up_checks || [])],
['council watch', formatList(packet.research_council?.recommended_watch_items || [])],
])
);
const grid = createElement('div', 'cards');
grid.appendChild(
buildItemCard(
'Plan Context',
packet.action_recommendation || 'monitor',
[
['narrative', packet.narrative || 'n/a'],
['primary draw', formatJsonInline(packet.primary_draw || {})],
['confirmation', formatJsonInline(packet.confirmation_chain || {})],
['entry zone', `${formatNumber(packet.entry_zone?.low, 3)} - ${formatNumber(packet.entry_zone?.high, 3)}`],
['stop', formatNumber(packet.stop_loss, 3)],
['targets', formatJsonInline(packet.targets || {})],
['invalidation', formatJsonInline(packet.invalidation || {})],
['expiry', formatTimestamp(packet.expiry_ts)],
]
)
);
const recentAlerts = alertRows?.items || packet.alerts || [];
recentAlerts.slice(0, 3).forEach((alert) => {
grid.appendChild(
buildItemCard(
alert.alert_type || 'alert',
alert.status || 'queued',
[
['channel', alert.channel || 'workbench'],
['message', alert.message || 'n/a'],
['payload', formatJsonInline(alert.payload || {})],
]
)
);
});
decisionPacketPanel.appendChild(grid);
}
function renderTrustScore(payload) {
clearNode(trustScorePanel);
const trustClass = trustClassFilterInput.value || 'all';
const models = (payload?.models || []).filter((model) => trustClass === 'all' || model.classification === trustClass);
trustScorePanel.appendChild(
buildInfoGrid([
['whitelist', formatList(payload?.whitelist || [])],
['observe', formatList(payload?.observe_list || [])],
['blacklist', formatList(payload?.blacklist || [])],
['next actions', formatList(payload?.next_actions || [])],
])
);
if (!models.length) {
trustScorePanel.appendChild(createElement('div', 'empty-state', 'No trust-score samples yet.'));
return;
}
const grid = createElement('div', 'cards');
models.forEach((model) => {
grid.appendChild(
buildItemCard(
model.model_code || 'model',
model.classification || 'observe',
[
['score', formatNumber(model.score, 1)],
['recommendation', model.recommendation || 'observe_only'],
['operator mode', model.operator_mode || 'unknown'],
['sample size', String(model.sample_size ?? 0)],
['real trades', String(model.real_trade_count ?? 0)],
['framework rate', formatPercent(model.framework_execution_rate)],
['out-of-model rate', formatPercent(model.out_of_model_trade_rate)],
['risk violation rate', formatPercent(model.risk_violation_rate)],
['expectancy R', formatNumber(model.expectancy_r, 2)],
['quality gate', String(Boolean(model.quality_gate_passed))],
['reasons', formatList(model.reasons || [])],
],
[],
formatJsonInline(model.session_recommendations || [])
)
);
});
trustScorePanel.appendChild(grid);
}
function renderLiveReadiness(payload) {
clearNode(liveReadinessPanel);
liveReadinessPanel.appendChild(
buildInfoGrid([
['ready for real live', String(Boolean(payload?.ready_for_real_live))],
['blocking reasons', formatList(payload?.blocking_reasons || [])],
['protocol version', payload?.operator_confirmation_protocol?.protocol_version || 'n/a'],
['latest live audits', String((payload?.recent_live_audits || []).length)],
['latest ticket', payload?.latest_ticket?.id || 'n/a'],
])
);
const grid = createElement('div', 'cards');
(payload?.checklist || []).forEach((item) => {
grid.appendChild(
buildItemCard(
item.code || 'check',
item.passed ? 'passed' : 'blocked',
[
['passed', String(Boolean(item.passed))],
['detail', item.detail || 'n/a'],
]
)
);
});
if ((payload?.checklist || []).length) {
liveReadinessPanel.appendChild(grid);
}
}
function renderLaunchReadiness(payload) {
clearNode(launchReadinessPanel);
launchReadinessPanel.appendChild(
buildInfoGrid([
['analysis only ready', String(Boolean(payload?.analysis_only_ready))],
['restricted decision support ready', String(Boolean(payload?.restricted_decision_support_ready))],
['decision support ready', String(Boolean(payload?.decision_support_ready))],
['execution assist ready', String(Boolean(payload?.execution_assist_ready))],
['real trade loop ready', String(Boolean(payload?.real_trade_loop_ready))],
['assisted trading ready', String(Boolean(payload?.assisted_trading_ready))],
['real live ready', String(Boolean(payload?.real_live_ready))],
['product mode', payload?.product_operating_mode || 'unknown'],
['product capabilities', formatJsonInline(payload?.product_capabilities || {})],
['boundary notes', formatList(payload?.product_boundary_notes || [])],
['blocking reasons', formatList(payload?.blocking_reasons || [])],
['warning reasons', formatList(payload?.warning_reasons || [])],
['next actions', formatList(payload?.next_actions || [])],
])
);
const grid = createElement('div', 'cards');
(payload?.checklist || []).forEach((item) => {
grid.appendChild(
buildItemCard(
item.code || 'check',
item.passed ? 'passed' : item.severity || 'warning',
[
['passed', String(Boolean(item.passed))],
['severity', item.severity || 'n/a'],
['detail', item.detail || 'n/a'],
]
)
);
});
if ((payload?.checklist || []).length) {
launchReadinessPanel.appendChild(grid);
}
}
function renderSessionFocus(payload) {
clearNode(sessionFocusPanel);
const items = payload?.items || [];
const filtered = filterBySession(items, (item) => item.session_code);
if (!filtered.length) {
sessionFocusPanel.appendChild(createElement('div', 'empty-state', 'No session focus rows available.'));
return;
}
const grid = createElement('div', 'cards');
filtered.forEach((item) => {
const recommendation = item.session_recommendation || {};
grid.appendChild(
buildItemCard(
item.session_code || 'session',
recommendation.classification || item.status || 'observe',
[
['status', item.status || 'unknown'],
['window', `${formatTimestamp(item.start_ts)} -> ${formatTimestamp(item.end_ts)}`],
['candidates', String(item.candidate_count ?? 0)],
['executable', String(item.executable_candidate_count ?? 0)],
['open executions', String(item.open_execution_count ?? 0)],
['alerts', String(item.alert_count ?? 0)],
['recommendation', `${recommendation.classification || 'observe'} @ ${formatNumber(recommendation.score, 1)}`],
['action hint', item.action_hint || 'observe'],
]
)
);
});
sessionFocusPanel.appendChild(grid);
}
function renderAlertHistory(alertRows) {
clearNode(alertHistoryPanel);
const items = filterBySession(alertRows?.items || [], (item) => (item.payload || {}).session_code);
if (!items.length) {
alertHistoryPanel.appendChild(createElement('div', 'empty-state', 'No alert history for current filters.'));
return;
}
const grid = createElement('div', 'cards');
items.slice(0, 8).forEach((alert) => {
grid.appendChild(
buildItemCard(
alert.alert_type || 'alert',
alert.status || 'queued',
[
['message', alert.message || 'n/a'],
['created', formatTimestamp(alert.created_ts)],
['channel', alert.channel || 'workbench'],
['payload', formatJsonInline(alert.payload || {})],
]
)
);
});
alertHistoryPanel.appendChild(grid);
}
function renderAlertQuality(summary) {
clearNode(alertQualityPanel);
const delivery = summary?.delivery_summary || {};
const alertFeedback = summary?.alert_feedback_summary || {};
const visualFeedback = summary?.visual_feedback_summary || {};
const reviewSummary = summary?.visual_review_summary || {};
const silentWindow = summary?.silent_window || {};
alertQualityPanel.appendChild(
buildInfoGrid([
['recent alerts', String(summary?.recent_alert_count ?? 0)],
['recent visual reviews', String(summary?.recent_visual_review_count ?? 0)],
['silent window', `${silentWindow.active ? 'active' : 'inactive'} | ${silentWindow.window_label || 'n/a'}`],
['delivery success', formatPercent(delivery.success_rate)],
['alert usefulness', formatPercent(alertFeedback.positive_rate)],
['visual helpfulness', formatPercent(visualFeedback.positive_rate)],
['quality flags', formatList(summary?.quality_flags || [])],
['operator summary', summary?.operator_summary || 'n/a'],
])
);
const grid = createElement('div', 'cards');
grid.appendChild(
buildItemCard(
'Delivery',
delivery.failed_count ? 'warning' : 'ok',
[
['attempts', String(delivery.count ?? 0)],
['delivered / failed / skipped', `${delivery.delivered_count ?? 0} / ${delivery.failed_count ?? 0} / ${delivery.skipped_count ?? 0}`],
['silent-window skips', String(delivery.silent_window_skip_count ?? 0)],
['external send attempts', String(delivery.external_attempt_count ?? 0)],
['latest delivery', `${delivery.latest_status || 'n/a'} @ ${delivery.latest_channel || 'n/a'}`],
['latest attempted', formatTimestamp(delivery.latest_attempted_ts)],
]
)
);
grid.appendChild(
buildItemCard(
'Silent Window',
silentWindow.active ? 'warning' : 'ok',
[
['status', silentWindow.active ? 'active' : (silentWindow.enabled ? 'inactive' : 'disabled')],
['window', silentWindow.window_label || 'n/a'],
['timezone', silentWindow.timezone || 'n/a'],
['next allowed', formatTimestamp(silentWindow.next_allowed_ts)],
]
)
);
grid.appendChild(
buildItemCard(
'Alert Feedback',
alertFeedback.negative_count ? 'warning' : 'ok',
[
['feedback rows', String(alertFeedback.count ?? 0)],
['positive / negative', `${alertFeedback.positive_count ?? 0} / ${alertFeedback.negative_count ?? 0}`],
['labels', formatJsonInline(alertFeedback.label_counts || {})],
['latest label', alertFeedback.latest_label || 'n/a'],
['latest feedback', formatTimestamp(alertFeedback.latest_created_ts)],
]
)
);
grid.appendChild(
buildItemCard(
'Visual Feedback',
visualFeedback.negative_count ? 'warning' : 'ok',
[
['feedback rows', String(visualFeedback.count ?? 0)],
['positive / negative', `${visualFeedback.positive_count ?? 0} / ${visualFeedback.negative_count ?? 0}`],
['labels', formatJsonInline(visualFeedback.label_counts || {})],
['latest label', visualFeedback.latest_label || 'n/a'],
['latest feedback', formatTimestamp(visualFeedback.latest_created_ts)],
]
)
);
grid.appendChild(
buildItemCard(
'Visual States',
reviewSummary.latest_visual_state || 'n/a',
[
['review rows', String(reviewSummary.count ?? 0)],
['state counts', formatJsonInline(reviewSummary.state_counts || {})],
['latest state', reviewSummary.latest_visual_state || 'n/a'],
['latest reviewed', formatTimestamp(reviewSummary.latest_reviewed_ts)],
]
)
);
alertQualityPanel.appendChild(grid);
}
function getVisualSnapshotArtifactPath(review) {
const snapshotPayload = review?.snapshot_payload || {};
return snapshotPayload.artifact_path || snapshotPayload?.payload?.artifact_path || review?.snapshot_artifact_path || 'n/a';
}
function getVisualFeedbackSummary(review) {
const summary = review?.feedback_summary || {};
const labels = summary.labels || {};
const labelParts = Object.entries(labels).map(([label, count]) => `${label}:${count}`);
return {
count: summary.count ?? (Array.isArray(review?.feedback) ? review.feedback.length : 0),
latestLabel: summary.latest_label || 'n/a',
labels: labelParts.length ? labelParts.join(', ') : 'none',
usefulCount: summary.useful_count ?? 0,
misleadingCount: summary.misleading_count ?? 0,
actionableCount: summary.actionable_count ?? 0,
};
}
function renderVisualReviews(reviewRows, decisionPacketId) {
clearNode(visualReviewsPanel);
const items = (reviewRows?.items || []).slice(0, 10);
const latest = items[0] || null;
const latestFeedback = getVisualFeedbackSummary(latest);
visualReviewsPanel.appendChild(
buildActionBar([
{
label: 'Cleanup Old Captures',
onClick: () => runAction('/trading/visual-watch/cleanup-captures', {
retention_hours: 168,
dry_run: false,
max_delete_files: 500,
}),
},
])
);
visualReviewsPanel.appendChild(
buildInfoGrid([
['decision packet', decisionPacketId || 'n/a'],
['reviews', String(items.length)],
['latest state', latest?.visual_state || 'n/a'],
['latest source', latest?.snapshot_source_name || 'n/a'],
['latest artifact', getVisualSnapshotArtifactPath(latest)],
['latest feedback', `${latestFeedback.count} | ${latestFeedback.latestLabel}`],
['feedback labels', latestFeedback.labels],
])
);
if (!items.length) {
visualReviewsPanel.appendChild(createElement('div', 'empty-state', 'No visual reviews recorded for the current decision packet.'));
return;
}
const grid = createElement('div', 'cards');
items.forEach((review) => {
const feedbackSummary = getVisualFeedbackSummary(review);
grid.appendChild(
buildItemCard(
`Watch ${review.watch_run_id || 'n/a'}`,
review.visual_state || 'unknown',
[
['snapshot', review.snapshot_id || 'n/a'],
['source', review.snapshot_source_name || 'n/a'],
['image path', review.snapshot_image_path || 'n/a'],
['artifact path', getVisualSnapshotArtifactPath(review)],
['reviewed', formatTimestamp(review.reviewed_ts)],
['confidence', formatNumber(review.visual_confidence, 2)],
['anomalies', formatList(review.ui_anomalies || [])],
['feedback', `${feedbackSummary.count} | ${feedbackSummary.latestLabel}`],
['feedback labels', feedbackSummary.labels],
['useful / misleading / actionable', `${feedbackSummary.usefulCount} / ${feedbackSummary.misleadingCount} / ${feedbackSummary.actionableCount}`],
],
[
{
label: 'Useful',
onClick: () => runAction(`/trading/visual-watch/reviews/${review.id}/feedback`, {
label: 'visual_helpful',
note: 'marked from workbench',
created_by: 'local_operator',
payload: { source: 'workbench', feedback_type: 'visual_helpful' },
}),
},
{
label: 'Misleading',
onClick: () => runAction(`/trading/visual-watch/reviews/${review.id}/feedback`, {
label: 'visual_misleading',
note: 'marked from workbench',
created_by: 'local_operator',
payload: { source: 'workbench', feedback_type: 'visual_misleading' },
}),
},
{
label: 'Actionable',
onClick: () => runAction(`/trading/visual-watch/reviews/${review.id}/feedback`, {
label: 'actionable',
note: 'marked from workbench',
created_by: 'local_operator',
payload: { source: 'workbench', feedback_type: 'actionable' },
}),
},
],
review.visual_notes || ''
)
);
});
visualReviewsPanel.appendChild(grid);
}
function renderOperatorConsole(noteRows, reviewPacket) {
clearNode(operatorConsolePanel);
const authorInput = createElement('input');
authorInput.type = 'text';
authorInput.placeholder = 'operator';
authorInput.value = 'local_operator';
const bodyInput = createElement('textarea');
bodyInput.placeholder = 'Add a session, execution, or decision note.';
bodyInput.rows = 4;
bodyInput.className = 'operator-note-textarea';
const controls = createElement('div', 'actions');
const saveButton = createElement('button', '', 'Save Note');
saveButton.type = 'button';
saveButton.addEventListener('click', async () => {
try {
await postJson('/trading/operator-notes', {
instrument_id: Number(instrumentIdInput.value || 1),
note_scope: 'workbench',
author: authorInput.value || 'local_operator',
body: bodyInput.value || '',
session_code: sessionFilterInput.value === 'all' ? null : sessionFilterInput.value,
tags: ['workbench'],
});
bodyInput.value = '';
await refreshDashboard();
} catch (error) {
showErrorPanel(String(error));
}
});
const packetButton = createElement('button', '', 'Generate Review Packet');
packetButton.type = 'button';
packetButton.addEventListener('click', async () => {
try {
await fetchJson(`/trading/review-packet?${queryString({ instrument_id: instrumentIdInput.value || '1' })}`);
await refreshDashboard();
} catch (error) {
showErrorPanel(String(error));
}
});
controls.appendChild(saveButton);
controls.appendChild(packetButton);
operatorConsolePanel.appendChild(authorInput);
operatorConsolePanel.appendChild(bodyInput);
operatorConsolePanel.appendChild(controls);
operatorConsolePanel.appendChild(
buildInfoGrid([
['review packet', formatTimestamp(reviewPacket?.generated_ts)],
['packet executions', String((reviewPacket?.recent_executions || []).length)],
['packet alerts', String((reviewPacket?.recent_alerts || []).length)],
['packet notes', String((reviewPacket?.recent_notes || []).length)],
['council stance', reviewPacket?.research_council?.payload?.decision?.overall_stance || 'n/a'],
['council confidence', reviewPacket?.research_council?.payload?.decision?.committee_confidence != null
? formatNumber(reviewPacket?.research_council?.payload?.decision?.committee_confidence, 2)
: 'n/a'],
['optimization health', reviewPacket?.research_council_optimization_review?.health_state || 'n/a'],
['optimization summary', reviewPacket?.research_council_optimization_review?.operator_summary || 'n/a'],
['optimization next actions', formatList(reviewPacket?.research_council_optimization_review?.recommended_actions || [])],
])
);
const councilDecision = reviewPacket?.research_council?.payload?.decision || null;
if (councilDecision) {
const bullCase = councilDecision.bull_case || {};
const bearCase = councilDecision.bear_case || {};
const newsAnalysis = councilDecision.news_event_analysis || {};
const visualAnalysis = councilDecision.visual_analysis || {};
operatorConsolePanel.appendChild(
buildItemCard(
'Research Council',
councilDecision.overall_stance || 'observe_only',
[
['research summary', councilDecision.research_summary || 'n/a'],
['operator summary', councilDecision.operator_summary || 'n/a'],
['committee confidence', formatNumber(councilDecision.committee_confidence, 2)],
['agreement', formatList(councilDecision.agreement_points || [])],
['disagreement', formatList(councilDecision.disagreement_points || [])],
['follow-up checks', formatList(councilDecision.follow_up_checks || [])],
['recommended watch', formatList(councilDecision.recommended_watch_items || [])],
['operator questions', formatList(councilDecision.operator_questions || [])],
['execution veto', formatList(councilDecision.execution_veto_signals || [])],
['news/event', newsAnalysis.summary || 'n/a'],
['visual', visualAnalysis.summary || 'n/a'],
],
[],
`Bull: ${bullCase.summary || 'n/a'} | Bear: ${bearCase.summary || 'n/a'}`
)
);
}
const notes = noteRows?.items || [];
const grid = createElement('div', 'cards');
notes.slice(0, 6).forEach((note) => {
grid.appendChild(
buildItemCard(
note.author || 'operator',
note.note_scope || 'general',
[
['session', note.session_code || 'n/a'],
['created', formatTimestamp(note.created_ts)],
['tags', formatList(note.tags || [])],
['body', note.body || ''],
]
)
);
});
if (!notes.length) {
operatorConsolePanel.appendChild(createElement('div', 'empty-state', 'No operator notes yet.'));
} else {
operatorConsolePanel.appendChild(grid);
}
}
function renderPreMarketPlan(plan) {
clearNode(preMarketPlanPanel);
preMarketPlanPanel.appendChild(
buildActionBar([
{
label: 'Generate Active Plan',
onClick: () => runAction('/trading/plans/generate', {
instrument_id: Number(instrumentIdInput.value || 1),
bias_tf: timeframeInput.value || '1m',
setup_tf: timeframeInput.value || '1m',
trigger_tf: timeframeInput.value || '1m',
activate: true,
}),
},
])
);
if (!plan) {
preMarketPlanPanel.appendChild(createElement('div', 'empty-state', 'No active plan for this instrument.'));
return;
}
preMarketPlanPanel.appendChild(
buildInfoGrid([
['narrative', plan.narrative || 'n/a'],
['primary draw', formatJsonInline(plan.primary_draw)],
['secondary draw', formatJsonInline(plan.secondary_draw)],
['bias', plan.bias || 'n/a'],
['allowed sessions', formatList(plan.allowed_sessions || [])],
['invalidations', formatJsonInline(plan.invalidations || [])],
['no-trade conditions', formatList(plan.no_trade_conditions || [])],
['status', plan.status || 'unknown'],
['activated', formatTimestamp(plan.activated_ts)],
])
);
}
function renderCandidates(items) {
clearNode(tradeCandidatesPanel);
tradeCandidatesPanel.appendChild(
buildActionBar([
{
label: 'Run Watch',
onClick: () => runAction('/trading/watch/run', {
instrument_id: Number(instrumentIdInput.value || 1),
setup_tf: timeframeInput.value || '1m',
trigger_tf: timeframeInput.value || '1m',
}),
},
])
);
const filtered = filterBySearch(filterBySession(items, (item) => item.session_code), (item) => [
item.id,
item.status,
item.side,
item.session_code,
item.ai_explanation,
...(item.reason_codes || []),
...(item.missing_conditions || []),
].join(' '));
if (!filtered.length) {
tradeCandidatesPanel.appendChild(createElement('div', 'empty-state', 'No candidate rows match the current filters.'));
return;
}
const trustModels = new Map();
const trustPayload = window.__latestTrustScorePayload || { models: [] };
(trustPayload.models || []).forEach((model) => trustModels.set(model.model_code, model));
const grid = createElement('div', 'cards');
filtered.forEach((candidate) => {
const trustModel = trustModels.get(candidate.model_code) || {};
const executionAllowed = trustModel.execution_allowed !== false && candidate.executable;
const detail = `quality ${formatNumber(candidate.quality_score)} | ${candidate.ai_explanation || 'no explanation'}`;
grid.appendChild(
buildItemCard(
`Candidate #${candidate.id}`,
candidate.status,
[
['model', candidate.model_code],
['side', candidate.side || 'n/a'],
['session', candidate.session_code || 'n/a'],
['confirmation chain', formatList(candidate.present_conditions || [])],
['missing requirements', formatList(candidate.missing_conditions || [])],
['entry zone', `${formatNumber(candidate.entry_low, 3)} - ${formatNumber(candidate.entry_high, 3)}`],
['stop', formatNumber(candidate.stop_loss, 3)],
['TP1 / TP2', `${formatNumber(candidate.tp1, 3)} / ${formatNumber(candidate.tp2, 3)}`],
['RR TP1 / TP2', `${formatNumber(candidate.rr_tp1)} / ${formatNumber(candidate.rr_tp2)}`],
['expiry', formatTimestamp(candidate.expiry_ts)],
['trust mode', trustModel.operator_mode || 'unknown'],
['management rules', formatJsonInline(candidate.management_rules)],
['evidence links', formatJsonInline(candidate.evidence)],
],
executionAllowed ? [
{
label: 'Build Paper Ticket',
onClick: () => runAction(`/trading/candidates/${candidate.id}/build-ticket`, {
execution_mode: 'paper',
account_equity: 10000,
risk_pct: 0.01,
}),
},
{
label: 'Build Semi-Auto Ticket',
onClick: () => runAction(`/trading/candidates/${candidate.id}/build-ticket`, {
execution_mode: 'semi_auto',
account_equity: 10000,
risk_pct: 0.01,
}),
},
] : [],
detail
)
);
});
tradeCandidatesPanel.appendChild(grid);
}
function renderTickets(items) {
clearNode(executionTicketsPanel);
const filtered = filterBySearch(filterBySession(items, (item) => (item.ticket_snapshot || {}).signal?.session_code), (item) => {
const snapshot = item.ticket_snapshot || {};
return [
item.id,
item.status,
item.execution_mode,
snapshot.signal?.side,
snapshot.signal?.session_code,
snapshot.decision?.meta?.ai_explanation,
...(snapshot.decision?.reasons || []),
].join(' ');
});
if (!filtered.length) {
executionTicketsPanel.appendChild(createElement('div', 'empty-state', 'No execution tickets match the current filters.'));
return;
}
const grid = createElement('div', 'cards');
filtered.forEach((ticket) => {
const snapshot = ticket.ticket_snapshot || {};
const signal = snapshot.signal || {};
const decision = snapshot.decision || {};
const riskGovernor = decision.meta?.risk_governor || {};
const riskAmount = Number(snapshot.account_equity || 0) * Number(decision.risk_pct || 0);
const actions = [];
actions.push({
label: 'Risk Check',
onClick: () => runAction(`/trading/tickets/${ticket.id}/risk-check`, {
account_equity: Number(snapshot.account_equity || 10000),
risk_pct: Number(decision.risk_pct || 0.01),
}),
});
if (ticket.status === 'awaiting_confirmation') {
actions.push({
label: 'Confirm',
onClick: () => runAction(`/trading/tickets/${ticket.id}/confirm`, { confirmed_by: 'local_operator' }),
});
}
if (ticket.status === 'confirmed' && ticket.execution_mode === 'paper') {
actions.push({
label: 'Paper Execute',
onClick: () => runAction(`/trading/tickets/${ticket.id}/paper-execute`, { confirmed_by: 'local_operator' }),
});
}
if (ticket.status === 'confirmed' && ticket.execution_mode === 'semi_auto') {
actions.push({
label: 'Semi-Auto Submit',
onClick: () => runAction(`/trading/tickets/${ticket.id}/semi-auto-submit`, { confirmed_by: 'local_operator' }),
});
}
grid.appendChild(
buildItemCard(
`Ticket #${ticket.id}`,
ticket.status,
[
['mode', ticket.execution_mode],
['side', signal.side || 'n/a'],
['entry zone', `${formatNumber(signal.entry_low, 3)} - ${formatNumber(signal.entry_high, 3)}`],
['stop', formatNumber(signal.stop_loss, 3)],
['TP1 / TP2', `${formatNumber(signal.tp1, 3)} / ${formatNumber(signal.tp2, 3)}`],
['RR', `${formatNumber(decision.meta?.tp1 ? Math.abs((Number(decision.meta.tp1) - Number(decision.entry_price || 0)) / Math.max(Math.abs(Number(decision.entry_price || 0) - Number(decision.meta.stop_loss || 0)), 1e-9)) : 0)} / ${formatNumber(decision.meta?.tp2 ? Math.abs((Number(decision.meta.tp2) - Number(decision.entry_price || 0)) / Math.max(Math.abs(Number(decision.entry_price || 0) - Number(decision.meta.stop_loss || 0)), 1e-9)) : 0)}`],
['risk amount', formatNumber(riskAmount, 2)],
['expiry', formatTimestamp(ticket.expires_ts)],
['management rules', formatJsonInline(signal.target_plan?.management_rules || [])],
['risk governor result', formatJsonInline(riskGovernor.reject_codes || [])],
['confirmed by', ticket.confirmed_by || 'n/a'],
['confirmed at', formatTimestamp(ticket.confirmed_at)],
],
actions,
decision.meta?.ai_explanation || ''
)
);
});
executionTicketsPanel.appendChild(grid);
}
function renderExecutions(items) {
clearNode(positionManagementPanel);
positionManagementPanel.appendChild(
buildActionBar([
{
label: 'Reconcile Real Trades',
onClick: () => runAction('/trading/reconcile', {
instrument_id: Number(instrumentIdInput.value || 1),
}),
},
])
);
const filtered = filterExecutions(items);
if (!filtered.length) {
positionManagementPanel.appendChild(createElement('div', 'empty-state', 'No execution rows match the current filters.'));
return;
}
const grid = createElement('div', 'cards');
filtered.forEach((execution) => {
const tpProgress = [
execution.tp1_hit ? 'TP1 hit' : 'TP1 open',
execution.tp2_hit ? 'TP2 hit' : 'TP2 open',
].join(' | ');
grid.appendChild(
buildItemCard(
`Execution #${execution.execution_id}`,
execution.execution_status,
[
['mode', execution.execution_mode],
['side', execution.side || 'n/a'],
['fill state', execution.fill_state || 'n/a'],
['position state', execution.position_state || 'n/a'],
['TP progress', tpProgress],
['stop state', execution.close_reason || 'active'],
['invalidation alerts', formatList(execution.error_tags || [])],
['ticket', execution.execution_ticket_id || 'n/a'],
['opened', formatTimestamp(execution.opened_ts)],
['closed', formatTimestamp(execution.closed_ts)],
['quality', formatNumber(execution.quality_score)],
['real trade mapping', formatJsonInline(execution.reconciliation_state || {})],
['bracket state', formatJsonInline(execution.bracket_state || {})],
['reconciliation', formatJsonInline(execution.reconciliation_state || {})],
],
[
{
label: 'Cancel',
onClick: () => runAction(`/trading/executions/${execution.execution_id}/cancel`, {}),
},
{
label: 'Close',
onClick: () => runAction(`/trading/executions/${execution.execution_id}/close`, {}),
},
{
label: 'Sync',
onClick: () => runAction(`/trading/executions/${execution.execution_id}/sync`, {}),
},
],
execution.ai_explanation || ''
)
);
});
positionManagementPanel.appendChild(grid);
}
function renderPostTradeReview(summary, journalItems, weeklyReview) {
clearNode(postTradeReviewPanel);
postTradeReviewPanel.appendChild(
buildActionBar([
{
label: 'Generate Journal Backfill',
onClick: () => runAction('/trading/journal/generate', {}),
},
])
);
const top = buildInfoGrid([
['entry count', String(summary?.entry_count ?? weeklyReview?.entry_count ?? 0)],
['real trades', String(weeklyReview?.real_trade_count ?? 0)],
['framework execution rate', formatPercent(weeklyReview?.framework_execution_rate)],
['out-of-model rate', formatPercent(weeklyReview?.out_of_model_trade_rate)],
['out-of-model real trades', formatPercent(weeklyReview?.out_of_model_real_trade_rate)],
['risk violation count', String(weeklyReview?.risk_violation_count ?? 0)],
['reconciliation issues', String(weeklyReview?.reconciliation_issue_count ?? 0)],
['high quality session', formatJsonInline(weeklyReview?.high_quality_session)],
['low quality session', formatJsonInline(weeklyReview?.low_quality_session)],
['next actions', formatList(weeklyReview?.next_week_actions || [])],
]);
postTradeReviewPanel.appendChild(top);
const filteredJournal = filterBySession(journalItems, (item) => item.session_code);
if (!filteredJournal.length) {
postTradeReviewPanel.appendChild(createElement('div', 'empty-state', 'No journal entries available for this instrument.'));
return;
}
const grid = createElement('div', 'cards');
filteredJournal.slice(0, 6).forEach((entry) => {
grid.appendChild(
buildItemCard(
`Journal #${entry.id}`,
entry.compliance_state || 'review',
[
['model compliance', String(Boolean(entry.model_compliance))],
['risk compliance', String(!entry.risk_violation)],
['execution violations', formatList(entry.execution_violations || [])],
['model failure', String(Boolean(entry.model_failure))],
['execution failure', String(Boolean(entry.execution_violation))],
['next actions', formatList(entry.next_actions || [])],
],
[],
formatJsonInline(entry.summary || {})
)
);
});
postTradeReviewPanel.appendChild(grid);
}
function drawCandles(items) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!Array.isArray(items) || items.length === 0) {
ctx.fillStyle = '#9db0d3';
ctx.font = '14px sans-serif';
ctx.fillText('No candle data available.', 24, 36);
return;
}
const candles = [...items].reverse();
const highs = candles.map((item) => Number(item.high || 0));
const lows = candles.map((item) => Number(item.low || 0));
const closes = candles.map((item) => Number(item.close || 0));
const minPrice = Math.min(...lows);
const maxPrice = Math.max(...highs);
const range = Math.max(maxPrice - minPrice, 1e-9);
const chartLeft = 40;
const chartRight = canvas.width - 16;
const chartTop = 16;
const chartBottom = canvas.height - 24;
const chartHeight = chartBottom - chartTop;
const chartWidth = chartRight - chartLeft;
ctx.strokeStyle = '#24304f';
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i += 1) {
const y = chartTop + (chartHeight / 4) * i;
ctx.beginPath();
ctx.moveTo(chartLeft, y);
ctx.lineTo(chartRight, y);
ctx.stroke();
}
ctx.strokeStyle = '#4f7cff';
ctx.lineWidth = 2;
ctx.beginPath();
closes.forEach((close, index) => {
const x = chartLeft + (chartWidth * index) / Math.max(closes.length - 1, 1);
const y = chartBottom - ((close - minPrice) / range) * chartHeight;
if (index === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
ctx.fillStyle = '#9db0d3';
ctx.font = '12px sans-serif';
ctx.fillText(maxPrice.toFixed(2), 4, chartTop + 10);
ctx.fillText(minPrice.toFixed(2), 4, chartBottom);
}
function showErrorPanel(message) {
[
marketStatePanel,
runtimeStatusPanel,
decisionPacketPanel,
trustScorePanel,
liveReadinessPanel,
launchReadinessPanel,
sessionFocusPanel,
preMarketPlanPanel,
tradeCandidatesPanel,
executionTicketsPanel,
alertHistoryPanel,
visualReviewsPanel,
positionManagementPanel,
operatorConsolePanel,
postTradeReviewPanel,
alertQualityPanel,
].forEach((panel) => {
clearNode(panel);
panel.appendChild(createElement('div', 'empty-state error-state', message));
});
}
function updateLastUpdated() {
setText(lastUpdated, `Last updated: ${new Date().toLocaleTimeString()}`);
}
function renderDashboardData({
health,
candles,
tradingPlan,
marketState,
runtimeStatus,
decisionPacket,
decisionAlerts,
visualReviews,
alertQualitySummary,
decisionPacketId,
trustScore,
liveReadiness,
launchReadiness,
sessionView,
tradeCandidates,
executionTickets,
tradingExecutions,
journalSummary,
operatorNotes,
reviewPacket,
journalItems,
weeklyReview,
}) {
window.__latestTrustScorePayload = trustScore;
renderHealth(health);
renderOverview(weeklyReview, tradeCandidates.items || [], executionTickets.items || [], tradingExecutions.items || []);
renderMarketState(marketState, tradingExecutions.items || []);
renderRuntimeStatus(runtimeStatus);
renderDecisionPacket(decisionPacket, decisionAlerts);
renderTrustScore(trustScore);
renderLiveReadiness(liveReadiness);
renderLaunchReadiness(launchReadiness);
renderSessionFocus(sessionView);
renderPreMarketPlan(tradingPlan);
renderCandidates(tradeCandidates.items || []);
renderTickets(executionTickets.items || []);
renderAlertHistory(decisionAlerts);
renderAlertQuality(alertQualitySummary);
renderVisualReviews(visualReviews, decisionPacketId);
renderExecutions(tradingExecutions.items || []);
renderOperatorConsole(operatorNotes, reviewPacket);
renderPostTradeReview(journalSummary, journalItems.items || [], weeklyReview);
drawCandles(candles.items || []);
updateActiveFilters();
updateLastUpdated();
}
async function refreshDashboard() {
const instrumentId = instrumentIdInput.value || '1';
const timeframe = timeframeInput.value || '1m';
const limit = limitInput.value || '20';
try {
const [health, candles, tradingPlan, marketState, runtimeStatus, decisionPacket, decisionAlerts, alertQualitySummary, trustScore, liveReadiness, launchReadiness, sessionView, tradeCandidates, executionTickets, tradingExecutions, journalSummary, operatorNotes, reviewPacket, journalItems, weeklyReview] = await Promise.all([
fetchJson('/health'),
fetchJson(`/candles?${queryString({ instrument_id: instrumentId, timeframe, limit })}`),
fetchJson(`/trading/plans/latest?${queryString({ instrument_id: instrumentId })}`).catch(() => null),
fetchJson(`/trading/market-state?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ market_state: {} })),
fetchJson('/trading/runtime-status').catch(() => ({ scheduler_state: 'unavailable', tasks: {}, current_blockers: ['runtime_status_unavailable'] })),
fetchJson(`/trading/decision-packet?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ status: 'unavailable', action_recommendation: 'monitor' })),
fetchJson(`/trading/alerts?${queryString({ instrument_id: instrumentId, limit: 10 })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/alerts/quality-summary?${queryString({ instrument_id: instrumentId, limit: 50, visual_limit: 50 })}`).catch(() => ({ recent_alert_count: 0, recent_visual_review_count: 0, delivery_summary: {}, alert_feedback_summary: {}, visual_feedback_summary: {}, visual_review_summary: {}, quality_flags: [], operator_summary: 'alert_quality_unavailable' })),
fetchJson(`/trading/trust-score?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ models: [], whitelist: [], observe_list: [], blacklist: [], next_actions: [] })),
fetchJson(`/trading/live-readiness?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ ready_for_real_live: false, blocking_reasons: ['live_readiness_unavailable'], checklist: [] })),
fetchJson(`/trading/launch-readiness?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ assisted_trading_ready: false, real_live_ready: false, blocking_reasons: ['launch_readiness_unavailable'], warning_reasons: [], checklist: [], next_actions: [] })),
fetchJson(`/trading/session-view?${queryString({ instrument_id: instrumentId, limit: 12 })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/candidates?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/tickets?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/executions?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/journal/summary?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ entry_count: 0, next_week_actions: [] })),
fetchJson(`/trading/operator-notes?${queryString({ instrument_id: instrumentId, limit: 10 })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/review-packet?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ generated_ts: null, recent_executions: [], recent_alerts: [], recent_notes: [] })),
fetchJson(`/trading/journal?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
fetchJson(`/trading/research/weekly-review?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ entry_count: 0, next_week_actions: [] })),
]);
const decisionPacketId = decisionPacket?.id || reviewPacket?.decision_packet?.id || null;
const visualReviews = decisionPacketId
? await fetchJson(`/trading/visual-watch/reviews?${queryString({ decision_packet_id: decisionPacketId, limit: 10 })}`).catch(() => ({ items: [] }))
: ({ items: [] });
renderDashboardData({
health,
candles,
tradingPlan,
marketState,
runtimeStatus,
decisionPacket,
decisionAlerts,
alertQualitySummary,
visualReviews,
decisionPacketId,
trustScore,
liveReadiness,
launchReadiness,
sessionView,
tradeCandidates,
executionTickets,
tradingExecutions,
journalSummary,
operatorNotes,
reviewPacket,
journalItems,
weeklyReview,
});
} catch (error) {
showErrorPanel(String(error));
}
}
function configureAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
const seconds = Number(autoRefreshSecondsInput.value || 0);
if (seconds > 0) {
autoRefreshTimer = setInterval(refreshDashboard, seconds * 1000);
}
}
refreshBtn.addEventListener('click', refreshDashboard);
executionStatusFilterInput.addEventListener('change', refreshDashboard);
sessionFilterInput.addEventListener('change', refreshDashboard);
searchTextInput.addEventListener('input', refreshDashboard);
trustClassFilterInput.addEventListener('change', refreshDashboard);
autoRefreshSecondsInput.addEventListener('change', configureAutoRefresh);
configureAutoRefresh();
refreshDashboard();