aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard/dashboard.html
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/dashboard/dashboard.html')
-rw-r--r--src/api/dashboard/dashboard.html145
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.