1/* Avasis M8. No framework. Motion per docs/design/MOTION.md Part 2: reveal (160ms), transcript line stagger (60ms/line, bounded cursor), 2 preflight resolving, numbers count-up between two real values (400ms), tab swap (160ms). Reduced motion: everything instant. */ 3(function () { 4 const doc = document.documentElement; 5 const api = document.body.dataset.api || 'https://api.avasis.ai'; 6 const root = document.body.dataset.root || ''; // '' in production, '/staging' on the staging copy 7 const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; 8 const $ = (s, r) => (r || document).querySelector(s); 9 const $$ = (s, r) => Array.from((r || document).querySelectorAll(s)); 10 const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]); 11 const J = async (path, opts) => { 12 const r = await fetch(api + path, Object.assign({ credentials: 'include', headers: Object.assign({ 'Content-Type': 'application/json' }, (opts && opts.headers) || {}) }, opts || {})); 13 let d = null; try { d = await r.json(); } catch (err) {} 14 return { ok: r.ok, status: r.status, data: d }; 15 }; 16 17 // ---- theme ------------------------------------------------------------ 18 document.addEventListener('click', (e) => { 19 if (!e.target.closest('.theme-toggle')) return; 20 const next = doc.getAttribute('data-theme') === 'light' ? 'dark' : 'light'; 21 doc.setAttribute('data-theme', next); 22 try { localStorage.setItem('theme', next); } catch (err) {} 23 }); 24 25 // ---- copy --------------------------------------------------------------- 26 document.addEventListener('click', async (e) => { 27 const b = e.target.closest('.copy'); if (!b) return; 28 let text = b.dataset.copy; 29 if (!text) { 30 const panel = b.closest('.panel'); 31 const visible = panel && ($$('.tabpanel', panel).find(p => !p.hidden) || panel); 32 const pre = visible && visible.querySelector('pre'); 33 if (!pre) return; 34 text = pre.dataset.full || pre.innerText; 35 } 36 try { 37 await navigator.clipboard.writeText(text); 38 b.classList.add('done'); b.querySelector('span').textContent = 'Copied'; 39 setTimeout(() => { b.classList.remove('done'); b.querySelector('span').textContent = 'Copy'; }, 1500); 40 } catch (err) {} 41 }); 42 43 // ---- generic tabs (data-group) ------------------------------------------ 44 function swapIn(el) { 45 if (reduced) { el.hidden = false; return; } 46 el.classList.add('out'); el.hidden = false; 47 requestAnimationFrame(() => requestAnimationFrame(() => el.classList.remove('out'))); 48 } 49 document.addEventListener('click', (e) => { 50 const t = e.target.closest('[role="tab"][data-tab]'); if (!t) return; 51 const group = t.closest('[data-group]')?.dataset.group; if (!group) return; 52 $$(`[role="tab"][data-tab]`).filter(x => x.closest('[data-group]')?.dataset.group === group).forEach(x => x.setAttribute('aria-selected', x === t)); 53 $$(`.tabpanel[data-group="${group}"]`).forEach(p => { const on = p.dataset.tab === t.dataset.tab; if (on && p.hidden) swapIn(p); else if (!on) p.hidden = true; }); 54 }); 55 56 const statusWord = s => String(s || '').replace(/_/g, ' '); 57 const ATTN_STATUS = ['unverified', 'reproduced', 'mixed', 'failing', 'stale']; 58 const ATTN_EXPIRY = ['possibly_outdated', 'outdated', 'superseded']; 59 const relTime = iso => { 60 const t = Date.parse(iso); if (!t) return ''; 61 const d = (Date.now() - t) / 1000; 62 if (d < 60) return 'just now'; 63 const u = d < 3600 ? [60, 'minute'] : d < 86400 ? [3600, 'hour'] : d < 86400 * 30 ? [86400, 'day'] : d < 86400 * 365 ? [86400 * 30, 'month'] : [86400 * 365, 'year']; 64 const n = Math.floor(d / u[0]); return n + ' ' + u[1] + (n === 1 ? '' : 's') + ' ago'; 65 }; 66 67 // ---- motion helpers (MOTION.md Part 2; ~60 lines in total, opacity/transform only) ------------- 68 const ms = () => performance.now(); 69 function reveal(el, delay) { // .reveal â .in on the next-next frame; under `reduced` just unhide 70 el.hidden = false; if (reduced) return; 71 el.classList.add('reveal'); 72 const go = () => requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('in'))); 73 delay ? setTimeout(go, delay) : go(); 74 } 75 function xfade(el, fn) { // value crossfade: old 1â0 (80ms), new 0â1 (120ms); colour is set by the class, instantly 76 if (reduced) { fn(); return; } 77 el.classList.add('xf'); setTimeout(() => { fn(); el.classList.remove('xf'); }, 80); 78 } 79 // transcript(pre, promise): the request line is already in the <pre>; a cursor blinks only between send and receive; 80 // the first `n` lines of the real response reveal 60 ms apart; the full JSON goes to data-full for [copy]. 81 async function transcript(pre, promise, n) { 82 $$('noscript', pre).forEach(x => x.remove()); 83 const cur = document.createElement('span'); cur.className = 'cur'; if (!reduced) pre.appendChild(cur); 84 const t0 = ms(); let data; 85 try { data = await promise; } catch (err) { data = { error: String(err && err.message || err) }; } 86 const dt = Math.round(ms() - t0); cur.remove(); 87 const lines = JSON.stringify(data, null, 1).split('\n'); pre.dataset.full = JSON.stringify(data, null, 1); 88 lines.slice(0, n).concat(lines.length >
88 n ? ['â¦'] : []).forEach((l, i) => { 89 const sp = document.createElement('span'); sp.textContent = l + '\n'; sp.hidden = true; pre.appendChild(sp); reveal(sp, i * 60); 90 }); 91 return { data, dt }; 92 } 93 const PF = (tool, version, platform) => fetch(api + '/v1/preflight?' + new URLSearchParams({ tool, version, platform: platform || 'any' })).then(r => r.json()); 94 95 // ---- hero transcript: the two real calls on load (HOME.md), the check form inline (three states) ---------- 96 const tr = $('[data-transcript]'); 97 if (tr) { 98 const note = $('[data-livenote]'), msEl = $('[data-ms]', note); const done = []; 99 const q = tr.dataset.q; const tool = tr.dataset.tool, version = tr.dataset.version, platform = tr.dataset.platform; 100 const finish = () => { if (msEl) msEl.textContent = ' · ' + Math.max(...done) + ' ms'; }; 101 transcript($('[data-term="search"]', tr), fetch(api + '/v1/problems?' + new URLSearchParams({ q, limit: 1 })).then(r => r.json()), 12).then(r => { done.push(r.dt); finish(); }); 102 const pfPre = $('[data-term="preflight"]', tr); 103 if (pfPre) transcript(pfPre, PF(tool, version, platform), 12).then(r => { done.push(r.dt); finish(); pfStatus(r.data); }); 104 function pfStatus(d) { 105 const st = $('[data-pf-status]', tr); if (!st || !d || !d.verdict) return; 106 st.innerHTML = '<span class="verdict-' + esc(d.verdict) + '">' + esc(d.verdict) + '</span> · ' + esc(d.recommendation || ''); 107 } 108 const form = $('[data-preflight-inline]'); 109 if (form) form.addEventListener('submit', async (e) => { 110 e.preventDefault(); const t = form.tool.value.trim(), v = form.version.value.trim(), p = form.platform.value; if (!t || !v) return; 111 const out = $('[data-pf-inline]', form); out.hidden = false; out.className = 'pf-inline mono'; out.textContent = 'checking ' + t + ' ' + v + ' on ' + (p || 'any') + ' â¦'; 112 $$('[role="tab"][data-tab="preflight"]', tr).forEach(b => b.click()); 113 if (pfPre) { $$('span', pfPre).forEach(x => x.remove()); pfPre.innerHTML = '<span class="k">> preflight(tool="' + esc(t) + '", version="' + esc(v) + '", platform="' + esc(p || 'any') + '")</span>\n<span class="c"># REST twin from this browser: GET /v1/preflight?tool=&version=&platform=</span>\n'; } 114 const r = pfPre ? await transcript(pfPre, PF(t, v, p), 12) : { data: await PF(t, v, p) }; 115 const d = r.data || {}; pfStatus(d); 116 const href = root + '/preflight/?' + new URLSearchParams({ tool: t, version: v, platform: p || 'any' }); 117 const line = d.verdict === 'unknown' ? 'We have no rows for this tool yet. That is not a clear; it means nobody has mapped it.' : (d.recommendation || d.error || ''); 118 out.innerHTML = (d.verdict ? '<span class="verdict-' + esc(d.verdict) + '">' + esc(d.verdict) + '</span> · ' : '') + esc(line) + ' <a class="link" href="' + href + '">full result â</a>'; 119 }); 120 } 121 122 // ---- /preflight/: the three response states (PREFLIGHT.md, DECISIONS §6) -------------------------------- 123 const pfPage = $('[data-pf-form]'); 124 if (pfPage) { 125 const out = $('[data-pf-out]'), res = $('[data-pf-result]'), rows = $('[data-pf-rows]'), cards = $('[data-pf-cards]'), pre = $('[data-term="pf"]'); 126 const note = $('[data-livenote]', out), sbr = $('[data-pf-sbr]'), unk = $('[data-pf-unknown]'); 127 const row = (k, v, cls) => '<div class="r"><span class="k">' + esc(k) + '</span><span class="v ' + (cls || '') + '">' + v + '</span></div>'; 128 const status = s => '<span class="status-' + esc(s) + (s === 'verified' ? '' : ' attn') + '">' + esc(s) + '</span>'; 129 async function run(t, v, p) { 130 out.hidden = false; res.dataset.state = 'checking'; res.dataset.verdict = ''; rows.innerHTML = ''; cards.innerHTML = ''; sbr.hidden = true; unk.hidden = true; note.hidden = true; 131 $('[data-pf-checking]', res).textContent = t + ' ' + v + ' on ' + (p || 'any'); 132 pre.innerHTML = '<span class="k">> preflight(tool="' + esc(t) + '", version="' + esc(v) + '", platform="' + esc(p || 'any') + '")</span>\n'; 133 const r = await transcript(pre, PF(t, v, p), 14); const d = r.data || {}; 134 note.hidden = false; $('[data-ms]', note).textContent = ' · ' + r.dt + ' ms'; 135 res.dataset.state = 'done'; res.dataset.verdict = d.verdict || 'error'; 136 if (!d.verdict) { rows.innerHTML = row('error', esc(d.error || d.detail || 'no verdict in the response')); return; } 137 const R = []; 138 R.push('<div class="verdict verdict-' + esc(d.verdict) + '">' + esc(d.verdict) + '</div>'); 139 R.push(row('', esc(d.tool_display || d.tool) + ' ' + esc(d.version) + (d.verdict === 'unknown' ? '' : ' on ' + esc(d.platform)))); 140 if (d.verdict === 'unknown') R.push(row('', 'We have no rows for this tool yet. That is not a clear; it means nobody has mapped it.')); 141 else if (d.verdict === 'clear') R.push(row('', 'No known breakage for this version on this platform.')); 142 else R.push(row('', esc(d.recommendation || ''))); 143 if (d.verdict !== 'unknown' && (d.verdict !== 'clear' || d.recommended_version)) R.push(row('recommended version', esc(d.recommended_version || 'none known'))); 144 R.push(row('checked', 'just now (' + esc(d.checked_at || '') + ')')); 145 R.forEach((h, i) => { const div = document.createElement('div'); div.innerHTML = h; const el = div.firstChild; el.hidden = true; rows.appendChild(el); reveal(el, 160 + i * 40); }); 146 if (d.verdict === 'unknown') unk.hidden = false;
147 (d.breakages || []).forEach(b => { 148 const av = b.affected_versions || {}; const facts = []; 149 if (av.min || av.max) facts.push('affected ' + esc(av.min || '?') + 'â' + esc(av.max || '?')); 150 if (b.fixed_in) facts.push('fixed in ' + esc(b.fixed_in)); 151 if ((b.safe_versions || []).length) facts.push('safe ' + esc(b.safe_versions.join(', '))); 152 facts.push(esc((b.platforms || []).join(' ') || 'any')); facts.push(esc(b.status || '')); 153 if (b.source_url) facts.push('<a class="link" href="' + esc(b.source_url) + '" rel="noopener">source â</a>'); 154 const fixes = (b.fixes || b.fix_links || []).map(f => '<li>' + (f.url ? '<a class="link" href="' + esc(f.url) + '">' + esc(f.title) + '</a>' : esc(f.title)) + ' · ' + status(f.verification_status || 'unverified') 155 + (f.passed != null ? ' · ' + esc(f.passed) + ' passed · ' + esc(f.failed || 0) + ' failed' : ' · ' + esc(f.checks || 0) + ' checks' + (typeof f.pass_rate === 'number' ? ' · ' + Math.round(f.pass_rate * 100) + '%' : '')) + (f.expiry_state ? ' · ' + esc(statusWord(f.expiry_state)) : '') + '</li>').join(''); 156 const card = document.createElement('div'); card.className = 'pf-card'; 157 card.innerHTML = '<div class="mono-cap">breakage · ' + esc(b.severity || '') + '</div><h3 data-author-text>' + esc(b.title || '') + '</h3><p class="facts mono">' + facts.join(' · ') + '</p>' + (b.summary ? '<p class="sum" data-author-text>' + esc(String(b.summary).slice(0, 600)) + '</p>' : '') + (fixes ? '<p class="mono-cap">fixes</p><ul class="fixes mono">' + fixes + '</ul>' : '') + (b.last_verified_at ? '<p class="sub mono">last verified ' + esc(relTime(b.last_verified_at)) + '</p>' : ''); 158 card.hidden = true; cards.appendChild(card); reveal(card, 320); 159 }); 160 if ((d.breakages || []).length) sbr.hidden = false; 161 } 162 pfPage.addEventListener('submit', (e) => { e.preventDefault(); const t = pfPage.tool.value.trim(), v = pfPage.version.value.trim(), p = pfPage.platform.value; if (!t || !v) return; history.replaceState(null, '', '?' + new URLSearchParams({ tool: t, version: v, platform: p })); run(t, v, p); }); 163 const qs = new URLSearchParams(location.search); 164 if (qs.get('tool') && qs.get('version')) { pfPage.tool.value = qs.get('tool'); pfPage.version.value = qs.get('version'); if (qs.get('platform')) pfPage.platform.value = qs.get('platform'); run(qs.get('tool'), qs.get('version'), pfPage.platform.value); } 165 } 166 167 // ---- fixes board filter -------------------------------------------------- 168 const board = $('#board'); 169 if (board) { 170 const cards = $$('.pcard', board); const state = { platform: '', app: '', q: '' }; const count = $('#count'); const none = $('#noresults'); 171 const qs = new URLSearchParams(location.search); 172 const apply = () => { 173 let n = 0; const words = state.q.split(/\s+/).filter(Boolean); 174 cards.forEach(c => { 175 const ok = (!state.platform || c.dataset.platform === state.platform) && (!state.app || c.dataset.app === state.app) && (!words.length || words.every(w => c.dataset.text.includes(w))); 176 c.hidden = !ok; if (ok) n++; 177 }); 178 if (count) count.textContent = n + (n === 1 ? ' problem' : ' problems'); 179 if (none) none.hidden = n > 0; 180 }; 181 const qIn = $('#q'); 182 if (qIn) { 183 qIn.addEventListener('input', (e) => { state.q = e.target.value.trim().toLowerCase(); apply(); }); 184 const q0 = qs.get('q'); if (q0) { qIn.value = q0; state.q = q0.trim().toLowerCase(); } 185 } 186 $$('[data-filter]').forEach(b => b.addEventListener('click', () => { 187 const f = b.dataset.filter, v = b.dataset.value; 188 state[f] = (state[f] === v && v) ? '' : v; 189 $$(`[data-filter="${f}"]`).forEach(x => x.setAttribute('aria-pressed', x.dataset.value === state[f] ? 'true' : 'false')); 190 apply(); 191 })); 192 apply(); 193 } 194 195 // ---- live stats: count from the build-time value to the live value (both real; never from 0), 400ms ease-out ---- 196 const statEls = $$('[data-stat]'); 197 if (statEls.length) { 198 fetch(api + '/v1/stats').then(r => r.json()).then(s => {
199 statEls.forEach(el => { 200 const m = el.dataset.statMinus; const v = m ? s[m.split(',')[0]] - s[m.split(',')[1]] : s[el.dataset.stat]; 201 if (typeof v !== 'number' || isNaN(v)) return; 202 const from = parseInt(el.textContent.replace(/[^\d-]/g, ''), 10); 203 if (reduced || isNaN(from) || from === v || Math.abs(v - from) > 1000) { el.textContent = v.toLocaleString(); return; } 204 const t0 = ms(); const step = () => { const t = Math.min(1, (ms() - t0) / 400), e = 1 - Math.pow(1 - t, 3); 205 el.textContent = Math.round(from + (v - from) * e).toLocaleString(); if (t < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); 206 }); 207 }).catch(() => {}); 208 } 209 210 // ---- hero: cycling placeholder --------------------------------------------- 211 const hs = $('.hsearch input[data-cycle]'); 212 if (hs && !reduced) { 213 let sigs = []; try { sigs = JSON.parse(hs.dataset.cycle) || []; } catch (err) {} 214 if (sigs.length > 1) { 215 let i = 0; 216 setInterval(() => { if (document.activeElement === hs || hs.value) return; i = (i + 1) % sigs.length; hs.placeholder = sigs[i]; }, 3000); 217 } 218 } 219 220 // ---- fix page: report pass / fail with an environment (no sign-in) ---------------- 221 // POST /v1/report/{id} {check_passed, environment?}; the verification block re-renders from the response 222 // (PROBLEM_PAGE.md "Report"). Never silently swallow a failure. 223 // relative "last verified" ages with the page; the absolute date stays in the text and the title 224 $$('[data-v-last][data-ts]').forEach(el => { const iso = el.dataset.ts; if (!iso) return; const r = relTime(iso); if (r) el.textContent = r + ' (' + iso.slice(0, 10) + ')'; }); 225 function rerender(sec, d) { 226 const v = (d.passed != null ? d.passed : null), f = (d.failed != null ? d.failed : null); 227 const checks = (typeof d.checks === 'number') ? d.checks : (v != null && f != null ? v + f : null); 228 const st = $('[data-v-status]', sec); 229 if (st && d.verification_status) xfade(st, () => { st.textContent = statusWord(d.verification_status); st.className = 'status-' + d.verification_status + (ATTN_STATUS.includes(d.verification_status) ? ' attn' : ''); }); 230 const ex = $('[data-v-expiry]', sec); 231 if (ex && d.expiry_state) { ex.textContent = statusWord(d.expiry_state); ex.className = 'expiry-' + d.expiry_state + (ATTN_EXPIRY.includes(d.expiry_state) ? ' attn' : ''); } 232 const rate = $('[data-v-rate]', sec); 233 if (rate && checks != null) { 234 if (!checks) rate.textContent = 'no checks reported yet'; 235 else if (v != null && f != null) { 236 const pct = Math.round(100 * v / (v + f)); 237 xfade(rate, () => { rate.innerHTML = '<span data-v-pct>' + pct + '%</span> · <span class="' + (f === 0 && v > 0 ? 'pass' : '') + '" data-v-passed>' + v + ' passed</span> · <span data-v-failed>' + f + ' failed</span>'; }); 238 } else if (typeof d.pass_rate === 'number') { const pe = $('[data-v-pct]', sec); if (pe) pe.textContent = Math.round(d.pass_rate * 100) + '%'; } 239 } 240 const lv = $('[data-v-last]', sec); 241 if (lv && d.last_verified_at) { lv.dataset.ts = d.last_verified_at; lv.title = d.last_verified_at.slice(0, 10); lv.textContent = relTime(d.last_verified_at) + ' (' + d.last_verified_at.slice(0, 10) + ')'; } 242 if (Array.isArray(d.environments)) { 243 const tb = $('[data-env-table]', sec); 244 if (tb) { 245 const rows = d.environments.slice().sort((a, b) => (b.passed || 0) - (a.passed || 0) || String(b.last_passed_at || '').localeCompare(String(a.last_passed_at || ''))); 246 tb.innerHTML = '<tr><th>os</th><th>tool version</th><th>passed</th><th>failed</th><th>last passed</th></tr>' + rows.map(e => { 247 const lp = e.last_passed_at ? new Date(e.last_passed_at) : null; 248 const lpd = lp ? lp.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }) : 'â'; 249 return '<tr><td>' + esc(e.os) + '</td><td>' + esc(e.tool_version || 'â') + '</td><td class="' + ((e.passed && !e.failed) ? 'pass' : '') + '">' + esc(e.passed || 0) + '</td><td>' + esc(e.failed || 0) + '</td><td title="' + esc(e.last_passed_at ? String(e.last_passed_at).slice(0, 10) : '') + '">' + esc(lpd) + '</td></tr>'; 250 }).join(''); 251 const n = $('[data-env-n]', sec); 252 if (n && checks != null) { const un = Math.max(0, checks - d.environments.reduce((a, e) => a + (e.passed || 0) + (e.failed || 0), 0)); n.textContent = un; n.parentNode.childNodes[1].textContent = ' report' + (un === 1 ? '' : 's') + ' without an environment. Counted in the overall rate only.'; } 253 } 254 } 255 // problem facts line: sums over every fix on the page (after the 80 ms crossfade has swapped the values) 256 setTimeout(() => { 257 const P = $$('[data-v-passed]').reduce((a, x) => a + (parseInt(x.textContent, 10) || 0), 0); 258 const F = $$('[data-v-failed]').reduce((a, x) => a + (parseInt(x.textContent, 10) || 0), 0); 259 const cp = $('[data-checks-passed]'); if (cp) cp.textContent = P; 260 const cf = $('[data-checks-failed]'); if (cf) cf.textContent = F; 261 const ck = $('[data-checks]'); if (ck) ck.textContent = P + F; 262 }, reduced ? 0 : 100); 263 } 264 $$('[data-report]').forEach(box => { 265 const sid = box.dataset.report; const msg = $('.msg', box); const form = $('.envform', box); const sec = box.closest('.fix'); 266 const buttons = $$('button[data-pass]', box); 267 let passed = null; 268 const tool = box.dataset.tool || ''; 269 let done = false; try { done = localStorage.getItem('reported-' + sid) === '1'; } catch (err) {} 270 const show = (text, cls) => { msg.className = 'msg mono' + (cls ? ' ' + cls : ''); msg.textContent = text; reveal(msg); }; 271 if (done) show('recorded from this browser already'); 272 buttons.forEach(b => b.addEventListener('click', () => { 273 passed = b.dataset.pass === '1'; 274 buttons.forEach(x => x.classList.toggle('btn-primary', x === b)); 275 reveal(form); msg.hidden = true; 276 const tv = form.tool_version;
276 if (tv && !tv.value && box.dataset.fixedIn) tv.placeholder = box.dataset.fixedIn; 277 })); 278 async function send(env) { 279 if (passed == null) return; 280 buttons.forEach(x => x.disabled = true); $$('button', form).forEach(x => x.disabled = true); 281 show('recordingâ¦'); 282 const body = { check_passed: passed }; if (env) body.environment = env; 283 let r = await J('/v1/report/' + sid, { method: 'POST', body: JSON.stringify(body) }); 284 if (!r.ok && r.status === 404) r = await J('/v1/solutions/' + sid + '/report', { method: 'POST', body: JSON.stringify(body) }); 285 if (r.status === 429) { show('Twenty reports an hour from one address is the limit. Try later.', 'notice'); buttons.forEach(x => x.disabled = false); $$('button', form).forEach(x => x.disabled = false); return; } 286 if (!r.ok) { show('not recorded: ' + ((r.data && (r.data.detail || r.data.message || r.data.error)) || ('HTTP ' + r.status)), 'notice'); buttons.forEach(x => x.disabled = false); $$('button', form).forEach(x => x.disabled = false); return; } 287 try { localStorage.setItem('reported-' + sid, '1'); } catch (err) {} 288 const d = r.data || {}; 289 const v = (d.passed != null ? d.passed : null), f = (d.failed != null ? d.failed : null); 290 const pct = typeof d.pass_rate === 'number' ? Math.round(d.pass_rate * 100) : (v != null && f != null && (v + f)) ? Math.round(100 * v / (v + f)) : null; 291 const parts = ['recorded: ' + (d.recorded || (passed ? 'pass' : 'fail'))]; 292 if (pct != null) parts.push(pct + '%'); 293 if (v != null) parts.push(v + ' passed'); if (f != null) parts.push(f + ' failed'); 294 if (d.verification_status) parts.push(statusWord(d.verification_status)); 295 if (env && d.environment_recorded === false) parts.push('environment not recorded'); 296 show(parts.join(' · ')); 297 form.hidden = true; 298 rerender(sec, d); 299 } 300 form.addEventListener('submit', (e) => { 301 e.preventDefault(); 302 const env = { os: form.os.value }; 303 if (form.os_version.value.trim()) env.os_version = form.os_version.value.trim(); 304 if (form.tool_version.value.trim()) env.tool_version = form.tool_version.value.trim(); 305 if (tool) env.tool = tool; 306 send(env); 307 }); 308 $('[data-noenv]', form).addEventListener('click', () => send(null)); 309 }); 310 311 // ---- dashboard ---------------------------------------------------------------- 312 const dash = $('#dash'); 313 if (dash) { 314 const page = dash.dataset.page; const q = new URLSearchParams(location.search); 315 const gate = $('#gate'), app = $('#app'), who = $('#who'); 316 const note = (cls, text) => { const n = $('#notice'); if (!n) return; n.hidden = false; n.className = 'notice ' + cls; n.textContent = text; }; 317 if (q.get('claimed') === '1') note('ok', 'Claimed. Your agent and this browser now share one account.'); 318 if (q.get('claim') === 'invalid') note('err', 'That claim link was already used or has expired. Ask your agent for a fresh one, or paste your token.'); 319 $('#door-token')?.addEventListener('submit', async (e) => { 320 e.preventDefault(); const t = $('#door-token input').value.trim(); if (!t) return; 321 const r = await J('/v1/auth/token', { method: 'POST', body: JSON.stringify({ token: t }) }); 322 if (r.ok) location.reload(); else note('err', (r.data && (r.data.detail || r.data.error)) || 'Token not accepted.'); 323 }); 324 $('#door-claim')?.addEventListener('submit', (e) => { 325 e.preventDefault(); let v = $('#door-claim input').value.trim(); if (!v) return; 326 if (!/^https?:/.test(v)) v = api + '/v1/dashboard/claim/' + encodeURIComponent(v.replace(/.*\//, '')); 327 location.href = v; 328 }); 329 $('#logout')?.addEventListener('click', async () => { await J('/v1/auth/logout', { method: 'POST' }); location.href = root + '/dashboard/'; }); 330 // agent tabs on the connect page (static panels) 331 $$('[data-agent-tab]').forEach(t => t.addEventListener('click', () => { 332 $$('[data-agent-tab]').forEach(x => x.setAttribute('aria-selected', x === t)); 333 $$('[data-agent-panel]').forEach(p => p.hidden = p.dataset.agentPanel !== t.dataset.agentTab); 334 })); 335 336 (async () => { 337 const r = await J('/v1/me'); 338 if (!r.ok) { gate.hidden = false; app.hidden = true; return; } 339 const me = r.data; gate.hidden = true; app.hidden = false; const ah = $('#app-head'); if (ah) ah.hidden = false;
340 const a = me.author || me; 341 if (who) who.textContent = (a.name || '') + ' · ' + (a.kind || '') + (me.is_admin ? ' · admin' : ''); 342 if (me.is_admin) $$('[data-admin-only]').forEach(el => el.hidden = false); 343 const F = { overview, publish, connect, queue, radar }[page]; if (F) F(me, a); 344 })(); 345 346 function rows(list, cols) { 347 if (!list || !list.length) return null; 348 return '<table class="tbl"><tr>' + cols.map(c => '<th>' + esc(c[0]) + '</th>').join('') + '</tr>' + list.map(row => '<tr>' + cols.map(c => '<td class="' + (c[2] || '') + '">' + c[1](row) + '</td>').join('') + '</tr>').join('') + '</table>'; 349 } 350 const pv = s => (s.passed != null ? s.passed : (s.verified_count || 0)), pf = s => (s.failed != null ? s.failed : (s.failed_count || 0)); 351 function overview(me, a) { 352 const sols = me.fixes || me.solutions || []; 353 $('#n-fixes').textContent = String(a.fixes_count != null ? a.fixes_count : sols.length); 354 $('#n-passed').textContent = String(sols.reduce((x, s) => x + pv(s), 0)); 355 if (a.rank) $('#n-rank').textContent = '#' + a.rank; 356 const slug = a.slug || a.display_slug; if (slug) $('#profile-link').href = root + '/u/' + slug + '/'; 357 $('#solutions').innerHTML = rows(sols, [['fix', s => (s.problem_slug ? `<a class="link" href="${root}/p/${esc(s.problem_slug)}/#s${s.id}">${esc(s.title)}</a>` : esc(s.title))], ['status', s => { const st = s.verification_status || (pv(s) > 0 && pf(s) === 0 ? 'verified' : 'unverified'); return '<span class="status-' + esc(st) + (ATTN_STATUS.includes(st) ? ' attn' : '') + '">' + esc(statusWord(st)) + '</span>'; }], ['passed', s => String(pv(s))], ['failed', s => String(pf(s))], ['id', s => 'solution_get(' + s.id + ')']]) || '<div class="empty">Nothing published yet. <a class="link" href="' + root + '/dashboard/publish/">Publish a fix</a>: the error, what didn\'t work, the steps, the check.</div>'; 358 impactView(a); 359 } 360 // ---- F5: impact (GET /v1/me/impact; every number is a count from the API, the sparkline is the 30-day series) ---- 361 function spark(series) { 362 const W = 100, H = 20, n = series.returns.length, max = Math.max(1, ...series.returns); 363 const x = i => (n > 1 ? (i * (W - 2)) / (n - 1) + 1 : W / 2).toFixed(1); 364 const y = v => (H - 2 - (v / max) * (H - 4)).toFixed(1); 365 const pts = series.returns.map((v, i) => x(i) + ',' + y(v)).join(' '); 366 const marks = series.returns.map((_, i) => { 367 const p = series.passes[i], f = series.fails[i]; 368 return (p ? `<rect class="p" x="${(x(i) - 1).toFixed(1)}" y="${H + 2}" width="2" height="3"><title>${p} passed</title></rect>` : '') 369 + (f ? `<rect class="status-failing" x="${(x(i) - 1).toFixed(1)}" y="${H + 5}" width="2" height="3"><title>${f} failed</title></rect>` : ''); 370 }).join(''); 371 const total = series.returns.reduce((a, b) => a + b, 0); 372 return `<svg class="spark" viewBox="0 0 ${W} ${H + 8}" role="img" aria-label="${total} returns in 30 days, ${series.passes.reduce((a, b) => a + b, 0)} passes, ${series.fails.reduce((a, b) => a + b, 0)} fails"><line class="base" x1="1" y1="${H - 1.5}" x2="${W - 1}" y2="${H - 1.5}" stroke-width="1"/><polyline class="line" points="${pts}"/>${marks}</svg>`; 373 } 374 const num = v => (v == null ? 'â' : String(v)); 375 const pct = r => (r == null ? 'â' : Math.round(r * 100) + '%'); 376 async function impactView(a) { 377 const r = await J('/v1/me/impact'); 378 if (!r.ok || !r.data) return; 379 const d = r.data, sec = $('#impact'); if (!sec) return; 380 const I = k => $(`[data-i="${k}"]`, sec); 381 const T = d.totals || {}, F = T.fixes || {}, A = T.agents_helped || {}, C = T.checks || {}, R = d.rank || {}; 382 I('published').textContent = num(F.approved); 383 I('fixes-sub').textContent = `live · ${num(F.pending)} pending review · ${num(F.rejected)} rejected · ${num(F.published)} published in all`; 384 I('agents').textContent = num(A.total); 385 I('agents-sub').textContent = `distinct agents that fetched a fix of yours · ${num(A.last_30d)} in the last 30 days · ${num(A.via_search)} by search, ${num(A.via_get)} by solution_get`; 386 I('checks').textContent = num(C.passed); 387 I('checks').classList.toggle('pass', (C.passed || 0) > 0); 388 I('checks-sub').textContent = `${num(C.failed)} failed · pass rate ${pct(C.pass_rate)} over ${num(C.total)} reports`;
389 I('rank').textContent = R.position ? '#' + R.position : 'â'; 390 I('rank-sub').innerHTML = `of ${num(R.of)} contributors · ${num(R.agents_helped)} check reports on your public fixes · <a class="link" href="${root}/leaders/">leaderboard</a>`; 391 const fixes = d.fixes || []; 392 const chip = f => { 393 const st = f.public ? (f.verification_status || 'unverified') : f.moderation_status; 394 return '<span class="status-' + esc(st) + (ATTN_STATUS.includes(st) || st === 'pending_review' ? ' attn' : '') + '">' + esc(statusWord(st)) + '</span>'; 395 }; 396 $('#imp-table').innerHTML = rows(fixes, [ 397 ['fix', f => (f.url ? `<a class="link" href="${esc(f.url)}">${esc(f.title)}</a>` : esc(f.title)) 398 + (f.reviewer_note ? `<div class="sub">reviewer: ${esc(f.reviewer_note)}</div>` : ''), 'fixcell'], 399 ['status', chip], 400 ['returns 7d · 30d', f => num(f.returns && f.returns.last_7d) + ' <span class="rate">·</span> ' + num(f.returns && f.returns.last_30d), 'n'], 401 ['passes · fails', f => (f.checks && f.checks.passed ? '<span class="pass">' + num(f.checks.passed) + '</span>' : '0') + ' <span class="rate">·</span> ' 402 + (f.checks && f.checks.failed ? '<span class="status-failing">' + num(f.checks.failed) + '</span>' : '0') 403 + ' <span class="rate">' + pct(f.checks && f.checks.pass_rate) + '</span>', 'n'], 404 ['rank +', f => num(f.rank_contribution), 'n'], 405 ['30 days', f => spark(f.series), 'n'] 406 ]) || '<div class="empty">No fixes yet, so no impact yet. <a class="link" href="' + root + '/dashboard/publish/">Publish a fix</a> and the numbers start here.</div>'; 407 const pend = fixes.filter(f => f.moderation_status === 'pending_review'); 408 $('#imp-pending').innerHTML = pend.map(f => `<div class="pend"> 409 <div class="ph"><b>${esc(f.title)}</b><span class="status-pending_review attn">pending review</span><span>${esc(relTime(f.created_at))}</span></div> 410 <ul>${(f.pending_reasons || []).map(x => `<li><span class="${x.severity === 'high' || x.severity === 'critical' ? 'status-pending_review' : ''}">${esc(x.why)}</span>${x.where ? ` <span class="where">· in ${esc(x.where)} · ${esc(x.rule)}</span>` : ''}${x.excerpt ? `<pre>${esc(x.excerpt)}</pre>` : ''}</li>`).join('')}</ul> 411 <div class="actions"><a class="btn sm" href="${esc(f.republish_url)}">fix and republish</a></div> 412 </div>`).join(''); 413 const S = d.sources || {}; 414 $('#imp-sources').textContent = `returns: ${S.returns || ''} · agents helped: ${S.agents_helped || ''} · checks: ${S.checks || ''}. Series in UTC days.`; 415 sec.hidden = false; 416 } 417 function publish() { 418 const f = $('#publish-form'); 419 const pre = q.get('problem') || q.get('signature'); 420 if (q.get('signature')) f.signature.value = q.get('signature'); 421 if (q.get('title')) f.title.value = q.get('title'); 422 if (q.get('app')) f.app.value = q.get('app'); 423 if (q.get('platform')) f.platform.value = q.get('platform'); 424 if (pre && !q.get('signature')) J('/v1/problems/' + encodeURIComponent(pre)).then(r => { const p = r.ok && (r.data.problem || r.data); if (p && p.signature) { f.signature.value = p.signature; f.app.value = p.app || ''; f.platform.value = p.platform || 'any'; } }); 425 f.addEventListener('submit', async (e) => { 426 e.preventDefault(); const fd = new FormData(f); const body = Object.fromEntries(fd.entries()); 427 body.content = body.steps; body.price_type = 'free'; body.price_cents = 0; 428 if (!body.model) body.model = 'unknown'; 429 let r = await J('/v1/fixes', { method: 'POST', body: JSON.stringify(body) }); 430 if (!r.ok && r.status === 404) r = await J('/v1/solutions', { method: 'POST', body: JSON.stringify(body) }); 431 if (r.ok) { 432 const d = r.data || {}; const red = d.redactions && d.redactions.length ? ' Redacted: ' + d.redactions.map(x => typeof x === 'string' ? x : (x.kind || x.pattern || JSON.stringify(x))).join(', ') + '.' : ''; 433 note('ok', 'Published.' + (d.url ? ' ' + d.url : '') + red); f.reset(); 434 } else note('err', (r.data && (r.data.detail || r.data.error)) || 'Could not publish.'); 435 }); 436 } 437 async function connect(me, a) { 438 $('#claim-status').textContent = a.claimed_at ? 'claimed on ' + String(a.claimed_at).slice(0, 10) : 'not claimed yet'; 439 $('#new-claim')?.addEventListener('click', async () => { const r = await J('/v1/me/claim-code', { method: 'POST' }); if (r.ok) { $('#claim-url').textContent = r.data.claim_url; $('#claim-url').hidden = false; } }); 440 $('#email-form')?.addEventListener('submit', async (e) => { e.preventDefault(); const em = $('#email-form input').value.trim(); const r = await J('/v1/me/email', { method: 'POST', body: JSON.stringify({ email: em }) }); note(r.ok ? 'ok' : 'err', r.ok ? 'Email saved: ' + r.data.email : ((r.data && r.data.detail) || 'Could not save.')); }); 441 if (a.email) $('#email-form input').value = a.email; 442 } 443 async function queue(me) { 444 if (!me.is_admin) { $('#admin-body').innerHTML = '<div class="empty">
444The queue is for admins.</div>'; return; } 445 const sortEl = $('[data-q-sort]'), toolEl = $('[data-q-tool]'), rejEl = $('[data-q-rejected]'), cnt = $('[data-q-count]'), rejSec = $('[data-q-rej]'); 446 const src = (p) => (p.sources || []).length ? p.sources.map(x => `<a class="link" href="${esc(x.url)}" rel="noopener">${esc(x.kind || 'link')}</a>`).join(' ') : (p.source_url ? `<a class="link" href="${esc(p.source_url)}" rel="noopener">${esc(p.source_kind || p.source || 'link')}</a>` : esc(p.source_kind || '')); 447 async function load() { 448 const qsx = new URLSearchParams({ sort: sortEl.value, limit: 200 }); if (toolEl.value.trim()) qsx.set('tool', toolEl.value.trim()); if (rejEl.checked) qsx.set('rejected', '1'); 449 let r = await J('/v1/admin/queue?' + qsx); 450 if (!r.ok && r.status === 404) r = await J('/v1/problems?status=queue&limit=200'); 451 const list = (r.data && (r.data.queue || r.data.problems || r.data.results || r.data)) || []; 452 const box = $('#queue'); 453 if (!Array.isArray(list) || !list.length) { box.innerHTML = '<div class="empty">Queue is empty.</div>'; cnt.textContent = ''; } 454 else { 455 cnt.textContent = list.length + ' in the queue · sorted by ' + (r.data.sort || sortEl.value); 456 // scraped text is data: rendered escaped, never followed. Flags (injection guard) are shown in amber as attention. 457 box.innerHTML = '<div class="tscroll"><table class="tbl queue"><tr><th title="distinct source urls / hours since first seen">velocity</th><th title="3 or more reports inside any 24 h window">wave</th><th>tool</th><th>version range</th><th>platform</th><th>error string · title</th><th>sources</th><th title="injection guard flags (guard.scan); the item is kept and shown as data">flags</th><th title="v3 heat, kept">heat</th><th></th></tr>' + list.map(p => `<tr> 458 <td class="mono">${esc((p.velocity != null ? Number(p.velocity) : 0).toFixed(3))}</td><td class="mono ${p.wave ? 'notice' : ''}">${p.wave ? 'wave' : 'â'}</td> 459 <td class="mono">${esc(p.tool || 'â')}</td><td class="mono">${esc(p.version_range || 'â')}</td><td class="mono">${esc(p.platform || 'any')}</td> 460 <td><span class="mono small" data-author-text>${esc((p.error_string || p.signature || '').slice(0, 160))}</span><br><span class="muted small" data-author-text>${esc((p.title || '').slice(0, 140))}</span>${p.first_seen ? `<br><span class="muted small">first seen ${esc(String(p.first_seen).slice(0, 10))} · ${esc(p.report_count || 0)} report${p.report_count === 1 ? '' : 's'} from ${esc(p.source_count || 0)} source kind${p.source_count === 1 ? '' : 's'}</span>` : ''}</td> 461 <td class="small">${src(p)}</td><td class="mono small ${(p.flags || []).length ? 'notice' : ''}">${esc((p.flags || []).join(' ') || 'â')}</td><td class="mono">
461${esc(p.heat || 0)}</td> 462 <td><div class="actions"><a class="btn sm" href="${root}/dashboard/publish/?signature=${encodeURIComponent(p.error_string || p.signature || p.title || '')}&title=${encodeURIComponent(p.title || '')}&app=${encodeURIComponent(p.app || '')}&platform=${encodeURIComponent(p.platform || 'any')}">solve</a><button class="btn sm" type="button" data-unsolved="${p.id}">list as unsolved</button><button class="btn sm" type="button" data-stale="${p.id}">stale</button></div></td></tr>`).join('') + '</table></div>'; 463 $$('[data-unsolved]').forEach(b => b.addEventListener('click', async () => { b.disabled = true; const r = await J('/v1/admin/queue/' + b.dataset.unsolved + '/list', { method: 'POST' }); note(r.ok ? 'ok' : 'err', r.ok ? 'Listed as unsolved.' : ((r.data && r.data.detail) || 'Failed')); load(); })); 464 $$('[data-stale]').forEach(b => b.addEventListener('click', async () => { b.disabled = true; const r = await J('/v1/admin/queue/' + b.dataset.stale + '/stale', { method: 'POST' }); note(r.ok ? 'ok' : 'err', r.ok ? 'Marked stale.' : ((r.data && r.data.detail) || 'Failed')); load(); })); 465 } 466 rejSec.hidden = !rejEl.checked; 467 if (rejEl.checked) { 468 const rej = (r.data && r.data.rejected) || [], runs = (r.data && r.data.runs) || []; 469 $('#rejected').innerHTML = rej.length ? '<div class="tscroll"><table class="tbl"><tr><th>when</th><th>reason</th><th>detail</th><th>tool</th><th>source</th><th>title</th></tr>' + rej.map(x => `<tr><td class="mono small">${esc(String(x.created_at || '').slice(0, 16).replace('T', ' '))}</td><td class="mono">${esc(x.rejected_reason || '')}</td><td class="mono small">${esc((x.detail || '').slice(0, 120))}</td><td class="mono">${esc(x.tool || 'â')}</td><td class="small">${x.source_url ? `<a class="link" href="${esc(x.source_url)}" rel="noopener">${esc(x.source_kind || 'link')}</a>` : esc(x.source_kind || '')}</td><td class="small" data-author-text>${esc((x.title || '').slice(0, 120))}</td></tr>`).join('') + '</table></div>' : '<div class="empty">Nothing rejected on record.</div>'; 470 $('#runs').innerHTML = runs.length ? '<p class="mono-cap" style="margin-top:18px">last runs</p><div class="tscroll"><table class="tbl"><tr><th>when</th><th>run</th><th>summary</th></tr>' + runs.map(e => { const pl = e.payload || {}; return `<tr><td class="mono small">${esc(String(e.created_at || '').slice(0, 16).replace('T', ' '))}</td><td class="mono small">${esc(pl.run_id || e.id)}</td><td class="mono small">${esc(pl.summary || JSON.stringify(pl).slice(0, 160))}</td></tr>`; }).join('') + '</table></div>' : ''; 471 } 472 } 473 [sortEl, rejEl].forEach(el => el.addEventListener('change', load)); toolEl.addEventListener('change', load); $('[data-q-reload]').addEventListener('click', load); 474 load(); 475 } 476 // ---- radar (RADAR.md; moved from api/static-for-site/radar.html, same bindings, site tokens) ---------- 477 async function radar(me) { 478 if (!me.is_admin) { $('#admin-body').innerHTML = '<div class="empty">Radar is for admins.</div>'; return; } 479 const R = $('[data-radar]'); const n = v => (v == null ? 0 : Number(v)); 480 const bar = (v, max) => { if (!max) return 'â';
480 const k = Math.round(6 * v / max); return 'â'.repeat(k) + 'â'.repeat(6 - k); }; 481 const th = (label, title, cls) => `<th class="${cls || ''}" title="${esc(title || '')}">${esc(label)}</th>`; 482 const td = (v, cls) => `<td class="${cls || ''}">${esc(v)}</td>`; 483 const num = (v, attn) => `<td class="n ${attn && n(v) > 0 ? 'notice' : ''}">${n(v)}</td>`; 484 const err = $('[data-r-error]', R); 485 async function load() { 486 err.hidden = true; 487 const days = $('[data-r-days]', R).value, tool = $('[data-r-tool]', R).value.trim(), platform = $('[data-r-platform]', R).value; 488 const [r1, r2, r3, r4] = await Promise.all([J('/v1/admin/radar?' + new URLSearchParams({ days, tool, platform, refresh: 1 })), J('/v1/stats'), J('/v1/breakages?status=active'), J('/v1/admin/moderation?status=pending_review')]); 489 if (r1.status === 404) { err.textContent = 'Radar is off on this API (RADAR_ENABLED=0).'; err.hidden = false; return; } 490 if (!r1.ok) { err.textContent = r1.status + ' from /v1/admin/radar' + (r1.data && r1.data.detail ? ': ' + r1.data.detail : ''); err.hidden = false; return; } 491 const bks = (r3.data && r3.data.breakages) || [], tools = (r3.data && r3.data.tools) || []; 492 bks.forEach(b => { if (!b.tool && tools.length === 1) { b.tool = tools[0].tool; b.tool_display = tools[0].tool_display; } }); // items carry no tool yet (api gap) 493 render(r1.data || {}, r2.data || {}, bks, r4.ok ? r4.data : null); 494 } 495 function render(d, s, bks, mod) { 496 $('[data-r-asof]', R).innerHTML = '<span class="pass">Live</span> · data as of ' + esc(new Date().toISOString().replace(/\.\d+Z$/, 'Z')) + ' · ' + esc(api.replace(/^https?:\/\//, '')); 497 const ns = d.north_star || {}; 498 $$('[data-r]', R).forEach(el => { const k = el.dataset.r; const v = k in ns ? ns[k] : (k === 'retention_days' ? (d.events || {}).retention_days : s[k]); el.textContent = n(v); }); 499 if (d.definitions && d.definitions.external_agents) $('[data-r-def]', R).textContent = d.definitions.external_agents; 500 const series = (d.north_star_series || []).slice().sort((a, b) => a.day < b.day ? -1 : 1); const byDay = {}; series.forEach(r => { byDay[r.day] = r; }); 501 const last7 = []; for (let i = 6; i >= 0; i--) { const dt = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10); last7.push(byDay[dt] || { day: dt, external_agents: 0 }); } 502 const mx = Math.max(...last7.map(r => n(r.external_agents)), 0); 503 $('[data-r-bars]', R).textContent = '7-day ' + last7.map(r => bar(n(r.external_agents), mx)[0]).join('') + ' ' + last7.map(r => n(r.external_agents)).join(' '); 504 const today = (d.daily || []).find(r => r.day === new Date().toISOString().slice(0, 10)) || {}; 505 const facts = [['lookups_today', s.lookups_today], ['preflights_today', s.preflights_today], ['hits_today', s.hits_today], ['agents_connected_7d', s.agents_connected_7d], 506 ['opt-in events today', n(today.lookups) + n(today.preflights) + n(today.reports)], ['fixes_pending', s.fixes_pending], ['problems_queued', s.problems_queued], ['breakages_active', s.breakages_active]]; 507 $('[data-r-today]', R).innerHTML = facts.map(([k, v]) => `<div><b>${n(v)}</b><span class="mono">${esc(k)}</span></div>`).join(''); 508 const ev = d.events || {}; 509 $('[data-r-events]', R).textContent = `${n(ev.count)} raw opt-in events kept` + (ev.oldest ? ` (oldest ${String(ev.oldest).slice(0, 10)}, newest ${String(ev.newest).slice(0, 10)})` : '') + `; retention ${n(ev.retention_days)} days. Every number here is from the database.`; 510 const dailyOpt = {}; (d.daily || []).forEach(r => { dailyOpt[r.day] = r; }); 511 const rows = [...new Set([...series.map(r => r.day), ...Object.keys(dailyOpt)])].sort().reverse(); 512 const mxAll = Math.max(...rows.map(k => n((byDay[k] || {}).external_agents)), 0); 513 const defs = { ext: 'distinct (ip_hash, client) NOT ours calling problem_search/preflight that day (from lookups)', searches: 'lookups kind=search that day (MCP problem_search + site search), ours included', hits: 'search + preflight lookups that returned at least one fix or breakage', preflights: 'lookups kind=preflight that day', known: 'opt-in preflight events answered known_breakage', optl: 'opt-in lookup events (hit + miss)', reports: 'opt-in report events (pass + fail)', fails: 'opt-in report events with check failed' }; 514 const dailyT = $('[data-r-daily]', R); $('[data-r-daily-empty]', R).hidden = !!rows.length; 515 dailyT.innerHTML = !rows.length ? '' : `<tr>${th('day', 'UTC day')}${th('external agents', defs.ext, 'n')}${th('searches', defs.searches, 'n')}${th('preflights', defs.preflights, 'n')}${th('hits', defs.hits, 'n')}${th('opt-in lookups', defs.optl, 'n')}${th('opt-in preflights', 'opt-in preflight events', 'n')}${th('known breakage', defs.known, 'n')}${th('reports', defs.reports, 'n')}${th('fails', defs.fails, 'n')}<th></th></tr>` + 516 rows.map(k => { const a = byDay[k] || {}, o = dailyOpt[k] || {}; return `<tr>${td(k)}${num(a.external_agents)}${num(a.searches)}${num(a.preflights)}${num(a.hits)}${num(o.lookups)}${num(o.preflights)}${num(o.hits, true)}${num(o.reports)}${num(o.fails, true)}${td(bar(n(a.external_agents), mxAll), 'bar')}</tr>`; }).join(''); 517 const bt = d.by_tool || []; $('[data-r-bytool-empty]', R).hidden = !!bt.length; 518 $('[data-r-bytool]', R).innerHTML = !bt.length ? '' : `<tr>${th('tool', 'normalized tool key')}
518${th('version', 'tool_version as sent, kept only if it parses as a version')}${th('platform', 'macos|linux|windows|web|any')}${th('lookups', 'opt-in lookup events', 'n')}${th('preflights', 'opt-in preflight events', 'n')}${th('hits', 'lookup hit + preflight known_breakage', 'n')}${th('misses', 'lookup miss + preflight clear', 'n')}${th('reports p/f', 'opt-in report events, passed / failed')}${th('external agents', 'distinct (ip_hash, client) NOT ours, summed over days', 'n')}${th('first seen', '')}${th('last seen', '')}</tr>` + 519 bt.map(r => `<tr>${td(r.tool || 'â')}${td(r.tool_version || 'â')}${td(r.platform || 'any')}${num(r.lookups)}${num(r.preflights)}${num(r.hits, true)}${num(r.misses)}${td(`${n(r.passes)}/${n(r.fails)}`)}${num(r.external_agents)}${td(r.first_seen)}${td(r.last_seen)}</tr>`).join(''); 520 // what is breaking: active breakages joined to by_tool preflight hits in the window, then last verified desc 521 const hitsFor = b => bt.filter(r => r.tool === b.tool).reduce((a, r) => a + n(r.preflights), 0); 522 const bk = bks.slice().sort((a, b) => hitsFor(b) - hitsFor(a) || String(b.last_verified_at || '').localeCompare(String(a.last_verified_at || ''))); 523 $('[data-r-breaking-empty]', R).hidden = !!bk.length; 524 $('[data-r-breaking]', R).innerHTML = !bk.length ? '' : `<tr>${th('tool', '')}${th('versions', '')}${th('platform', '')}${th('severity', '')}${th('fixed in', '')}${th('preflight hits', 'opt-in preflight events on the tool in the window', 'n')}${th('fixes', 'fixes linked to the breakage; how many are verified')}${th('', '')}</tr>` + 525 bk.map(b => { const fx = b.fixes || []; const nv = fx.filter(f => f.verification_status === 'verified').length; return `<tr>${td(b.tool_display || b.tool)}${td((b.affected_versions || {}).text || 'â')}${td((b.platforms || []).join(' ') || 'any')}${td(b.severity)}${td(b.fixed_in || 'â')}${num(hitsFor(b))}<td class="mono">${fx.length} (<span class="${nv ? 'pass' : ''}">${nv}</span> <span class="status-verified${nv ? '' : ' dim'}">verified</span>)</td><td>${b.problem && b.problem.url ? `<a class="link" href="${esc(b.problem.url)}">â</a>` : ''}</td></tr>`; }).join(''); 526 const health = [['fixes', s.fixes], ['fixes_verified', s.fixes_verified], ['fixes_unverified', s.fixes_unverified], ['fixes_expired', s.fixes_expired], ['fixes_pending', s.fixes_pending], ['problems_queued', s.problems_queued], ['reports_with_environment', s.reports_with_environment], ['checks', s.checks]]; 527 $('[data-r-health]', R).innerHTML = health.map(([k, v]) => `<div><b class="${k === 'fixes_verified' && n(v) ? 'pass' : ''}">${n(v)}</b><span class="mono">${esc(k)}</span></div>`).join(''); 528 const pend = mod ? ((mod.fixes || mod.items || mod.results || (Array.isArray(mod) ? mod : [])).length) : null; 529 $('[data-r-mod]', R).innerHTML = pend == null ? 'moderation queue not readable with this session' : `<span class="${pend ? 'notice' : ''}">${pend} pending</span> · ${n(s.fixes_pending)} fixes_pending`; 530 $('[data-r-allow]', R).innerHTML = `<tr>${th('ip_hash', 'left(sha256(salt || ip), 24), shown masked')}${th('label', '')}${th('added', '')}</tr>` + (d.allowlist || []).map(a => `<tr>${td(a.ip_hash)}${td(a.label)}${td(String(a.created_at || '').slice(0, 10))}</tr>`).join(''); 531 $('[data-r-ours]', R).textContent = 'Also ours by client name: ' + (d.ours_clients || []).join(', ') + '. Definition: radar_is_ours(ip_hash, client), migration 010.'; 532 } 533 $('[data-r-add]', R).addEventListener('submit', async (e) => { 534 e.preventDefault(); const ip = $('[data-r-add-ip]', R).value.trim(), label = $('[data-r-add-label]', R).value.trim(); if (!ip || !label) return; 535 const r = await J('/v1/admin/radar/allowlist', { method: 'POST', body: JSON.stringify({ ip, label }) }); 536 if (!r.ok) { err.textContent = r.status + ' adding to the allowlist (admin session cookie required)'; err.hidden = false; return; } 537 $('[data-r-add-ip]', R).value = ''; $('[data-r-add-label]', R).value = ''; load(); 538 }); 539 ['[data-r-days]', '[data-r-platform]', '[data-r-tool]'].forEach(sel => $(sel, R).addEventListener('change', load)); $('[data-r-reload]', R).addEventListener('click', load); 540 load(); 541 } 542 } 543})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.