diff options
| author | tv <tv@krebsco.de> | 2026-11-05 02:06:49 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-05 02:06:49 +0100 |
| commit | 3531e97f1b1d2894538e43b453dc0b5a925c3910 (patch) | |
| tree | f8e6a8d960d8fff7fb08838bda41784efb110b4b /src/api | |
| parent | 78b750f8934b948e8c2c0ccc1188004dceccd433 (diff) | |
dashboard: let evolving nodes shine
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 45 |
1 files changed, 44 insertions, 1 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index d07f190..1bd2a19 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -2016,6 +2016,48 @@ function openDashboard(id) { }; requestAnimationFrame(step); } +// Nodes that entered a new epoch (a remake: `remakes.md`) shine: a golden halo grows +// from the node and fades, as a breath, larger. Known for this node and its peers +// (`/peers`: `localEpoch`, each peer's `epoch`); others' epochs the page doesn't see. +const SHINE_MS = 1600; +const knownEpochs = new Map(); +function epochsArrived() { + if (!state) return; + const current = new Map(); + if (state.peers.localEpoch) current.set(state.node.id, state.peers.localEpoch.id); + for (const [id, peer] of Object.entries(state.peers.peers)) { + if (peer.epoch) current.set(id, peer.epoch.id); + } + for (const [id, epoch] of current) { + const was = knownEpochs.get(id); + if (was !== undefined && was !== epoch) shine(id); + knownEpochs.set(id, epoch); + } +} +function shine(id) { + if (document.hidden || !positions.has(id)) return; + const layer = document.getElementById("flashes"); + const halo = svg("circle", { fill: "#f59e0b", stroke: "#fbbf24", "stroke-width": 3 }); + layer.append(halo); + const started = performance.now(); + const step = now => { + const p = positions.get(id); + const t = Math.min(1, Math.max(0, (now - started) / SHINE_MS)); + if (!p || t >= 1) { + halo.remove(); + return; + } + // In and out as a breath; growing from the node to about four times its size + // (staying as it is where motion is to be calm). + halo.setAttribute("cx", p.x); + halo.setAttribute("cy", p.y); + halo.setAttribute("r", calm.matches ? 22 : 10 + 36 * Math.sqrt(t)); + halo.setAttribute("fill-opacity", (.55 * Math.sin(Math.PI * t)).toFixed(2)); + halo.setAttribute("stroke-opacity", (.9 * Math.sin(Math.PI * t)).toFixed(2)); + requestAnimationFrame(step); + }; + requestAnimationFrame(step); +} // A dragged node let go of: dynamic, it moves to its place among the others again; // dragged with the middle button, it is pinned there. function dropped() { @@ -2488,7 +2530,7 @@ function connect() { // Changes while not connected went unseen: what is open is read again. if (state !== null) documentChanged({ all: true }); state = snapshot; stateAt = Date.now(); live = true; - status("live", true); pulse(); render(); trafficArrived(); + status("live", true); pulse(); render(); trafficArrived(); epochsArrived(); if (treeShown && writerTree === null) loadTree(); handOverLinks(); }); @@ -2496,6 +2538,7 @@ function connect() { if (!state) return; state = mergePatch(state, JSON.parse(event.data)); stateAt = Date.now(); pulse(); render(); trafficArrived(); + epochsArrived(); }); events.addEventListener("document", event => { pulse(); documentChanged(JSON.parse(event.data)); }); events.addEventListener("messages", event => messagesArrived(JSON.parse(event.data))); |
