aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-31 06:55:20 +0100
committertv <tv@krebsco.de>2026-10-31 06:55:20 +0100
commit210e40178082f485d46bdcdaaf433356f7c9919a (patch)
treedf3440b066edb512b0e272399be718eac6594c62
parentc5f743ef0f91e4afd0661824d4ec7df1483dcbad (diff)
dashboard: add clocks
-rw-r--r--docs/openapi.yaml5
-rw-r--r--src/api/dashboard/dashboard.html23
-rw-r--r--src/swarm/skew.rs6
3 files changed, 30 insertions, 4 deletions
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index 6eab2d1..a5780e3 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -1805,7 +1805,7 @@ components:
clock:
type: object
description: This node's clock against its active peers' (`measurements.md`, Clocks).
- required: [peers, offsetMs, behind, ahead, off]
+ required: [peers, offsetMs, behind, ahead, off, limitMs]
properties:
peers:
type: integer
@@ -1822,6 +1822,9 @@ components:
off:
type: boolean
description: Ours differs from more than half of theirs that way.
+ limitMs:
+ type: integer
+ description: --clock-skew-warn.
LockWaits:
type: object
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 03fa177..153d7cb 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -25,6 +25,7 @@
border-bottom: 1px solid var(--line); flex-wrap: wrap; }
header h1 { font-size: 16px; margin: 0; }
#connection { margin-left: auto; color: var(--muted); }
+ #clock-warning, #peers .clock-off { color: var(--warn); }
#connection.live { color: var(--ok); }
#connection.down { color: var(--bad); }
/* A dot in the status's colour that breathes once with every event. */
@@ -137,6 +138,7 @@
<header>
<h1>continuum</h1>
<span id="node" class="id"></span>
+ <span id="clock-warning"></span>
<span id="connection"><span id="dot"></span><span id="connection-text">connecting…</span></span>
</header>
<main>
@@ -220,6 +222,13 @@ function name(id) {
const member = state.graph.members[id];
return (member && member.alias) || (state.peers.peers[id] || {}).alias || short(id);
}
+// Milliseconds as signed seconds: "+5.0 s".
+const seconds = ms => (ms < 0 ? "−" : "+") + (Math.abs(ms) / 1000).toFixed(1) + " s";
+// 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;
+ return !!clock && limit > 0 && Math.abs(clock.offsetMs) > clock.withinMs + limit;
+}
function age(secs) {
if (secs === null || secs === undefined) return "";
if (secs < 120) return secs + " s";
@@ -248,7 +257,8 @@ function focus(id) {
// --- Peers ---
function renderPeers() {
const table = document.getElementById("peers");
- const rows = [el("tr", {}, ...["peer", "status", "seen", "via", "rtt", "neighbour", "same epoch"]
+ const rows = [el("tr", {}, ...["peer", "status", "seen", "via", "rtt", "clock", "neighbour",
+ "same epoch"]
.map(h => el("th", {}, h)))];
for (const [id, peer] of Object.entries(state.peers.peers)) {
const destination = peer.destination;
@@ -265,6 +275,10 @@ function renderPeers() {
el("td", {}, age(aged("lastSeenSecsAgo", peer.lastSeenSecsAgo))),
el("td", { class: "id" }, via),
el("td", {}, rtt),
+ // 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 " +
+ peer.clock.withinMs + " ms" : "" }, peer.clock ? seconds(peer.clock.offsetMs) : ""),
el("td", {}, peer.neighbour ? "yes" : ""),
el("td", {}, peer.sameEpoch === null || peer.sameEpoch === undefined
? "" : peer.sameEpoch ? "yes" : "no"));
@@ -641,8 +655,10 @@ function renderGraph() {
fill: member.self ? "var(--self)" : reported ? "var(--fg)" : "var(--panel)",
stroke: member.self ? "var(--self)" : dormant ? "var(--bad)" : "var(--fg)",
"stroke-width": dormant ? 2.5 : 1.5 }, pressed);
+ const clock = (state.peers.peers[id] || {}).clock;
tooltip(circle, id + (reported ? "" : " (no links reported)") +
- (dormant ? " (dormant)" : ""));
+ (dormant ? " (dormant)" : "") +
+ (clockOff(clock) ? " (clock " + seconds(clock.offsetMs) + " off this node's)" : ""));
const label = drawing("name " + id, "text", upright({ x: p.x + 10, y: p.y + 4,
"font-size": 12, fill: "var(--fg)", cursor: "pointer" }), pressed);
text(label, name(id));
@@ -994,6 +1010,9 @@ function render(ages = false) {
if (!state) return;
document.getElementById("node").textContent =
(state.node.alias ? state.node.alias + " · " : "") + state.node.id;
+ const skew = state.stats.clock;
+ document.getElementById("clock-warning").textContent = skew && skew.off
+ ? "clock " + seconds(skew.offsetMs) + " off its peers'" : "";
held = false;
const boxes = [["peers-box", renderPeers], ["figures-box", renderStats]];
// Its ages are in tooltips only: drawn on events, not every second.
diff --git a/src/swarm/skew.rs b/src/swarm/skew.rs
index af11e5e..f5d9858 100644
--- a/src/swarm/skew.rs
+++ b/src/swarm/skew.rs
@@ -34,6 +34,8 @@ pub(crate) struct Skew {
/// Whether ours is off: more than half of them differ from it beyond the limit,
/// the same way. Never with a limit of 0.
pub(crate) off: bool,
+ /// The limit (`--clock-skew-warn`).
+ pub(crate) limit_ms: u64,
}
/// Ours against the peers' clocks `offsets` (each ahead of ours), with `limit`.
@@ -59,6 +61,7 @@ pub(crate) fn of(offsets: &[ClockOffset], limit: Duration) -> Skew {
behind,
ahead,
off: limit_ms > 0 && (ahead * 2 > peers || behind * 2 > peers),
+ limit_ms,
}
}
@@ -143,6 +146,7 @@ mod tests {
// A limit of 0: never.
assert!(!of(&[offset(60_000, 10)], Duration::ZERO).off);
// No peers: nothing known.
- assert_eq!(of(&[], limit), Skew::default());
+ let none = of(&[], limit);
+ assert_eq!((none.peers, none.offset_ms, none.off), (0, None, false));
}
}