aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard/dashboard.html
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-05 02:06:49 +0100
committertv <tv@krebsco.de>2026-11-05 02:06:49 +0100
commit3531e97f1b1d2894538e43b453dc0b5a925c3910 (patch)
treef8e6a8d960d8fff7fb08838bda41784efb110b4b /src/api/dashboard/dashboard.html
parent78b750f8934b948e8c2c0ccc1188004dceccd433 (diff)
dashboard: let evolving nodes shine
Diffstat (limited to 'src/api/dashboard/dashboard.html')
-rw-r--r--src/api/dashboard/dashboard.html45
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)));