PageSourceSearch

https://avasis.ai/static/site.e6e9b0b10a.js

js avasis.ai collected 2026-10-05 14:39:21 UTC 50,757 bytes, 543 lines download raw bytes

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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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">&gt; 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=&amp;version=&amp;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">&gt; 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.