aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-05 03:04:33 +0100
committertv <tv@krebsco.de>2026-11-05 03:04:33 +0100
commit34305caa35e75a7b6ae49adc616125cb631aee3d (patch)
tree40630d246bdb59a415d6ed065fdf1e8ad5290435
parentc5f67f56f46ea5c0b300a2e0eb00a7368efe4f2a (diff)
dashboard: human-friendly figures
-rw-r--r--docs/dashboard.md8
-rw-r--r--src/api/dashboard/dashboard.html53
2 files changed, 56 insertions, 5 deletions
diff --git a/docs/dashboard.md b/docs/dashboard.md
index 4067da5..3a92018 100644
--- a/docs/dashboard.md
+++ b/docs/dashboard.md
@@ -242,6 +242,12 @@ The figures of what is in focus, as a tree: this node's `/admin/stats`, or a pee
`/peers` entry and links. Branches stay as opened across updates, and ages count on
between them. What each figure means: [`measurements.md`](measurements.md).
+Some values get a reading after them, muted, in parentheses: a node ID its alias
+(where it has one; a key that is a node ID too), a time in Unix milliseconds in RFC 3339
+(a key ending in `At`, `Until` or `Since`, or `at`, `until`, `since`, `alive`), bytes
+in KiB, MiB, GiB (a key ending in `Bytes`, or `bytes`; from 1 KiB), and bytes per
+second likewise (`…BytesPerSec`, `bytesPerSec`).
+
## Document
The swarm's document, as this node holds it, its first level open. A branch is read
@@ -262,6 +268,8 @@ to the whole swarm like any other:
Protocol data (`_continuum`) is written only by users in `--protocol-writers`, and epoch
metadata and the swarm ID by nobody ([`trust.md`](trust.md)). A key containing `/`, or an empty
key, is listed but can't be opened.
+In the protocol data, values and keys get the same readings as in the figures: the
+aliases of node IDs (a member's entry, a vote, a coordinator), times and sizes.
## Console
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index ede191d..7be738c 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -254,6 +254,8 @@
.tree details[open] > summary::before { content: "▾"; }
.tree details > .note { padding-left: 1.1em; }
.key { color: var(--muted); }
+ /* What a value or key means, after it (`gloss`): an alias, a time, a size. */
+ .gloss { color: var(--muted); }
</style>
</head>
<body>
@@ -584,6 +586,39 @@ if (handOver && Array.isArray(handOver.figures)) {
open.clear();
for (const path of handOver.figures) if (typeof path === "string") open.add(path);
}
+// What a value means, shown muted in parentheses after it (`glossed`): a node ID's
+// alias, a Unix-milliseconds time in RFC 3339 (a key that names a time: …At, …Until,
+// …Since, at, since, alive), bytes in KiB, MiB, GiB (a key ending in …Bytes, or
+// bytes), and bytes per second likewise (…BytesPerSec, bytesPerSec). Null if nothing to add.
+const DATE_MS = [946684800000, 4102444800000];
+function aliasOf(id) {
+ if (!state || typeof id !== "string") return null;
+ const member = state.graph.members[id], peer = state.peers.peers[id];
+ return (member && member.alias) || (peer && peer.alias) || null;
+}
+function bytesReadable(bytes) {
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
+ let unit = 0;
+ while (Math.abs(bytes) >= 1024 && unit < units.length - 1) { bytes /= 1024; unit++; }
+ return (unit === 0 ? String(bytes) : bytes.toFixed(1)) + " " + units[unit];
+}
+function gloss(key, value) {
+ if (typeof value === "string") return aliasOf(value);
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
+ const name = String(key);
+ // By how the key ends: `savedBytesSecsAgo` is no size.
+ if (/(^b|B)ytesPerSec$/.test(name)) return value >= 1024 ? bytesReadable(value) + "/s" : null;
+ if (/(^b|B)ytes$/.test(name)) return value >= 1024 ? bytesReadable(value) : null;
+ if ((/(At|Until|Since)$/.test(name) || /^(at|until|since|alive)$/.test(name)) &&
+ value >= DATE_MS[0] && value < DATE_MS[1]) {
+ return new Date(value).toISOString().replace(".000Z", "Z");
+ }
+ return null;
+}
+// `gloss`'s, as an element to put after the value (or the key: an alias), if any.
+function glossed(text) {
+ return text === null ? [] : [" ", el("span", { class: "gloss" }, "(" + text + ")")];
+}
function tree(key, value, path) {
// Empty: nothing to open.
if (value !== null && typeof value === "object" && Object.keys(value).length === 0) {
@@ -597,7 +632,7 @@ function tree(key, value, path) {
if (details.open) open.add(path); else open.delete(path);
});
const entries = Array.isArray(value) ? value.map((v, i) => [String(i), v]) : Object.entries(value);
- const summary = el("summary", {}, el("span", { class: "key" }, key),
+ const summary = el("summary", {}, el("span", { class: "key" }, key), ...glossed(aliasOf(key)),
Array.isArray(value) ? " [" + value.length + "]" : "");
// Opened or closed on press, as the peers' rows are focused: the tree is drawn anew
// every second (ages), and a click whose press and release land on two drawings is
@@ -614,8 +649,9 @@ function tree(key, value, path) {
for (const [k, v] of entries) details.append(tree(k, v, path + "." + k));
return details;
}
- return el("div", { class: "leaf" }, el("span", { class: "key" }, key + ": "),
- JSON.stringify(aged(key, value)));
+ return el("div", { class: "leaf" }, el("span", { class: "key" }, key,
+ ...glossed(aliasOf(key)), ": "), JSON.stringify(aged(key, value)),
+ ...glossed(gloss(key, value)));
}
function renderStats() {
const title = document.getElementById("figures-title");
@@ -1483,8 +1519,14 @@ function addButton(kind, path, known, place) {
return button;
}
+// In the protocol's data (`_continuum`): node IDs, times and sizes glossed (`gloss`).
+const glossedPath = path => path[0] === "_continuum";
function editableLeaf(key, shown, value, path) {
- const leaf = el("div", { class: "leaf" }, el("span", { class: "key" }, key + ": "), shown);
+ const glosses = glossedPath(path);
+ // The key one element (kept while editing), its alias in it.
+ const leaf = el("div", { class: "leaf" }, el("span", { class: "key" }, key,
+ ...(glosses ? glossed(aliasOf(key)) : []), ": "), shown,
+ ...(glosses ? glossed(gloss(key, value)) : []));
if (!editable(path)) return leaf;
leaf.title = "Double-click to edit";
leaf.addEventListener("dblclick", event => {
@@ -1532,7 +1574,8 @@ function documentEntry(key, summary, path) {
}
const details = el("details", {});
const described = document.createTextNode(describe(summary));
- const head = el("summary", {}, el("span", { class: "key" }, key), " ", described);
+ const head = el("summary", {}, el("span", { class: "key" }, key),
+ ...(glossedPath(path) ? glossed(aliasOf(key)) : []), " ", described);
details.append(head);
// A double click edits the whole of it: read, then shown in a field.
if (editable(path)) {