aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard/dashboard.html
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/dashboard/dashboard.html')
-rw-r--r--src/api/dashboard/dashboard.html72
1 files changed, 72 insertions, 0 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 9132275..b1aa821 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -161,6 +161,9 @@
.legend .dormant::before { border-top-width: 1.5px; --colour: var(--muted);
box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--bad) 50%, transparent); }
.legend .silent::before { border-top-style: dotted; --colour: var(--muted); }
+ /* A chevron on a link, as on the map: a writer's tree, the way its changes go. */
+ .legend .writer-tree::before { content: "\203A"; width: auto; height: auto; border: none;
+ font-weight: bold; font-size: 15px; line-height: 0; color: var(--muted); }
.legend .hint::before { display: none; }
/* A kind of message: a dot in its colour, as on the map. */
.legend .message::before { width: 8px; height: 8px; border: none; border-radius: 50%;
@@ -263,6 +266,7 @@
<div class="map-controls">
<button id="links-toggle" class="toggle">links: live</button>
<button id="rtt-toggle" class="toggle">layout: static</button>
+ <button id="tree-toggle" class="toggle">tree: off</button>
<button id="legend-toggle" class="toggle">hide legend</button>
</div>
<div id="legends">
@@ -417,6 +421,7 @@ function focus(id) {
} catch { /* not where history works */ }
render();
markPeerMessages();
+ loadTree();
}
// --- Peers ---
@@ -608,6 +613,10 @@ function renderLegend(measured) {
el("span", { class: "relayed" }, "relayed"),
el("span", { class: "dormant" }, "dormant or down"),
el("span", { class: "silent" }, "an end reports no links"));
+ if (treeShown) {
+ items.push(el("span", { class: "writer-tree" }, name(treeWriter()) +
+ "'s tree: the way its changes go (of the node in focus)"));
+ }
document.getElementById("legend").replaceChildren(...items);
}
@@ -711,6 +720,27 @@ function relax(nodes, pairs, width, height, gain, most, only) {
// the median one's, within bounds. Then, when the lengths change, the map moves to
// them a little every frame (`settle`), not at once.
let byRtt = false;
+// A writer's tree on the map (`swarm::tree`, `GET /admin/dashboard/tree`): of the node in
+// focus, else of this node; asked for anew when the writer changes and every 10 s.
+let treeShown = false;
+try { treeShown = localStorage.getItem("continuum-tree") === "1"; } catch (_) {}
+let writerTree = null;
+const treeWriter = () => focused !== null && !isPair(focused) ? focused
+ : state ? state.node.id : null;
+function loadTree() {
+ const writer = treeWriter();
+ if (!treeShown || writer === null) { writerTree = null; return; }
+ fetch("dashboard/tree?writer=" + encodeURIComponent(writer))
+ .then(response => response.ok ? response.json() : null)
+ .then(loaded => {
+ if (loaded && loaded.writer === treeWriter()) {
+ writerTree = loaded;
+ if (state) renderGraph();
+ }
+ })
+ .catch(() => {});
+}
+setInterval(() => { if (treeShown) loadTree(); }, 10000);
try { byRtt = localStorage.getItem("continuum-by-rtt") === "1"; } catch (_) {}
// One spring per pair of nodes, intended or measured or both: [a, b, length, loose].
// Measured links shape the map; a link only intended (nothing measured backs it) is
@@ -1030,6 +1060,30 @@ function renderGraph() {
tooltip(covering, descriptions.join("\n"));
}
// Above every line, so none crosses them.
+ // The tree of the writer in focus (`treeShown`): a chevron on each link from parent to
+ // child, at its middle, pointing the way changes go, in the colour of the link.
+ if (treeShown && writerTree && writerTree.writer === treeWriter()) {
+ for (const [child, parent] of Object.entries(writerTree.parents)) {
+ const p = parent && positions.get(parent), q = positions.get(child);
+ if (!p || !q) continue;
+ const length = Math.max(Math.hypot(q.x - p.x, q.y - p.y), 1);
+ const ux = (q.x - p.x) / length, uy = (q.y - p.y) / length;
+ const mx = (p.x + q.x) / 2, my = (p.y + q.y) / 2;
+ const d = "M " + (mx - ux * 5 - uy * 5) + " " + (my - uy * 5 + ux * 5) + " L " +
+ (mx + ux * 5) + " " + (my + uy * 5) + " L " + (mx - ux * 5 + uy * 5) + " " +
+ (my - uy * 5 - ux * 5);
+ const shape = { d, fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" };
+ // The colour of the link's line in use (its transport's), as the line it is on.
+ const edge = measured.get(pairKey(parent, child));
+ const colour = edge ? colourOf(edge.link.transport) : "var(--intended)";
+ labels.push(drawing("tree under " + child, "path",
+ { ...shape, stroke: "var(--panel)", "stroke-width": 6 }));
+ const chevron = drawing("tree " + child, "path",
+ { ...shape, stroke: colour, "stroke-width": 2.5 });
+ tooltip(chevron, "in " + name(writerTree.writer) + "'s tree: " + name(parent) + " → " + name(child));
+ labels.push(chevron);
+ }
+ }
children.push(...labels);
// Above the lines and their labels, below the nodes.
children.push(...hits);
@@ -1549,6 +1603,23 @@ linksToggle.addEventListener("click", () => {
const live = !state || state.graph.live !== false;
fetch("dashboard/links?live=" + !live, { method: "POST" }).catch(() => {});
});
+// A writer's tree, shown and hidden (`treeShown`).
+const treeToggle = document.getElementById("tree-toggle");
+function showTreeToggle() {
+ treeToggle.textContent = treeShown ? "tree: on" : "tree: off";
+ treeToggle.title = treeShown
+ ? "The tree of the node in focus (or this node) as a writer: the way its changes go. Click to hide it"
+ : "Click to show the tree of the node in focus (or this node) as a writer: the way its changes go";
+}
+showTreeToggle();
+treeToggle.addEventListener("click", () => {
+ treeShown = !treeShown;
+ try { localStorage.setItem("continuum-tree", treeShown ? "1" : "0"); } catch (_) {}
+ showTreeToggle();
+ writerTree = null;
+ loadTree();
+ if (state) renderGraph();
+});
// Links as long as their round trips, and back.
const rttToggle = document.getElementById("rtt-toggle");
function showRttToggle() {
@@ -2289,6 +2360,7 @@ function connect() {
if (state !== null) documentChanged({ all: true });
state = snapshot; stateAt = Date.now(); live = true;
status("live", true); pulse(); render(); trafficArrived();
+ if (treeShown && writerTree === null) loadTree();
handOverLinks();
});
events.addEventListener("patch", event => {