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 searchTextInput = document.getElementById('searchText'); 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 preMarketPlanPanel = document.getElementById('preMarketPlanPanel'); const tradeCandidatesPanel = document.getElementById('tradeCandidatesPanel'); const executionTicketsPanel = document.getElementById('executionTicketsPanel'); const positionManagementPanel = document.getElementById('positionManagementPanel'); 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 search = getSearchText(); if (executionStatus !== 'all') active.push(`execution=${executionStatus}`); 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'; return filterBySearch( items.filter((item) => statusFilter === 'all' || item.execution_status === statusFilter), (item) => [ item.execution_id, item.execution_status, item.side, item.session_code, item.close_reason, item.ai_explanation, ...(item.error_tags || []), ].join(' ') ); } 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 || []; 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)], ['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]) => { grid.appendChild( buildItemCard( name, 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}`], ['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'], ['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'], ]) ); 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 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(items, (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 grid = createElement('div', 'cards'); filtered.forEach((candidate) => { 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)], ['management rules', formatJsonInline(candidate.management_rules)], ['evidence links', formatJsonInline(candidate.evidence)], ], [ { 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(items, (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); 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)], ['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)], ['framework execution rate', formatPercent(weeklyReview?.framework_execution_rate)], ['out-of-model rate', formatPercent(weeklyReview?.out_of_model_trade_rate)], ['risk violation count', String(weeklyReview?.risk_violation_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); if (!journalItems.length) { postTradeReviewPanel.appendChild(createElement('div', 'empty-state', 'No journal entries available for this instrument.')); return; } const grid = createElement('div', 'cards'); journalItems.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, preMarketPlanPanel, tradeCandidatesPanel, executionTicketsPanel, positionManagementPanel, postTradeReviewPanel, ].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, tradeCandidates, executionTickets, tradingExecutions, journalSummary, journalItems, weeklyReview, }) { renderHealth(health); renderOverview(weeklyReview, tradeCandidates.items || [], executionTickets.items || [], tradingExecutions.items || []); renderMarketState(marketState, tradingExecutions.items || []); renderRuntimeStatus(runtimeStatus); renderDecisionPacket(decisionPacket, decisionAlerts); renderPreMarketPlan(tradingPlan); renderCandidates(tradeCandidates.items || []); renderTickets(executionTickets.items || []); renderExecutions(tradingExecutions.items || []); 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, tradeCandidates, executionTickets, tradingExecutions, journalSummary, 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/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/journal?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })), fetchJson(`/trading/research/weekly-review?${queryString({ instrument_id: instrumentId })}`).catch(() => ({ entry_count: 0, next_week_actions: [] })), ]); renderDashboardData({ health, candles, tradingPlan, marketState, runtimeStatus, decisionPacket, decisionAlerts, tradeCandidates, executionTickets, tradingExecutions, journalSummary, 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); searchTextInput.addEventListener('input', refreshDashboard); autoRefreshSecondsInput.addEventListener('change', configureAutoRefresh); configureAutoRefresh(); refreshDashboard();