From 9f53a58701292a29b1c49120e79374b433a4cf6a Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 23 May 2026 08:30:14 -0400 Subject: [PATCH] Harden boot path + surface errors instead of blanking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A friend on Chrome reported the page renders briefly then drops to the plain theme background (Brave/Chromium fine). Defensive changes so the next blank-page report comes with a visible cause: - main.ts installs Vue config.errorHandler, window.onerror, and window.unhandledrejection listeners. Any uncaught error pins a red banner to the top of the page with the error message, so the user sees what crashed instead of a blank background. - App.vue's onMounted now wraps each data fetch in a safeLoad helper. Promise.allSettled (vs allSettled used previously: was a plain Promise.all) means one failed file can't reject the whole batch. Errors log to the console and show as a non-fatal "Boot warning" toast in the share bar. This doesn't fix the root cause yet — needs Chrome devtools console output to pinpoint — but ensures the next person who hits it sees an actionable error. Co-Authored-By: Claude Opus 4.7 (1M context) --- character-builder/frontend/src/App.vue | 47 ++++++++++++++++++------ character-builder/frontend/src/main.ts | 51 +++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/character-builder/frontend/src/App.vue b/character-builder/frontend/src/App.vue index 1fc43f6..7cc1e5e 100644 --- a/character-builder/frontend/src/App.vue +++ b/character-builder/frontend/src/App.vue @@ -65,27 +65,46 @@ watch( { immediate: true }, ); +const bootError = ref(''); + +async function safeLoad(label: string, fn: () => Promise): Promise { + try { + return await fn(); + } catch (e: any) { + const msg = `[${label}] ${e?.message || e}`; + // eslint-disable-next-line no-console + console.error(msg, e); + if (!bootError.value) bootError.value = msg; + return null; + } +} + onMounted(async () => { - await Promise.all([ - loadCharacterXp().then((d) => (charXp.value = d)), + // Each load is isolated — a single failed fetch can't bring down the rest + // of the page (or leave us looking at a blank background). + await Promise.allSettled([ + safeLoad('character-xp', loadCharacterXp).then( + (d) => d && (charXp.value = d), + ), ...SPECS.map((s) => - loadSpec(s).then((d) => (specTables.value = { ...specTables.value, [s]: d })), + safeLoad(`spec-${s}`, () => loadSpec(s)).then( + (d) => d && (specTables.value = { ...specTables.value, [s]: d }), + ), ), - loadFaction('atreides').then( - (d) => (factionTables.value = { ...factionTables.value, atreides: d }), + safeLoad('faction-atreides', () => loadFaction('atreides')).then( + (d) => d && (factionTables.value = { ...factionTables.value, atreides: d }), ), - loadFaction('harkonnen').then( - (d) => (factionTables.value = { ...factionTables.value, harkonnen: d }), + safeLoad('faction-harkonnen', () => loadFaction('harkonnen')).then( + (d) => d && (factionTables.value = { ...factionTables.value, harkonnen: d }), ), ]); - // Load all skill trees in background so class switching is instant + for (const c of CLASSES) { - loadSkillTree(c.id).then( - (d) => (skillTrees.value = { ...skillTrees.value, [c.id]: d }), + safeLoad(`skills-${c.id}`, () => loadSkillTree(c.id)).then( + (d) => d && (skillTrees.value = { ...skillTrees.value, [c.id]: d }), ); } - // If URL has ?b=, load that build. const params = new URLSearchParams(window.location.search); const code = params.get('b'); if (code) { @@ -236,6 +255,12 @@ const specMeta: Record = {
{{ shareStatus }} + Boot warning: {{ bootError }}