aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-31 23:06:54 +0100
committertv <tv@krebsco.de>2026-10-31 23:06:54 +0100
commit6dc46f63b34c89a7dc1d5123e3008c4f41a5ea7b (patch)
treec076fec42db39f4a9b294eb1222222ac832062d6 /src/api
parent3c819d60328893be8a0429e138ea54097ee7d6e5 (diff)
dashboard: add console
Diffstat (limited to 'src/api')
-rw-r--r--src/api/dashboard/dashboard.html88
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);