From 242baf5deb2d5c8baabab99bc6deef8fe3d60596 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 20 Jun 2026 10:23:59 -0400 Subject: [PATCH] fix(ui): on-screen error overlay so companion crashes are visible without a console chrome://inspect isn't always reachable on the Android companion WebView, so the real error stayed invisible. Add a plain-DOM, screenshot-able overlay (built without Vue so it survives a crash in Vue itself) that shows the captured error message + stack and a Copy button for the full window.__archyErrors buffer. Co-Authored-By: Claude Opus 4.8 (1M context) --- neode-ui/src/main.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/neode-ui/src/main.ts b/neode-ui/src/main.ts index 0487a84f..50c00d43 100644 --- a/neode-ui/src/main.ts +++ b/neode-ui/src/main.ts @@ -45,6 +45,52 @@ interface ArchyErrorEntry { when: string; source: string; message: string; info? const errorLog: ArchyErrorEntry[] = [] ;(window as unknown as { __archyErrors?: ArchyErrorEntry[] }).__archyErrors = errorLog +// On-screen, screenshot-able error overlay. Built with plain DOM (not a Vue +// component) so it still appears when the crash is in Vue itself or in a runtime +// with no dev console (the Android companion WebView — chrome://inspect isn't +// always reachable). The user can read/screenshot the real error and the stack +// straight off the device, and "Copy" puts the full ring buffer on the clipboard. +function showErrorOverlay() { + const id = 'archy-error-overlay' + let box = document.getElementById(id) + if (!box) { + box = document.createElement('div') + box.id = id + box.style.cssText = [ + 'position:fixed', 'left:8px', 'right:8px', 'bottom:8px', 'z-index:2147483647', + 'max-height:45vh', 'overflow:auto', 'background:rgba(20,0,0,0.92)', 'color:#ffd7d7', + 'font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace', 'padding:10px 12px', + 'border:1px solid #ff5a5a', 'border-radius:10px', 'white-space:pre-wrap', + 'word-break:break-word', '-webkit-user-select:text', 'user-select:text', + ].join(';') + document.body.appendChild(box) + } + const latest = errorLog[errorLog.length - 1] + const dump = errorLog + .map((e) => `[${e.source}] ${e.message}${e.info ? ` (${e.info})` : ''}${e.stack ? `\n${e.stack.split('\n').slice(0, 4).join('\n')}` : ''}`) + .join('\n──\n') + box.innerHTML = '' + const bar = document.createElement('div') + bar.style.cssText = 'display:flex;gap:8px;justify-content:space-between;align-items:center;margin-bottom:6px' + const title = document.createElement('b') + title.textContent = `⚠ App error (${errorLog.length}) — screenshot or Copy` + title.style.color = '#ff8a8a' + const btns = document.createElement('div') + const mkBtn = (label: string, fn: () => void) => { + const b = document.createElement('button') + b.textContent = label + b.style.cssText = 'margin-left:6px;background:#ff5a5a;color:#1a0000;border:0;border-radius:6px;padding:3px 9px;font-weight:700' + b.onclick = fn + return b + } + btns.appendChild(mkBtn('Copy', () => { navigator.clipboard?.writeText(JSON.stringify(errorLog, null, 2)) })) + btns.appendChild(mkBtn('Dismiss', () => { box?.remove() })) + bar.appendChild(title); bar.appendChild(btns) + const body = document.createElement('div') + body.textContent = latest ? dump : 'no error captured' + box.appendChild(bar); box.appendChild(body) +} + function recordError(source: string, err: unknown, info?: string) { const e = err as { message?: string; stack?: string } | undefined const message = (e?.message ?? String(err)) || 'unknown error' @@ -58,6 +104,8 @@ function recordError(source: string, err: unknown, info?: string) { try { useToast().error(`Something went wrong: ${short}`) } catch { /* toast itself failed — the console + ring buffer still have it */ } + // Always show the on-device overlay so the error is visible without a console. + try { showErrorOverlay() } catch { /* overlay is best-effort */ } } app.config.errorHandler = (err, _instance, info) => recordError('Vue Error', err, info)