Skip to content

Commit 1a55dc3

Browse files
fix(live/public): single-source transport classification, scoped popup Escape, About modal, themed backdrops
live.html: - case 'progress' merges d.state||d so flat and {state} payloads both apply. - ETA ticker denominator uses runTotalNodes() not raw snapshotSize. - resetLiveForNewRun zeroes _cb.slow/_cb.skipped (stale counters on re-run). - error popup gets a scoped capture-phase Escape handler removed on every close path; backdrop tokenized to var(--glass-bg) (no hardcoded rgba). public.html: - new classifyTransport(st) is the single source of truth for WG/V2/unknown, used by the table renderer, the type filter, AND the detail drawer. An unresolved transport (service_type now flows null, not the old '?? 1' default) renders a neutral — everywhere instead of mislabeling as WireGuard, and the filter matches neither WG nor V2. Drawer Service meta and filter previously still used the stale 'service_type !== 2' test — fixed. - error popup scoped capture-phase Escape; drawer Escape bails when popup open. - added 11th aria-hidden <th> so header cells match the 11 body <td>. about-modal.js (new): self-contained openAboutModal() — themed, a11y (role=dialog, aria-modal, focus trap/restore), scoped Escape, backdrop-click close, idempotent. Referenced by both public pages.
1 parent f9ef9e0 commit 1a55dc3

3 files changed

Lines changed: 266 additions & 19 deletions

File tree

