From e9ba6e3119b41935a36addca9cb0afa85074b7ac Mon Sep 17 00:00:00 2001 From: tv Date: Fri, 6 Nov 2026 18:26:07 +0100 Subject: dashboard: rates without links --- src/api/dashboard/dashboard.html | 111 +++++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index e5ed7a7..3eda30a 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -177,6 +177,11 @@ /* 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 @@ -312,7 +317,7 @@
Drag the space left of these boxes to the right to hide the column.
-

Console following…

+

Console following…

@@ -1860,7 +1865,8 @@ if (handOver) { moveView(); } // The fragment that opens another node's dashboard (`id`'s) as this page is now -// (`handOver`). What is in focus stays, but `id` itself: its page is its own. +// (`handOver`). What is in focus stays, but `id` itself: its page is its own. Also this +// node's own page, reloaded for a new version (`id` null). function handOff(id) { const settings = {}; try { @@ -1877,7 +1883,7 @@ function handOff(id) { focused: focused === id ? null : focused, settings, legend: !document.getElementById("legends").hidden, - links: state.graph.live !== false, + links: state ? state.graph.live !== false : undefined, figures: [...open], document: [...documentOpen], })); @@ -2324,6 +2330,39 @@ 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); @@ -2343,8 +2382,9 @@ 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]) => - el("span", { class: "message", style: "--colour: " + colour }, + legend.prepend(...KIND_COLOURS.map(([kind, colour]) => kind === "links" + ? linksToggleInLegend(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)); @@ -2400,7 +2440,7 @@ function trafficArrived() { for (const [kind, count] of Object.entries(counts[way])) { const was = before[way][kind] || { messages: 0, bytes: 0 }; const grown = Math.max(0, count.messages - was.messages); - if (grown > 0) { + if (grown > 0 && (linksShown || !isLinks(kind))) { kinds[kind] = { messages: grown, bytes: Math.max(0, count.bytes - was.bytes) }; total += grown; } @@ -2425,36 +2465,44 @@ function trafficArrived() { // 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`): the averages (`messages`, `bytes`: per window); and when -// the counts were last taken. +// 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. 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. - const grown = (counts, before, what) => ["sent", "received"].reduce((sum, way) => { - const total = of => Object.values(of[way]).reduce((sum, count) => sum + count[what], 0); + // 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); 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, messages, bytes) => { - const known = rates.get(key); - rates.set(key, { messages: average(known && known.messages, messages / secs), - bytes: average(known && known.bytes, bytes / secs) }); + 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); }; - 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"), bytes = grown(counts, before, "bytes"); - sample(id, messages, bytes); - messagesInAll += messages; - bytesInAll += bytes; + 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); } - 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); } @@ -2462,11 +2510,13 @@ 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. +// link between other nodes, a node that is no peer, or before two updates came. With +// LINKS or without (`linksShown`). function showRates() { const filtering = consoleFiltered && focused !== null; const peer = filtering ? peerOf(focused) : null; - const shown = filtering && peer === null ? undefined : rates.get(peer); + const both = filtering && peer === null ? undefined : rates.get(peer); + const shown = both && both[linksShown ? "all" : "withoutLinks"]; 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; @@ -2563,7 +2613,8 @@ function filterConsole() { const filtering = consoleFiltered && focused !== null; log.classList.toggle("filtering", filtering); document.getElementById("console-none").hidden = - !filtering || log.querySelector(":scope > div.peer-focused") !== null; + !filtering || log.querySelector(":scope > div.peer-focused" + + (linksShown ? "" : ":not(.links-kind)")) !== null; const view = filtering ? focused : null; if (view !== consoleView) { consoleView = view; @@ -2577,6 +2628,7 @@ function consoleLine(message) { "data-related": message.related || "", "data-peer": message.peer, class: (shownMessage && shownMessage.seq === message.seq ? "focused" : "") + + (isLinks(message.counted) ? " links-kind" : "") + (relatedMarked !== null && message.related === relatedMarked ? " related" : "") + peerClasses(message.peer), title: message.way === "refused" ? "refused: the transport wouldn't send it" : "" }, @@ -2706,6 +2758,11 @@ function connect() { if (snapshot.page !== PAGE_VERSION) { events.close(); status("new version; reloading…", false); + // As this page is now (`handOff`): the map, what's open, the focus; reloaded with + // the URL's fragment, which the new page takes (`handOver`). + try { + history.replaceState(null, "", location.pathname + location.search + handOff(null)); + } catch (_) { /* not where history works: reloaded as it is */ } location.reload(); return; } -- cgit v1.3.1