From 0e03a77bcb1d39f1fbda23d8e704ac40302afb93 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:10 +0100 Subject: dashboard: without LINKS; LINKS removed The dashboard reads every member's links from the members themselves; LINKS and LINKS_REPLY, swarm::links and their wire tags go (wire version bumped). Nodes the browser can't reach are drawn from what the reachable ones measure. Auth plan M3. --- src/api/dashboard/dashboard.html | 143 +++++++++------------------------------ 1 file changed, 31 insertions(+), 112 deletions(-) (limited to 'src/api/dashboard/dashboard.html') diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 5b776cd..57a3a0f 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -197,11 +197,6 @@ /* After the sliders, as tall; the kinds and the sliders greyed while the dots are off. */ .legend button.toggle { float: none; margin-left: 0; line-height: 1em; } .legend.off .message, .legend.off .slider { opacity: .45; } - /* The LINKS kind, a toggle: shown (in the console, as dots, in the rates) or not. */ - .legend button.message::before { content: ""; display: inline-block; vertical-align: middle; - margin-right: 4px; } - .legend button.message.hidden-kind { text-decoration: line-through; } - #console.no-links > div.links-kind { display: none; } /* The map and, over its top, its controls (links, layout, legend) and the legend: they take no room from the map, which stays as it is, the legend shown or hidden. What is under them shows through; their contents are on an underlay of their own @@ -316,7 +311,6 @@
- @@ -324,8 +318,8 @@
this node - reports its links - reports none (no LINKS_REPLY) + its links known + its links unknown (not subscribed) dormant (to this node) clock off the swarm's in focus (click a node) @@ -398,7 +392,7 @@
Drag the space left of these boxes to the right to hide the column.
-

Console following…

+

Console following…

