aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard/dashboard.html
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 00:47:39 +0100
committertv <tv@krebsco.de>2026-11-07 00:47:39 +0100
commit6d0ef1dd825805cee445fc1dd40337151a8cafb6 (patch)
tree5a959f46bb0f57105a16f1ede4ef060c86e1b493 /src/api/dashboard/dashboard.html
parent06dca76de30422a9cfdb45a7879c83b3f91cc1e4 (diff)
dashboard: remake with golden count down halos
Diffstat (limited to 'src/api/dashboard/dashboard.html')
-rw-r--r--src/api/dashboard/dashboard.html87
1 files changed, 55 insertions, 32 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 06bf267..9390af4 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -1368,19 +1368,30 @@ function renderGraph() {
const dormant = (state.peers.peers[id] || {}).status === "dormant";
const radius = member.self ? 8 : 6;
mark(id, "circle", { cx: p.x, cy: p.y, r: radius + 6, fill: "rgb(127 127 127)" });
- // In a remake (`inRemakes`): amber; its coordinator also ringed.
+ // In a remake (`inRemakes`): amber; its coordinator also with a halo, or deciding.
const voting = amber.has(id);
- if (coordinating.has(id)) {
- // 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: countdownTurn(id, p) } : {}) }));
+ // Its remake's wait (`countdowns`), and whether it is over: settled but not decided
+ // yet (a question mark on it, below), or committed with this node's wait to
+ // switch over (nothing: it switches any moment).
+ const countdown = coordinating.has(id) ? countdowns.get(id) : undefined;
+ const over = countdown !== undefined && countdown.until <= performance.now();
+ const deciding = over && countdown.kind === "settle";
+ if (coordinating.has(id) && !over) {
+ // While it settles, and once committed until this node switches: a golden halo,
+ // smaller than the one in focus and over it, waning counterclockwise from the top
+ // of the screen, however the map is turned; whole while no wait is known yet
+ // (just proposed, or committed before this node saw it). A circle half the halo's
+ // radius, its stroke as wide as the radius: a disc, its dashes a slice of it.
+ const dash = countdownDash(id, COUNTDOWN_R / 2);
+ children.push(drawing("countdown " + id, "circle", { cx: p.x, cy: p.y,
+ r: COUNTDOWN_R / 2, fill: "none", stroke: AMBER, "stroke-width": COUNTDOWN_R,
+ "stroke-opacity": .55, "pointer-events": "none",
+ ...(dash ? { "stroke-dasharray": dash, transform: countdownTurn(p) } : {}) }));
if (dash && countdownFrame === null) countdownFrame = requestAnimationFrame(tickCountdowns);
}
+ // Deciding: hollow, so its question mark shows.
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)",
+ fill: deciding ? "var(--panel)" : voting ? AMBER : member.self ? "var(--self)" : reported ? "var(--fg)" : "var(--panel)",
stroke: dormant ? "var(--bad)" : voting ? AMBER : member.self ? "var(--self)" : "var(--fg)",
"stroke-width": dormant ? 2.5 : 1.5 }, pressed);
// Its clock against the swarm's, as the members' reports chain it (the node's).
@@ -1397,7 +1408,14 @@ function renderGraph() {
const nameLabel = drawing("name " + id, "text", upright({ x: p.x + 10, y: p.y + 4,
"font-size": 12, fill: "var(--fg)", cursor: "pointer" }), pressed);
label(nameLabel, name(id));
- children.push(circle, ...pin, nameLabel);
+ // Deciding: a golden question mark in it. Without the map's labels' edge in the
+ // background colour (`#graph text`, 4 px wide: a style, which only a style
+ // overrides), which would cover the node's rim.
+ const asking = deciding ? [drawing("deciding " + id, "text", upright({ x: p.x, y: p.y,
+ "font-size": radius * 1.7, "font-weight": "bold", fill: AMBER, style: "stroke: none",
+ "text-anchor": "middle", "dominant-baseline": "central", "pointer-events": "none" }))] : [];
+ for (const question of asking) text(question, "?");
+ children.push(circle, ...pin, ...asking, nameLabel);
}
marks.pointed.replaceChildren(...marked.pointed);
marks.shown.replaceChildren(...marked.shown);
@@ -2234,7 +2252,7 @@ function breathe(id, { fill, rim, to, ms, opacity }) {
// Remakes on the map (`remakes` in the snapshot: those of this node's epoch, with their
// coordinator, phase and voters). While one is open (proposed, or committed until they
-// switch), its coordinator and voters are amber, the coordinator also ringed; a small
+// switch), its coordinator and voters are amber, the coordinator with a halo; a small
// amber breath as one is proposed (its coordinator) and as each vote comes. Decided, a
// pop over all of them: green if committed, red if aborted or expired, and they turn
// back. Only changes seen while the page is open: those of before show as they are.
@@ -2302,11 +2320,12 @@ function remakesArrived() {
}
// 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.
+// this node's wait to switch). Each is a golden halo, whole at first, waning
+// counterclockwise to the top as the wait runs out: the votes', then, once committed,
+// whole again, the switch's, gone as the epoch switches.
const countdowns = new Map();
+// The halo's radius: within the one of a node in focus (13), drawn over it.
+const COUNTDOWN_R = 11;
// When each wait was first seen (`remakesArrived`), by remake ID and kind.
const anchors = new Map();
let countdownFrame = null;
@@ -2316,31 +2335,35 @@ function countdownDash(id, r) {
if (!countdown) return null;
const left = Math.min(1, Math.max(0, (countdown.until - performance.now()) / countdown.total));
const around = 2 * Math.PI * r;
- const shown = countdown.kind === "settle" ? 1 - left : left;
- return (around * shown).toFixed(1) + " " + around.toFixed(1);
+ return (around * left).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;
+// Where a halo at `p` starts: 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, so what is left ends further back counterclockwise as it wanes.
+function countdownTurn(p) {
+ return "rotate(" + (-90 - degrees(view.turn)) + " " + p.x + " " + p.y + ")";
}
-// Every frame while a ring counts down: its dashes, and nothing else drawn anew.
+// Every frame while a halo counts: its dashes and where they start, nothing else drawn
+// anew. Both: a key's element may have been drawn before it counted, and then it
+// starts at its right, going clockwise, until the next render turns it.
function tickCountdowns() {
countdownFrame = null;
const now = performance.now();
if (document.hidden || ![...countdowns.values()].some(c => c.until > now - 100)) return;
+ // A wait just over: drawn anew, for what shows then (`render`: a question mark, or
+ // nothing).
+ const ended = [...countdowns.values()].filter(c => c.until <= now && !c.ended);
+ for (const countdown of ended) countdown.ended = true;
+ if (ended.length) renderGraph();
for (const id of countdowns.keys()) {
- const ring = drawn.get("coordinator " + id);
- if (ring) {
+ const ring = drawn.get("countdown " + id);
+ if (ring && ring.isConnected) {
const dash = countdownDash(id, Number(ring.getAttribute("r")));
- if (dash) ring.setAttribute("stroke-dasharray", dash);
+ if (dash) {
+ ring.setAttribute("stroke-dasharray", dash);
+ ring.setAttribute("transform", countdownTurn(
+ { x: Number(ring.getAttribute("cx")), y: Number(ring.getAttribute("cy")) }));
+ }
}
}
countdownFrame = requestAnimationFrame(tickCountdowns);