diff options
| author | tv <tv@krebsco.de> | 2026-10-31 23:06:54 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-31 23:06:54 +0100 |
| commit | 6dc46f63b34c89a7dc1d5123e3008c4f41a5ea7b (patch) | |
| tree | c076fec42db39f4a9b294eb1222222ac832062d6 /src/api/dashboard | |
| parent | 3c819d60328893be8a0429e138ea54097ee7d6e5 (diff) | |
dashboard: add console
Diffstat (limited to 'src/api/dashboard')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 88 |
1 files changed, 66 insertions, 22 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 7ac33fe..5eb4a5c 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -44,12 +44,12 @@ /* The columns and rows as dragged (`--columns`, `--rows`, set by `sizes`). */ main { flex: 1; min-height: 0; display: grid; gap: 16px; padding: 16px; grid-template-columns: var(--columns, minmax(0, 3fr) minmax(0, 2fr)); - grid-template-rows: var(--rows, auto minmax(0, 1fr) minmax(0, 1fr)); - grid-template-areas: "map peers" "map figures" "map document"; } + grid-template-rows: var(--rows, auto minmax(0, .4fr) minmax(0, .4fr) minmax(0, .2fr)); + grid-template-areas: "map peers" "map figures" "map document" "map console"; } @media (max-width: 900px) { body { height: auto; } - main { grid-template-columns: minmax(0, 1fr); grid-template-rows: 70vh auto 70vh 70vh; - grid-template-areas: "map" "peers" "figures" "document"; } + main { grid-template-columns: minmax(0, 1fr); grid-template-rows: 70vh auto 70vh 70vh 40vh; + grid-template-areas: "map" "peers" "figures" "document" "console"; } } section { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 12px; min-width: 0; min-height: 0; display: flex; flex-direction: column; } @@ -66,6 +66,10 @@ #peers-box { grid-area: peers; } #figures-box { grid-area: figures; } #document-box { grid-area: document; } + #console-box { grid-area: console; } + #console-box .scroll { flex: 1; } + .console { font-family: ui-monospace, monospace; font-size: 12px; white-space: pre-wrap; } + .console .when { color: var(--muted); } svg { flex: 1; min-height: 0; width: 100%; display: block; cursor: default; } /* Dragged to pan or to move a node, also by touch. */ /* Dragged, not selected: neither its labels' text nor the picture itself. */ @@ -187,6 +191,10 @@ <h2>Document</h2> <div class="scroll"><div id="document" class="tree"></div></div> </section> + <section id="console-box"> + <h2>Console</h2> + <div class="scroll" id="console-scroll"><div id="console" class="console"></div></div> + </section> </main> <script> "use strict"; @@ -1447,22 +1455,23 @@ flowsToggle.addEventListener("click", () => { showFlowsToggle(); if (!flowsShown) { flows.length = 0; - lastTraffic = null; document.getElementById("flows").replaceChildren(); } }); let lastTraffic = null; const flows = []; let flowFrame = null; +// Messages by kind, of a peer's `traffic.sent` or `.received`. function messages(byKind) { - return Object.values(byKind || {}).reduce((sum, counter) => sum + counter.messages, 0); + return Object.fromEntries(Object.entries(byKind || {}) + .map(([kind, counter]) => [kind, counter.messages])); } +// The messages exchanged with each peer since the last update: on the map (`flows`, +// if shown) and in the console (`logTraffic`). function trafficArrived() { - if (!state || !flowsShown || calm.matches || document.hidden) { - lastTraffic = null; - return; - } + if (!state) return; const self = state.node.id, now = performance.now(); + const dots = flowsShown && !calm.matches && !document.hidden; const current = new Map(); for (const [id, peer] of Object.entries(state.peers.peers)) { if (peer.traffic) { @@ -1475,7 +1484,16 @@ function trafficArrived() { const before = lastTraffic.get(id); if (!before) continue; for (const [way, from, to] of [["sent", self, id], ["received", id, self]]) { - const count = Math.min(FLOWS_AT_MOST, Math.max(0, counts[way] - before[way])); + const kinds = {}; + let total = 0; + for (const [kind, count] of Object.entries(counts[way])) { + const grown = Math.max(0, count - (before[way][kind] || 0)); + if (grown > 0) { kinds[kind] = grown; total += grown; } + } + if (total === 0) continue; + logTraffic(way, id, total, kinds); + if (!dots) continue; + const count = Math.min(FLOWS_AT_MOST, total); for (let i = 0; i < count; i++) { flows.push({ from, to, start: now + i * FLOW_SPREAD_MS / count }); } @@ -1485,6 +1503,22 @@ function trafficArrived() { lastTraffic = current; if (flows.length > 0 && flowFrame === null) flowFrame = requestAnimationFrame(drawFlows); } + +// --- Console: what this node exchanged with its peers, a line per peer and way at +// every update (the same counts as the map's messages), the latest at the bottom, +// `CONSOLE_LINES` at most. Scrolled along while at the bottom. --- +const CONSOLE_LINES = 200; +function logTraffic(way, peer, total, kinds) { + const log = document.getElementById("console"); + const scroll = document.getElementById("console-scroll"); + const following = scroll.scrollTop + scroll.clientHeight >= scroll.scrollHeight - 4; + const when = new Date().toLocaleTimeString(); + const parts = Object.entries(kinds).map(([kind, count]) => kind + " " + count).join(", "); + log.append(el("div", {}, el("span", { class: "when" }, when + " "), + (way === "sent" ? "→ " : "← ") + name(peer) + ": " + total + " (" + parts + ")")); + while (log.childElementCount > CONSOLE_LINES) log.firstElementChild.remove(); + if (following) scroll.scrollTop = scroll.scrollHeight; +} function drawFlows() { flowFrame = null; const layer = document.getElementById("flows"); @@ -1624,9 +1658,9 @@ window.addEventListener("resize", () => { layoutAgain(); render(); }); // --- Sizes: the space between boxes dragged to resize them, double-clicked to reset; // kept in this browser --- const main = document.querySelector("main"); -// The map's share of the width; the peers' height in px; the figures' share of what -// the peers leave. Null: as laid out by default. -let sizes = { columns: null, peers: null, split: null }; +// The map's share of the width; the peers' height in px; the console's share of what +// the peers leave, and the figures' share of the rest. Null: as laid out by default. +let sizes = { columns: null, peers: null, split: null, console: null }; try { sizes = { ...sizes, ...JSON.parse(localStorage.getItem("continuum-sizes") || "{}") }; } catch (_) {} function applySizes() { @@ -1634,11 +1668,15 @@ function applySizes() { if (sizes.columns === null) style.removeProperty("--columns"); else style.setProperty("--columns", "minmax(0, " + sizes.columns + "fr) minmax(0, " + (1 - sizes.columns) + "fr)"); - if (sizes.peers === null && sizes.split === null) style.removeProperty("--rows"); - else { + if (sizes.peers === null && sizes.split === null && sizes.console === null) { + style.removeProperty("--rows"); + } else { const split = sizes.split === null ? .5 : sizes.split; + const consoleShare = sizes.console === null ? .2 : sizes.console; + const rest = 1 - consoleShare; style.setProperty("--rows", (sizes.peers === null ? "auto" : sizes.peers + "px") + - " minmax(0, " + split + "fr) minmax(0, " + (1 - split) + "fr)"); + " minmax(0, " + split * rest + "fr) minmax(0, " + (1 - split) * rest + "fr)" + + " minmax(0, " + consoleShare + "fr)"); } main.classList.toggle("peers-sized", sizes.peers !== null); try { localStorage.setItem("continuum-sizes", JSON.stringify(sizes)); } catch (_) {} @@ -1646,16 +1684,18 @@ function applySizes() { applySizes(); const box = id => document.getElementById(id).getBoundingClientRect(); // Which space between boxes `event` is over, if any: "columns", "peers" (below the -// peers) or "split" (between figures and document). Only side by side. +// peers), "split" (between figures and document) or "console" (above the console). +// Only side by side. function gapAt(event) { if (event.target !== main) return null; const map = box("map"), peers = box("peers-box"), figures = box("figures-box"), - documentBox = box("document-box"); + documentBox = box("document-box"), consoleBox = box("console-box"); if (map.right >= peers.left) return null; if (event.clientX >= map.right && event.clientX <= peers.left) return "columns"; if (event.clientX < peers.left || event.clientX > peers.right) return null; if (event.clientY >= peers.bottom && event.clientY <= figures.top) return "peers"; if (event.clientY >= figures.bottom && event.clientY <= documentBox.top) return "split"; + if (event.clientY >= documentBox.bottom && event.clientY <= consoleBox.top) return "console"; return null; } function resizeTo(gap, event) { @@ -1666,9 +1706,13 @@ function resizeTo(gap, event) { sizes.columns = clamp((event.clientX - half - map.left) / (peers.right - map.left - 2 * half), .2, .8); } else if (gap === "peers") { - const peers = box("peers-box"), documentBox = box("document-box"); + const peers = box("peers-box"), consoleBox = box("console-box"); sizes.peers = Math.round(clamp(event.clientY - half - peers.top, 60, - documentBox.bottom - peers.top - 4 * half - 160)); + consoleBox.bottom - peers.top - 6 * half - 200)); + } else if (gap === "console") { + const figures = box("figures-box"), consoleBox = box("console-box"); + sizes.console = clamp((consoleBox.bottom - event.clientY - half) / + (consoleBox.bottom - figures.top - 4 * half), .05, .8); } else { const figures = box("figures-box"), documentBox = box("document-box"); sizes.split = clamp((event.clientY - half - figures.top) / @@ -1690,7 +1734,7 @@ main.addEventListener("pointermove", event => { } const gap = gapAt(event); main.classList.toggle("resize-columns", gap === "columns"); - main.classList.toggle("resize-rows", gap === "peers" || gap === "split"); + main.classList.toggle("resize-rows", gap === "peers" || gap === "split" || gap === "console"); }); main.addEventListener("pointerdown", event => { resizing = gapAt(event); |
