diff --git a/research_ui/app.js b/research_ui/app.js index 4c11ebe..cb95c17 100644 --- a/research_ui/app.js +++ b/research_ui/app.js @@ -2,6 +2,7 @@ const CONFIG = { registryPath: "/outputs/runs/runs_index.json", launchControlPath: "/api/launch-control", paperHealthPath: "/api/paper-sessions-health", + paperAlertsPath: "/api/paper-sessions-alerts", brokerHealthPath: "/api/broker-submissions-health", hyperliquidSurfacePath: "/api/hyperliquid-surface", pretradeHandoffPath: "/api/pretrade-handoff-intake", @@ -24,6 +25,7 @@ const state = { detailCache: new Map(), isLoading: false, paperHealth: null, + paperAlerts: null, brokerHealth: null, hyperliquidSurface: null, pretradeIntake: null, @@ -81,6 +83,8 @@ const elements = { compareSummaryLong: document.getElementById("compare-summary-long"), compareBody: document.getElementById("compare-body"), opsSummary: document.getElementById("ops-summary"), + paperSummary: document.getElementById("paper-summary"), + paperPanelBody: document.getElementById("paper-panel-body"), pretradeSummary: document.getElementById("pretrade-summary"), pretradePanelBody: document.getElementById("pretrade-panel-body"), paperTotalSessions: document.getElementById("paper-total-sessions"), @@ -169,7 +173,7 @@ async function fetchAll(showNotice = false, silent = false) { try { const registryResponse = await fetchJson(CONFIG.registryPath); - const [launchControl, paperHealth, brokerHealth, hyperliquidSurface, pretradeIntake, stepbitWorkspace, metaTradeWorkspace] = await Promise.all([ + const [launchControl, paperHealth, paperAlerts, brokerHealth, hyperliquidSurface, pretradeIntake, stepbitWorkspace, metaTradeWorkspace] = await Promise.all([ fetchJsonSafe(CONFIG.launchControlPath, { status: "error", available: false, @@ -184,6 +188,15 @@ async function fetchAll(showNotice = false, silent = false) { status_counts: {}, message: "Paper health unavailable.", }), + fetchJsonSafe(CONFIG.paperAlertsPath, { + status: "error", + available: false, + total_sessions: 0, + status_counts: {}, + alert_counts: {}, + alerts: [], + message: "Paper alerts unavailable.", + }), fetchJsonSafe(CONFIG.brokerHealthPath, { status: "error", available: false, @@ -231,6 +244,7 @@ async function fetchAll(showNotice = false, silent = false) { state.generatedAt = registryResponse.generated_at || null; state.launchControl = launchControl; state.paperHealth = paperHealth; + state.paperAlerts = paperAlerts; state.brokerHealth = brokerHealth; state.hyperliquidSurface = hyperliquidSurface; state.pretradeIntake = pretradeIntake; @@ -688,6 +702,7 @@ function renderRuntimeChip(label, value, tone) { function renderOps() { const paperHealth = state.paperHealth || {}; + const paperAlerts = state.paperAlerts || {}; const brokerHealth = state.brokerHealth || {}; const hyperliquidSurface = state.hyperliquidSurface || {}; const pretradeIntake = state.pretradeIntake || {}; @@ -695,7 +710,9 @@ function renderOps() { const metaTradeWorkspace = state.metaTradeWorkspace || {}; elements.paperTotalSessions.textContent = String(paperHealth.total_sessions || 0); - elements.paperHealthMeta.textContent = buildPaperMeta(paperHealth); + elements.paperHealthMeta.textContent = buildPaperMeta(paperHealth, paperAlerts); + elements.paperSummary.textContent = buildPaperSummary(paperHealth, paperAlerts); + elements.paperPanelBody.innerHTML = buildPaperPanel(paperHealth, paperAlerts); elements.brokerTotalSessions.textContent = String(brokerHealth.total_sessions || 0); elements.brokerHealthMeta.textContent = buildBrokerMeta(brokerHealth); elements.hyperliquidState.textContent = buildHyperliquidState(hyperliquidSurface); @@ -706,7 +723,7 @@ function renderOps() { elements.stepbitMeta.textContent = buildStepbitMeta(stepbitWorkspace); elements.metaTradeState.textContent = metaTradeWorkspace.available ? "Ready" : "Boundary"; elements.metaTradeMeta.textContent = buildMetaTradeMeta(metaTradeWorkspace); - elements.opsSummary.textContent = buildOpsSummary(paperHealth, brokerHealth, hyperliquidSurface, pretradeIntake); + elements.opsSummary.textContent = buildOpsSummary(paperHealth, paperAlerts, brokerHealth, hyperliquidSurface, pretradeIntake); elements.sidebarBoundaryMeta.textContent = [ stepbitWorkspace.available ? "Stepbit connected" : "Stepbit boundary", metaTradeWorkspace.available ? "Meta Trade connected" : "Meta Trade boundary", @@ -773,18 +790,153 @@ function keyValue(label, value) { `; } -function buildPaperMeta(health) { +function buildPaperMeta(health, alerts) { if (health.status === "error") { return health.message || "Paper health unavailable"; } if (!health.available) { return "No paper session root yet."; } + if (alerts?.available && alerts.has_alerts) { + const latestAlert = alerts.latest_alert_code ? titleCase(alerts.latest_alert_code) : "Alert active"; + return `${latestAlert} · threshold ${alerts.stale_after_minutes || "-"}m`; + } return health.latest_session_id ? `${health.latest_session_id} · ${titleCase(health.latest_session_status || "unknown")}` : "No paper sessions yet."; } +function buildPaperSummary(health, alerts) { + if (health.status === "error" || alerts.status === "error") { + return "Paper operations unavailable."; + } + if (!health.available) { + return "Waiting for the first paper session root."; + } + if (alerts.has_alerts) { + return `${alerts.alerts?.length || 0} active alert(s) across ${health.total_sessions || 0} session(s)`; + } + if ((alerts.running_sessions || []).length) { + return `${alerts.running_sessions.length} running session(s) below stale threshold`; + } + return health.latest_session_id + ? `Latest session ${health.latest_session_id} looks healthy` + : "No paper sessions recorded yet."; +} + +function buildPaperPanel(health, alerts) { + if (health.status === "error" || alerts.status === "error") { + const message = alerts.message || health.message || "The paper operations surface could not be loaded."; + return ` +