diff options
| author | tv <tv@krebsco.de> | 2026-11-05 02:58:27 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-05 02:58:27 +0100 |
| commit | c5f67f56f46ea5c0b300a2e0eb00a7368efe4f2a (patch) | |
| tree | e4a9235173e546b008fb222be12862bd0075b96e /src/api | |
| parent | c10f9144b1436d4918855a657715b453a6228a44 (diff) | |
dashboard: add epoch switch indicator
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 44 | ||||
| -rw-r--r-- | src/api/dashboard/mod.rs | 1 |
2 files changed, 44 insertions, 1 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 5180038..ede191d 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -1236,8 +1236,13 @@ function renderGraph() { // In a remake (`inRemakes`): amber; its coordinator also ringed. const voting = amber.has(id); if (coordinating.has(id)) { + // Counting down once committed (`countdowns`): from the top, as a clock's hand. + const dash = countdownDash(id, radius + 4); children.push(drawing("coordinator " + id, "circle", { cx: p.x, cy: p.y, r: radius + 4, - fill: "none", stroke: AMBER, "stroke-width": 1, "pointer-events": "none" })); + fill: "none", stroke: AMBER, "stroke-width": 1, "pointer-events": "none", + ...(dash ? { "stroke-dasharray": dash, + transform: "rotate(-90 " + p.x + " " + p.y + ")" } : {}) })); + if (dash && countdownFrame === null) countdownFrame = requestAnimationFrame(tickCountdowns); } const circle = drawing("node " + id, "circle", { cx: p.x, cy: p.y, r: radius, fill: voting ? AMBER : member.self ? "var(--self)" : reported ? "var(--fg)" : "var(--panel)", @@ -2096,8 +2101,15 @@ function remakesArrived() { breathe(id, { fill, rim: fill, to: 26, ms: 600, opacity: .6 }); } }; + countdowns.clear(); for (const remake of remakes) { seen.add(remake.id); + // Committed: this node's wait to switch, as of now (re-anchored with every update). + if (remake.phase === "committed" && typeof remake.switchInMs === "number" && + remake.switchDelayMs > 0) { + countdowns.set(remake.coordinator, { until: performance.now() + remake.switchInMs, + total: remake.switchDelayMs }); + } const was = knownRemakes.get(remake.id); if (remakesKnown) { const breath = id => breathe(id, { fill: AMBER, rim: AMBER, to: 20, ms: 900, opacity: .45 }); @@ -2113,6 +2125,9 @@ function remakesArrived() { } knownRemakes.set(remake.id, remake); } + if (countdowns.size > 0 && countdownFrame === null) { + countdownFrame = requestAnimationFrame(tickCountdowns); + } // Gone from the snapshot: this node switched to the next epoch (the records stay in // the old one's document). Committed then, if it was still open. for (const [id, remake] of knownRemakes) { @@ -2125,6 +2140,33 @@ function remakesArrived() { } remakesKnown = true; } +// Committed remakes' coordinators, by node ID: when this node switches (by +// `performance.now()`) and the whole wait (ms). Their ring counts down: a full circle +// from the top, going counterclockwise as the wait runs out, gone as the epoch switches. +const countdowns = new Map(); +let countdownFrame = null; +// The ring's dashes for `id` (radius `r`), if it counts down: the part left. +function countdownDash(id, r) { + const countdown = countdowns.get(id); + if (!countdown) return null; + const left = Math.min(1, Math.max(0, (countdown.until - performance.now()) / countdown.total)); + const around = 2 * Math.PI * r; + return (around * left).toFixed(1) + " " + around.toFixed(1); +} +// Every frame while a ring counts down: its dashes, and nothing else drawn anew. +function tickCountdowns() { + countdownFrame = null; + const now = performance.now(); + if (document.hidden || ![...countdowns.values()].some(c => c.until > now - 100)) return; + for (const id of countdowns.keys()) { + const ring = drawn.get("coordinator " + id); + if (ring) { + const dash = countdownDash(id, Number(ring.getAttribute("r"))); + if (dash) ring.setAttribute("stroke-dasharray", dash); + } + } + countdownFrame = requestAnimationFrame(tickCountdowns); +} // Who is amber now (`remakesArrived`), and who of them coordinates: of the open // remakes, those not past the epoch yet (`knownEpochs`). function inRemakes() { diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs index e83f485..831df6f 100644 --- a/src/api/dashboard/mod.rs +++ b/src/api/dashboard/mod.rs @@ -323,6 +323,7 @@ async fn build(state: &AppState, graph: &Graph, live: bool) -> JsonValue { let replica = state.doc.read().await; // The remakes of our epoch, with who voted: the map shows them. let remakes = crate::remake::current( + &state.remake, &replica.doc, replica.epoch, state.clock.now_millis(), |
