compares plc.directory with other mirrors
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

update descriptions, connect from last seq

dawn 828a1203 9305c5a2

+51 -43
+34 -40
index.html
··· 84 84 th .tip { 85 85 display: none; 86 86 position: absolute; 87 - top: calc(100% + 10px); 87 + top: calc(100% + 8px); 88 88 left: 0; 89 - background: #1a1a1a; 90 - border: 1px solid #333; 89 + background: #1c1c1c; 90 + border: 1px solid #2e2e2e; 91 91 border-radius: 5px; 92 - color: #c0c0c0; 93 - font-size: 0.72rem; 92 + color: #aaa; 93 + font-size: 0.71rem; 94 94 font-weight: 400; 95 95 text-transform: none; 96 96 letter-spacing: 0; 97 - line-height: 1.55; 98 - padding: 0.55rem 0.75rem; 99 - width: 230px; 97 + line-height: 1.6; 98 + padding: 0.5rem 0.7rem; 99 + width: 220px; 100 100 z-index: 200; 101 101 white-space: normal; 102 102 box-shadow: 0 6px 20px rgba(0,0,0,0.6); ··· 106 106 content: ''; 107 107 position: absolute; 108 108 top: -5px; 109 - left: 14px; 109 + left: 12px; 110 110 width: 8px; height: 8px; 111 - background: #1a1a1a; 112 - border-left: 1px solid #333; 113 - border-top: 1px solid #333; 111 + background: #1c1c1c; 112 + border-left: 1px solid #2e2e2e; 113 + border-top: 1px solid #2e2e2e; 114 114 transform: rotate(45deg); 115 115 } 116 116 th .tip .tip-title { 117 117 display: block; 118 - color: #e0e0e0; 118 + color: #ccc; 119 119 font-weight: 600; 120 - margin-bottom: 0.3rem; 121 - text-transform: uppercase; 122 - font-size: 0.68rem; 123 - letter-spacing: 0.06em; 120 + margin-bottom: 0.25rem; 121 + font-size: 0.71rem; 124 122 } 125 123 th .tip em { 126 - color: #888; 124 + color: #777; 127 125 font-style: normal; 128 126 } 129 127 th:hover .tip { display: block; } 130 128 /* right-anchored for right-aligned columns so they don't overflow */ 131 129 th.th-r .tip { left: auto; right: 0; } 132 - th.th-r .tip::before { left: auto; right: 14px; } 130 + th.th-r .tip::before { left: auto; right: 12px; } 133 131 134 132 tbody td { 135 133 padding: 0.38rem 0; ··· 490 488 <span class="tip-label">host</span> 491 489 <span class="tip"> 492 490 <span class="tip-title">host</span> 493 - Mirror being tracked by the server. 494 - <em>● green</em> = connected, <em>● red</em> = disconnected (will reconnect automatically). 491 + mirror being tracked. <em>● green</em> = connected, <em>● red</em> = disconnected (auto-reconnects). 495 492 </span> 496 493 </th> 497 494 <th class="th-r"> 498 495 <span class="tip-label">ops</span> 499 496 <span class="tip"> 500 497 <span class="tip-title">ops</span> 501 - Operations received from this host, summed across all stored snapshots. 502 - Used as the numerator for coverage and missed calculations. 498 + ops received by this mirror, summed across all stored snapshots (~6 hrs). 503 499 </span> 504 500 </th> 505 501 <th class="th-r"> 506 502 <span class="tip-label">coverage</span> 507 503 <span class="tip"> 508 504 <span class="tip-title">coverage</span> 509 - Share of plc.directory's ops in the 15-min window that this mirror also delivered. 510 - <em>100%</em> = no ops missed. Calculated as <em>mirror ops ÷ primary ops</em>. 505 + share of plc.directory's ops that this mirror also received, across all snapshots. <em>100%</em> = nothing missed. 511 506 </span> 512 507 </th> 513 508 <th class="th-r"> 514 509 <span class="tip-label">missed</span> 515 510 <span class="tip"> 516 511 <span class="tip-title">missed</span> 517 - Ops seen by plc.directory in the 15-min window that this mirror has <em>not</em> delivered. 518 - May indicate the mirror is lagging, dropping events, or behind on propagation. 512 + ops plc.directory received that this mirror never delivered, summed across all snapshots. these are point-in-time misses — the op may have arrived late. 519 513 </span> 520 514 </th> 521 515 <th class="th-r"> 522 516 <span class="tip-label">lag p50 / p95 / p99</span> 523 517 <span class="tip"> 524 518 <span class="tip-title">lag p50 / p95 / p99</span> 525 - How many milliseconds after plc.directory this mirror delivered each op, corrected for network distance. 526 - Correction = <em>mirror OTT − primary OTT</em>, where OTT = TCP RTT ÷ 2 (SYN/SYN-ACK, pure network latency). 527 - Positive = mirror is behind. Small negatives = within noise floor. 519 + how many ms after plc.directory this mirror delivered each op, corrected for network distance. correction = <em>mirror ott − primary ott</em>, where ott = tcp rtt ÷ 2. positive = mirror is behind. small negatives = within noise. 528 520 </span> 529 521 </th> 530 522 <th class="th-r"> 531 523 <span class="tip-label" style="border:none"> </span> 532 524 <span class="tip"> 533 525 <span class="tip-title">coverage bar</span> 534 - Visual ratio of <em style="color:#4a8a4a">received</em> (green) vs <em style="color:#8a4a4a">missed</em> (red) ops in the 15-min window. 526 + visual ratio of <em style="color:#4a8a4a">received</em> vs <em style="color:#8a4a4a">missed</em> ops across all snapshots. 535 527 </span> 536 528 </th> 537 529 </tr> ··· 779 771 return rm ? `${h}h ${rm}m ago` : `${h}h ago`; 780 772 } 781 773 782 - function covColor(cov) { 774 + function covColor(cov, extra = 0) { 783 775 if (cov === null || cov === undefined) return '#222'; 784 - if (cov >= 0.9999) return '#2d6e2d'; 785 - if (cov >= 0.90) return '#7a3022'; 786 - return '#6e2222'; 776 + if (cov < 0.9999) return cov >= 0.90 ? '#7a3022' : '#6e2222'; 777 + if (extra > 0) return '#5a4a10'; 778 + return '#2d6e2d'; 787 779 } 788 780 789 781 function renderHistory(stats, snaps) { ··· 815 807 816 808 for (const snap of snaps) { 817 809 const sm = snap.mirrors[url]; 810 + const extraCount = Object.values(sm?.extraCids ?? {}).reduce((n, c) => n + c.length, 0); 818 811 const cell = document.createElement('div'); 819 812 cell.className = 'hist-cell'; 820 - cell.style.background = covColor(sm?.coverage); 813 + cell.style.background = covColor(sm?.coverage, extraCount); 821 814 822 815 const dt = new Date(snap.ts); 823 816 const timeStr = dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); ··· 826 819 <div class="ft-time">${timeStr} &nbsp;·&nbsp; ${fmtAgo(Date.now() - snap.ts)}</div> 827 820 <div class="ft-row"><span class="ft-key">coverage</span><span class="ft-val">${covStr}</span></div> 828 821 <div class="ft-row"><span class="ft-key">ops</span><span class="ft-val">${(sm?.ops ?? 0).toLocaleString()} / ${(sm?.primaryOps ?? 0).toLocaleString()}</span></div> 829 - <div class="ft-row"><span class="ft-key">missed</span><span class="ft-val">${(sm?.missed ?? 0).toLocaleString()}</span></div>`; 822 + <div class="ft-row"><span class="ft-key">missed</span><span class="ft-val">${(sm?.missed ?? 0).toLocaleString()}</span></div> 823 + <div class="ft-row"><span class="ft-key">extra</span><span class="ft-val">${Object.values(sm?.extraCids ?? {}).reduce((n, c) => n + c.length, 0).toLocaleString()}</span></div>`; 830 824 831 825 cell.addEventListener('mouseenter', e => showFloatTip(e, html)); 832 826 cell.addEventListener('mousemove', moveFloatTip); ··· 1102 1096 const timeStr = dt.toLocaleString([], { month: 'short', day: 'numeric', 1103 1097 hour: '2-digit', minute: '2-digit' }); 1104 1098 1105 - document.getElementById('missTitle').textContent = `${mirrorName} — missed ops`; 1099 + document.getElementById('missTitle').textContent = `${mirrorName} — snapshot detail`; 1106 1100 document.getElementById('missTime').textContent = timeStr; 1107 1101 1108 1102 const mirrorBase = url.replace(/^wss?:\/\//, 'https://').replace(/\/$/, ''); ··· 1136 1130 html += extraDids.sort().map(did => renderDidBlock(did, extra[did], false)).join(''); 1137 1131 } 1138 1132 if (!html) { 1139 - html = '<div class="miss-empty">no missed ops recorded for this interval</div>'; 1133 + html = '<div class="miss-empty">no discrepancies recorded for this interval</div>'; 1140 1134 } 1141 1135 body.innerHTML = html; 1142 1136 ··· 1207 1201 }; 1208 1202 1209 1203 setInterval(tick, BUCKET_MS); 1210 - setInterval(fetchStats, 2000); 1204 + setInterval(fetchStats, 60_000); 1211 1205 fetchStats(); // immediate first load 1212 1206 </script> 1213 1207 </body>
+17 -3
server.ts
··· 51 51 url: string; 52 52 connected: boolean; 53 53 totalEvents: number; // persisted lifetime total 54 + lastSeq: number | null; // last seen seq, used for gapless reconnect 54 55 lagSamples: number[]; // TCP-OTT-corrected lag in ms 55 56 rttSamples: number[]; // TCP SYN/SYN-ACK RTT samples in ms 56 57 ws: WebSocket | null; ··· 58 59 59 60 interface SavedTotals { 60 61 totals: Record<string, number>; // url -> lifetime totalEvents 62 + seqs: Record<string, number>; // url -> last seen seq 61 63 } 62 64 63 65 // ── state ───────────────────────────────────────────────────────────────────── ··· 66 68 const mirrors = new Map<string, MirrorState>(); 67 69 const snapshots: Snapshot[] = []; 68 70 const savedTotals = new Map<string, number>(); // populated by loadData() 71 + const savedSeqs = new Map<string, number>(); // populated by loadData() 69 72 70 73 // ── persistence ─────────────────────────────────────────────────────────────── 71 74 ··· 77 80 const raw = readFileSync(`${DATA_DIR}/totals.json`, "utf-8"); 78 81 const data = JSON.parse(raw) as SavedTotals; 79 82 for (const [url, n] of Object.entries(data.totals ?? {})) savedTotals.set(url, n); 83 + for (const [url, n] of Object.entries(data.seqs ?? {})) savedSeqs.set(url, n); 80 84 } catch { /* no totals yet */ } 81 85 82 86 // load snapshot files — named <ts>.json, sorted oldest-first ··· 99 103 100 104 function saveTotals(): void { 101 105 const totals: Record<string, number> = {}; 102 - for (const [url, m] of mirrors) totals[url] = m.totalEvents; 106 + const seqs: Record<string, number> = {}; 107 + for (const [url, m] of mirrors) { 108 + totals[url] = m.totalEvents; 109 + if (m.lastSeq !== null) seqs[url] = m.lastSeq; 110 + } 103 111 try { 104 - writeFileSync(`${DATA_DIR}/totals.json`, JSON.stringify({ totals })); 112 + writeFileSync(`${DATA_DIR}/totals.json`, JSON.stringify({ totals, seqs })); 105 113 } catch (e) { 106 114 console.error("failed to save totals:", e); 107 115 } ··· 127 135 mirrors.set(url, { 128 136 name, url, connected: false, 129 137 totalEvents: savedTotals.get(url) ?? 0, 138 + lastSeq: savedSeqs.get(url) ?? null, 130 139 lagSamples: [], rttSamples: [], ws: null, 131 140 }); 132 141 connect(url); ··· 139 148 140 149 let ws: WebSocket; 141 150 try { 142 - ws = new WebSocket(url + "/export/stream"); 151 + const streamUrl = m.lastSeq !== null 152 + ? `${url}/export/stream?after=${m.lastSeq}` 153 + : `${url}/export/stream`; 154 + ws = new WebSocket(streamUrl); 143 155 } catch (e) { 144 156 console.error(`[${url}] WebSocket create failed:`, e); 145 157 setTimeout(() => connect(url), 10_000); ··· 172 184 173 185 const cid = ev.cid as string | undefined; 174 186 const did = ev.did as string | undefined; 187 + const seq = ev.seq as number | undefined; 175 188 if (!cid) return; 176 189 177 190 m.totalEvents++; 191 + if (seq !== undefined && (m.lastSeq === null || seq > m.lastSeq)) m.lastSeq = seq; 178 192 const now = Date.now(); 179 193 180 194 if (!tracker.has(cid)) {