aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/access.md3
-rw-r--r--docs/dashboard.md17
-rw-r--r--src/api/cors.rs7
-rw-r--r--src/api/dashboard/dashboard.html76
4 files changed, 90 insertions, 13 deletions
diff --git a/docs/access.md b/docs/access.md
index 38ae180..50c0265 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -173,7 +173,8 @@ it may do.
Pages of other origins, the dashboards of other members, may call a node with a token:
its own links (`/admin/dashboard/links/own`, and its events) and messages
(`/admin/dashboard/messages/own/events`, and one written out,
-`/admin/dashboard/messages/own/{seq}`), `/auth/refresh` and `/auth/whoami`, nothing
+`/admin/dashboard/messages/own/{seq}`), its figures and peers (`/admin/stats`,
+`/peers`), `/auth/refresh` and `/auth/whoami`, nothing
else. A node allows the origins of the dashboards its member
list names (`--dashboard-url`) and those of `--cors-origins`; without credentials (no
cookie or password crosses origins: the page sends its token).
diff --git a/docs/dashboard.md b/docs/dashboard.md
index 590f72f..da69a16 100644
--- a/docs/dashboard.md
+++ b/docs/dashboard.md
@@ -287,14 +287,18 @@ What the Figures box shows is what is in focus:
* **Nothing** (the default): this node's figures.
* **A node** (click it on the map, or its row in Peers): this node's entry for that
- peer (as `/peers` has it), and the links that member reports. Ringed on the map,
+ peer (as `/peers` has it), and the links that member reports. A node the page
+ subscribes to: its own figures first, as this node's are (its `/admin/stats`, read
+ from it every 2 s while in focus, with the page's token), then this node's entry for
+ it (`as this node sees it`) and its links, each a branch. Ringed on the map,
highlighted in Peers. A second click on its row, a click beside the nodes or a click
on this node goes back.
* **A link** (click it): whether the member list intends it, each end's report of it
(transport, relayed, round trip, dormant, its other transports, and how old the
- report is), and, where this node is an end, the link as this node has it in `/peers`:
- where messages go, its paths (each with its connection and round trip), and the
- addresses known of the other end. Both ends are ringed.
+ report is), and, where this node or a node the page subscribes to is an end, the link
+ as that end has it in its `/peers` (a subscribed node's read from it, as its
+ figures): where messages go, its paths (each with its connection and round trip),
+ and the addresses known of the other end. Both ends are ringed.
The focus is in the page's address (`#<node ID>`, or the two IDs of a link), so a
reload keeps it and a link to the page can name it.
@@ -321,8 +325,9 @@ The box is as tall as its rows, up to eight; then it scrolls.
## Figures
The figures of what is in focus, as a tree, its keys at the top level and how many
-beside the title: this node's `/admin/stats`, or a peer's `/peers` entry and, after it,
-the links it reports (`links`), where the page has them.
+beside the title: this node's `/admin/stats`; a subscribed node's own (`Node:`), with
+this node's entry for it and its links after; or a peer's `/peers` entry and, after
+it, the links it reports (`links`), where the page has them.
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
diff --git a/src/api/cors.rs b/src/api/cors.rs
index fc8270d..287daf1 100644
--- a/src/api/cors.rs
+++ b/src/api/cors.rs
@@ -1,6 +1,7 @@
//! Pages of other origins reading this node: the dashboards of other members, which a
//! page subscribes to for their own links and messages (`/admin/dashboard/links/own`,
-//! `/admin/dashboard/messages/own/events`, and one written out), refreshing its
+//! `/admin/dashboard/messages/own/events`, and one written out) and figures
+//! (`/admin/stats`, `/peers`), refreshing its
//! token on any node (`/auth/refresh`, `/auth/whoami`). Only those endpoints, only for
//! the origins of the dashboards the member list names and those of `--cors-origins`,
//! and without credentials: a page sends its token itself (`Authorization: Bearer`),
@@ -26,6 +27,8 @@ fn open(path: &str) -> bool {
|| matches!(
path,
"/admin/dashboard/links/own"
+ | "/admin/stats"
+ | "/peers"
| "/admin/dashboard/links/own/events"
| "/admin/dashboard/messages/own/events"
| "/auth/refresh"
@@ -145,6 +148,8 @@ mod tests {
assert!(open("/admin/dashboard/links/own"));
assert!(open("/admin/dashboard/messages/own/42"));
assert!(!open("/admin/dashboard/messages/own/x"));
+ assert!(open("/admin/stats") && open("/peers"));
+ assert!(!open("/peers/continuum:00"));
assert!(open("/auth/refresh"));
assert!(!open("/auth/login"));
assert!(!open("/store"));
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 6d32d55..d6f034b 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -655,6 +655,14 @@ let focused = handOver ? handOver.focused || null : (() => {
})();
const pairKey = (a, b) => a < b ? a + " " + b : b + " " + a;
const isPair = key => key !== null && key.includes(" ");
+// What subscribed nodes in focus say of themselves (`renderStats`): a node's figures
+// (`/admin/stats`) and peers (`/peers`), or a link's ends' peers, read from each every
+// 2 s while in focus, with the page's narrow token (`readFigures`). By node ID:
+// `stats`, `peers`, and `why` where they weren't read.
+const figuresOf = new Map();
+// The latest reading's number: an earlier one still on its way neither draws nor reads
+// again.
+let figuresTimer = null, figuresRound = 0;
function focus(id) {
focused = id === null || id === state.node.id ? null : id;
if (shownMessage !== null) {
@@ -670,6 +678,7 @@ function focus(id) {
render();
markPeerMessages();
loadTree();
+ readFigures();
}
// --- Peers ---
@@ -859,6 +868,36 @@ function tree(key, value, path) {
...glossed(aliasOf(key)), ": "), JSON.stringify(aged(key, value)),
...glossed(gloss(key, value)));
}
+// Read every 2 s while in focus (`figuresOf`, `figuresTimer` with `focused`).
+async function readFigures() {
+ clearTimeout(figuresTimer);
+ figuresTimer = null;
+ const round = ++figuresRound;
+ if (focused === null || !state) return;
+ const reading = focused;
+ const ids = (isPair(reading) ? reading.split(" ") : [reading])
+ .filter(id => id !== state.node.id && subscriptions.has(id));
+ await Promise.all(ids.map(async id => {
+ const get = async path => {
+ const response = await fetch(endpointOf(id, path), { cache: "no-store",
+ headers: { Authorization: "Bearer " + narrowToken.token, Accept: "application/json" } });
+ const answer = await response.json().catch(() => ({}));
+ if (!response.ok) throw new Error(response.status + " " + (answer.error || response.statusText));
+ return answer;
+ };
+ try {
+ if (!narrowToken) throw new Error(tokenWhy);
+ const [stats, peers] = await Promise.all([isPair(reading) ? null : get("stats"), get("../peers")]);
+ const known = figuresOf.get(id) || {};
+ figuresOf.set(id, { stats: stats || known.stats || null, peers: peers.peers || peers });
+ } catch (error) {
+ figuresOf.set(id, { ...(figuresOf.get(id) || {}), why: error.message || String(error) });
+ }
+ }));
+ if (round !== figuresRound || focused !== reading) return;
+ if (ids.length > 0) render();
+ figuresTimer = setTimeout(readFigures, 2000);
+}
function renderStats() {
const title = document.getElementById("figures-title");
const what = el("span", { class: "what" });
@@ -926,12 +965,16 @@ function renderStats() {
}
// Where this node is an end: the link itself, as `/peers` has the other end (where
// messages go, the paths, the addresses). The whole entry is the node's focus.
+ // Each end's paths to the other: this node's, and a subscribed node's as it says
+ // (`figuresOf`).
const self = state.node.id;
- const other = a === self ? b : b === self ? a : null;
- const peer = other !== null && state.peers.peers[other];
- if (peer) {
- shown.push(tree("this node's paths", {
- destination: peer.destination, addresses: peer.addresses }, "paths"));
+ for (const [x, y] of [[a, b], [b, a]]) {
+ const peers = x === self ? state.peers.peers : (figuresOf.get(x) || {}).peers;
+ const peer = peers && peers[y];
+ if (peer) {
+ shown.push(tree(x === self ? "this node's paths" : name(x) + "'s paths", {
+ destination: peer.destination, addresses: peer.addresses }, "paths." + (x === self ? "self" : x)));
+ }
}
title.replaceChildren("Link: ", what);
retitle(title, title.textContent);
@@ -955,6 +998,26 @@ function renderStats() {
what.replaceChildren(...(alias ? [alias, " "] : []), el("span", { class: "id" }, focused));
const peer = state.peers.peers[focused];
const report = state.graph.measured[focused];
+ // A subscribed node: its own figures at the top level, as this node's are, then this
+ // node's `/peers` entry for it and the links it reports, each a branch.
+ if (subscriptions.has(focused)) {
+ const known = figuresOf.get(focused) || {};
+ const figures = Object.entries(known.stats || {});
+ shown = figures.map(([k, v]) => tree(k, v, "stats." + k));
+ if (known.why) {
+ shown.push(el("div", { class: "note" }, "Its figures not read: " + known.why));
+ } else if (!known.stats) {
+ shown.push(el("div", { class: "note" }, "Reading its figures…"));
+ }
+ shown.push(peer ? tree("as this node sees it", peer, "seen")
+ : el("div", { class: "key leaf" }, "Not a peer of this node."));
+ if (report) shown.push(tree("links", report, "links"));
+ title.replaceChildren("Node: ", what, el("span", { class: "count" },
+ describe({ type: "map", size: figures.length + 1 + (report ? 1 : 0) })));
+ retitle(title, title.textContent);
+ document.getElementById("stats").replaceChildren(...shown);
+ return;
+ }
// Its `/peers` entry is the box, as this node's stats are: its keys at the top level,
// and after them the links the member reports.
const entries = Object.entries(peer || {});
@@ -2532,6 +2595,7 @@ function toggleSubscription(id) {
}
keepSubscriptions();
render();
+ readFigures();
}
// The subscriptions' reports on the map, over what this node has of them: fresher.
// What they cover of the node's state is kept aside (`overlaid`: by node, the node's
@@ -4089,9 +4153,11 @@ function connect() {
}
// Changes while not connected went unseen: what is open is read again.
if (state !== null) documentChanged({ all: true });
+ const first = state === null;
state = snapshot; stateAt = Date.now(); live = true; overlaid.clear();
status("live", true); pulse(); render(); trafficArrived(); epochsArrived();
remakesArrived();
+ if (first) readFigures();
if (treeShown && writerTree === null) loadTree();
if (token === null && tokenTimer === null) obtainTokens();
// Followed before the page knew which node it is: not any more.