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.html92
1 files changed, 83 insertions, 9 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 1f27544..5180038 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -1208,6 +1208,7 @@ function renderGraph() {
children.push(...labels);
// Above the lines and their labels, below the nodes.
children.push(...hits);
+ const { amber, coordinating } = inRemakes();
for (const id of nodes) {
const p = positions.get(id), member = state.graph.members[id];
const reported = id in state.graph.measured;
@@ -1232,14 +1233,21 @@ function renderGraph() {
const dormant = (state.peers.peers[id] || {}).status === "dormant";
const radius = member.self ? 8 : 6;
mark(id, "circle", { cx: p.x, cy: p.y, r: radius + 6, fill: "rgb(127 127 127)" });
+ // In a remake (`inRemakes`): amber; its coordinator also ringed.
+ const voting = amber.has(id);
+ if (coordinating.has(id)) {
+ children.push(drawing("coordinator " + id, "circle", { cx: p.x, cy: p.y, r: radius + 4,
+ fill: "none", stroke: AMBER, "stroke-width": 1, "pointer-events": "none" }));
+ }
const circle = drawing("node " + id, "circle", { cx: p.x, cy: p.y, r: radius,
- fill: member.self ? "var(--self)" : reported ? "var(--fg)" : "var(--panel)",
- stroke: member.self ? "var(--self)" : dormant ? "var(--bad)" : "var(--fg)",
+ fill: voting ? AMBER : member.self ? "var(--self)" : reported ? "var(--fg)" : "var(--panel)",
+ stroke: dormant ? "var(--bad)" : voting ? AMBER : member.self ? "var(--self)" : "var(--fg)",
"stroke-width": dormant ? 2.5 : 1.5 }, pressed);
// Its clock against the swarm's, as the members' reports chain it (the node's).
const clock = ((state.graph.clocks || {})[id]);
tooltip(circle, id + (reported ? "" : " (no links reported)") +
(dormant ? " (dormant)" : "") +
+ (coordinating.has(id) ? " (coordinates a remake)" : voting ? " (voted for a remake)" : "") +
(clock && clock.off ? " (clock " + seconds(clock.offsetMs) + " off the swarm's, ±" +
clock.withinMs + " ms)" : ""));
// Pinned (middle-clicked): a dot in the middle.
@@ -2042,29 +2050,93 @@ function epochsArrived() {
}
}
function shine(id) {
+ breathe(id, { fill: "#f59e0b", rim: "#fbbf24", to: 46, ms: SHINE_MS, opacity: .55 });
+}
+// A halo growing from node `id` from about its size `to` a radius, in and out as a
+// breath over `ms`, at most `opacity` (the rim more); staying its size where motion is
+// to be calm. Above the nodes (`flashes`).
+function breathe(id, { fill, rim, to, ms, opacity }) {
if (document.hidden || !positions.has(id)) return;
const layer = document.getElementById("flashes");
- const halo = svg("circle", { fill: "#f59e0b", stroke: "#fbbf24", "stroke-width": 3 });
+ const halo = svg("circle", { fill, stroke: rim, "stroke-width": 3 });
layer.append(halo);
const started = performance.now();
const step = now => {
const p = positions.get(id);
- const t = Math.min(1, Math.max(0, (now - started) / SHINE_MS));
+ const t = Math.min(1, Math.max(0, (now - started) / ms));
if (!p || t >= 1) {
halo.remove();
return;
}
- // In and out as a breath; growing from the node to about four times its size
- // (staying as it is where motion is to be calm).
halo.setAttribute("cx", p.x);
halo.setAttribute("cy", p.y);
- halo.setAttribute("r", calm.matches ? 22 : 10 + 36 * Math.sqrt(t));
- halo.setAttribute("fill-opacity", (.55 * Math.sin(Math.PI * t)).toFixed(2));
- halo.setAttribute("stroke-opacity", (.9 * Math.sin(Math.PI * t)).toFixed(2));
+ halo.setAttribute("r", calm.matches ? (10 + to) / 2 : 10 + (to - 10) * Math.sqrt(t));
+ halo.setAttribute("fill-opacity", (opacity * Math.sin(Math.PI * t)).toFixed(2));
+ halo.setAttribute("stroke-opacity", (Math.min(1, opacity * 1.6) * Math.sin(Math.PI * t)).toFixed(2));
requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
+
+// Remakes on the map (`remakes` in the snapshot: those of this node's epoch, with their
+// coordinator, phase and voters). While one is open (proposed, or committed until they
+// switch), its coordinator and voters are amber, the coordinator also ringed; a small
+// amber breath as one is proposed (its coordinator) and as each vote comes. Decided, a
+// pop over all of them: green if committed, red if aborted or expired, and they turn
+// back. Only changes seen while the page is open: those of before show as they are.
+const AMBER = "#f59e0b";
+const knownRemakes = new Map();
+let remakesKnown = false;
+function remakesArrived() {
+ if (!state) return;
+ const remakes = state.remakes || [];
+ const seen = new Set();
+ const pop = (remake, fill) => {
+ for (const id of new Set([remake.coordinator, ...remake.voters])) {
+ breathe(id, { fill, rim: fill, to: 26, ms: 600, opacity: .6 });
+ }
+ };
+ for (const remake of remakes) {
+ seen.add(remake.id);
+ const was = knownRemakes.get(remake.id);
+ if (remakesKnown) {
+ const breath = id => breathe(id, { fill: AMBER, rim: AMBER, to: 20, ms: 900, opacity: .45 });
+ if (!was && remake.phase === "proposed") breath(remake.coordinator);
+ // The coordinator's own vote with its proposal: one breath.
+ const before = new Set(was ? was.voters : [remake.coordinator]);
+ if (remake.phase === "proposed" || (was && was.phase === "proposed")) {
+ for (const voter of remake.voters) if (!before.has(voter)) breath(voter);
+ }
+ if (was && was.phase === "proposed" && remake.phase !== "proposed") {
+ pop(remake, remake.phase === "committed" ? "#16a34a" : "#dc2626");
+ }
+ }
+ knownRemakes.set(remake.id, remake);
+ }
+ // Gone from the snapshot: this node switched to the next epoch (the records stay in
+ // the old one's document). Committed then, if it was still open.
+ for (const [id, remake] of knownRemakes) {
+ if (seen.has(id)) continue;
+ if (remake.phase === "proposed" &&
+ (knownEpochs.get(state.node.id) || 0) > remake.fromGeneration) {
+ pop(remake, "#16a34a");
+ }
+ knownRemakes.delete(id);
+ }
+ remakesKnown = true;
+}
+// Who is amber now (`remakesArrived`), and who of them coordinates: of the open
+// remakes, those not past the epoch yet (`knownEpochs`).
+function inRemakes() {
+ const amber = new Set(), coordinating = new Set();
+ for (const remake of (state && state.remakes) || []) {
+ if (remake.phase !== "proposed" && remake.phase !== "committed") continue;
+ const still = id => (knownEpochs.get(id) || 0) <= remake.fromGeneration;
+ for (const id of [remake.coordinator, ...remake.voters]) if (still(id)) amber.add(id);
+ if (still(remake.coordinator)) coordinating.add(remake.coordinator);
+ }
+ return { amber, coordinating };
+}
// A dragged node let go of: dynamic, it moves to its place among the others again;
// dragged with the middle button, it is pinned there.
function dropped() {
@@ -2538,6 +2610,7 @@ function connect() {
if (state !== null) documentChanged({ all: true });
state = snapshot; stateAt = Date.now(); live = true;
status("live", true); pulse(); render(); trafficArrived(); epochsArrived();
+ remakesArrived();
if (treeShown && writerTree === null) loadTree();
handOverLinks();
});
@@ -2546,6 +2619,7 @@ function connect() {
state = mergePatch(state, JSON.parse(event.data)); stateAt = Date.now(); pulse(); render();
trafficArrived();
epochsArrived();
+ remakesArrived();
});
events.addEventListener("document", event => { pulse(); documentChanged(JSON.parse(event.data)); });
events.addEventListener("messages", event => messagesArrived(JSON.parse(event.data)));