aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-31 23:07:11 +0100
committertv <tv@krebsco.de>2026-10-31 23:07:11 +0100
commit05ea71b3f69a0e2792519b84f7cc61df5c7056e1 (patch)
tree8dc27bc7f299e0ee350da1ee65a0f71160229de8 /src/api
parent6dc46f63b34c89a7dc1d5123e3008c4f41a5ea7b (diff)
dashboard: make boxes collapsible
Diffstat (limited to 'src/api')
-rw-r--r--src/api/dashboard/dashboard.html89
1 files changed, 72 insertions, 17 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 5eb4a5c..ec36abf 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -56,6 +56,14 @@
section h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .04em;
color: var(--muted); margin: 0 0 8px; flex: none; }
section h2 .what { text-transform: none; letter-spacing: 0; color: var(--fg); }
+ /* Collapsed: the title only, and nothing drawn in it (`busy`). */
+ button.collapse { font: inherit; padding: 0 4px 0 0; color: var(--muted); background: none;
+ border: none; cursor: pointer; }
+ button.collapse:hover { color: var(--fg); }
+ section.collapsed > :not(h2) { display: none; }
+ section.collapsed { align-self: start; }
+ section.collapsed h2 { margin: 0; }
+ section.collapsed h2 .toggle { display: none; }
section h2 .what .id { color: var(--muted); font-weight: normal; word-break: break-all; }
/* The space between boxes, where they are resized. */
main.resize-columns, main.resize-columns * { cursor: col-resize; }
@@ -184,7 +192,7 @@
<div class="scroll" id="peers-scroll"><table id="peers"></table></div>
</section>
<section id="figures-box">
- <h2 id="figures-title">Figures</h2>
+ <h2><span id="figures-title">Figures</span></h2>
<div class="scroll"><div id="stats" class="tree"></div></div>
</section>
<section id="document-box">
@@ -668,6 +676,7 @@ function layoutAgain() {
lastShape = "";
}
function renderGraph() {
+ if (document.getElementById("map").classList.contains("collapsed")) return;
const root = document.getElementById("graph");
const width = root.clientWidth || 800, height = root.clientHeight || 560;
root.setAttribute("viewBox", "0 0 " + width + " " + height);
@@ -1185,6 +1194,8 @@ document.getElementById("document").replaceChildren(
// A box is busy while text in it is selected: drawing it anew would lose the selection,
// so it waits (`held`) until it's gone.
function busy(box) {
+ // Collapsed: drawn when opened (`release`).
+ if (box.classList.contains("collapsed")) return true;
const selection = window.getSelection();
if (!selection || selection.isCollapsed || !selection.rangeCount) return false;
return box.contains(selection.getRangeAt(0).commonAncestorContainer);
@@ -1471,7 +1482,8 @@ function messages(byKind) {
function trafficArrived() {
if (!state) return;
const self = state.node.id, now = performance.now();
- const dots = flowsShown && !calm.matches && !document.hidden;
+ const dots = flowsShown && !calm.matches && !document.hidden &&
+ !document.getElementById("map").classList.contains("collapsed");
const current = new Map();
for (const [id, peer] of Object.entries(state.peers.peers)) {
if (peer.traffic) {
@@ -1508,17 +1520,30 @@ function trafficArrived() {
// 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;
+// The lines, kept while the box is collapsed and drawn when it's opened (`showConsole`).
+const consoleLines = [];
+function consoleLine([when, text]) {
+ return el("div", {}, el("span", { class: "when" }, when + " "), text);
+}
function logTraffic(way, peer, total, kinds) {
+ const parts = Object.entries(kinds).map(([kind, count]) => kind + " " + count).join(", ");
+ const line = [new Date().toLocaleTimeString(),
+ (way === "sent" ? "→ " : "← ") + name(peer) + ": " + total + " (" + parts + ")"];
+ consoleLines.push(line);
+ if (consoleLines.length > CONSOLE_LINES) consoleLines.shift();
+ if (document.getElementById("console-box").classList.contains("collapsed")) return;
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 + ")"));
+ log.append(consoleLine(line));
while (log.childElementCount > CONSOLE_LINES) log.firstElementChild.remove();
if (following) scroll.scrollTop = scroll.scrollHeight;
}
+function showConsole() {
+ document.getElementById("console").replaceChildren(...consoleLines.map(consoleLine));
+ const scroll = document.getElementById("console-scroll");
+ scroll.scrollTop = scroll.scrollHeight;
+}
function drawFlows() {
flowFrame = null;
const layer = document.getElementById("flows");
@@ -1668,20 +1693,18 @@ 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 && 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 * rest + "fr) minmax(0, " + (1 - split) * rest + "fr)" +
- " minmax(0, " + consoleShare + "fr)");
- }
+ // A collapsed box's row as tall as its title.
+ const shut = id => document.getElementById(id).classList.contains("collapsed");
+ const split = sizes.split === null ? .5 : sizes.split;
+ const consoleShare = sizes.console === null ? .2 : sizes.console;
+ const rest = 1 - consoleShare;
+ const share = (id, fr) => shut(id) ? "auto" : "minmax(0, " + fr + "fr)";
+ style.setProperty("--rows", (sizes.peers === null || shut("peers-box") ? "auto"
+ : sizes.peers + "px") + " " + share("figures-box", split * rest) + " " +
+ share("document-box", (1 - split) * rest) + " " + share("console-box", consoleShare));
main.classList.toggle("peers-sized", sizes.peers !== null);
try { localStorage.setItem("continuum-sizes", JSON.stringify(sizes)); } catch (_) {}
}
-applySizes();
const box = id => document.getElementById(id).getBoundingClientRect();
// Which space between boxes `event` is over, if any: "columns", "peers" (below the
// peers), "split" (between figures and document) or "console" (above the console).
@@ -1751,6 +1774,38 @@ const resized = () => {
};
main.addEventListener("pointerup", resized);
main.addEventListener("pointercancel", resized);
+// --- Boxes collapsed to their titles, and opened again; kept in this browser. A
+// collapsed box isn't drawn (`busy`), so it costs nothing while the node updates. ---
+const collapsed = new Set();
+try {
+ for (const id of JSON.parse(localStorage.getItem("continuum-collapsed") || "[]")) {
+ collapsed.add(id);
+ }
+} catch (_) {}
+for (const section of document.querySelectorAll("main > section")) {
+ const button = el("button", { class: "collapse" });
+ section.querySelector("h2").prepend(button);
+ const show = () => {
+ const shut = collapsed.has(section.id);
+ section.classList.toggle("collapsed", shut);
+ button.textContent = shut ? "▸" : "▾";
+ button.title = shut ? "Open" : "Collapse";
+ };
+ show();
+ button.addEventListener("click", () => {
+ if (collapsed.has(section.id)) collapsed.delete(section.id); else collapsed.add(section.id);
+ try { localStorage.setItem("continuum-collapsed", JSON.stringify([...collapsed])); }
+ catch (_) {}
+ show();
+ applySizes();
+ if (collapsed.has(section.id)) return;
+ if (section.id === "console-box") showConsole();
+ if (section.id === "map") layoutAgain();
+ release();
+ render();
+ });
+}
+applySizes();
main.addEventListener("dblclick", event => {
const gap = gapAt(event);
if (!gap) return;