@@ -1953,7 +1947,6 @@ function render(ages = false) { const skew = state.stats.clock; document.getElementById("clock-warning").textContent = skew && skew.off ? "clock " + seconds(skew.offsetMs) + " off its peers'" : ""; - showLinksToggle(); held = false; const boxes = [["peers-box", renderPeers], ["figures-box", renderStats]]; // Its ages are in tooltips only: drawn on events, not every second. @@ -1994,21 +1987,6 @@ legendToggle.addEventListener("click", () => { }); // Hidden as on the page it was opened from (`handOver`). if (handOver && handOver.legend === false) showLegend(false); -// The members' links subscribed to (`live`: each tells of a change as it happens), or -// only asked for every 30 s; the node's choice, for every page (`POST -// /admin/dashboard/links`), shown as the state has it. -const linksToggle = document.getElementById("links-toggle"); -function showLinksToggle() { - const live = !state || state.graph.live !== false; - linksToggle.textContent = live ? "links: live" : "links: every 30 s"; - linksToggle.title = live - ? "The members tell of their links' changes as they happen. Click to only ask them every 30 s (fewer messages; for every page on this node)" - : "The members are asked for their links every 30 s. Click to have them tell of changes as they happen (for every page on this node)"; -} -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() { @@ -2089,21 +2067,10 @@ function handOff(id) { focused: focused === id ? null : focused, settings, legend: !document.getElementById("legends").hidden, - links: state ? state.graph.live !== false : undefined, figures: [...open], document: [...documentOpen], })); } -// Links live or every 30 s as on the page it was opened from (`handOver`), once its -// first state says otherwise. The node's choice: for every page open on it. -let linksHandedOver = false; -function handOverLinks() { - if (linksHandedOver || !handOver || typeof handOver.links !== "boolean") return; - linksHandedOver = true; - if ((state.graph.live !== false) !== handOver.links) { - fetch("dashboard/links?live=" + handOver.links, { method: "POST" }).catch(() => {}); - } -} // `[x, y]` turned by `angle`. function turned(x, y, angle) { const c = Math.cos(angle), s = Math.sin(angle); @@ -2744,8 +2711,7 @@ function messages(byKind) { // The colour of a kind of message (`traffic`'s kinds), on the map and in the console; // the replies as their requests. const KIND_COLOURS = [["sync", "#ea580c"], ["blob", "#a16207"], ["hello", "#16a34a"], - ["status", "#e11d48"], ["identity", "#0d9488"], ["paths", "var(--muted)"], - ["links", "#db2777"]]; + ["status", "#e11d48"], ["identity", "#0d9488"], ["paths", "var(--muted)"]]; const kindColour = kind => (KIND_COLOURS.find(([name]) => kind === name || kind === name + "Reply") || [null, "var(--fg)"])[1]; // How large the dots are drawn, times `dotRadius` (the slider after their legend, kept @@ -2759,39 +2725,6 @@ let flowSpeed = .3; try { flowSpeed = Number(localStorage.getItem("continuum-dot-speed")) || .3; } catch (_) {} flowSpeed = Math.min(.9, Math.max(.1, flowSpeed)); let dotSlider = null, speedSlider = null; -// LINKS and their replies (`traffic`'s `links`), and whether they are shown -// (`linksToggleInLegend`). -const isLinks = kind => kind === "links"; -let linksShown = true; -try { linksShown = localStorage.getItem("continuum-links-shown") !== "off"; } catch (_) {} -// The LINKS kind in the legend, a toggle: whether LINKS and their replies, which go only -// while a dashboard asks for the members' links, are shown: in the console, as dots on -// the map, and counted in the console's rates. Off, what is shown is the swarm's own -// traffic (kept in this browser). -function linksToggleInLegend(colour) { - const button = el("button", { class: "toggle message", style: "--colour: " + colour }, "links"); - const show = () => { - button.classList.toggle("hidden-kind", !linksShown); - document.getElementById("console").classList.toggle("no-links", !linksShown); - button.title = linksShown - ? "LINKS go only while a dashboard asks for the members' links. Click to leave them out: of the console, the dots and the rates" - : "Left out of the console, the dots and the rates. Click to show them again"; - }; - button.addEventListener("click", () => { - linksShown = !linksShown; - try { localStorage.setItem("continuum-links-shown", linksShown ? "on" : "off"); } catch (_) {} - if (!linksShown) { - for (let i = flows.length - 1; i >= 0; i--) if (isLinks(flows[i].kind)) flows.splice(i, 1); - } - show(); - showRates(); - filterConsole(); - const scroll = document.getElementById("console-scroll"); - scroll.scrollTop = scroll.scrollHeight; - }); - show(); - return button; -} function showMessagesLegend() { const legend = document.getElementById("messages-legend"); legend.classList.toggle("off", !flowsShown); @@ -2811,9 +2744,8 @@ function showMessagesLegend() { try { localStorage.setItem("continuum-dot-speed", String(flowSpeed)); } catch (_) {} }); // Before the toggle, which ends the line. - legend.prepend(...KIND_COLOURS.map(([kind, colour]) => kind === "links" - ? linksToggleInLegend(colour) - : el("span", { class: "message", style: "--colour: " + colour }, + legend.prepend(...KIND_COLOURS.map(([kind, colour]) => + el("span", { class: "message", style: "--colour: " + colour }, kind === "paths" ? "paths (pings)" : kind)), el("label", { class: "slider" }, "size", slider), el("label", { class: "slider" }, "speed", speed)); @@ -2876,7 +2808,7 @@ function addFlows(node, current, before, skip) { for (const [kind, count] of Object.entries(counts[way])) { const earlier = was[way][kind] || { messages: 0, bytes: 0 }; const grown = Math.max(0, count.messages - earlier.messages); - if (grown > 0 && (linksShown || !isLinks(kind))) { + if (grown > 0) { kinds[kind] = { messages: grown, bytes: Math.max(0, count.bytes - earlier.bytes) }; total += grown; } @@ -2923,7 +2855,6 @@ function remoteMessagesArrived(id, entry, list) { const now = performance.now(); const links = new Map(); for (const message of kept) { - if (!linksShown && isLinks(message.counted)) continue; const key = message.from + " " + message.to; const link = links.get(key) || { from: message.from, to: message.to, kinds: {}, total: 0 }; const kind = link.kinds[message.counted] || (link.kinds[message.counted] = { messages: 0, bytes: 0 }); @@ -2949,44 +2880,37 @@ function remoteMessagesArrived(id, entry, list) { // weighs in by how long ago it was, `e^(-age/window)`), from the counts each update // brings. The first sample is taken as it is, so they needn't climb from 0 first. --- const RATE_WINDOWS_S = [60, 300, 900]; -// By peer, and in all (`null`), with LINKS (`all`) and without (`withoutLinks`): the -// averages (`messages`, `bytes`: per window); and when the counts were last taken. +// By peer, and in all (`null`): the averages (`messages`, `bytes`: per window); and when +// the counts were last taken. const rates = new Map(); let ratesAt = null; function measureRates(current) { const now = performance.now(); if (lastTraffic !== null && ratesAt !== null && now > ratesAt) { const secs = (now - ratesAt) / 1000; - // Of `what` (`messages`, `bytes`), sent and received, by how much each grew; LINKS - // left out if `withoutLinks`. - const grown = (counts, before, what, withoutLinks) => ["sent", "received"].reduce((sum, way) => { - const total = of => Object.entries(of[way]).reduce((sum, [kind, count]) => - withoutLinks && isLinks(kind) ? sum : sum + count[what], 0); + // Of `what` (`messages`, `bytes`), sent and received, by how much each grew. + const grown = (counts, before, what) => ["sent", "received"].reduce((sum, way) => { + const total = of => Object.values(of[way]).reduce((sum, count) => sum + count[what], 0); return sum + Math.max(0, total(counts) - total(before)); }, 0); const average = (known, rate) => known ? known.map((average, i) => rate + (average - rate) * Math.exp(-secs / RATE_WINDOWS_S[i])) : RATE_WINDOWS_S.map(() => rate); - const sample = (key, variant, messages, bytes) => { - const known = rates.get(key) || {}; - const was = known[variant]; - known[variant] = { messages: average(was && was.messages, messages / secs), - bytes: average(was && was.bytes, bytes / secs) }; - rates.set(key, known); + const sample = (key, messages, bytes) => { + const was = rates.get(key); + rates.set(key, { messages: average(was && was.messages, messages / secs), + bytes: average(was && was.bytes, bytes / secs) }); }; - for (const variant of ["all", "withoutLinks"]) { - const withoutLinks = variant === "withoutLinks"; - let messagesInAll = 0, bytesInAll = 0; - for (const [id, counts] of current) { - const before = lastTraffic.get(id); - if (!before) continue; - const messages = grown(counts, before, "messages", withoutLinks); - const bytes = grown(counts, before, "bytes", withoutLinks); - sample(id, variant, messages, bytes); - messagesInAll += messages; - bytesInAll += bytes; - } - sample(null, variant, messagesInAll, bytesInAll); + let messagesInAll = 0, bytesInAll = 0; + for (const [id, counts] of current) { + const before = lastTraffic.get(id); + if (!before) continue; + const messages = grown(counts, before, "messages"); + const bytes = grown(counts, before, "bytes"); + sample(id, messages, bytes); + messagesInAll += messages; + bytesInAll += bytes; } + sample(null, messagesInAll, bytesInAll); // Peers gone from the list: their averages too. for (const key of [...rates.keys()]) if (key !== null && !current.has(key)) rates.delete(key); } @@ -2994,13 +2918,11 @@ function measureRates(current) { showRates(); } // The averages shown: in all, or with the filter, of the peer in focus; none (–) for a -// link between other nodes, a node that is no peer, or before two updates came. With -// LINKS or without (`linksShown`). +// link between other nodes, a node that is no peer, or before two updates came. function showRates() { const filtering = consoleFiltered && focused !== null; const peer = filtering ? peerOf(focused) : null; - const both = filtering && peer === null ? undefined : rates.get(peer); - const shown = both && both[linksShown ? "all" : "withoutLinks"]; + const shown = filtering && peer === null ? undefined : rates.get(peer); const format = rate => rate < 100 ? rate.toFixed(2) : String(Math.round(rate)); // Bytes in the unit the largest of the three needs, all three alike. const largest = shown ? Math.max(...shown.bytes) : 0; @@ -3109,8 +3031,7 @@ function filterConsole() { const filtering = consoleFiltered && focused !== null; log.classList.toggle("filtering", filtering); document.getElementById("console-none").hidden = - !filtering || log.querySelector(":scope > div.peer-focused" + - (linksShown ? "" : ":not(.links-kind)")) !== null; + !filtering || log.querySelector(":scope > div.peer-focused") !== null; const view = filtering ? focused : null; if (view !== consoleView) { consoleView = view; @@ -3128,7 +3049,6 @@ function consoleLine(message) { "data-peer": message.node ? "" : message.peer, "data-from": message.from || "", "data-to": message.to || "", class: (shownMessage && shownMessage.seq === message.seq ? "focused" : "") + - (isLinks(message.counted) ? " links-kind" : "") + (relatedMarked !== null && message.related === relatedMarked ? " related" : "") + peerClasses(message), title: message.way === "refused" ? "refused: the transport wouldn't send it" @@ -3273,7 +3193,6 @@ function connect() { status("live", true); pulse(); render(); trafficArrived(); epochsArrived(); remakesArrived(); if (treeShown && writerTree === null) loadTree(); - handOverLinks(); if (token === null && tokenTimer === null) obtainTokens(); for (const id of subscriptions) if (!subscribed.has(id)) follow(id); }); @@ -3451,8 +3370,8 @@ document.addEventListener("visibilitychange", () => { } } }); -// A click on the status: live, everything now (a tick on the node, the members asked -// again, what's open of the document read again); otherwise, reconnecting now. +// A click on the status: live, everything now (a tick on the node, what's open of the +// document read again); otherwise, reconnecting now. connection.title = "Click to refresh now"; connection.style.cursor = "pointer"; connection.addEventListener("click", () => { -- cgit v1.3.1