From 9e387a0a098ae343d0abc4239cddcf6f030d5e23 Mon Sep 17 00:00:00 2001 From: jay Date: Thu, 11 Jun 2026 12:10:46 -0400 Subject: [PATCH] Boot-failure seatbelt: no future crash becomes a silent white screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/app.html | 58 ++++++++++++++++++++++++++ frontend/src/routes/+layout.svelte | 4 ++ frontend/src/routes/admin/+page.svelte | 8 ++++ goodnews/api.py | 1 + goodnews/queries.py | 5 +++ 5 files changed, 76 insertions(+) diff --git a/frontend/src/app.html b/frontend/src/app.html index b730922..df5580c 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -19,8 +19,66 @@ %sveltekit.head% + +
%sveltekit.body%
+ diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index abd592a..680ea4b 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,8 +1,12 @@ {@render children()} diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index d379fbc..d2c14ff 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -362,6 +362,11 @@
{stats.content.latest_brief_size}In today's brief
{healthy}/{sources.length}Sources healthy
{stats.accounts.total}Accounts
+ {#if stats.client_errors} +
0}> + {stats.client_errors.today}Load errors today +
+ {/if} {: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 { diff --git a/goodnews/api.py b/goodnews/api.py index 2e064e6..abaa4ea 100644 --- a/goodnews/api.py +++ b/goodnews/api.py @@ -444,6 +444,7 @@ _EVENT_KINDS = { "share_ub", "copy_source", "native_share", "not_today", "less_like_this", "hide_topic", "replace_used", "replace_none", "paywall_replace", "paywalled_source_open", + "client_error", # boot-failure seatbelt beacon (blank-screen risk signal) } diff --git a/goodnews/queries.py b/goodnews/queries.py index 9b2b765..0a3fefe 100644 --- a/goodnews/queries.py +++ b/goodnews/queries.py @@ -565,6 +565,11 @@ def admin_stats(conn: sqlite3.Connection, days: int = 30) -> dict: "top_topics": top_topics, "shares": shares, "daily": daily, + # Boot-failure seatbelt signal — blank-screen risk surfacing. + "client_errors": { + "today": scalar("SELECT COUNT(*) FROM events WHERE kind='client_error' AND day=date('now')"), + "window": kc.get("client_error", 0), + }, }