diff options
Diffstat (limited to 'src/api/dashboard/dashboard.html')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 145 |
1 files changed, 100 insertions, 45 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index efbf892..3f02a64 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -774,11 +774,25 @@ function renderStats() { // Drawn once: a message doesn't change, and drawing it anew would scroll it back up. if (stats.dataset.message === String(message.seq)) return; stats.dataset.message = String(message.seq); - what.textContent = message.type + " " + WAYS[message.way] + " " + name(message.peer); + what.textContent = message.node + ? message.type + " " + name(message.to) + " ← " + name(message.from) + : message.type + " " + WAYS[message.way] + " " + name(message.peer); const field = (key, value) => el("div", { class: "leaf" }, el("span", { class: "key" }, key + ": "), value); title.replaceChildren("Message: ", what); retitle(title, title.textContent); + // A subscribed node's: logged there, the message itself not read from it. + if (message.node) { + document.getElementById("stats").replaceChildren( + field("at", clockTime(new Date(message.at))), + field("from", message.from), + field("to", message.to), + field("logged by", message.node), + field("type", message.type), + field("bytes", String(message.bytes)), + el("div", { class: "note" }, "Written out only on the node that logged it (its console).")); + return; + } document.getElementById("stats").replaceChildren( field("at", clockTime(new Date(message.at))), field(message.way === "received" ? "from" : "to", message.peer), @@ -1224,7 +1238,8 @@ function renderGraph() { const marking = new Map(); for (const [message, how] of [[pointedMessage, "pointed"], [shownMessage, "shown"]]) { if (message === null) continue; - for (const key of [state.node.id, message.peer, pairKey(state.node.id, message.peer)]) { + const [a, b] = message.node ? [message.from, message.to] : [state.node.id, message.peer]; + for (const key of [a, b, pairKey(a, b)]) { marking.set(key, how); } } @@ -2292,9 +2307,12 @@ function follow(id, delay = 2000) { const member = state && state.graph.members[id]; if (!member || !member.dashboard) return fail("it publishes no dashboard (--dashboard-url)"); if (!narrowToken) return fail(tokenWhy); + // Its endpoints, beside its page (`…/admin/dashboard` → `…/admin/dashboard/…`). + const base = member.dashboard.split("#")[0].replace(/\/+$/, ""); + const at = path => new URL(path, base).href; let url; try { - url = new URL("dashboard/links/own/events", member.dashboard.split("#")[0].replace(/\/+$/, "")).href; + url = at("dashboard/links/own/events"); } catch (_) { return fail("its dashboard's address isn't one"); } @@ -2310,25 +2328,19 @@ function follow(id, delay = 2000) { render(); } }).catch(error => fail(error.message || String(error))); - // Its traffic, for its messages on the map: while it goes, quietly; a failure of its - // own leaves the links be. - let trafficUrl; - try { - trafficUrl = new URL("dashboard/traffic/own/events", url).href; - } catch (_) { - return; - } - const followTraffic = (wait = 2000) => { + // Its messages, for the console and the map: while they come, quietly; a failure of + // their own leaves the links be. + const messagesUrl = at("dashboard/messages/own/events"); + const followMessages = (wait = 2000) => { if (controller.signal.aborted) return; - read(trafficUrl, controller.signal, () => { entry.trafficLive = true; wait = 2000; }, - (kind, data) => { if (kind === "traffic") remoteTrafficArrived(id, entry, JSON.parse(data)); }) + read(messagesUrl, controller.signal, () => { entry.messagesLive = true; wait = 2000; }, + (kind, data) => { if (kind === "messages") remoteMessagesArrived(id, entry, JSON.parse(data)); }) .catch(() => { - entry.trafficLive = false; - entry.traffic = null; - if (!controller.signal.aborted) setTimeout(() => followTraffic(Math.min(60000, wait * 2)), wait); + entry.messagesLive = false; + if (!controller.signal.aborted) setTimeout(() => followMessages(Math.min(60000, wait * 2)), wait); }); }; - followTraffic(); + followMessages(); } // Server-sent events from `url`, read with this page's narrow token (`EventSource` // can't send one): `opened` once answered, `event(kind, data)` for each; rejects when it @@ -2644,8 +2656,9 @@ graph.addEventListener("mousemove", event => { if (event.target === graph) hover graph.addEventListener("mouseleave", () => hover(null)); // --- Messages on the map: this node's traffic with each peer (`/peers`, `traffic`), -// and each subscribed node's with its peers (`remoteTrafficArrived`), as it grew since -// the last update, drawn as dots running along the link the way the messages went. Counts come with each update (every 2 s), so the dots are spread +// as it grew since the last update, and each subscribed node's messages with its peers +// (`remoteMessagesArrived`), drawn as dots running along the link the way the messages +// went. Counts come with each update (every 2 s), so the dots are spread // over the next 2 s, a few at most per link and way: a picture of the traffic, not a // record of it. All at one speed (`flowSpeed`), so over a longer link they arrive // later. Kept above everything else on the map (`flows`, after what `show` places). --- @@ -2839,24 +2852,49 @@ function dotsNow(gap) { !calm.matches && !document.hidden && !document.getElementById("map").classList.contains("collapsed"); } -// A subscribed node's traffic (`followTraffic`), as `trafficArrived` this node's: its -// messages with this node are this node's to draw, and what it received from another -// subscribed node that one's (as what it sent). -function remoteTrafficArrived(id, entry, traffic) { +// A subscribed node's messages (`follow`), those of a tick: listed in the console and +// run along its links, as this node's own. Its messages with this node are this node's +// to show, and what it received from another subscribed node that one's (as what it +// sent): each message once. Each named by both ends (`from`, `to`) and the node that +// logged it (`node`), its number and exchange made its own (`node:seq`). +function remoteMessagesArrived(id, entry, list) { if (!state) return; - const current = new Map(); - for (const [peer, counts] of Object.entries(traffic)) { - current.set(peer, { sent: messages(counts.sent), received: messages(counts.received) }); + const self = state.node.id; + const wall = Date.now(), gap = wall - (entry.messagesAt || 0); + entry.messagesAt = wall; + const kept = []; + for (const message of list) { + if (message.way === "refused" || message.peer === self) continue; + if (message.way === "received" && message.peer !== id && subscriptions.has(message.peer) && + (subscribed.get(message.peer) || {}).messagesLive) continue; + const [from, to] = message.way === "sent" ? [id, message.peer] : [message.peer, id]; + kept.push({ ...message, seq: id + ":" + message.seq, node: id, from, to, + related: message.related ? id + ":" + message.related : null }); } - const wall = Date.now(), gap = wall - (entry.trafficAt || 0); - entry.trafficAt = wall; - if (entry.traffic && dotsNow(gap)) { - const self = state.node.id; - addFlows(id, current, entry.traffic, (way, peer) => peer === self || - (way === "received" && peer !== id && subscriptions.has(peer) && - (subscribed.get(peer) || {}).trafficLive)); + if (kept.length === 0) return; + if (dotsNow(gap)) { + const now = performance.now(); + const links = new Map(); + for (const message of kept) { + if (!linksShown && isLinks(message.counted)) continue; + const key = message.from + " " + message.to; + const link = links.get(key) || { from: message.from, to: message.to, kinds: {}, total: 0 }; + const kind = link.kinds[message.counted] || (link.kinds[message.counted] = { messages: 0, bytes: 0 }); + kind.messages += 1; + kind.bytes += message.bytes; + link.total += 1; + links.set(key, link); + } + for (const { from, to, kinds, total } of links.values()) { + const shown = dotsFor(kinds, total); + shown.forEach((dot, i) => { + flows.push({ from, to, start: now + i * FLOW_SPREAD_MS / shown.length, + kind: dot.kind, r: dotRadius(dot.bytes) }); + }); + } + if (flows.length > 0 && flowFrame === null) flowFrame = requestAnimationFrame(drawFlows); } - entry.traffic = current; + messagesArrived(kept); } // --- The console's rates: messages and bytes per second sent and received, per peer @@ -2977,15 +3015,27 @@ function peerOf(key) { return a === self ? b : b === self ? a : null; } // A console line's tint for the node or link pointed at on the map, or in focus. -function peerClasses(peer) { - return (peer === peerOf(focused) ? " peer-focused" : "") + - (peer === peerOf(hovered) ? " peer-hovered" : ""); +// Whether a message (its line's `data-…`: `peer` for this node's, `from` and `to` for a +// subscribed node's) is one with the node or over the link `key` stands for. +function about(data, key) { + if (key === null || !state) return false; + if (!data.peer) { + if (!data.from) return false; + return isPair(key) ? key === pairKey(data.from, data.to) + : key !== state.node.id && (data.from === key || data.to === key); + } + const peer = peerOf(key); + return peer !== null && data.peer === peer; +} +function peerClasses(message) { + const data = { peer: message.node ? "" : message.peer, from: message.from, to: message.to }; + return (about(data, focused) ? " peer-focused" : "") + + (about(data, hovered) ? " peer-hovered" : ""); } function markPeerMessages() { - const focusedPeer = peerOf(focused), hoveredPeer = peerOf(hovered); for (const line of document.getElementById("console").children) { - line.classList.toggle("peer-focused", focusedPeer !== null && line.dataset.peer === focusedPeer); - line.classList.toggle("peer-hovered", hoveredPeer !== null && line.dataset.peer === hoveredPeer); + line.classList.toggle("peer-focused", about(line.dataset, focused)); + line.classList.toggle("peer-hovered", about(line.dataset, hovered)); } filterConsole(); } @@ -3023,16 +3073,21 @@ function filterConsole() { } // A line: when, which way, with whom, the type (in its kind's colour) and the size. function consoleLine(message) { + // A subscribed node's (`remoteMessagesArrived`): its receiver, and whom from. + const between = message.node ? name(message.to) + " ← " + name(message.from) + " " + : WAYS[message.way] + " " + name(message.peer) + " "; const line = el("div", { "data-seq": String(message.seq), "data-related": message.related || "", - "data-peer": message.peer, + "data-peer": message.node ? "" : message.peer, + "data-from": message.from || "", "data-to": message.to || "", class: (shownMessage && shownMessage.seq === message.seq ? "focused" : "") + (isLinks(message.counted) ? " links-kind" : "") + (relatedMarked !== null && message.related === relatedMarked ? " related" : "") + - peerClasses(message.peer), - title: message.way === "refused" ? "refused: the transport wouldn't send it" : "" }, + peerClasses(message), + title: message.way === "refused" ? "refused: the transport wouldn't send it" + : message.node ? "logged by " + name(message.node) + ", subscribed to" : "" }, el("span", { class: "when" }, clockTime(new Date(message.at)) + " "), - WAYS[message.way] + " " + name(message.peer) + " ", + between, el("span", { style: "color: " + kindColour(message.counted) }, message.type), " " + size(message.bytes)); // On press, as the peers' rows: lines come and go under the pointer. |
