fix(web): Started toggle survives the Executions card's 5s poll

The card's own htmx poll re-renders the whole table from the server every
5s, always starting fresh at "relative" -- so a toggled-to-absolute cell
snapped back on the next poll. Each button now has a stable id
(started-{run_id}-{attempt}, or started-vm-{vmx} on an unmatched-VM row);
app.js remembers which format was showing per id and re-applies it to the
fresh button after every swap, rather than freezing the node itself (which
would also freeze its relative-time text, badge, duration, everything else
in that row).
This commit is contained in:
2026-07-26 00:25:33 +02:00
parent 0d9c5ab353
commit bfa90190ea
4 changed files with 60 additions and 20 deletions
+40 -11
View File
@@ -197,27 +197,56 @@
}).observe(stack, { childList: true });
}
document.body.addEventListener("htmx:afterSwap", function (evt) {
var target = evt.detail && evt.detail.target;
if (target && target.id === "action-slot") {
processSlot(target);
}
});
// Executions "Started" cell: click swaps between the relative and
// absolute time already rendered into the button's own data attributes --
// no request. Delegated on body (not the button itself) so it keeps
// working after the Executions card's own 5s htmx poll replaces the row.
//
// The poll re-renders the whole card from the server every 5s, always
// starting back at "relative" -- so the choice is remembered here, keyed
// by the button's own id (stable across polls: run id/attempt or vmx),
// and re-applied to the fresh button restoreTimeToggles() finds after each
// swap, rather than freezing the node itself (which would also freeze its
// relative-time text, badge, duration -- everything else in the row).
var timeToggleState = {};
function applyTimeToggle(btn, showing) {
var shown = showing === "relative" ? "absolute" : "relative";
btn.textContent = btn.dataset[showing];
btn.title = btn.dataset[shown];
btn.dataset.showing = showing;
}
function restoreTimeToggles(root) {
var buttons = root.querySelectorAll ? root.querySelectorAll(".time-toggle[id]") : [];
Array.prototype.forEach.call(buttons, function (btn) {
var saved = timeToggleState[btn.id];
if (saved && saved !== btn.dataset.showing) {
applyTimeToggle(btn, saved);
}
});
}
document.body.addEventListener("click", function (evt) {
var btn = evt.target.closest(".time-toggle");
if (!btn) {
return;
}
var next = btn.dataset.showing === "relative" ? "absolute" : "relative";
var shown = next === "relative" ? "absolute" : "relative";
btn.textContent = btn.dataset[next];
btn.title = btn.dataset[shown];
btn.dataset.showing = next;
applyTimeToggle(btn, next);
if (btn.id) {
timeToggleState[btn.id] = next;
}
});
document.body.addEventListener("htmx:afterSwap", function (evt) {
var target = evt.detail && evt.detail.target;
if (target && target.id === "action-slot") {
processSlot(target);
}
if (target) {
restoreTimeToggles(target);
}
});
document.addEventListener("DOMContentLoaded", function () {