about-modal.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/* about-modal.js — shared, self-contained About modal for public.html + live.html.
2+
*
3+
* Served from repo root by express.static(__dirname). No dependencies, no build
4+
* step. Defines a single global `openAboutModal()`. Content is distilled from
5+
* about.html so the two stay in sync conceptually — keep this accurate to that
6+
* page. Styling uses ONLY the CSS tokens both pages already define (no hardcoded
7+
* colors). Backdrop uses --glass-bg; the card uses --bg-card-solid/--border/--text.
8+
*
9+
* Accessibility: role="dialog", aria-modal, aria-label, a focusable close button,
10+
* Escape-to-close (scoped capture-phase listener removed on close), backdrop
11+
* click to close, and focus moved into the modal on open. Idempotent: a second
12+
* call removes any existing instance first, so it never stacks duplicates.
13+
*/
14+
(function () {
15+
'use strict';
16+
17+
var OVERLAY_ID = 'aboutModalOverlay';
18+
19+
// Distilled from about.html. Each section: { h: heading, p: [paragraphs] }.
20+
// Plain text (no HTML) — rendered with textContent so it's injection-safe.
21+
var SECTIONS = [
22+
{
23+
h: 'What this tester does',
24+
p: [
25+
'The Sentinel Node Tester scans the full active node list on the Sentinel chain, connects to each node, sends real traffic through it, and records the result. This is not a chain-only lookup — it is an end-to-end test of the handshake, the tunnel, and the throughput.',
26+
'A full scan of the network is called a batch. Batches run back-to-back during public testing, so the numbers shown are always based on recent, live evidence.'
27+
]
28+
},
29+
{
30+
h: 'On-chain performance oracle',
31+
p: [
32+
'The tester is a primary publisher of node performance and concurrent-user data to the Sentinel chain. Results are committed on-chain so any consumer can ingest them directly via RPC — no off-chain API required.'
33+
]
34+
},
35+
{
36+
h: 'What the numbers mean',
37+
p: [
38+
'Protocol — the VPN transport the node advertises (WireGuard or V2Ray). Connection test — whether the cryptographic handshake completed. Speed — measured download throughput over the live tunnel, in Mbps. Recent uptime — how often recent tests have succeeded. Speed over time — a sparkline of throughput across recent batches. Peers — active connected clients reported by the node at test time.'
39+
]
40+
},
41+
{
42+
h: 'Where the data comes from',
43+
p: [
44+
'The node list is pulled from the Sentinel chain (RPC-first, with LCD fallback across multiple public endpoints). Every result on this site is generated by a real session on that node — nothing is synthetic, and nothing is cached from third parties.'
45+
]
46+
},
47+
{
48+
h: 'What is never shown to the public',
49+
p: [
50+
'The admin wallet, plan IDs, fee-grant internals, and any operator-side controls stay on the admin surface. This public site is a read-only view of network health. There is nothing to click, nothing to start, and nothing to pay — by design.'
51+
]
52+
}
53+
];
54+
55+
function buildSection(section) {
56+
var wrap = document.createElement('section');
57+
wrap.style.cssText = 'margin-top:18px';
58+
59+
var h = document.createElement('h3');
60+
h.textContent = section.h;
61+
h.style.cssText =
62+
'font-size:12px;font-weight:700;letter-spacing:1.4px;text-transform:uppercase;' +
63+
'color:var(--accent);margin:0 0 10px';
64+
wrap.appendChild(h);
65+
66+
section.p.forEach(function (text) {
67+
var p = document.createElement('p');
68+
p.textContent = text;
69+
p.style.cssText =
70+
'color:var(--text);font-size:13px;line-height:1.7;margin:0 0 10px';
71+
wrap.appendChild(p);
72+
});
73+
return wrap;
74+
}
75+
76+
window.openAboutModal = function openAboutModal() {
77+
// Idempotent: never stack duplicates.
78+
var existing = document.getElementById(OVERLAY_ID);
79+
if (existing) existing.remove();
80+
81+
var previouslyFocused = document.activeElement;
82+
83+
var overlay = document.createElement('div');
84+
overlay.id = OVERLAY_ID;
85+
overlay.style.cssText =
86+
'position:fixed;inset:0;background:var(--glass-bg);' +
87+
'backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);' +
88+
'z-index:10000;display:flex;align-items:center;justify-content:center;padding:24px;';
89+
90+
var card = document.createElement('div');
91+
card.setAttribute('role', 'dialog');
92+
card.setAttribute('aria-modal', 'true');
93+
card.setAttribute('aria-label', 'About Sentinel Node Tester');
94+
card.tabIndex = -1;
95+
card.style.cssText =
96+
'background:var(--bg-card-solid);border:1px solid var(--border);border-radius:16px;' +
97+
'padding:24px 28px;max-width:720px;width:100%;max-height:85vh;overflow-y:auto;' +
98+
'color:var(--text);font-family:var(--font-display);line-height:1.55';
99+
100+
// Header row: title + close button.
101+
var header = document.createElement('div');
102+
header.style.cssText =
103+
'display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:6px';
104+
105+
var titleWrap = document.createElement('div');
106+
var title = document.createElement('h2');
107+
title.textContent = 'About Sentinel Node Tester';
108+
title.style.cssText =
109+
'font-size:20px;font-weight:700;letter-spacing:-0.2px;margin:0 0 8px;color:var(--text)';
110+
var intro = document.createElement('p');
111+
intro.textContent =
112+
'Sentinel is a decentralized privacy network of independent nodes that carry ' +
113+
'encrypted traffic. This tool measures every node continuously so you know, at a ' +
114+
'glance, which nodes are healthy, fast, and reachable right now.';
115+
intro.style.cssText = 'color:var(--text-dim);font-size:14px;line-height:1.6;margin:0';
116+
titleWrap.appendChild(title);
117+
titleWrap.appendChild(intro);
118+
119+
var closeBtn = document.createElement('button');
120+
closeBtn.type = 'button';
121+
closeBtn.setAttribute('aria-label', 'Close');
122+
closeBtn.textContent = '×'; // ×
123+
closeBtn.style.cssText =
124+
'flex-shrink:0;cursor:pointer;background:transparent;border:1px solid var(--border);' +
125+
'color:var(--text-dim);font-size:20px;line-height:1;width:34px;height:34px;border-radius:8px;' +
126+
'display:flex;align-items:center;justify-content:center;';
127+
128+
header.appendChild(titleWrap);
129+
header.appendChild(closeBtn);
130+
card.appendChild(header);
131+
132+
SECTIONS.forEach(function (s) { card.appendChild(buildSection(s)); });
133+
134+
overlay.appendChild(card);
135+
document.body.appendChild(overlay);
136+
137+
function closeAboutModal() {
138+
document.removeEventListener('keydown', onKeydown, true);
139+
overlay.remove();
140+
// Restore focus to the control that opened the modal, if still present.
141+
try {
142+
if (previouslyFocused && typeof previouslyFocused.focus === 'function' &&
143+
document.contains(previouslyFocused)) {
144+
previouslyFocused.focus();
145+
}
146+
} catch (_) { /* focus restoration is best-effort */ }
147+
}
148+
149+
function onKeydown(e) {
150+
if (e.key === 'Escape' || e.key === 'Esc') {
151+
// Scoped to this modal: stop propagation so a page-level Escape handler
152+
// (e.g. a drawer close) doesn't also fire and close a lower layer.
153+
e.stopImmediatePropagation();
154+
e.preventDefault();
155+
closeAboutModal();
156+
}
157+
}
158+
159+
closeBtn.addEventListener('click', closeAboutModal);
160+
overlay.addEventListener('click', function (e) {
161+
if (e.target === overlay) closeAboutModal();
162+
});
163+
document.addEventListener('keydown', onKeydown, true);
164+
165+
// Move focus into the modal so keyboard + screen-reader users land here.
166+
try { closeBtn.focus(); } catch (_) { try { card.focus(); } catch (_) {} }
167+
};
168+
})();

