An event_type with no template was a 400 at ingest. That reads like caution and behaves like deletion: gogobee retries a 400 to its cap and then parks the row forever, so rejecting a type Pete hadn't learned to phrase didn't defer the event, it destroyed it. companion_hire went that way. It has been emitted from `!expedition hire` since the combat-engine work landed and has never once reached the site — the game logged a successful emit every time, and the queue row simply never sent. The mitigation on the books was "always deploy Pete first", which is a thing a person has to remember rather than a property of the system. So invert it. An unknown type now warns, gets counted, and publishes on a neutral fallback. 400 is kept for facts that are actually invalid: no guid, or a name that failed the fact-guard. gogobee can ship a new event type any day of the week now; the worst case is a thin card until Pete learns the words. It is thinner than it sounds in practice. gogobee authors dispatch prose from the fact's fields with no per-type switch, so an unrecognised type still arrives with a real headline and lede and is allowed to use them. The fallback only shows through when the model is off or the prose-guard refused the output. Untemplated types never post live to Matrix, whatever tier they claim. A thin card among cards is cheap and reversible; pinging everyone in the room with a dispatch Pete couldn't phrase is neither. The daily digest still carries it, one line among many, which is the right volume for something we don't understand yet. And give companion_hire its template. Pete is the one being hired, so it is first-person like his duels — third-person Pete filling in as a cleric reads as somebody else reporting on him. The admin status page grows a "dispatches with no template" panel, so the next one of these is a to-do list Pete can see rather than an archaeology dig. Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
112 lines
6.3 KiB
HTML
112 lines
6.3 KiB
HTML
{{define "title"}}Source health — {{.SiteTitle}}{{end}}
|
||
|
||
{{define "main"}}
|
||
{{$admin := .Admin}}
|
||
<section class="mt-2 mb-8">
|
||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 sm:p-8 shadow-pete">
|
||
<p class="text-sm uppercase tracking-[0.2em] text-[color:var(--ink)]/50">{{if $admin}}owner view{{else}}live status{{end}}</p>
|
||
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-1">Source health</h1>
|
||
<p class="mt-2 max-w-2xl text-[color:var(--ink)]/70">
|
||
{{if $admin}}Per-feed poll status and content stats. Rows that are currently failing float to the top.
|
||
{{else}}Every feed Pete follows and whether it's still updating. This runs on a best-effort basis, so a source going quiet for a bit is normal.{{end}}
|
||
</p>
|
||
<div class="mt-4 flex flex-wrap gap-2">
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-[color:var(--ink)]/5 px-3 py-1 text-sm font-semibold tabular-nums">
|
||
{{len .Sources}} sources
|
||
</span>
|
||
{{if .DegradedCnt}}
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-500/15 text-red-700 dark:text-red-300 px-3 py-1 text-sm font-semibold tabular-nums">
|
||
⚠ {{.DegradedCnt}} {{if $admin}}degraded{{else}}delayed{{end}}
|
||
</span>
|
||
{{else}}
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 px-3 py-1 text-sm font-semibold">
|
||
✓ all updating
|
||
</span>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{{/* Adventure dispatch types gogobee sent that Pete has no template for. These
|
||
published on the neutral fallback — nothing was dropped — so this is a
|
||
vocabulary to-do, not an incident. Admin-only; absent when there are none. */}}
|
||
{{if and $admin .UnknownAdv}}
|
||
<section class="mb-8">
|
||
<div class="rounded-3xl bg-amber-500/10 border-2 border-amber-500/30 p-6">
|
||
<h2 class="font-display text-xl font-bold">Dispatches with no template</h2>
|
||
<p class="mt-1 text-sm text-[color:var(--ink)]/70 max-w-2xl">
|
||
gogobee sent these event types since the last restart and Pete published them on the
|
||
generic fallback. Nothing was lost — they just read thin until he learns the words.
|
||
</p>
|
||
<ul class="mt-3 flex flex-wrap gap-2">
|
||
{{range .UnknownAdv}}
|
||
<li class="inline-flex items-center gap-2 rounded-full bg-[color:var(--card)] border border-amber-500/40 px-3 py-1 text-sm">
|
||
<code class="font-mono">{{.EventType}}</code>
|
||
<span class="tabular-nums text-[color:var(--ink)]/60">×{{.Count}}</span>
|
||
</li>
|
||
{{end}}
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
{{end}}
|
||
|
||
<div class="overflow-x-auto rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete">
|
||
<table class="w-full {{if $admin}}min-w-[52rem]{{else}}min-w-[28rem]{{end}} text-sm">
|
||
<thead>
|
||
<tr class="text-left text-[color:var(--ink)]/50 uppercase text-xs tracking-wider border-b-2 border-[color:var(--ink)]/10">
|
||
<th class="px-4 py-3 font-semibold">Source</th>
|
||
<th class="px-4 py-3 font-semibold">Channel</th>
|
||
<th class="px-4 py-3 font-semibold">Status</th>
|
||
<th class="px-4 py-3 font-semibold">Last update</th>
|
||
{{if $admin}}
|
||
<th class="px-4 py-3 font-semibold">Last poll</th>
|
||
<th class="px-4 py-3 font-semibold text-right">Items</th>
|
||
<th class="px-4 py-3 font-semibold text-right">Stories</th>
|
||
<th class="px-4 py-3 font-semibold text-right">Paywall</th>
|
||
<th class="px-4 py-3 font-semibold">Last story</th>
|
||
<th class="px-4 py-3 font-semibold">Last posted</th>
|
||
{{end}}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{{range .Sources}}
|
||
<tr class="border-b border-[color:var(--ink)]/5 last:border-0 align-top">
|
||
<td class="px-4 py-3 font-semibold whitespace-nowrap">{{.Name}}</td>
|
||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{.Channel}}</td>
|
||
<td class="px-4 py-3 whitespace-nowrap">
|
||
{{if .NeverRun}}
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-[color:var(--ink)]/10 px-2.5 py-1 text-xs font-semibold">◌ idle</span>
|
||
{{else if .Healthy}}
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 px-2.5 py-1 text-xs font-semibold">● live</span>
|
||
{{else if $admin}}
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-500/15 text-red-700 dark:text-red-300 px-2.5 py-1 text-xs font-semibold"
|
||
title="{{.LastError}}">✕ {{.Failures}} fail{{if ne .Failures 1}}s{{end}}</span>
|
||
{{else}}
|
||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-500/15 text-red-700 dark:text-red-300 px-2.5 py-1 text-xs font-semibold">✕ delayed</span>
|
||
{{end}}
|
||
</td>
|
||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{if .LastSuccessAt.IsZero}}never{{else}}{{timeAgo .LastSuccessAt}}{{end}}</td>
|
||
{{if $admin}}
|
||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{if .LastPollAt.IsZero}}never{{else}}{{timeAgo .LastPollAt}}{{end}}</td>
|
||
<td class="px-4 py-3 text-right tabular-nums">{{.LastItemCount}}</td>
|
||
<td class="px-4 py-3 text-right tabular-nums" title="{{.Classified}} classified of {{.Total}}">{{.Total}}</td>
|
||
<td class="px-4 py-3 text-right tabular-nums {{if gt .PaywallRate 50}}text-amber-600 dark:text-amber-400 font-semibold{{end}}">
|
||
{{if .Total}}{{.PaywallRate}}%{{else}}—{{end}}
|
||
</td>
|
||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{if .LastSeenAt.IsZero}}—{{else}}{{timeAgo .LastSeenAt}}{{end}}</td>
|
||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{if .LastPostedAt.IsZero}}—{{else}}{{timeAgo .LastPostedAt}}{{end}}</td>
|
||
{{end}}
|
||
</tr>
|
||
{{if $admin}}{{if .LastError}}{{if not .Healthy}}
|
||
<tr class="border-b border-[color:var(--ink)]/5">
|
||
<td colspan="10" class="px-4 pb-3 -mt-1 text-xs text-red-600 dark:text-red-400 font-mono break-all">{{.LastError}}</td>
|
||
</tr>
|
||
{{end}}{{end}}{{end}}
|
||
{{else}}
|
||
<tr><td colspan="{{if $admin}}10{{else}}4{{end}}" class="px-4 py-8 text-center text-[color:var(--ink)]/50">No sources configured.</td></tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{{end}}
|