feat(run-page): tick the run-duration timer between SSE pushes
CI / Lint + build + test (push) Successful in 1m34s
Release / release (push) Has been cancelled

Adds a 1s client-side ticker that rewrites .run-duration text from a
data-started-at attribute, so the header timer on /runs/{id}
increments every second while the run is active. When an SSE swap
lands a fresh header the new server-rendered value seamlessly takes
over; when the run goes terminal the template drops the attribute
and the ticker silently skips the node, leaving the final elapsed in
place.

Other templ_*.go churn is cosmetic — regenerator versions differ
between CI and local and only the filename field in templ.Error
callsites changed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 21:53:40 -04:00
parent 05ceb8e042
commit c545028903
9 changed files with 263 additions and 198 deletions
+36 -1
View File
@@ -84,7 +84,42 @@
});
});
// --- 3. permalink scroll + highlight on load ------------------------
// --- 3. live duration tick ------------------------------------------
//
// .run-duration spans carry data-started-at (RFC3339) while the run is
// non-terminal. Every second we rewrite their text with the current
// elapsed so the header timer ticks between SSE pushes. When an SSE
// swap drops the attribute (run finished), the tick silently skips it
// and the server-rendered final value stays put.
function formatDuration(ms) {
if (ms < 0) { ms = 0; }
if (ms < 1000) { return Math.floor(ms) + 'ms'; }
if (ms < 10000) { return (ms / 1000).toFixed(1) + 's'; }
if (ms < 60000) { return Math.floor(ms / 1000) + 's'; }
if (ms < 3600000) {
var m = Math.floor(ms / 60000);
var s = Math.floor((ms % 60000) / 1000);
return m + 'm ' + s + 's';
}
var h = Math.floor(ms / 3600000);
var mm = Math.floor((ms % 3600000) / 60000);
return h + 'h ' + mm + 'm';
}
function tickDurations() {
var now = Date.now();
var nodes = document.querySelectorAll('.run-duration[data-started-at]');
nodes.forEach(function (el) {
var startMs = Date.parse(el.getAttribute('data-started-at'));
if (isNaN(startMs)) { return; }
el.textContent = formatDuration(now - startMs);
});
}
setInterval(tickDurations, 1000);
// --- 4. permalink scroll + highlight on load ------------------------
function scrollToHash() {
var hash = (location.hash || '').replace(/^#/, '');