aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-30 18:11:57 +0100
committertv <tv@krebsco.de>2026-10-30 18:11:57 +0100
commit46a7d204c81b002f960a9bca97941df7a98c43d4 (patch)
treedcf519abd3b5e48dd3c768648fc4d5dca03d6ce3 /src/api
parentf65ecfdf313658a3979a2d0add53e342ce4b97df (diff)
dashboard: show all links between peers
Diffstat (limited to 'src/api')
-rw-r--r--src/api/dashboard/dashboard.html81
-rw-r--r--src/api/dashboard/mod.rs8
2 files changed, 61 insertions, 28 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index c87341f..a8b697e 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -53,6 +53,8 @@
.legend span::before { content: ""; display: inline-block; width: 18px; height: 0;
vertical-align: middle; margin-right: 4px; border-top: 2px solid var(--colour, currentColor); }
.legend .intended::before { border-top-style: dashed; --colour: var(--intended); }
+ .legend .used::before { border-top-width: 3px; --colour: var(--muted); }
+ .legend .other::before { border-top-width: 1px; --colour: var(--muted); }
.legend .direct::before { --colour: var(--muted); }
.legend .relayed::before { border-top-style: dashed; --colour: var(--muted); }
.legend .dormant::before { border-top-style: dotted; --colour: var(--bad); }
@@ -242,14 +244,19 @@ function colourOf(transport) {
function renderLegend(measured) {
const transports = new Set();
- for (const { link } of measured.values()) transports.add(link.transport || "");
+ for (const { link } of measured.values()) {
+ transports.add(link.transport || "");
+ for (const other of link.others || []) transports.add(other.transport);
+ }
const items = [el("span", { class: "intended" }, "intended (member list)")];
for (const transport of [...transports].sort()) {
const item = el("span", {}, transport || "unknown transport");
item.style.setProperty("--colour", colourOf(transport || null));
items.push(item);
}
- items.push(el("span", { class: "direct" }, "direct"),
+ items.push(el("span", { class: "used" }, "in use"),
+ el("span", { class: "other" }, "other transport"),
+ el("span", { class: "direct" }, "direct"),
el("span", { class: "relayed" }, "relayed"),
el("span", { class: "dormant" }, "dormant"));
document.getElementById("legend").replaceChildren(...items);
@@ -263,13 +270,16 @@ function edges() {
intended.set(key, [a, b]);
}
}
- // One measured edge per pair: the fresher report wins.
+ // One measured edge per pair: a report with an RTT, then the fresher.
const measured = new Map();
for (const [a, report] of Object.entries(state.graph.measured)) {
for (const link of report.links) {
const key = a < link.peer ? a + " " + link.peer : link.peer + " " + a;
const known = measured.get(key);
- if (!known || report.secsAgo < known.secsAgo) {
+ const timed = typeof link.rttMs === "number";
+ const knownTimed = known && typeof known.link.rttMs === "number";
+ if (!known || (timed && !knownTimed) ||
+ (timed === knownTimed && report.secsAgo < known.secsAgo)) {
measured.set(key, { a, b: link.peer, link, secsAgo: report.secsAgo });
}
}
@@ -335,31 +345,50 @@ function renderGraph() {
children.push(svg("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y,
stroke: "var(--intended)", "stroke-dasharray": "4 4", "stroke-width": 1 }));
}
+ const ms = rtt => rtt < 10 ? rtt.toFixed(1) : String(Math.round(rtt));
for (const { a, b, link, secsAgo } of measured.values()) {
const p = positions.get(a), q = positions.get(b);
if (!p || !q) continue;
- const line = svg("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y,
- stroke: link.dormant ? "var(--bad)" : colourOf(link.transport), "stroke-width": 2 });
- // Dotted where dormant, dashed where relayed, solid where direct.
- if (link.dormant) {
- line.setAttribute("stroke-dasharray", "1 4");
- line.setAttribute("stroke-linecap", "round");
- } else if (link.relayed) {
- line.setAttribute("stroke-dasharray", "7 4");
- }
- const title = svg("title");
- title.textContent = name(a) + " – " + name(b) + (link.transport ? " via " + link.transport : "") +
- (link.relayed ? ", relayed" : "") + (link.dormant ? ", dormant" : "") +
- (typeof link.rttMs === "number" ? ", " + link.rttMs.toFixed(1) + " ms" : "") +
- ", measured " + age(secsAgo + elapsed()) + " ago";
- line.append(title);
- children.push(line);
- if (typeof link.rttMs === "number") {
- const label = svg("text", { x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 - 3,
- "text-anchor": "middle", "font-size": 10, fill: "var(--muted)" });
- label.textContent = link.rttMs < 10 ? link.rttMs.toFixed(1) : Math.round(link.rttMs);
- children.push(label);
- }
+ // The transport in use thick, in the middle; the others thin, beside it.
+ const lines = [{ transport: link.transport, rttMs: link.rttMs, used: true }];
+ for (const other of link.others || []) lines.push({ ...other, used: false });
+ const length = Math.max(Math.hypot(q.x - p.x, q.y - p.y), 1);
+ const nx = -(q.y - p.y) / length, ny = (q.x - p.x) / length;
+ lines.forEach((line, i) => {
+ // 0, then +1, -1, +2, -2 … steps of 6 px to the side.
+ const side = i === 0 ? 0 : (i % 2 ? 1 : -1) * Math.ceil(i / 2) * 6;
+ const ox = nx * side, oy = ny * side;
+ const drawn = svg("line", { x1: p.x + ox, y1: p.y + oy, x2: q.x + ox, y2: q.y + oy,
+ stroke: link.dormant ? "var(--bad)" : colourOf(line.transport),
+ "stroke-width": line.used ? 3 : 1.2 });
+ // Dotted where dormant, dashed where relayed, solid where direct.
+ if (link.dormant) {
+ drawn.setAttribute("stroke-dasharray", line.used ? "1 5" : "1 3");
+ drawn.setAttribute("stroke-linecap", "round");
+ } else if (line.used && link.relayed) {
+ drawn.setAttribute("stroke-dasharray", "7 4");
+ }
+ const title = svg("title");
+ title.textContent = name(a) + " – " + name(b) +
+ (line.transport ? " via " + line.transport : "") + (line.used ? " (in use)" : "") +
+ (line.used && link.relayed ? ", relayed" : "") + (link.dormant ? ", dormant" : "") +
+ (typeof line.rttMs === "number" ? ", " + line.rttMs.toFixed(1) + " ms" : "") +
+ ", measured " + age(secsAgo + elapsed()) + " ago";
+ drawn.append(title);
+ children.push(drawn);
+ if (typeof line.rttMs === "number") {
+ // The one in use at the middle, larger; the others further along, beside theirs.
+ const t = line.used ? .5 : .3;
+ const out = line.used ? -4 : Math.sign(side) * 9;
+ const label = svg("text", {
+ x: p.x + (q.x - p.x) * t + ox + nx * out,
+ y: p.y + (q.y - p.y) * t + oy + ny * out + (line.used ? 0 : 3),
+ "text-anchor": "middle", "font-size": line.used ? 11 : 9,
+ fill: line.used ? "var(--fg)" : "var(--muted)" });
+ label.textContent = ms(line.rttMs);
+ children.push(label);
+ }
+ });
}
for (const id of nodes) {
const p = positions.get(id), member = state.graph.members[id];
diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs
index adaed10..6c7f1c5 100644
--- a/src/api/dashboard/mod.rs
+++ b/src/api/dashboard/mod.rs
@@ -35,9 +35,9 @@ use crate::transport::Link;
/// How often the snapshot is built and compared, while anyone looks.
const TICK: Duration = Duration::from_secs(2);
/// How often members are asked for their links, and the graph recomputed.
-const LINKS_EVERY: Duration = Duration::from_secs(30);
+const LINKS_EVERY: Duration = Duration::from_secs(10);
/// A member's links older than this are left out (it didn't answer since).
-const LINKS_KEPT_FOR: Duration = Duration::from_secs(90);
+const LINKS_KEPT_FOR: Duration = Duration::from_secs(30);
/// The producer runs on this long after the last request for the state.
const LINGER: Duration = Duration::from_secs(60);
/// Members asked for their links at most, each round.
@@ -230,6 +230,10 @@ fn links_json(links: &[Link]) -> JsonValue {
"relayed": link.relayed,
"rttMs": link.rtt_us.map(|us| f64::from(us) / 1000.0),
"dormant": link.dormant,
+ "others": link.others.iter().map(|other| json!({
+ "transport": other.transport,
+ "rttMs": other.rtt_us.map(|us| f64::from(us) / 1000.0),
+ })).collect::<Vec<_>>(),
})
})
.collect()