aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-04 22:15:50 +0100
committertv <tv@krebsco.de>2026-11-04 22:15:50 +0100
commitf8da04220992bd64caa66522b3a689eb1b03fbbc (patch)
tree06814148a92f88d9e8ba0a1148111814c6bcea2e /src/api/dashboard
parentf137a43b44684959a5e1de1f1e54566d8995926f (diff)
dashboard: hop delays
Diffstat (limited to 'src/api/dashboard')
-rw-r--r--src/api/dashboard/dashboard.html38
1 files changed, 35 insertions, 3 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 94e3dfb..876c386 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -32,6 +32,8 @@
header h1 { font-size: 16px; margin: 0; }
#connection { margin-left: auto; color: var(--muted); }
#clock-warning, #peers .clock-off { color: var(--warn); }
+ /* A minute's bar each, the last 15: a link's trend at a glance. */
+ #peers .spark { font-family: ui-monospace, monospace; letter-spacing: 0; opacity: 0.7; }
#connection.live { color: var(--ok); }
#connection.down { color: var(--bad); }
/* A dot in the status's colour that breathes once with every event. */
@@ -429,9 +431,11 @@ function renderPeers() {
const table = document.getElementById("peers");
const headers = { reaches: "How many members besides this node it reaches (an active " +
"session with each), as it said; +: those of them this node doesn't reach itself (it " +
- "is a way to them)" };
- const rows = [el("tr", {}, ...["peer", "status", "seen", "via", "rtt", "clock", "neighbour",
- "reaches", "same epoch"]
+ "is a way to them)", hop: "How long its sync messages take to come, by its clock and " +
+ "this node's (its offset taken off): this minute's mean, and the last 15 minutes' as " +
+ "a line; good to half the round trip, but the trend exactly" };
+ const rows = [el("tr", {}, ...["peer", "status", "seen", "via", "rtt", "hop", "clock",
+ "neighbour", "reaches", "same epoch"]
.map(h => el("th", { title: headers[h] || "" }, h)))];
// Whom this node reaches, to tell those a peer reaches and it doesn't.
const ours = new Set(state.peers.localReaches || []);
@@ -450,6 +454,7 @@ function renderPeers() {
el("td", {}, age(aged("lastSeenSecsAgo", peer.lastSeenSecsAgo))),
el("td", { class: "id" }, via),
el("td", {}, rtt),
+ hopCell(peer.hopDelay),
// Its clock against this node's, flagged beyond the limit (`--clock-skew-warn`).
el("td", { class: clockOff(peer.clock) ? "clock-off" : "", title: peer.clock
? "ahead of this node's by " + peer.clock.offsetMs + " ms, give or take " +
@@ -469,6 +474,33 @@ function renderPeers() {
? (rows[8].offsetTop + rows[8].offsetHeight) + "px" : "";
}
+// How long a peer's sync messages take to come (`hopDelay`): the latest minute's mean,
+// and the last 15 minutes' as a line of bars (a minute without messages blank), each
+// figure in the tooltip.
+function hopCell(hop) {
+ if (!hop) return el("td", { title: "no sync message from it yet while its clock was known" }, "");
+ const minutes = hop.byMinuteMs || [];
+ const known = minutes.filter(ms => typeof ms === "number");
+ const latest = known.length ? known[known.length - 1] : null;
+ const low = Math.min(...known), high = Math.max(...known);
+ const bars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
+ const line = minutes.map(ms => typeof ms !== "number" ? " "
+ : bars[high > low ? Math.round((ms - low) / (high - low) * (bars.length - 1)) : 0]).join("");
+ const within = ms => ms === null || ms === undefined ? "beyond the last bucket" : "within " + ms + " ms";
+ const title = [
+ hop.count + " messages since this node started, mean " + hop.meanMs + " ms",
+ "half " + within(hop.halfWithinMs) + ", 99% " + within(hop.p99WithinMs),
+ hop.halfRttMs !== null && hop.halfRttMs !== undefined
+ ? "half the round trip: " + hop.halfRttMs + " ms (beyond it: waiting in queues, or the clock offset's error)"
+ : "no recent round trip",
+ hop.belowZero ? hop.belowZero + " took less than nothing by the clocks (the offset's error)" : "",
+ "the last 15 minutes' means, oldest first: " +
+ minutes.map(ms => typeof ms === "number" ? ms : "–").join(", "),
+ ].filter(Boolean).join("\n");
+ return el("td", { title },
+ latest === null ? "" : latest + " ms ", el("span", { class: "spark" }, line));
+}
+
// Whom a peer reaches (`reaches`, as it said; null: it hasn't said in this session), as
// a count, and those of them this node doesn't reach (`ours`), marked: the peer is a way
// to them. Each by name in the tooltip.