aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 00:14:08 +0100
committertv <tv@krebsco.de>2026-11-07 00:14:08 +0100
commit06dca76de30422a9cfdb45a7879c83b3f91cc1e4 (patch)
tree80ca470bfe3cd458d1c6943f85247d0de488d71f /src/api
parentdf40d6d2aed2d1a3b5c02a0a2e14c7c234d035ef (diff)
dashboard: polish remake rings
Diffstat (limited to 'src/api')
-rw-r--r--src/api/dashboard/dashboard.html55
1 files changed, 42 insertions, 13 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 771b49b..06bf267 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -1371,12 +1371,12 @@ 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.
+ // Filling while it settles, counting down once committed (`countdowns`): both
+ // from the top of the screen, however the map is turned.
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",
- ...(dash ? { "stroke-dasharray": dash,
- transform: "rotate(-90 " + p.x + " " + p.y + ")" } : {}) }));
+ ...(dash ? { "stroke-dasharray": dash, transform: countdownTurn(id, p) } : {}) }));
if (dash && countdownFrame === null) countdownFrame = requestAnimationFrame(tickCountdowns);
}
const circle = drawing("node " + id, "circle", { cx: p.x, cy: p.y, r: radius,
@@ -2253,12 +2253,22 @@ function remakesArrived() {
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 });
+ // Settling: how long its votes are collected; committed: this node's wait to switch.
+ // Anchored when first seen, not with every update (each moved it by however old the
+ // update was: jumps, with short waits), and from there on whole: it starts and ends
+ // at the top, even where most of the wait was over before this page saw it.
+ const [kind, left] = remake.phase === "proposed" && typeof remake.settleInMs === "number"
+ ? ["settle", remake.settleInMs]
+ : remake.phase === "committed" && typeof remake.switchInMs === "number" &&
+ remake.switchDelayMs > 0 ? ["switch", remake.switchInMs] : [null, 0];
+ const key = remake.id + " " + kind;
+ if (kind && !anchors.has(key)) {
+ anchors.set(key, { until: performance.now() + left, total: Math.max(1, left) });
}
+ // Committed and over (it switches any moment): an empty ring, not a whole one again.
+ const anchor = anchors.get(key) ||
+ (remake.phase === "committed" && anchors.get(remake.id + " switch"));
+ if (anchor) countdowns.set(remake.coordinator, { ...anchor, kind: kind || "switch" });
const was = knownRemakes.get(remake.id);
if (remakesKnown) {
const breath = id => breathe(id, { fill: AMBER, rim: AMBER, to: 20, ms: 900, opacity: .45 });
@@ -2287,20 +2297,39 @@ function remakesArrived() {
}
knownRemakes.delete(id);
}
+ for (const key of anchors.keys()) if (!seen.has(key.split(" ")[0])) anchors.delete(key);
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.
+// Open remakes' coordinators, by node ID: when the wait ends (by `performance.now()`),
+// the whole wait (ms), and which: `settle` (votes collected) or `switch` (committed,
+// this node's wait to switch). The ring of one settling fills: empty at first, growing
+// counterclockwise from the top; once committed it counts down: a full circle from the
+// top, its end going back counterclockwise as the wait runs out, gone as the epoch
+// switches. Both meet at the top, so the one hands over to the other there.
const countdowns = new Map();
+// When each wait was first seen (`remakesArrived`), by remake ID and kind.
+const anchors = new Map();
let countdownFrame = null;
-// The ring's dashes for `id` (radius `r`), if it counts down: the part left.
+// The ring's dashes for `id` (radius `r`), if it counts: the part filled, or 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);
+ const shown = countdown.kind === "settle" ? 1 - left : left;
+ return (around * shown).toFixed(1) + " " + around.toFixed(1);
+}
+// Where the ring of `id` (at `p`) starts, and which way it goes: a circle's stroke
+// starts at its right and goes clockwise; turned a quarter back (and back from the
+// map's own turn, `view`), it starts at the top of the screen. Settling, mirrored
+// about the upright through its centre: counterclockwise from there.
+function countdownTurn(id, p) {
+ const turn = "rotate(" + (-90 - degrees(view.turn)) + " " + p.x + " " + p.y + ")";
+ if ((countdowns.get(id) || {}).kind !== "settle") return turn;
+ const a = degrees(view.turn);
+ // The mirror about the screen's upright through p, in the map's turned frame.
+ return "rotate(" + -a + " " + p.x + " " + p.y + ") translate(" + 2 * p.x + " 0) scale(-1 1) rotate(" +
+ a + " " + p.x + " " + p.y + ") " + turn;
}
// Every frame while a ring counts down: its dashes, and nothing else drawn anew.
function tickCountdowns() {