From 97efd51fd50008f0e69998ad0e31bf726b12dbec Mon Sep 17 00:00:00 2001 From: Mike Wichers Date: Sat, 21 Feb 2026 18:48:06 -0500 Subject: [PATCH] =?UTF-8?q?Auto-commit:=20v2.13=20=E2=80=94=20Add=20Live?= =?UTF-8?q?=20Status=20diagnostic=20panel=20to=20run=5Fdetails=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend (web/routes/run.py): Extended /run//status JSON response with server_timestamp, db_log_count, and latest_log_timestamp so clients can detect whether the DB is being written to independently of the log text. - Frontend (templates/run_details.html): • Added Live Status Panel above the System Log card, showing: - Polling state badge (Initializing / Requesting / Waiting Ns / Error / Idle) - Last Successful Update timestamp (HH:MM:SS, updated every successful poll) - DB diagnostics (log count + latest log timestamp from server response) - Last Error message displayed inline when a poll fails - Force Refresh button to immediately trigger a new poll • Refactored JS polling loop: countdown timer with clearCountdown/ startWaitCountdown helpers, forceRefresh() clears pending timers before re-polling, explicit pollTimer/countdownInterval tracking. Co-Authored-By: Claude Sonnet 4.6 --- templates/run_details.html | 91 ++++++++++++++++++++++++++++++++++++-- web/routes/run.py | 5 ++- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/templates/run_details.html b/templates/run_details.html index 3e9ddfe..7e105da 100644 --- a/templates/run_details.html +++ b/templates/run_details.html @@ -286,6 +286,25 @@ {% endif %} + +
+
+
+
+ Poll: + Initializing... + Last update: + + +
+ +
+ +
+
+
@@ -344,6 +363,8 @@ const costEl = document.getElementById('run-cost'); let lastLog = ''; + let pollTimer = null; + let countdownInterval = null; // Phase → colour mapping (matches utils.log phase labels) const PHASE_COLORS = { @@ -386,10 +407,67 @@ return ''; } + // --- Live Status Panel helpers --- + + function clearCountdown() { + if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } + } + + function setPollState(text, badgeClass) { + const el = document.getElementById('poll-state'); + if (el) { el.className = 'badge ' + badgeClass; el.innerText = text; } + } + + function setPollError(msg) { + const el = document.getElementById('poll-error-msg'); + if (!el) return; + if (msg) { el.innerText = 'Last error: ' + msg; el.style.display = ''; } + else { el.innerText = ''; el.style.display = 'none'; } + } + + function startWaitCountdown(seconds, isError) { + clearCountdown(); + let rem = seconds; + const cls = isError ? 'bg-danger' : 'bg-secondary'; + const prefix = isError ? 'Error — retry in' : 'Waiting'; + setPollState(prefix + ' (' + rem + 's)', cls); + countdownInterval = setInterval(() => { + rem--; + if (rem <= 0) { clearCountdown(); } + else { setPollState(prefix + ' (' + rem + 's)', cls); } + }, 1000); + } + + function forceRefresh() { + clearCountdown(); + if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } + updateLog(); + } + + // --- Main polling function --- + function updateLog() { + setPollState('Requesting...', 'bg-primary'); fetch(`/run/${runId}/status`) .then(response => response.json()) .then(data => { + // Update "Last Successful Update" timestamp + const now = new Date(); + const lastUpdateEl = document.getElementById('last-update-time'); + if (lastUpdateEl) lastUpdateEl.innerText = now.toLocaleTimeString(); + + // Update DB diagnostics + const diagEl = document.getElementById('db-diagnostics'); + if (diagEl) { + const parts = []; + if (data.db_log_count !== undefined) parts.push('DB logs: ' + data.db_log_count); + if (data.latest_log_timestamp) parts.push('Latest: ' + String(data.latest_log_timestamp).substring(11, 19)); + diagEl.innerText = parts.join(' | '); + } + + // Clear any previous poll error + setPollError(null); + // Update Status Text + current phase const statusLabel = data.status.charAt(0).toUpperCase() + data.status.slice(1); if (data.status === 'running') { @@ -435,11 +513,13 @@ } } - // Poll if running + // Schedule next poll or stop if (data.status === 'running' || data.status === 'queued') { - setTimeout(updateLog, 2000); + startWaitCountdown(2, false); + pollTimer = setTimeout(updateLog, 2000); } else { - // If the run was active when we loaded the page, reload now that it's finished to show artifacts + setPollState('Idle', 'bg-success'); + // If the run was active when we loaded the page, reload to show artifacts if (initialStatus === 'running' || initialStatus === 'queued') { window.location.reload(); } @@ -447,7 +527,10 @@ }) .catch(err => { console.error("Polling failed:", err); - setTimeout(updateLog, 5000); + const errMsg = err.message || String(err); + setPollError(errMsg); + startWaitCountdown(5, true); + pollTimer = setTimeout(updateLog, 5000); }); } diff --git a/web/routes/run.py b/web/routes/run.py index 16c546f..5fb5116 100644 --- a/web/routes/run.py +++ b/web/routes/run.py @@ -138,7 +138,10 @@ def run_status(id): "log": log_content, "cost": run.cost, "percent": run.progress, - "start_time": run.start_time.timestamp() if run.start_time else None + "start_time": run.start_time.timestamp() if run.start_time else None, + "server_timestamp": datetime.utcnow().isoformat() + "Z", + "db_log_count": len(logs), + "latest_log_timestamp": last_log.timestamp.isoformat() if last_log else None, } if last_log: