aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:29 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:29 +0100
commit03f6af0ef3b6917f07861f32283d8b1fd885c238 (patch)
tree863f6c80c7e724ce779e545f3e592d86c790b111 /src/api/dashboard
parentfa695849c3b854670df7642b91a7937316e32b5b (diff)
dashboard: message flashes
messages: off / dots / flash: a message expands at its own speed and leaves an afterglow fading out in the direction it went.
Diffstat (limited to 'src/api/dashboard')
-rw-r--r--src/api/dashboard/dashboard.html124
1 files changed, 93 insertions, 31 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 17ff44b..ee8627c 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -329,7 +329,7 @@
</div>
<div class="legend" id="legend"></div>
<div class="legend" id="messages-legend"><button id="flows-toggle"
- class="toggle">messages: on</button></div>
+ class="toggle">messages: dots</button></div>
<div class="legend">
<span class="hint">drag to pan, wheel or pinch to zoom, turn with two fingers;
drag a node to move it, middle-click it to pin it there or let it go,
@@ -339,7 +339,7 @@
</div>
</div>
<div id="node-menu" hidden></div>
- <svg id="graph"><g id="view"><g id="flows" pointer-events="none"></g><g id="flashes" pointer-events="none"></g></g></svg>
+ <svg id="graph"><g id="view"><g id="flows" pointer-events="none"><g id="flow-dots"></g><g id="flow-streaks"></g></g><g id="flashes" pointer-events="none"></g></g></svg>
</div>
</section>
<section id="peers-box">
@@ -2712,29 +2712,55 @@ graph.addEventListener("mouseleave", () => hover(null));
// (`remoteMessagesArrived`), drawn as dots running along the link the way the messages
// went. Counts come with each update (every 2 s), so the dots are spread
// over the next 2 s, a few at most per link and way: a picture of the traffic, not a
-// record of it. All at one speed (`flowSpeed`), so over a longer link they arrive
-// later. Kept above everything else on the map (`flows`, after what `show` places). ---
+// record of it. As dots, all at one speed (`flowSpeed`), so over a longer link they
+// arrive later; as flashes, each crosses its link as fast as the message did (half the
+// link's round trip) and leaves an afterglow that draws back into where it went
+// (`FLASH_GLOW_MS`). Kept above everything else on the map (`flows`, after what `show`
+// places). ---
const FLOW_SPREAD_MS = 2000, FLOWS_AT_MOST = 6;
-// Shown or not (`flows-toggle`, kept in this browser; shown at first).
-let flowsShown = true;
-try { flowsShown = localStorage.getItem("continuum-flows") !== "0"; } catch (_) {}
+// A flash's afterglow, after it arrived; and how long it takes over a link whose round
+// trip isn't known.
+const FLASH_GLOW_MS = 700, FLASH_UNKNOWN_MS = 50;
+// How they are shown (`flows-toggle`, kept in this browser; dots at first): `dots`,
+// `flash` or `off`.
+const FLOW_MODES = ["dots", "flash", "off"];
+let flowsMode = "dots";
+try {
+ const kept = localStorage.getItem("continuum-flows");
+ flowsMode = FLOW_MODES.includes(kept) ? kept : kept === "0" ? "off" : "dots";
+} catch (_) {}
+let flowsShown = flowsMode !== "off";
const flowsToggle = document.getElementById("flows-toggle");
function showFlowsToggle() {
- flowsToggle.textContent = flowsShown ? "messages: on" : "messages: off";
- flowsToggle.title = "Click to " + (flowsShown ? "hide" : "show") +
- " the messages running along this node's links";
+ const next = FLOW_MODES[(FLOW_MODES.indexOf(flowsMode) + 1) % FLOW_MODES.length];
+ flowsToggle.textContent = "messages: " + flowsMode;
+ flowsToggle.title = "Click for " + (next === "off" ? "none"
+ : next === "dots" ? "dots running along the links"
+ : "flashes: each as fast as the message, with an afterglow toward where it went");
}
showFlowsToggle();
flowsToggle.addEventListener("click", () => {
- flowsShown = !flowsShown;
- try { localStorage.setItem("continuum-flows", flowsShown ? "1" : "0"); } catch (_) {}
+ flowsMode = FLOW_MODES[(FLOW_MODES.indexOf(flowsMode) + 1) % FLOW_MODES.length];
+ flowsShown = flowsMode !== "off";
+ try { localStorage.setItem("continuum-flows", flowsMode); } catch (_) {}
showFlowsToggle();
showMessagesLegend();
if (!flowsShown) {
flows.length = 0;
- document.getElementById("flows").replaceChildren();
+ document.getElementById("flow-dots").replaceChildren();
+ document.getElementById("flow-streaks").replaceChildren();
}
});
+// How long a message takes from `from` to `to`: half the round trip either end
+// measured, as the map has it (`FLASH_UNKNOWN_MS` if neither did).
+function oneWayMs(from, to) {
+ for (const [a, b] of [[from, to], [to, from]]) {
+ const report = state && state.graph.measured[a];
+ const link = report && (report.links || []).find(link => link.peer === b);
+ if (link && typeof link.rttMs === "number") return link.rttMs / 2;
+ }
+ return FLASH_UNKNOWN_MS;
+}
let lastTraffic = null;
// When the last update's traffic came, by the wall clock (it runs on while the computer
// sleeps, unlike `performance.now()` on some systems).
@@ -2775,7 +2801,7 @@ function showMessagesLegend() {
try { localStorage.setItem("continuum-dot-scale", String(dotScale)); } catch (_) {}
});
const speed = speedSlider = el("input", { type: "range", min: ".1", max: ".9", step: ".1",
- title: "How fast the dots run" });
+ title: "How fast the dots run (flashes run as fast as the messages did)" });
speed.value = String(flowSpeed);
speed.addEventListener("input", () => {
flowSpeed = Number(speed.value);
@@ -2788,7 +2814,8 @@ function showMessagesLegend() {
el("label", { class: "slider" }, "size", slider),
el("label", { class: "slider" }, "speed", speed));
}
- dotSlider.disabled = speedSlider.disabled = !flowsShown;
+ dotSlider.disabled = !flowsShown;
+ speedSlider.disabled = flowsMode !== "dots";
}
showMessagesLegend();
// A dot's radius for a message of `bytes`: grows with the size's logarithm, from 1.5
@@ -3155,37 +3182,72 @@ function showConsole() {
}
function drawFlows() {
flowFrame = null;
- const layer = document.getElementById("flows");
const now = performance.now();
- // How far along its link each is (0 to 1), as long as the link is now: arrived, or
- // its link gone, it goes.
- const along = flow => {
+ // Where each is: as a dot, how far along its link (0 to 1) as long as the link is now;
+ // as a flash, its head and its afterglow's tail (each 0 to 1) and how bright. Arrived
+ // (a flash: its afterglow gone), or its link gone, it goes.
+ const where = flow => {
const p = positions.get(flow.from), q = positions.get(flow.to);
if (!p || !q) return null;
const length = Math.max(Math.hypot(q.x - p.x, q.y - p.y), 1);
- return { p, q, t: (now - flow.start) * flowSpeed / length };
+ if (flowsMode === "flash") {
+ const travel = flow.travel || (flow.travel = Math.max(1, oneWayMs(flow.from, flow.to)));
+ const age = now - flow.start, glow = Math.max(0, (age - travel) / FLASH_GLOW_MS);
+ return { p, q, length, done: glow >= 1, head: Math.min(1, age / travel),
+ tail: 1 - (1 - Math.min(1, glow)) ** 2, opacity: 1 - Math.min(1, glow) };
+ }
+ const t = (now - flow.start) * flowSpeed / length;
+ return { p, q, length, done: t >= 1, t };
};
for (let i = flows.length - 1; i >= 0; i--) {
- const at = along(flows[i]);
- if (at === null || at.t >= 1) flows.splice(i, 1);
+ const at = where(flows[i]);
+ if (at === null || at.done) flows.splice(i, 1);
}
- const dots = [];
+ const dots = [], streaks = [];
for (const flow of flows) {
if (now < flow.start) continue;
- const { p, q, t } = along(flow);
- dots.push({ x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t, r: flow.r, kind: flow.kind,
- opacity: Math.sin(Math.PI * t) });
+ const at = where(flow);
+ const { p, q } = at;
+ const r = flow.r * dotScale, colour = kindColour(flow.kind);
+ if (flowsMode === "flash") {
+ // From the tail, a point, widening to the head, rounded: brightest where the
+ // message is, drawing back toward it as it fades.
+ const ux = (q.x - p.x) / at.length, uy = (q.y - p.y) / at.length;
+ const hx = p.x + (q.x - p.x) * at.head, hy = p.y + (q.y - p.y) * at.head;
+ const tx = p.x + (q.x - p.x) * Math.min(at.tail, at.head);
+ const ty = p.y + (q.y - p.y) * Math.min(at.tail, at.head);
+ const w = r * .8, nx = -uy * w, ny = ux * w;
+ const f = n => n.toFixed(1);
+ streaks.push({ colour, opacity: at.opacity, d: "M" + f(tx) + " " + f(ty) +
+ "L" + f(hx + nx) + " " + f(hy + ny) +
+ "A" + f(w) + " " + f(w) + " 0 0 0 " + f(hx - nx) + " " + f(hy - ny) + "Z" });
+ } else {
+ dots.push({ x: p.x + (q.x - p.x) * at.t, y: p.y + (q.y - p.y) * at.t, r, colour,
+ opacity: Math.sin(Math.PI * at.t) });
+ }
}
- while (layer.children.length < dots.length) layer.append(svg("circle"));
- while (layer.children.length > dots.length) layer.lastChild.remove();
+ const layer = (id, count, tag) => {
+ const group = document.getElementById(id);
+ while (group.children.length < count) group.append(svg(tag));
+ while (group.children.length > count) group.lastChild.remove();
+ return group;
+ };
+ const dotLayer = layer("flow-dots", dots.length, "circle");
dots.forEach((dot, i) => {
- const circle = layer.children[i];
+ const circle = dotLayer.children[i];
circle.setAttribute("cx", dot.x);
circle.setAttribute("cy", dot.y);
- circle.setAttribute("r", (dot.r * dotScale).toFixed(1));
- circle.setAttribute("fill", kindColour(dot.kind));
+ circle.setAttribute("r", dot.r.toFixed(1));
+ circle.setAttribute("fill", dot.colour);
circle.setAttribute("opacity", dot.opacity.toFixed(2));
});
+ const streakLayer = layer("flow-streaks", streaks.length, "path");
+ streaks.forEach((streak, i) => {
+ const path = streakLayer.children[i];
+ path.setAttribute("d", streak.d);
+ path.setAttribute("fill", streak.colour);
+ path.setAttribute("opacity", streak.opacity.toFixed(2));
+ });
if (flows.length > 0) flowFrame = requestAnimationFrame(drawFlows);
}