lightweight com.atproto.sync.listReposByCollection
45
fork

Configure Feed

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

stat diffing in admin

phil 251254be 2cbb4542

+76 -14
+10
src/server/admin/page.css
··· 227 227 font-family: ui-monospace, 'SF Mono', monospace; 228 228 } 229 229 230 + /* ── Delta annotations (rate/gauge changes between polls) ─────────────── */ 231 + 232 + .delta { 233 + font-size: 0.72rem; 234 + font-weight: 400; 235 + color: #999; 236 + margin-left: 0.3rem; 237 + font-variant-numeric: tabular-nums; 238 + } 239 + 230 240 /* ── Misc ─────────────────────────────────────────────────────────────── */ 231 241 232 242 .empty {
+66 -14
src/server/admin/page.js
··· 5 5 6 6 const POLL_INTERVAL_MS = 5000; 7 7 8 + /** Previous poll snapshot and timestamp for computing deltas. */ 9 + let prevSnapshot = null; 10 + let prevTimeMs = null; 11 + 8 12 // ── Formatting helpers ─────────────────────────────────────────────────── 9 13 10 14 /** Format a number with locale-appropriate grouping, or "—" for non-numbers. */ ··· 36 40 return max > 0 ? Math.min(100, (value / max) * 100) : 0; 37 41 } 38 42 43 + // ── Delta helpers ──────────────────────────────────────────────────────── 44 + 45 + /** Seconds elapsed since the previous poll, or 0 on first poll. */ 46 + function dtSecs() { 47 + return prevTimeMs ? Math.max((Date.now() - prevTimeMs) / 1000, 0.1) : 0; 48 + } 49 + 50 + /** Format a counter delta as a rate badge, e.g. "+8.4/s". Returns "" if unchanged. */ 51 + function rate(cur, prev) { 52 + const dt = dtSecs(); 53 + if (prev == null || dt <= 0) return ""; 54 + const delta = cur - prev; 55 + if (delta === 0) return ""; 56 + const r = delta / dt; 57 + const sign = r > 0 ? "+" : ""; 58 + const text = Math.abs(r) >= 10 59 + ? `${sign}${Math.round(r)}/s` 60 + : `${sign}${r.toFixed(1)}/s`; 61 + return ` <span class="delta">${text}</span>`; 62 + } 63 + 64 + /** Format a gauge delta as an arrow badge, e.g. "↑5" or "↓22". Returns "" if unchanged. */ 65 + function gauge(cur, prev) { 66 + if (prev == null) return ""; 67 + const delta = cur - prev; 68 + if (delta === 0) return ""; 69 + const arrow = delta > 0 ? "\u2191" : "\u2193"; 70 + return ` <span class="delta">${arrow}${formatNumber(Math.abs(delta))}</span>`; 71 + } 72 + 39 73 // ── HTML fragment builders ─────────────────────────────────────────────── 40 74 41 75 /** Wrap content in a card with a title. */ ··· 43 77 return `<div class="card"><h2>${title}</h2>${body}</div>`; 44 78 } 45 79 46 - /** A key/value row. Value can contain HTML (badges, styled spans, etc.). */ 47 - function statRow(label, value) { 80 + /** A key/value row. Value can contain HTML (badges, styled spans, etc.). 81 + * Optional `delta` is appended after the value (e.g. rate or gauge badge). */ 82 + function statRow(label, value, delta) { 48 83 return `<div class="stat-row"> 49 84 <span class="label">${label}</span> 50 - <span class="value">${value}</span> 85 + <span class="value">${value}${delta || ""}</span> 51 86 </div>`; 52 87 } 53 88 ··· 77 112 ? `<span class="badge badge-success">complete</span>` 78 113 : `<span class="badge badge-warning">in progress</span>`; 79 114 115 + const p = prevSnapshot; 80 116 let html = 81 117 statRow("Upstream crawl", badge) 82 - + statRow("Repos crawled", formatNumber(status.repos_queued_total)) 83 - + statRow("Resync queue depth", formatNumber(status.resync_queue_depth)) 84 - + statRow("Discovery queue depth", formatNumber(status.discovery_queue_depth)) 85 - + statRow("Resyncs completed", formatNumber(status.resyncs_completed_total)) 86 - + statRow("Resync-buffered events", formatNumber(status.resync_buffer_count)); 118 + + statRow("Repos crawled", formatNumber(status.repos_queued_total), 119 + rate(status.repos_queued_total, p?.repos_queued_total)) 120 + + statRow("Resync queue depth", formatNumber(status.resync_queue_depth), 121 + gauge(status.resync_queue_depth, p?.resync_queue_depth)) 122 + + statRow("Discovery queue depth", formatNumber(status.discovery_queue_depth), 123 + gauge(status.discovery_queue_depth, p?.discovery_queue_depth)) 124 + + statRow("Resyncs completed", formatNumber(status.resyncs_completed_total), 125 + rate(status.resyncs_completed_total, p?.resyncs_completed_total)) 126 + + statRow("Resync-buffered events", formatNumber(status.resync_buffer_count), 127 + gauge(status.resync_buffer_count, p?.resync_buffer_count)); 87 128 88 129 if (status.upstream_backfill_completed_at) { 89 130 const when = new Date(status.upstream_backfill_completed_at).toLocaleString(); ··· 94 135 } 95 136 96 137 function renderCollectionsCard(status) { 138 + const p = prevSnapshot; 97 139 const html = 98 140 statRow("Distinct", formatNumber(status.distinct_collections)) 99 141 100 142 + sectionHeading("From resync") 101 - + statRow("Indexed by account", formatNumber(status.collection_births_resync)) 102 - + statRow("De-indexed by account", formatNumber(status.collection_deaths_resync)) 143 + + statRow("Indexed by account", formatNumber(status.collection_births_resync), 144 + rate(status.collection_births_resync, p?.collection_births_resync)) 145 + + statRow("De-indexed by account", formatNumber(status.collection_deaths_resync), 146 + rate(status.collection_deaths_resync, p?.collection_deaths_resync)) 103 147 104 148 + sectionHeading("From firehose") 105 - + statRow("Indexed by account", formatNumber(status.collection_births_firehose)) 106 - + statRow("De-indexed by account", formatNumber(status.collection_deaths_firehose)) 149 + + statRow("Indexed by account", formatNumber(status.collection_births_firehose), 150 + rate(status.collection_births_firehose, p?.collection_births_firehose)) 151 + + statRow("De-indexed by account", formatNumber(status.collection_deaths_firehose), 152 + rate(status.collection_deaths_firehose, p?.collection_deaths_firehose)) 107 153 108 154 return card("Collections", html); 109 155 } ··· 141 187 } 142 188 143 189 function renderDispatcherCard(dispatcher) { 144 - let html = statRow("Workers", formatNumber(dispatcher.worker_count)) 145 - + statRow("Cooling", formatNumber(dispatcher.cooling.length)); 190 + const pd = prevSnapshot?.dispatcher; 191 + let html = statRow("Workers", formatNumber(dispatcher.worker_count), 192 + gauge(dispatcher.worker_count, pd?.worker_count)) 193 + + statRow("Cooling", formatNumber(dispatcher.cooling.length), 194 + gauge(dispatcher.cooling.length, pd?.cooling?.length)); 146 195 147 196 // Busy accounts — expandable list of DIDs currently being resynced 148 197 if (dispatcher.busy.length > 0) { ··· 266 315 267 316 const status = await response.json(); 268 317 render(status); 318 + 319 + prevSnapshot = status; 320 + prevTimeMs = Date.now(); 269 321 270 322 dot.classList.add("connected"); 271 323 timestamp.textContent = new Date().toLocaleTimeString();