import { createApp } from 'vue' import { createPinia } from 'pinia' import './style.css' import App from './App.vue' import router from './router' import i18n from './i18n' import { displayVersion } from '@/utils/version' // Clipboard polyfill for HTTP (non-secure) contexts where navigator.clipboard is unavailable if (!navigator.clipboard) { Object.defineProperty(navigator, 'clipboard', { value: { async writeText(text: string) { const ta = document.createElement('textarea') ta.value = text ta.style.cssText = 'position:fixed;opacity:0' document.body.appendChild(ta) ta.select() document.execCommand('copy') document.body.removeChild(ta) }, async readText() { return '' }, }, }) } import { useToast } from '@/composables/useToast' const app = createApp(App) const pinia = createPinia() app.use(pinia) app.use(router) app.use(i18n) // Global version formatter — normalizes version labels to a single "v" prefix // (some sources already carry one, which produced "vv1.2.3"). Use `$ver(x)` in // templates instead of hard-coding a `v` prefix. app.config.globalProperties.$ver = displayVersion // 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 // 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' 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 */ } // 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) // 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')