aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard/dashboard.html
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:10 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:10 +0100
commitc016fc783ff29e9069a446e17876046df64760a8 (patch)
tree036850a3e3a580dfa5b73f91be53b59c267ce82a /src/api/dashboard/dashboard.html
parent7c6716358503abee5fab960c45e5fc2de5f61203 (diff)
dashboard: clocks on the page
The page compares the clocks the reports on its map carry: this node's peers', and those of the nodes it subscribes to; the node no longer gathers them from LINKS (api/dashboard/clocks.rs goes). Auth plan M3 (preparing LINKS' removal).
Diffstat (limited to 'src/api/dashboard/dashboard.html')
-rw-r--r--src/api/dashboard/dashboard.html51
1 files changed, 49 insertions, 2 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 3f02a64..5b776cd 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -546,6 +546,52 @@ function name(id) {
}
// Milliseconds as signed seconds: "+5.0 s".
const seconds = ms => (ms < 0 ? "−" : "+") + (Math.abs(ms) / 1000).toFixed(1) + " s";
+// Every member's clock against the swarm's, for the map: from the clocks the reports
+// carry of their peers (`clock` on each link: this node's own and subscribed nodes'),
+// chained from this node along the narrowest bounds, then taken against their median.
+// A member is off when it differs from the median beyond its chained bound and the
+// limit (`--clock-skew-warn`); with fewer than three members compared, none is (two
+// can't tell whose clock is off). An estimate for the eye: each node judges its own
+// clock by its peers' (`clock-warning`). By member: { offsetMs, withinMs, off }.
+function swarmClocks() {
+ // Each way between two members: how far the far end's clock is ahead, give or take.
+ const edges = new Map();
+ const edge = (from, to, offset, within) => {
+ if (!edges.has(from)) edges.set(from, []);
+ edges.get(from).push([to, offset, within]);
+ };
+ for (const [reporter, report] of Object.entries(state.graph.measured)) {
+ for (const link of report.links || []) {
+ if (!link.clock) continue;
+ edge(reporter, link.peer, link.clock.offsetMs, link.clock.withinMs);
+ edge(link.peer, reporter, -link.clock.offsetMs, link.clock.withinMs);
+ }
+ }
+ // Ahead of this node's, along the narrowest chained bounds (Dijkstra; the maps are small).
+ const reached = new Map(), queue = [[0, state.node.id, 0]];
+ while (queue.length) {
+ let next = 0;
+ for (let i = 1; i < queue.length; i++) if (queue[i][0] < queue[next][0]) next = i;
+ const [within, member, offset] = queue.splice(next, 1)[0];
+ if (reached.has(member)) continue;
+ reached.set(member, { offset, within });
+ for (const [to, step, stepWithin] of edges.get(member) || []) {
+ if (!reached.has(to)) queue.push([within + stepWithin, to, offset + step]);
+ }
+ }
+ const offsets = [...reached.values()].map(r => r.offset).sort((a, b) => a - b);
+ const median = offsets[Math.floor(offsets.length / 2)] || 0;
+ const limit = (state.stats.clock || {}).limitMs || 0;
+ const judged = reached.size >= 3 && limit > 0;
+ const clocks = {};
+ for (const [member, { offset, within }] of reached) {
+ const offsetMs = offset - median;
+ clocks[member] = { offsetMs, withinMs: within,
+ off: judged && Math.abs(offsetMs) > within + limit };
+ }
+ return clocks;
+}
+
// Whether `clock` (a peer's, `/peers`) differs from this node's beyond the limit for sure.
function clockOff(clock) {
const limit = (state.stats.clock || {}).limitMs || 0;
@@ -1391,6 +1437,7 @@ function renderGraph() {
// Above the lines and their labels, below the nodes.
children.push(...hits);
const { amber, coordinating } = inRemakes();
+ const clocks = swarmClocks();
for (const id of nodes) {
const p = positions.get(id), member = state.graph.members[id];
const reported = id in state.graph.measured;
@@ -1411,7 +1458,7 @@ function renderGraph() {
hoverable(element, id);
};
// A clock off the swarm's: a dashed orange ring.
- if (((state.graph.clocks || {})[id] || {}).off) {
+ if ((clocks[id] || {}).off) {
children.push(drawing("clock " + id, "circle", { cx: p.x, cy: p.y, r: 11, fill: "none",
stroke: "var(--warn)", "stroke-width": 1.5, "stroke-dasharray": "3 2" }));
}
@@ -1451,7 +1498,7 @@ function renderGraph() {
stroke: dormant ? "var(--bad)" : voting ? AMBER : ring ? (following.status === "live" ? "var(--self)" : "var(--muted)") : member.self ? "var(--self)" : "var(--fg)",
"stroke-width": dormant ? 2.5 : ring ? radius * 2 / 3 : 1.5 }, pressed);
// Its clock against the swarm's, as the members' reports chain it (the node's).
- const clock = ((state.graph.clocks || {})[id]);
+ const clock = clocks[id];
tooltip(circle, id + (reported ? "" : " (no links reported)") +
(following ? (following.status === "live" ? " (subscribed)" :
" (subscribed, not reached from here: " + following.why + ")") : "") +