|
| 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 | +})(); |
0 commit comments