Observability + warming guardrails (Codex)

* client_error details, not just a count: new client_errors table + POST
  /api/client-error (reason/path/user-agent/time) + GET /api/admin/client-errors.
  The boot-seatbelt beacon now sends the reason + path (once per page); the admin
  Overview lists the recent errors so we can tell chunk vs SW vs API vs JS — the
  truth meter for the next day as the new SW propagates.
* Deploy warming now also hits the shell, routes (/play /account /admin), SW,
  version.json, word lists, and icons/logo/font — not just immutable chunks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-11 12:31:32 -04:00
parent 370d62270b
commit 61f575ba6d
7 changed files with 105 additions and 13 deletions
+25
View File
@@ -32,11 +32,14 @@
feedback = await getJSON('/api/admin/feedback');
candidates = await getJSON('/api/admin/candidates');
wpPool = await getJSON('/api/admin/word/pool');
clientErrors = await getJSON('/api/admin/client-errors');
} catch {
error = "Couldn't load stats.";
}
});
let clientErrors = $state([]);
// --- Games: Daily Word pool ---
let wpWord = $state('');
let wpResult = $state(null); // lookup result for the current input
@@ -369,6 +372,20 @@
{/if}
</div>
{#if clientErrors.length}
<h2>Recent load errors <span class="count">(last {clientErrors.length})</span></h2>
<ul class="cerrs">
{#each clientErrors as e (e.created_at + e.reason)}
<li>
<span class="ce-when">{fdate(e.created_at)}</span>
<span class="ce-reason">{e.reason || '—'}</span>
<span class="ce-path">{e.path || '/'}</span>
<span class="ce-ua">{e.user_agent}</span>
</li>
{/each}
</ul>
{/if}
{:else if section === 'content'}
<h2>Corpus</h2>
<div class="cards">
@@ -1009,6 +1026,14 @@
.stat.alert { background: #f3e0e0; }
.stat.alert .n { color: #9a3b3b; }
.cerrs { list-style: none; padding: 0; margin: 10px 0 0; display: flex; flex-direction: column; gap: 6px; }
.cerrs li { display: grid; grid-template-columns: auto 1fr auto; gap: 6px 12px; align-items: baseline;
font-size: 0.82rem; padding: 8px 12px; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
.ce-when { color: var(--muted); white-space: nowrap; }
.ce-reason { font-family: var(--label); color: #9a3b3b; }
.ce-path { color: var(--accent-deep); white-space: nowrap; }
.ce-ua { grid-column: 1 / -1; color: var(--muted); font-size: 0.72rem;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Games — Daily Word pool */
.wp-lookup { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 14px 0 6px; }