From c016fc783ff29e9069a446e17876046df64760a8 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:10 +0100 Subject: 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). --- src/api/dashboard/dashboard.html | 51 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) (limited to 'src/api/dashboard/dashboard.html') 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 + ")") : "") + -- cgit v1.3.1