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:
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user