2026-01-24 22:59:20 +00:00
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css'
import App from './App.vue'
import router from './router'
2026-03-11 13:45:59 +00:00
import i18n from './i18n'
2026-06-17 19:21:42 -04:00
import { displayVersion } from '@/utils/version'
2026-04-02 18:20:52 +01:00
// 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 '' } ,
} ,
} )
}
2026-01-24 22:59:20 +00:00
const app = createApp ( App )
const pinia = createPinia ( )
app . use ( pinia )
app . use ( router )
2026-03-11 13:45:59 +00:00
app . use ( i18n )
2026-01-24 22:59:20 +00:00
2026-06-17 19:21:42 -04:00
// 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
2026-06-20 08:05:51 -04:00
// 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
2026-06-20 10:23:59 -04:00
// 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 )
}
2026-06-20 08:05:51 -04:00
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 ( )
2026-06-24 05:55:49 -04:00
// Log SILENTLY: a global handler error is almost always something we should
// fix at the source, not interrupt the user for. Keep the full record on the
// console + the window.__archyErrors ring buffer, and make the screenshot-able
// overlay available ON DEMAND (window.__archyShowErrors(), or the debug view)
// — but do NOT auto-pop a red toast / overlay over the UI. Components that
// need to tell the user about a *specific, actionable* failure still call
// toast.error() directly; this catch-all stays out of the way.
2026-06-20 08:05:51 -04:00
console . error ( ` [ ${ source } ] ` , err , info ? ? '' )
2026-06-24 05:55:49 -04:00
}
// Expose the on-demand error overlay + ring buffer so a crash that only repros
// in a runtime without a console (Android companion WebView) is still
// retrievable: call `window.__archyShowErrors()` to screenshot/Copy them.
; ( window as unknown as { __archyShowErrors ? : ( ) = > void } ) . __archyShowErrors = ( ) = > {
2026-06-20 10:23:59 -04:00
try { showErrorOverlay ( ) } catch { /* overlay is best-effort */ }
2026-03-21 01:11:05 +00:00
}
2026-06-20 08:05:51 -04:00
app . config . errorHandler = ( err , _instance , info ) = > recordError ( 'Vue Error' , err , info )
2026-06-20 18:58:52 -04:00
// After a frontend deploy the browser's cached index.html still points at the
// OLD hashed chunks (e.g. AppSession-Cq93o4ao.js), which 404 — Vite then throws
// "Failed to fetch dynamically imported module". The fix is to reload once so
// the browser pulls the fresh index + chunk map. Guard with sessionStorage so a
// genuinely-broken chunk can't trap us in a reload loop.
function isStaleChunkError ( err : unknown ) : boolean {
const msg = ( err as { message? : string } ) ? . message ? ? String ( err ? ? '' )
return /Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed|ChunkLoadError|dynamically imported module/i . test ( msg )
}
function reloadOnceForStaleChunk ( err : unknown ) : boolean {
if ( ! isStaleChunkError ( err ) ) return false
try {
const KEY = 'archy-chunk-reload-at'
const last = Number ( sessionStorage . getItem ( KEY ) || '0' )
// Only reload if we haven't already tried in the last 10s (loop guard).
if ( Date . now ( ) - last < 10 _000 ) return false
sessionStorage . setItem ( KEY , String ( Date . now ( ) ) )
} catch { /* sessionStorage unavailable — reload anyway, once is better than stuck */ }
// Cache-bust the navigation so the stale index isn't served again.
location . reload ( )
return true
}
2026-06-24 05:55:49 -04:00
// Known-benign environmental noise — expected on some deployments and not
// actionable by the user or us, so it shouldn't even occupy a ring-buffer slot
// (which would push out real errors). The PWA service worker can't register
// over a self-signed cert (it needs a trusted cert or localhost); on those
// nodes the SW/offline cache simply doesn't run, which is fine. Logged at debug
// only. (A trusted cert is the real fix — tracked separately, #56.)
function isBenignEnvironmentError ( err : unknown ) : boolean {
const msg = ( err as { message? : string } ) ? . message ? ? String ( err ? ? '' )
return /Failed to register a ServiceWorker|ServiceWorker.*(SSL|certificate|SecurityError)|An SSL certificate error occurred when fetching the script/i . test ( msg )
}
2026-06-20 08:05:51 -04:00
// 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.
2026-06-20 18:58:52 -04:00
window . addEventListener ( 'error' , ( ev ) = > {
if ( reloadOnceForStaleChunk ( ev . error ? ? ev . message ) ) return
2026-06-24 05:55:49 -04:00
if ( isBenignEnvironmentError ( ev . error ? ? ev . message ) ) { console . debug ( '[benign]' , ev . message ) ; return }
2026-06-20 18:58:52 -04:00
recordError ( 'window.error' , ev . error ? ? ev . message )
} )
window . addEventListener ( 'unhandledrejection' , ( ev ) = > {
if ( reloadOnceForStaleChunk ( ev . reason ) ) return
2026-06-24 05:55:49 -04:00
if ( isBenignEnvironmentError ( ev . reason ) ) { console . debug ( '[benign]' , ev . reason ) ; return }
2026-06-20 18:58:52 -04:00
recordError ( 'unhandledrejection' , ev . reason )
} )
2026-06-20 08:05:51 -04:00
2026-01-24 22:59:20 +00:00
app . mount ( '#app' )