live.html

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,7 +1844,10 @@ <h1 class="live-page-title">
18441844
if (!isRunLive()) return;
18451845
const etaEl = document.getElementById('cbEta');
18461846
if (!etaEl) return;
1847-
const snap = _cb.snapshotSize;
1847+
// Use the same run-total source cbRender() uses (prefers
1848+
// _liveState.totalNodes, falls back to _cb.snapshotSize) so the ticker
1849+
// and the rendered progress don't diverge on resume → no per-second flicker.
1850+
const snap = runTotalNodes();
18481851
const tested = (Array.isArray(resultsArr) && resultsArr.length > 0) ? resultsArr.length : _cb.tested;
18491852
const remaining = snap > 0 ? snap - tested : 0;
18501853
const startedAt = _cb.startedAt ? new Date(_cb.startedAt).getTime() : 0;
@@ -2087,7 +2090,9 @@ <h1 class="live-page-title">
20872090
_cb.batchId = null;
20882091
_cb.tested = 0;
20892092
_cb.passed = 0;
2093+
_cb.slow = 0;
20902094
_cb.failed = 0;
2095+
_cb.skipped = 0;
20912096
_cb.snapshotSize = 0;
20922097
// Drop the prior run's sweep total too: runTotalNodes() now prefers
20932098
// _liveState.totalNodes, so leaving it set would carry the old "Y" into
@@ -2395,7 +2400,11 @@ <h1 class="live-page-title">
23952400
case 'progress': {
23962401
// Numeric checkpoints from the pipeline (testedNodes, etc.).
23972402
// Treat as a partial state update so counters/header refresh.
2398-
mergeLiveState(d);
2403+
// Server forwards { type:'progress', state:{…} } — merge d.state
2404+
// (not d) so type/state don't pollute _liveState and totalNodes/
2405+
// testedNodes/activeRunNumber actually lift. Fall back to d for any
2406+
// legacy flat-payload emitter.
2407+
mergeLiveState(d.state || d);
23992408
break;
24002409
}
24012410
default: break;
@@ -2601,19 +2610,36 @@ <h1 class="live-page-title">
26012610

26022611
const overlay = document.createElement('div');
26032612
overlay.id = 'errorPopupOverlay';
2604-
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:9999;display:flex;align-items:center;justify-content:center;';
2605-
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
2613+
overlay.style.cssText = 'position:fixed;inset:0;background:var(--glass-bg);backdrop-filter:blur(8px);z-index:9999;display:flex;align-items:center;justify-content:center;';
2614+
// Scoped Escape handler — closes ONLY this popup (top-most layer) and
2615+
// removes itself on close. stopImmediatePropagation so it can't bubble
2616+
// to any lower-layer Escape handler and double-close.
2617+
const closeErrorPopup = () => {
2618+
document.removeEventListener('keydown', onPopupKeydown, true);
2619+
overlay.remove();
2620+
};
2621+
function onPopupKeydown(e) {
2622+
if (e.key === 'Escape' || e.key === 'Esc') {
2623+
e.stopImmediatePropagation();
2624+
e.preventDefault();
2625+
closeErrorPopup();
2626+
}
2627+
}
2628+
document.addEventListener('keydown', onPopupKeydown, true);
2629+
overlay.onclick = (e) => { if (e.target === overlay) closeErrorPopup(); };
26062630
const popup = document.createElement('div');
26072631
popup.style.cssText = 'background:var(--bg-card-solid);border:1px solid var(--border);border-radius:16px;padding:24px 28px;max-width:780px;width:92%;max-height:85vh;overflow-y:auto;color:var(--text);font-family:var(--font-display);line-height:1.55';
26082632
popup.innerHTML = `
26092633
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
26102634
<h2 style="font-size:18px;font-weight:700;letter-spacing:1px;margin:0">Node Error Details</h2>
2611-
<span onclick="document.getElementById('errorPopupOverlay').remove()" style="cursor:pointer;color:var(--text-dim);font-size:22px;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:50%">&times;</span>
2635+
<span id="errorPopupCloseX" style="cursor:pointer;color:var(--text-dim);font-size:22px;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:50%">&times;</span>
26122636
</div>
26132637
<div id="errorPopupBody" style="font-size:13px;color:var(--text-dim)">Loading…</div>
26142638
`;
26152639
overlay.appendChild(popup);
26162640
document.body.appendChild(overlay);
2641+
const xBtn = document.getElementById('errorPopupCloseX');
2642+
if (xBtn) xBtn.onclick = closeErrorPopup;
26172643

26182644
try {
26192645
const res = await fetch('/api/public/node/' + encodeURIComponent(addr) + '/errors?limit=1', { credentials: 'same-origin' });
@@ -2733,9 +2759,11 @@ <h2 style="font-size:18px;font-weight:700;letter-spacing:1px;margin:0">Node Erro
27332759
27342760
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:18px">
27352761
<button id="errorPopupCopyBtn" class="row-copy-btn" style="padding:6px 14px">⎘ Copy Full Report</button>
2736-
<button onclick="document.getElementById('errorPopupOverlay').remove()" class="row-copy-btn" style="padding:6px 14px">Close</button>
2762+
<button id="errorPopupCloseBtn" class="row-copy-btn" style="padding:6px 14px">Close</button>
27372763
</div>
27382764
`;
2765+
const closeBtn = document.getElementById('errorPopupCloseBtn');
2766+
if (closeBtn) closeBtn.onclick = closeErrorPopup;
27392767
const copyBtn = document.getElementById('errorPopupCopyBtn');
27402768
if (copyBtn) {
27412769
copyBtn.onclick = async () => {

0 commit comments

Comments
 (0)