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), + }, }