From b3633ec52520f6f491d1f449966a9e127270bad2 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 20 Jun 2026 08:05:51 -0400 Subject: [PATCH] fix(ui): surface real error instead of generic toast + catch async errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The global Vue errorHandler swallowed every crash into "Something went wrong. Please refresh the page." — which hides exactly what we need to diagnose the companion-app (Android WebView) post-login crash. Now: - the toast shows the real (truncated) error message; - a 25-entry ring buffer is kept on window.__archyErrors for retrieval where there's no console (companion WebView via chrome://inspect, or a debug view); - window 'error' and 'unhandledrejection' listeners catch async/non-Vue errors that Vue's errorHandler misses (e.g. a JS API absent in an older WebView). Co-Authored-By: Claude Opus 4.8 (1M context) --- neode-ui/src/main.ts | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/neode-ui/src/main.ts b/neode-ui/src/main.ts index 1b3af2f8..0487a84f 100644 --- a/neode-ui/src/main.ts +++ b/neode-ui/src/main.ts @@ -37,10 +37,35 @@ app.use(i18n) // templates instead of hard-coding a `v` prefix. app.config.globalProperties.$ver = displayVersion -app.config.errorHandler = (err, _instance, info) => { - console.error('[Vue Error]', err, info) - const { error } = useToast() - error('Something went wrong. Please refresh the page.') +// Keep a small ring buffer of the most recent errors on `window.__archyErrors` +// so a crash that only reproduces inside a specific runtime (e.g. the Android +// companion WebView, where there's no easy console) can be retrieved after the +// fact — read it from chrome://inspect, or we can surface it in a debug view. +interface ArchyErrorEntry { when: string; source: string; message: string; info?: string; stack?: string } +const errorLog: ArchyErrorEntry[] = [] +;(window as unknown as { __archyErrors?: ArchyErrorEntry[] }).__archyErrors = errorLog + +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' + const entry: ArchyErrorEntry = { when: new Date().toISOString(), source, message, info, stack: e?.stack } + errorLog.push(entry) + if (errorLog.length > 25) errorLog.shift() + console.error(`[${source}]`, err, info ?? '') + // Surface the real message (truncated) instead of a generic toast — this is a + // test/bug-bash build, and "Something went wrong" hides exactly what we need. + const short = message.length > 140 ? `${message.slice(0, 140)}…` : message + try { + useToast().error(`Something went wrong: ${short}`) + } catch { /* toast itself failed — the console + ring buffer still have it */ } } +app.config.errorHandler = (err, _instance, info) => recordError('Vue Error', err, info) + +// Vue's errorHandler only catches errors raised synchronously inside Vue's +// lifecycle/reactivity. Async rejections and plain runtime errors (e.g. a JS +// API missing in an older WebView) slip past it, so catch those too. +window.addEventListener('error', (ev) => recordError('window.error', ev.error ?? ev.message)) +window.addEventListener('unhandledrejection', (ev) => recordError('unhandledrejection', ev.reason)) + app.mount('#app')