declarative relay deployment on hetzner relay-eval.waow.tech
atproto relay
14
fork

Configure Feed

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

relay-eval dashboard: classification legend, granular timestamps, polish

- replace classification sentences with compact horizontal legend + tooltips
- ago() now shows "2h 10m ago" instead of "2h ago" for better run differentiation
- run nav shows local clock time alongside relative time
- subtle CSS refinements: box-shadow, transitions, antialiasing, smoother radii

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz 358576f7 97486213

+50 -43
+50 -43
relay-eval/src/static/index.html
··· 18 18 background: var(--bg); color: var(--fg); 19 19 padding: 2.5rem 2rem; max-width: 960px; margin: 0 auto; 20 20 line-height: 1.55; 21 + -webkit-font-smoothing: antialiased; 21 22 } 22 23 h1 { font-size: 1.25rem; font-weight: 600; color: var(--fg); letter-spacing: -0.02em; } 23 24 .subtitle { color: var(--muted); font-size: 0.85rem; margin-top: 0.2rem; } ··· 25 26 /* summary card */ 26 27 .summary { 27 28 margin-top: 1.5rem; padding: 0.9rem 1.1rem; 28 - background: var(--surface); border: 1px solid var(--border); border-radius: 6px; 29 + background: var(--surface); border: 1px solid var(--border); border-radius: 8px; 29 30 font-size: 0.85rem; color: var(--fg); line-height: 1.7; 31 + box-shadow: 0 1px 3px rgba(0,0,0,0.3); 30 32 } 31 33 .summary .stat { font-weight: 600; } 32 34 ··· 36 38 margin-top: 1.1rem; font-size: 0.8rem; color: var(--muted); 37 39 width: fit-content; 38 40 } 39 - .op-legend a { color: var(--muted); text-decoration: none; } 41 + .op-legend a { color: var(--muted); text-decoration: none; transition: color 0.1s; } 40 42 .op-legend a:hover { color: var(--fg); text-decoration: underline; } 41 43 .sym { font-size: 0.85em; margin-right: 0.3rem; } 42 44 .run-by { font-size: 0.75rem; color: var(--muted); } 43 - .run-by a { color: var(--muted); text-decoration: none; } 45 + .run-by a { color: var(--muted); text-decoration: none; transition: color 0.1s; } 44 46 .run-by a:hover { color: var(--fg); text-decoration: underline; } 45 47 46 48 /* sections */ ··· 50 52 /* tables */ 51 53 table { width: 100%; border-collapse: collapse; } 52 54 th { 53 - text-align: left; padding: 0.45rem 0.6rem; font-size: 0.7rem; font-weight: 500; 55 + text-align: left; padding: 0.5rem 0.6rem; font-size: 0.7rem; font-weight: 500; 54 56 color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; 55 57 border-bottom: 1px solid var(--border-strong); 56 58 } 57 - td { padding: 0.4rem 0.6rem; font-size: 0.82rem; border-bottom: 1px solid var(--border); } 59 + td { padding: 0.45rem 0.6rem; font-size: 0.82rem; border-bottom: 1px solid var(--border); } 60 + tr { transition: background 0.1s; } 58 61 .num { text-align: right; font-variant-numeric: tabular-nums; } 59 62 th.num { text-align: right; } 60 63 61 64 /* relay cell */ 62 65 .rn { white-space: nowrap; } 63 - .relay-link { color: var(--fg); text-decoration: none; } 66 + .relay-link { color: var(--fg); text-decoration: none; transition: color 0.1s; } 64 67 .relay-link:hover { color: var(--accent); text-decoration: underline; } 65 68 tr.dimmed td { opacity: 0.35; } 66 69 67 70 /* coverage bar */ 68 71 .bar { display: flex; align-items: center; gap: 1px; width: 72px; } 69 - .bar-ok { height: 5px; border-radius: 2px; background: var(--green); } 70 - .bar-miss { height: 5px; border-radius: 2px; background: var(--red); opacity: 0.5; } 72 + .bar-ok { height: 5px; border-radius: 3px; background: var(--green); } 73 + .bar-miss { height: 5px; border-radius: 3px; background: var(--red); opacity: 0.5; } 71 74 72 75 /* classification */ 73 76 .c-gap { color: var(--red); } ··· 76 79 .outlier { font-size: 0.7rem; color: var(--yellow); margin-left: 0.4rem; } 77 80 .outlier-note { color: var(--yellow); font-size: 0.8rem; margin-top: 0.5rem; line-height: 1.5; } 78 81 82 + /* classification legend */ 83 + .class-legend { 84 + display: flex; gap: 1.25rem; margin-bottom: 0.75rem; font-size: 0.78rem; 85 + } 86 + .class-legend span { display: flex; align-items: center; gap: 0.35rem; } 87 + .cdot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; } 88 + 79 89 /* expandable */ 80 90 .xrow { cursor: pointer; user-select: none; } 81 91 .xrow:hover td { background: var(--surface); } 82 - .xi { display: inline-block; width: 1.1em; text-align: center; } 92 + .xi { display: inline-block; width: 1.1em; text-align: center; transition: transform 0.15s; } 83 93 .xbody { display: none; } 84 94 .xbody.open { display: table-row-group; } 85 95 .drow td { padding-left: 2.2rem; font-size: 0.78rem; color: var(--muted); } 86 - .did-link { color: var(--accent); text-decoration: none; font-size: 0.75rem; } 96 + .did-link { color: var(--accent); text-decoration: none; font-size: 0.75rem; transition: color 0.1s; } 87 97 .did-link:hover { text-decoration: underline; } 88 98 89 99 /* tooltip via css */ ··· 95 105 color: var(--fg); padding: 0.35rem 0.6rem; border-radius: 4px; 96 106 font-size: 0.72rem; white-space: nowrap; pointer-events: none; 97 107 opacity: 0; transition: opacity 0.12s; z-index: 10; 108 + box-shadow: 0 2px 8px rgba(0,0,0,0.4); 98 109 } 99 110 .tip:hover::after { opacity: 1; } 100 111 101 112 /* runs nav */ 102 113 .nav { 103 114 margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border); 104 - display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; 115 + display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; 105 116 } 106 117 .nav-label { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-right: 0.3rem; } 107 118 .nav a { 108 - font-size: 0.78rem; color: var(--accent); text-decoration: none; cursor: pointer; 109 - padding: 0.15rem 0.45rem; border-radius: 3px; border: 1px solid transparent; 119 + font-size: 0.75rem; color: var(--accent); text-decoration: none; cursor: pointer; 120 + padding: 0.2rem 0.5rem; border-radius: 4px; border: 1px solid transparent; 121 + transition: all 0.1s; 110 122 } 111 123 .nav a:hover { border-color: var(--border-strong); background: var(--surface); } 124 + .nav .time-detail { color: var(--muted); font-size: 0.65rem; margin-left: 0.15rem; } 112 125 113 126 .empty { color: var(--muted); font-style: italic; padding: 3rem; text-align: center; } 114 127 .loading { color: var(--muted); padding: 3rem; text-align: center; } ··· 147 160 const m = Math.floor((Date.now() - new Date(iso).getTime()) / 60000); 148 161 if (m < 1) return 'just now'; 149 162 if (m < 60) return m + 'm ago'; 150 - const h = Math.floor(m / 60); 151 - if (h < 24) return h + 'h ago'; 152 - return Math.floor(h / 24) + 'd ago'; 163 + const hr = Math.floor(m / 60), rm = m % 60; 164 + if (hr < 24) return rm > 0 ? `${hr}h ${rm}m ago` : `${hr}h ago`; 165 + const d = Math.floor(hr / 24); 166 + return d + 'd ago'; 167 + } 168 + 169 + function clockTime(iso) { 170 + const d = new Date(iso); 171 + return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); 153 172 } 154 173 155 174 function winLabel(s) { ··· 160 179 161 180 function utc(iso) { return iso.replace('T', ' ').replace('Z', '') + ' UTC'; } 162 181 163 - function pct(n, t) { return t === 0 ? '—' : (n / t * 100).toFixed(2) + '%'; } 182 + function pct(n, t) { return t === 0 ? '\u2014' : (n / t * 100).toFixed(2) + '%'; } 164 183 165 184 function tip(text, t) { return `<span class="tip" data-tip="${t}">${text}</span>`; } 166 185 ··· 183 202 + `</div>`; 184 203 } 185 204 186 - function groups(stats) { 187 - const g = {}; 188 - for (const s of stats) { 189 - const o = op(s.host); 190 - if (!g[o.name]) g[o.name] = { op: o, relays: [] }; 191 - g[o.name].relays.push(s); 192 - } 193 - const sorted = Object.values(g).sort((a, b) => 194 - Math.max(...b.relays.map(r => r.unique_dids)) - Math.max(...a.relays.map(r => r.unique_dids)) 195 - ); 196 - for (const x of sorted) x.relays.sort((a, b) => b.unique_dids - a.unique_dids); 197 - return sorted; 198 - } 199 - 200 205 function render(data) { 201 206 if (!data) return '<p class="empty">no runs yet</p>'; 202 207 ··· 230 235 rd[d.relay].dids.push(d); 231 236 } 232 237 233 - // outlier detection: flag relays with event counts >1.5x the median 234 - const active = data.stats.filter(s => s.events > 0).map(s => s.events).sort((a, b) => a - b); 235 - const median = active.length > 0 ? active[Math.floor(active.length / 2)] : 0; 238 + // outlier detection 239 + const actv = data.stats.filter(s => s.events > 0).map(s => s.events).sort((a, b) => a - b); 240 + const median = actv.length > 0 ? actv[Math.floor(actv.length / 2)] : 0; 236 241 const outliers = new Set(); 237 242 if (median > 0) { 238 243 for (const s of data.stats) { ··· 287 292 288 293 if (withMisses.length > 0) { 289 294 h += `<p class="sec">why accounts were missed</p>`; 290 - h += `<p class="sec-desc">`; 291 - h += `each missed account is resolved to understand the cause. `; 292 - h += `<span class="c-gap">missed (active)</span> \u2014 the account has a working PDS but this relay didn\u2019t see it. `; 293 - h += `<span class="c-unr">unresolvable</span> \u2014 the DID couldn\u2019t be looked up (DNS failure, PLC outage, brand-new account). `; 294 - h += `<span class="c-dead">deactivated</span> \u2014 account is deactivated or deleted.`; 295 - h += `</p>`; 295 + h += `<div class="class-legend">`; 296 + h += `<span><span class="cdot" style="background:var(--red)"></span> ${tip('missed (active)', 'account has a working PDS but this relay didn\u2019t see it')}</span>`; 297 + h += `<span><span class="cdot" style="background:var(--yellow)"></span> ${tip('unresolvable', 'DID lookup failed \u2014 DNS issue, PLC outage, or brand-new account')}</span>`; 298 + h += `<span><span class="cdot" style="background:var(--muted)"></span> ${tip('deactivated', 'account is deactivated or deleted')}</span>`; 299 + h += `</div>`; 296 300 297 301 h += `<table><thead><tr>`; 298 - h += `<th>relay</th><th class="num">missed (active)</th><th class="num">unresolvable</th>`; 299 - h += `<th class="num">deactivated</th><th class="num">total</th>`; 302 + h += `<th>relay</th>`; 303 + h += `<th class="num">${tip('active', 'account has a working PDS but this relay didn\u2019t see it')}</th>`; 304 + h += `<th class="num">${tip('unresolvable', 'DID lookup failed')}</th>`; 305 + h += `<th class="num">${tip('deactivated', 'account is deactivated or deleted')}</th>`; 306 + h += `<th class="num">total</th>`; 300 307 h += `</tr></thead>`; 301 308 302 309 for (const s of withMisses) { ··· 356 363 357 364 let nh = '<span class="nav-label">previous runs</span>'; 358 365 for (const r of runs.slice(0, 8)) { 359 - nh += `<a onclick="loadRun(${r.id})">${ago(r.timestamp)}</a>`; 366 + nh += `<a onclick="loadRun(${r.id})">${ago(r.timestamp)} <span class="time-detail">${clockTime(r.timestamp)}</span></a>`; 360 367 } 361 368 nav.className = 'nav'; 362 369 nav.innerHTML = nh;