diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/cors.rs | 7 | ||||
| -rw-r--r-- | src/api/dashboard/dashboard.html | 76 |
2 files changed, 77 insertions, 6 deletions
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. |
