Auto-commit: v2.13 — Add Live Status diagnostic panel to run_details UI

- Backend (web/routes/run.py): Extended /run/<id>/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 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 18:48:06 -05:00
parent 4e39e18dfe
commit 97efd51fd5
2 changed files with 91 additions and 5 deletions

View File

@@ -286,6 +286,25 @@
</div> </div>
{% endif %} {% endif %}
<!-- Live Status Panel -->
<div class="card shadow-sm mb-3">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-3 flex-wrap">
<span class="small text-muted fw-semibold">Poll:</span>
<span id="poll-state" class="badge bg-secondary">Initializing...</span>
<span class="small text-muted">Last update:</span>
<span id="last-update-time" class="small fw-bold text-info"></span>
<span id="db-diagnostics" class="small text-muted"></span>
</div>
<button class="btn btn-sm btn-outline-info py-0 px-2" onclick="forceRefresh()" title="Immediately trigger a new poll request">
<i class="fas fa-bolt me-1"></i>Force Refresh
</button>
</div>
<div id="poll-error-msg" class="small text-danger mt-1" style="display:none;"></div>
</div>
</div>
<!-- Collapsible Log --> <!-- Collapsible Log -->
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#logCollapse"> <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#logCollapse">
@@ -344,6 +363,8 @@
const costEl = document.getElementById('run-cost'); const costEl = document.getElementById('run-cost');
let lastLog = ''; let lastLog = '';
let pollTimer = null;
let countdownInterval = null;
// Phase → colour mapping (matches utils.log phase labels) // Phase → colour mapping (matches utils.log phase labels)
const PHASE_COLORS = { const PHASE_COLORS = {
@@ -386,10 +407,67 @@
return ''; 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() { function updateLog() {
setPollState('Requesting...', 'bg-primary');
fetch(`/run/${runId}/status`) fetch(`/run/${runId}/status`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .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 // Update Status Text + current phase
const statusLabel = data.status.charAt(0).toUpperCase() + data.status.slice(1); const statusLabel = data.status.charAt(0).toUpperCase() + data.status.slice(1);
if (data.status === 'running') { if (data.status === 'running') {
@@ -435,11 +513,13 @@
} }
} }
// Poll if running // Schedule next poll or stop
if (data.status === 'running' || data.status === 'queued') { if (data.status === 'running' || data.status === 'queued') {
setTimeout(updateLog, 2000); startWaitCountdown(2, false);
pollTimer = setTimeout(updateLog, 2000);
} else { } 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') { if (initialStatus === 'running' || initialStatus === 'queued') {
window.location.reload(); window.location.reload();
} }
@@ -447,7 +527,10 @@
}) })
.catch(err => { .catch(err => {
console.error("Polling failed:", err); console.error("Polling failed:", err);
setTimeout(updateLog, 5000); const errMsg = err.message || String(err);
setPollError(errMsg);
startWaitCountdown(5, true);
pollTimer = setTimeout(updateLog, 5000);
}); });
} }

View File

@@ -138,7 +138,10 @@ def run_status(id):
"log": log_content, "log": log_content,
"cost": run.cost, "cost": run.cost,
"percent": run.progress, "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: if last_log: