Boot-failure seatbelt: no future crash becomes a silent white screen

Per Codex. A branded recovery card in app.html shows if the app hasn't mounted
in 7s, or on a pre-mount JS error/unhandledrejection — with a "Refresh Upbeat
Bytes" button. A chunk/preload failure (vite:preloadError) reloads once
(sessionStorage-guarded). +layout calls window.__ubBooted() on mount to clear
the card + timer. A pre-mount failure also fires a tiny anonymous client_error
beacon; the admin Overview now shows "Load errors today" (red if >0) so we can
see if blank-risk is happening in the wild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-11 12:10:46 -04:00
parent 254db67055
commit 9e387a0a09
5 changed files with 76 additions and 0 deletions
+4
View File
@@ -1,8 +1,12 @@
<script>
import '../app.css';
import { onMount } from 'svelte';
import FeedbackModal from '$lib/components/FeedbackModal.svelte';
import { fb, openFeedback, closeFeedback } from '$lib/feedback.svelte.js';
let { children } = $props();
// Tell the boot-failure seatbelt (app.html) the app mounted — clears the
// recovery card + timeout as soon as the shell hydrates.
onMount(() => window.__ubBooted?.());
</script>
{@render children()}
+8
View File
@@ -362,6 +362,11 @@
<div class="stat"><span class="n">{stats.content.latest_brief_size}</span><span class="l">In today's brief</span></div>
<div class="stat"><span class="n">{healthy}/{sources.length}</span><span class="l">Sources healthy</span></div>
<div class="stat"><span class="n">{stats.accounts.total}</span><span class="l">Accounts</span></div>
{#if stats.client_errors}
<div class="stat" class:alert={stats.client_errors.today > 0}>
<span class="n">{stats.client_errors.today}</span><span class="l">Load errors today</span>
</div>
{/if}
</div>
{:else if section === 'content'}
@@ -1002,6 +1007,9 @@
.factions .act:hover { color: var(--accent-deep); border-bottom-color: var(--accent); }
.factions .act.del:hover { color: #9a3b3b; border-bottom-color: #9a3b3b; }
.stat.alert { background: #f3e0e0; }
.stat.alert .n { color: #9a3b3b; }
/* Games — Daily Word pool */
.wp-lookup { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 14px 0 6px; }
.wp-lookup input {