From f705ed2e93d4ad3a696d7b9bc61f3eef3ab6d12a Mon Sep 17 00:00:00 2001 From: Leticia Date: Tue, 31 Mar 2026 18:55:14 +0200 Subject: [PATCH 1/4] hardening(desktop): close remaining electron p1 gaps --- desktop/main.js | 144 ++++++++++++++++++++---------- desktop/renderer/app.js | 62 +++++++++++-- desktop/renderer/modules/utils.js | 35 ++++++++ 3 files changed, 186 insertions(+), 55 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index e032f39..391b793 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -618,57 +618,39 @@ async function monitorResearchUiStartup() { } } -function resolvePythonCommand() { +function resolvePythonCandidates() { const isWindows = process.platform === "win32"; const localVenv = path.join(PROJECT_ROOT, ".venv", isWindows ? "Scripts" : "bin", isWindows ? "python.exe" : "python"); - if (localVenv && require("fs").existsSync(localVenv)) { - return localVenv; - } - return process.env.PYTHON || (isWindows ? "python" : "python3"); -} - -function extractServerUrl(line) { - const match = String(line).match(/URL:\s*(http:\/\/[^\s]+)/i); - return match ? match[1] : null; -} - -async function startResearchUiServer({ forceRestart = false } = {}) { - if (forceRestart) { - stopResearchUiServer({ force: true }); - } - - if (researchServerProcess) return; - - const existingUrl = await detectResearchUiServerUrl(); - if (existingUrl) { - researchServerOwned = false; - markResearchUiReady(existingUrl, "external"); - appendLog(`[startup] reusing existing research_ui server at ${existingUrl}`); - return; - } - - updateWorkspaceState({ - status: "starting", - serverUrl: null, - error: null, - source: "managed", + const candidates = [ + localVenv, + process.env.PYTHON || "", + isWindows ? "python" : "python3", + ] + .map((value) => String(value || "").trim()) + .filter(Boolean); + + return [...new Set(candidates)].filter((candidate) => { + if (!path.isAbsolute(candidate)) return true; + try { + fs.accessSync(candidate, fs.constants.R_OK); + return true; + } catch (_error) { + return false; + } }); - scheduleResearchStartupTimeout(); +} - researchServerProcess = spawn(resolvePythonCommand(), [SERVER_SCRIPT], { - cwd: PROJECT_ROOT, - windowsHide: true, - stdio: ["ignore", "pipe", "pipe"], - }); +function bindResearchUiProcess(processHandle, pythonCommand, candidates, candidateIndex) { + researchServerProcess = processHandle; researchServerOwned = true; - researchServerProcess.stdout.setEncoding("utf8"); - researchServerProcess.stderr.setEncoding("utf8"); + processHandle.stdout.setEncoding("utf8"); + processHandle.stderr.setEncoding("utf8"); monitorResearchUiStartup().catch((error) => { appendLog(`[startup-monitor-error] ${error.message}`); }); - researchServerProcess.stdout.on("data", (chunk) => { + processHandle.stdout.on("data", (chunk) => { String(chunk || "") .split(/\r?\n/) .forEach((line) => { @@ -678,7 +660,7 @@ async function startResearchUiServer({ forceRestart = false } = {}) { if (discoveredUrl) { isResearchUiReachable(discoveredUrl) .then((reachable) => { - if (!reachable || !researchServerProcess) return; + if (!reachable || !researchServerProcess || researchServerProcess !== processHandle) return; markResearchUiReady(discoveredUrl, "managed"); }) .catch(() => {}); @@ -686,7 +668,7 @@ async function startResearchUiServer({ forceRestart = false } = {}) { }); }); - researchServerProcess.stderr.on("data", (chunk) => { + processHandle.stderr.on("data", (chunk) => { String(chunk || "") .split(/\r?\n/) .forEach((line) => { @@ -695,7 +677,8 @@ async function startResearchUiServer({ forceRestart = false } = {}) { }); }); - researchServerProcess.on("exit", (code, signal) => { + processHandle.on("exit", (code, signal) => { + if (researchServerProcess !== processHandle) return; researchServerProcess = null; researchServerOwned = false; clearResearchStartupTimer(); @@ -707,9 +690,20 @@ async function startResearchUiServer({ forceRestart = false } = {}) { }); }); - researchServerProcess.on("error", (error) => { - researchServerProcess = null; - researchServerOwned = false; + processHandle.on("error", (error) => { + if (researchServerProcess === processHandle) { + researchServerProcess = null; + researchServerOwned = false; + } + const shouldRetry = + ["EACCES", "EPERM", "ENOENT"].includes(error?.code || "") + && candidateIndex < candidates.length - 1; + if (shouldRetry) { + const nextCommand = candidates[candidateIndex + 1]; + appendLog(`[spawn-error] ${pythonCommand} failed (${error.code}). Retrying with ${nextCommand}.`); + launchResearchUiProcess(candidates, candidateIndex + 1); + return; + } clearResearchStartupTimer(); updateWorkspaceState({ status: "error", @@ -721,6 +715,60 @@ async function startResearchUiServer({ forceRestart = false } = {}) { }); } +function launchResearchUiProcess(candidates, candidateIndex = 0) { + const pythonCommand = candidates[candidateIndex]; + appendLog(`[startup] launching research_ui with ${pythonCommand}`); + const child = spawn(pythonCommand, [SERVER_SCRIPT], { + cwd: PROJECT_ROOT, + windowsHide: true, + stdio: ["ignore", "pipe", "pipe"], + }); + bindResearchUiProcess(child, pythonCommand, candidates, candidateIndex); +} + +function extractServerUrl(line) { + const match = String(line).match(/URL:\s*(http:\/\/[^\s]+)/i); + return match ? match[1] : null; +} + +async function startResearchUiServer({ forceRestart = false } = {}) { + if (forceRestart) { + stopResearchUiServer({ force: true }); + } + + if (researchServerProcess) return; + + const existingUrl = await detectResearchUiServerUrl(); + if (existingUrl) { + researchServerOwned = false; + markResearchUiReady(existingUrl, "external"); + appendLog(`[startup] reusing existing research_ui server at ${existingUrl}`); + return; + } + + updateWorkspaceState({ + status: "starting", + serverUrl: null, + error: null, + source: "managed", + }); + scheduleResearchStartupTimeout(); + + const pythonCandidates = resolvePythonCandidates(); + if (!pythonCandidates.length) { + clearResearchStartupTimer(); + updateWorkspaceState({ + status: "error", + serverUrl: null, + error: "No usable Python interpreter was found for research_ui startup.", + source: "managed", + }); + return; + } + + launchResearchUiProcess(pythonCandidates, 0); +} + function stopResearchUiServer({ force = false } = {}) { clearResearchStartupTimer(); if (!researchServerProcess || !researchServerOwned) return; @@ -754,7 +802,7 @@ function createMainWindow() { preload: path.join(DESKTOP_ROOT, "preload.js"), contextIsolation: true, nodeIntegration: false, - sandbox: false, + sandbox: true, }, }); diff --git a/desktop/renderer/app.js b/desktop/renderer/app.js index 709b83f..71ba61c 100644 --- a/desktop/renderer/app.js +++ b/desktop/renderer/app.js @@ -17,6 +17,7 @@ import { stripWrappingQuotes as stripQuotes, titleCase as titleCaseValue, toneClass as resolveToneClass, + LruCache, uniqueRunIds as dedupeRunIds, } from "./modules/utils.js"; import { @@ -51,8 +52,12 @@ const CONFIG = { maxSweepRows: 5, maxSweepDecisionCompare: 4, persistDebounceMs: 400, + maxDetailCacheEntries: 50, + maxConsecutiveRefreshErrors: 3, }; +let unsubscribeWorkspaceState = null; + const state = { workspace: { status: "starting", serverUrl: null, logs: [], error: null }, snapshot: null, @@ -61,14 +66,20 @@ const state = { sweepDecisionStore: defaultSweepDecisionStore(), sweepDecisionLoaded: false, selectedRunIds: [], - detailCache: new Map(), + detailCache: new LruCache(CONFIG.maxDetailCacheEntries), experimentsWorkspace: { status: "idle", configs: [], sweeps: [], error: null, updatedAt: null }, experimentConfigPreviewCache: new Map(), isSubmittingLaunch: false, launchFeedback: "Use deterministic inputs or ask from chat.", refreshTimer: null, isStepbitSubmitting: false, - snapshotStatus: { status: "idle", error: null, lastSuccessAt: null }, + snapshotStatus: { + status: "idle", + error: null, + lastSuccessAt: null, + consecutiveErrors: 0, + refreshPaused: false, + }, isRetryingWorkspace: false, chatMessages: [ { @@ -177,7 +188,7 @@ document.addEventListener("DOMContentLoaded", async () => { state.workspace = initialState; renderWorkspaceState(); renderWorkflow(); - window.quantlabDesktop.onWorkspaceState((payload) => { + unsubscribeWorkspaceState = window.quantlabDesktop.onWorkspaceState((payload) => { state.workspace = payload; renderWorkspaceState(); if (payload.serverUrl) ensureRefreshLoop(); @@ -186,8 +197,12 @@ document.addEventListener("DOMContentLoaded", async () => { }); window.addEventListener("beforeunload", () => { - if (state.refreshTimer) window.clearInterval(state.refreshTimer); + stopRefreshLoop(); if (state.workspacePersistTimer) window.clearTimeout(state.workspacePersistTimer); + if (unsubscribeWorkspaceState) { + unsubscribeWorkspaceState(); + unsubscribeWorkspaceState = null; + } if (state.workspaceStoreLoaded) { window.quantlabDesktop.saveShellWorkspaceStore(serializeShellWorkspaceStore()).catch(() => {}); } @@ -296,12 +311,23 @@ function bindEvents() { } function ensureRefreshLoop() { + if (!state.workspace.serverUrl) return; refreshSnapshot(); if (!state.refreshTimer) state.refreshTimer = window.setInterval(refreshSnapshot, CONFIG.refreshIntervalMs); } +function stopRefreshLoop() { + if (state.refreshTimer) { + window.clearInterval(state.refreshTimer); + state.refreshTimer = null; + } +} + async function refreshSnapshot() { - if (!state.workspace.serverUrl) return; + if (!state.workspace.serverUrl) { + stopRefreshLoop(); + return; + } try { const runsRegistry = await window.quantlabDesktop.requestJson(CONFIG.runsIndexPath); state.detailCache.clear(); @@ -318,7 +344,13 @@ async function refreshSnapshot() { brokerHealth: extra[2].status === "fulfilled" ? extra[2].value : state.snapshot?.brokerHealth || null, stepbitWorkspace: extra[3].status === "fulfilled" ? extra[3].value : state.snapshot?.stepbitWorkspace || null, }; - state.snapshotStatus = { status: "ok", error: null, lastSuccessAt: new Date().toISOString() }; + state.snapshotStatus = { + status: "ok", + error: null, + lastSuccessAt: new Date().toISOString(), + consecutiveErrors: 0, + refreshPaused: false, + }; const validIds = new Set(getRuns().map((run) => run.run_id)); const filteredSelection = state.selectedRunIds.filter((runId) => validIds.has(runId)); if (filteredSelection.length !== state.selectedRunIds.length) { @@ -334,10 +366,15 @@ async function refreshSnapshot() { refreshExperimentsWorkspace({ focusTab: false, silent: true }); } } catch (error) { + const consecutiveErrors = (state.snapshotStatus.consecutiveErrors || 0) + 1; + const refreshPaused = consecutiveErrors >= CONFIG.maxConsecutiveRefreshErrors; + if (refreshPaused) stopRefreshLoop(); state.snapshotStatus = { status: "error", error: error?.message || "The local API is unavailable.", lastSuccessAt: state.snapshotStatus.lastSuccessAt, + consecutiveErrors, + refreshPaused, }; renderWorkspaceState(); // Keep the shell usable even if optional surfaces are down. @@ -689,10 +726,13 @@ function buildRuntimeAlert() { }; } if (state.snapshotStatus.status === "error") { + const pausedSuffix = state.snapshotStatus.refreshPaused + ? " Automatic refresh paused after repeated failures." + : ""; return { tone: "warn", actionLabel: "Retry API", - message: `API unavailable: ${state.snapshotStatus.error || "local request failed."}`, + message: `API unavailable: ${state.snapshotStatus.error || "local request failed."}${pausedSuffix}`, }; } if (state.workspace.status === "starting") { @@ -714,7 +754,15 @@ async function retryWorkspaceRuntime() { state.workspace = await window.quantlabDesktop.restartWorkspaceServer(); renderWorkspaceState(); } + state.snapshotStatus = { + ...state.snapshotStatus, + consecutiveErrors: 0, + refreshPaused: false, + }; await refreshSnapshot(); + if (state.workspace.serverUrl && !state.refreshTimer && state.snapshotStatus.status !== "error") { + ensureRefreshLoop(); + } } finally { state.isRetryingWorkspace = false; renderWorkspaceState(); diff --git a/desktop/renderer/modules/utils.js b/desktop/renderer/modules/utils.js index 69cf5c6..2f4259b 100644 --- a/desktop/renderer/modules/utils.js +++ b/desktop/renderer/modules/utils.js @@ -196,6 +196,41 @@ export function parseCsvRows(text, limit = Infinity) { return rows; } +export class LruCache { + constructor(maxSize = 50) { + this.maxSize = Math.max(1, Number(maxSize) || 50); + this.cache = new Map(); + } + + clear() { + this.cache.clear(); + } + + has(key) { + return this.cache.has(key); + } + + get(key) { + if (!this.cache.has(key)) return undefined; + const value = this.cache.get(key); + this.cache.delete(key); + this.cache.set(key, value); + return value; + } + + set(key, value) { + if (this.cache.has(key)) { + this.cache.delete(key); + } + this.cache.set(key, value); + while (this.cache.size > this.maxSize) { + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + return value; + } +} + function splitCsvLine(line) { const values = []; let current = ""; From 2d2f84fbea8dc21addd4b6a694a3c148ac8134da Mon Sep 17 00:00:00 2001 From: Leticia Date: Thu, 2 Apr 2026 11:17:01 +0200 Subject: [PATCH 2/4] fix(desktop): address sourcery review feedback --- desktop/main.js | 34 +++++++++++++++++++++---------- desktop/renderer/modules/utils.js | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index 391b793..978f063 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -41,6 +41,9 @@ let mainWindow = null; let researchServerProcess = null; let researchServerOwned = false; let researchStartupTimer = null; +let researchUiPythonCandidates = []; +let researchUiPythonCandidateIndex = 0; +let researchUiPythonCommand = ""; let workspaceState = { status: "idle", serverUrl: null, @@ -640,7 +643,7 @@ function resolvePythonCandidates() { }); } -function bindResearchUiProcess(processHandle, pythonCommand, candidates, candidateIndex) { +function bindResearchUiProcess(processHandle) { researchServerProcess = processHandle; researchServerOwned = true; @@ -695,13 +698,16 @@ function bindResearchUiProcess(processHandle, pythonCommand, candidates, candida researchServerProcess = null; researchServerOwned = false; } + const nextCandidateIndex = researchUiPythonCandidateIndex + 1; const shouldRetry = ["EACCES", "EPERM", "ENOENT"].includes(error?.code || "") - && candidateIndex < candidates.length - 1; + && nextCandidateIndex < researchUiPythonCandidates.length; if (shouldRetry) { - const nextCommand = candidates[candidateIndex + 1]; - appendLog(`[spawn-error] ${pythonCommand} failed (${error.code}). Retrying with ${nextCommand}.`); - launchResearchUiProcess(candidates, candidateIndex + 1); + researchUiPythonCandidateIndex = nextCandidateIndex; + const nextCommand = researchUiPythonCandidates[nextCandidateIndex]; + researchUiPythonCommand = nextCommand; + appendLog(`[spawn-error] ${researchUiPythonCommand} failed (${error.code}). Retrying with ${nextCommand}.`); + launchResearchUiProcess(); return; } clearResearchStartupTimer(); @@ -715,15 +721,16 @@ function bindResearchUiProcess(processHandle, pythonCommand, candidates, candida }); } -function launchResearchUiProcess(candidates, candidateIndex = 0) { - const pythonCommand = candidates[candidateIndex]; +function launchResearchUiProcess() { + const pythonCommand = researchUiPythonCandidates[researchUiPythonCandidateIndex]; + researchUiPythonCommand = pythonCommand; appendLog(`[startup] launching research_ui with ${pythonCommand}`); const child = spawn(pythonCommand, [SERVER_SCRIPT], { cwd: PROJECT_ROOT, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], }); - bindResearchUiProcess(child, pythonCommand, candidates, candidateIndex); + bindResearchUiProcess(child); } function extractServerUrl(line) { @@ -754,8 +761,10 @@ async function startResearchUiServer({ forceRestart = false } = {}) { }); scheduleResearchStartupTimeout(); - const pythonCandidates = resolvePythonCandidates(); - if (!pythonCandidates.length) { + researchUiPythonCandidates = resolvePythonCandidates(); + researchUiPythonCandidateIndex = 0; + researchUiPythonCommand = ""; + if (!researchUiPythonCandidates.length) { clearResearchStartupTimer(); updateWorkspaceState({ status: "error", @@ -766,7 +775,7 @@ async function startResearchUiServer({ forceRestart = false } = {}) { return; } - launchResearchUiProcess(pythonCandidates, 0); + launchResearchUiProcess(); } function stopResearchUiServer({ force = false } = {}) { @@ -786,6 +795,9 @@ function stopResearchUiServer({ force = false } = {}) { } researchServerProcess = null; researchServerOwned = false; + researchUiPythonCandidates = []; + researchUiPythonCandidateIndex = 0; + researchUiPythonCommand = ""; } function createMainWindow() { diff --git a/desktop/renderer/modules/utils.js b/desktop/renderer/modules/utils.js index 2f4259b..1c742bf 100644 --- a/desktop/renderer/modules/utils.js +++ b/desktop/renderer/modules/utils.js @@ -227,7 +227,7 @@ export class LruCache { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } - return value; + return this; } } From 9181906e002781b696fae9690b8f53f4c993cacf Mon Sep 17 00:00:00 2001 From: Leticia Date: Thu, 2 Apr 2026 11:23:37 +0200 Subject: [PATCH 3/4] fix(desktop): harden startup retries --- desktop/main.js | 23 +++++++++++++++-------- desktop/renderer/app.js | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/desktop/main.js b/desktop/main.js index 978f063..559aee7 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -635,7 +635,7 @@ function resolvePythonCandidates() { return [...new Set(candidates)].filter((candidate) => { if (!path.isAbsolute(candidate)) return true; try { - fs.accessSync(candidate, fs.constants.R_OK); + fs.accessSync(candidate, fs.constants.R_OK | fs.constants.X_OK); return true; } catch (_error) { return false; @@ -643,6 +643,17 @@ function resolvePythonCandidates() { }); } +function retryResearchUiProcess(nextReason) { + const nextCandidateIndex = researchUiPythonCandidateIndex + 1; + if (nextCandidateIndex >= researchUiPythonCandidates.length) return false; + const previousCommand = researchUiPythonCommand || researchUiPythonCandidates[researchUiPythonCandidateIndex]; + researchUiPythonCandidateIndex = nextCandidateIndex; + researchUiPythonCommand = researchUiPythonCandidates[nextCandidateIndex]; + appendLog(`[${nextReason}] ${previousCommand} failed. Retrying with ${researchUiPythonCommand}.`); + launchResearchUiProcess(); + return true; +} + function bindResearchUiProcess(processHandle) { researchServerProcess = processHandle; researchServerOwned = true; @@ -684,6 +695,8 @@ function bindResearchUiProcess(processHandle) { if (researchServerProcess !== processHandle) return; researchServerProcess = null; researchServerOwned = false; + const shouldRetry = code !== 0 && workspaceState.status === "starting" && retryResearchUiProcess("startup-exit"); + if (shouldRetry) return; clearResearchStartupTimer(); updateWorkspaceState({ status: "stopped", @@ -698,16 +711,10 @@ function bindResearchUiProcess(processHandle) { researchServerProcess = null; researchServerOwned = false; } - const nextCandidateIndex = researchUiPythonCandidateIndex + 1; const shouldRetry = ["EACCES", "EPERM", "ENOENT"].includes(error?.code || "") - && nextCandidateIndex < researchUiPythonCandidates.length; + && retryResearchUiProcess("spawn-error"); if (shouldRetry) { - researchUiPythonCandidateIndex = nextCandidateIndex; - const nextCommand = researchUiPythonCandidates[nextCandidateIndex]; - researchUiPythonCommand = nextCommand; - appendLog(`[spawn-error] ${researchUiPythonCommand} failed (${error.code}). Retrying with ${nextCommand}.`); - launchResearchUiProcess(); return; } clearResearchStartupTimer(); diff --git a/desktop/renderer/app.js b/desktop/renderer/app.js index 71ba61c..4e39241 100644 --- a/desktop/renderer/app.js +++ b/desktop/renderer/app.js @@ -311,7 +311,7 @@ function bindEvents() { } function ensureRefreshLoop() { - if (!state.workspace.serverUrl) return; + if (!state.workspace.serverUrl || state.snapshotStatus.refreshPaused) return; refreshSnapshot(); if (!state.refreshTimer) state.refreshTimer = window.setInterval(refreshSnapshot, CONFIG.refreshIntervalMs); } From 6656ddaa264b4410fe4a784caf480c3298002f17 Mon Sep 17 00:00:00 2001 From: Leticia Date: Thu, 2 Apr 2026 11:57:00 +0200 Subject: [PATCH 4/4] feat(desktop): add execution frontier dashboard --- desktop/renderer/app.js | 108 +++++++++++++++++++++++++++++++++++- desktop/renderer/index.html | 18 ++++++ desktop/renderer/styles.css | 72 ++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 1 deletion(-) diff --git a/desktop/renderer/app.js b/desktop/renderer/app.js index 4e39241..d5e3881 100644 --- a/desktop/renderer/app.js +++ b/desktop/renderer/app.js @@ -38,6 +38,7 @@ const CONFIG = { launchControlPath: "/api/launch-control", paperHealthPath: "/api/paper-sessions-health", brokerHealthPath: "/api/broker-submissions-health", + hyperliquidSurfacePath: "/api/hyperliquid-surface", stepbitWorkspacePath: "/api/stepbit-workspace", detailArtifacts: ["report.json", "run_report.json"], experimentsConfigDir: "configs/experiments", @@ -103,6 +104,10 @@ const elements = { runtimeAlert: document.getElementById("runtime-alert"), runtimeRetry: document.getElementById("runtime-retry"), runtimeChips: document.getElementById("runtime-chips"), + frontierMeta: document.getElementById("frontier-meta"), + frontierSummary: document.getElementById("frontier-summary"), + frontierGrid: document.getElementById("frontier-grid"), + frontierCallout: document.getElementById("frontier-callout"), chatLog: document.getElementById("chat-log"), chatForm: document.getElementById("chat-form"), chatInput: document.getElementById("chat-input"), @@ -335,6 +340,7 @@ async function refreshSnapshot() { window.quantlabDesktop.requestJson(CONFIG.launchControlPath), window.quantlabDesktop.requestJson(CONFIG.paperHealthPath), window.quantlabDesktop.requestJson(CONFIG.brokerHealthPath), + window.quantlabDesktop.requestJson(CONFIG.hyperliquidSurfacePath), window.quantlabDesktop.requestJson(CONFIG.stepbitWorkspacePath), ]); state.snapshot = { @@ -342,7 +348,8 @@ async function refreshSnapshot() { launchControl: extra[0].status === "fulfilled" ? extra[0].value : state.snapshot?.launchControl || null, paperHealth: extra[1].status === "fulfilled" ? extra[1].value : state.snapshot?.paperHealth || null, brokerHealth: extra[2].status === "fulfilled" ? extra[2].value : state.snapshot?.brokerHealth || null, - stepbitWorkspace: extra[3].status === "fulfilled" ? extra[3].value : state.snapshot?.stepbitWorkspace || null, + hyperliquidSurface: extra[3].status === "fulfilled" ? extra[3].value : state.snapshot?.hyperliquidSurface || null, + stepbitWorkspace: extra[4].status === "fulfilled" ? extra[4].value : state.snapshot?.stepbitWorkspace || null, }; state.snapshotStatus = { status: "ok", @@ -680,6 +687,7 @@ function renderMarkupInto(container, markup) { function renderWorkspaceState() { const { status, serverUrl, error, source } = state.workspace; const runs = getRuns(); + const frontier = getFrontierSnapshot(); const stepbit = state.snapshot?.stepbitWorkspace?.live_urls || {}; const paperCount = state.snapshot?.paperHealth?.total_sessions || 0; const brokerCount = state.snapshot?.brokerHealth?.total_sessions || 0; @@ -713,9 +721,107 @@ function renderWorkspaceState() { createRuntimeChipNode("Stepbit app", stepbit.frontend_reachable ? "up" : "down", stepbit.frontend_reachable ? "up" : "down"), createRuntimeChipNode("Stepbit core", stepbit.core_ready ? "ready" : stepbit.core_reachable ? "up" : "down", stepbit.core_ready ? "up" : stepbit.core_reachable ? "warn" : "down"), ); + renderFrontierDashboard(frontier); renderChatAdapterStatus(); } +function getFrontierSnapshot() { + const paper = state.snapshot?.paperHealth || null; + const broker = state.snapshot?.brokerHealth || null; + const hyperliquid = state.snapshot?.hyperliquidSurface || null; + const stepbit = state.snapshot?.stepbitWorkspace?.live_urls || {}; + return { paper, broker, hyperliquid, stepbit }; +} + +function renderFrontierDashboard(frontier) { + if (!elements.frontierMeta || !elements.frontierSummary || !elements.frontierGrid || !elements.frontierCallout) return; + const { paper, broker, hyperliquid, stepbit } = frontier; + const paperReady = Boolean(paper?.available && paper?.total_sessions); + const brokerReady = Boolean(broker?.available); + const brokerAlerts = Boolean(broker?.has_alerts); + const hyperliquidReady = Boolean(hyperliquid?.available); + const hyperliquidAlerts = Boolean(hyperliquid?.submit_has_alerts); + const stepbitReady = Boolean(stepbit?.core_ready || stepbit?.backend_reachable || stepbit?.frontend_reachable); + + clearElement(elements.frontierSummary); + appendChildren( + elements.frontierSummary, + createSummaryCardNode("Paper sessions", String(paper?.total_sessions ?? 0), paperReady ? "tone-positive" : "tone-warning"), + createSummaryCardNode("Broker boundary", brokerReady ? "Visible" : "Missing", brokerAlerts ? "tone-negative" : brokerReady ? "tone-positive" : "tone-warning"), + createSummaryCardNode("Hyperliquid surface", hyperliquidReady ? titleCase(hyperliquid?.latest_ready_artifact_type || "Ready") : "Missing", hyperliquidAlerts ? "tone-negative" : hyperliquidReady ? "tone-positive" : "tone-warning"), + createSummaryCardNode("Stepbit boundary", stepbitReady ? "Reachable" : "Down", stepbitReady ? "tone-positive" : "tone-warning"), + ); + + clearElement(elements.frontierGrid); + appendChildren( + elements.frontierGrid, + createFrontierCard("Paper bridge", paperReady ? "ready for promotion review" : "still building local session evidence", [ + ["Latest session", paper?.latest_session_id || "-"], + ["Latest status", titleCase(paper?.latest_session_status || "none")], + ["Issue watch", paper?.latest_issue_session_id || "none"], + ], paperReady ? "positive" : "warning"), + createFrontierCard("Broker boundary", brokerReady ? "validation and alerts visible" : "no broker validation surface yet", [ + ["Latest submit", broker?.latest_submit_session_id || "-"], + ["Latest state", broker?.latest_submit_state || "-"], + ["Alerts", broker?.has_alerts ? String((broker?.alerts || []).length) : "none"], + ], brokerAlerts ? "negative" : brokerReady ? "positive" : "warning"), + createFrontierCard("Hyperliquid", hyperliquidReady ? "signer and submit surfaces are tracked" : "surface not indexed yet", [ + ["Latest artifact", hyperliquid?.latest_ready_artifact_type || "-"], + ["Signature state", titleCase(hyperliquid?.signature_state || "unknown")], + ["Alert status", titleCase(hyperliquid?.submit_alert_status || "unknown")], + ], hyperliquidAlerts ? "negative" : hyperliquidReady ? "positive" : "warning"), + createFrontierCard("Stepbit", stepbitReady ? "optional runtime boundary is reachable" : "external boundary is offline", [ + ["Frontend", stepbit?.frontend_reachable ? "up" : "down"], + ["Backend", stepbit?.backend_reachable ? "up" : "down"], + ["Core", stepbit?.core_ready ? "ready" : stepbit?.core_reachable ? "up" : "down"], + ], stepbitReady ? "positive" : "warning"), + ); + + elements.frontierMeta.textContent = [ + paperReady ? `${paper.total_sessions} paper sessions` : "paper not yet visible", + brokerReady ? `${formatNumericCount(broker?.total_sessions || 0)} broker sessions` : "broker not yet visible", + hyperliquidReady ? `${formatNumericCount(hyperliquid?.submit_health?.total_sessions || 0)} Hyperliquid submit sessions` : "Hyperliquid surface not yet visible", + ].join(" ยท "); + + elements.frontierCallout.textContent = buildFrontierCallout({ paper, broker, hyperliquid, stepbit }); +} + +function buildFrontierCallout({ paper, broker, hyperliquid, stepbit }) { + if (broker?.has_alerts) { + return "Broker alerts are present. Inspect the broker boundary before trusting any submit-oriented flow."; + } + if (hyperliquid?.submit_has_alerts) { + return "Hyperliquid submit alerts are present. Inspect submit sessions and reconciliation before moving further."; + } + if (paper?.available && paper?.total_sessions) { + return `Paper sessions are visible with ${paper.total_sessions} tracked sessions. Use the ops view to decide which sessions are ready to bridge.`; + } + if (stepbit?.backend_reachable || stepbit?.core_reachable) { + return "The optional Stepbit boundary is partially reachable. Keep it as an external helper, not as QuantLab's control plane."; + } + return "Launch a run or wait for local session artifacts so the frontier cards can surface real operator state."; +} + +function createSummaryCardNode(label, value, tone = "") { + return createElementNode("article", { className: "summary-card frontier-summary-card" }, [ + createElementNode("div", { className: "label", text: label }), + createElementNode("div", { className: `value ${tone}`, text: value }), + ]); +} + +function createFrontierCard(title, subtitle, rows, tone = "neutral") { + return createElementNode("article", { className: `frontier-card tone-${tone}` }, [ + createElementNode("div", { className: "frontier-card-head" }, [ + createElementNode("div", { className: "section-label", text: title }), + createElementNode("p", { className: "frontier-card-subtitle", text: subtitle }), + ]), + createElementNode("dl", { className: "frontier-metric-list metric-list compact" }, rows.flatMap(([label, value]) => [ + createElementNode("dt", { text: label }), + createElementNode("dd", { text: value }), + ])), + ]); +} + function buildRuntimeAlert() { if (state.workspace.status === "error" || state.workspace.status === "stopped") { const recentLogs = (state.workspace.logs || []).slice(-4).join("\n"); diff --git a/desktop/renderer/index.html b/desktop/renderer/index.html index 0ddf84b..81e3528 100644 --- a/desktop/renderer/index.html +++ b/desktop/renderer/index.html @@ -131,6 +131,24 @@

Service state

+
+
+
+ +

Paper, broker, and Hyperliquid

+
+
Waiting for local readiness surfaces...
+
+
+
+
+
+ + + +
+
+
diff --git a/desktop/renderer/styles.css b/desktop/renderer/styles.css index 5181533..102579b 100644 --- a/desktop/renderer/styles.css +++ b/desktop/renderer/styles.css @@ -406,6 +406,78 @@ button:disabled { .runtime-chip.warn { color: var(--warning); border-color: rgba(240, 179, 93, 0.32); } .runtime-chip.down { color: var(--danger); border-color: rgba(242, 125, 136, 0.32); } +.frontier-panel { + display: grid; + gap: 16px; +} + +.frontier-summary { + margin-top: 2px; +} + +.frontier-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; +} + +.frontier-card { + border: 1px solid var(--border); + border-radius: 16px; + padding: 16px; + background: linear-gradient(180deg, rgba(18, 28, 40, 0.94), rgba(8, 14, 22, 0.82)); + display: grid; + gap: 12px; +} + +.frontier-card.tone-positive { + border-color: rgba(118, 211, 156, 0.28); +} + +.frontier-card.tone-warning { + border-color: rgba(240, 179, 93, 0.28); +} + +.frontier-card.tone-negative { + border-color: rgba(242, 125, 136, 0.28); +} + +.frontier-card-subtitle { + margin: 6px 0 0; + color: var(--muted); + line-height: 1.5; +} + +.frontier-metric-list { + margin: 0; +} + +.frontier-metric-list dt { + color: var(--muted); + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.frontier-metric-list dd { + margin: 4px 0 0; + font-weight: 600; + line-height: 1.45; +} + +.frontier-callout { + border: 1px solid var(--border); + border-radius: 16px; + padding: 14px 16px; + background: rgba(8, 14, 22, 0.82); + color: var(--muted); + line-height: 1.55; +} + +.frontier-actions { + margin-top: 2px; +} + .workflow-panel { display: grid; gap: 16px;