feat(web): immediate summarize + status poll + ASCII tapir spinner

Clicking "Summarize" now runs the summary in the background (when a Processor is
wired) instead of only queuing it. The handler flips the DB flag, kicks off the
work on a detached context, and returns an animated "processing" card that polls
GET /v/{id}/status every 2s via HTMX. Status returns the summary card once it
lands (no poll → polling stops), the animation while in-flight, or the Queued
card otherwise. Queue-only behaviour is unchanged when no Processor is set.

The spinner is a CSS-only cross-fade of three ASCII tapir frames — no JS, honours
prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 19:48:52 +02:00
co-authored by Claude Opus 4.8
parent 404f74c55c
commit 25215cbcbd
5 changed files with 622 additions and 195 deletions
+33
View File
@@ -140,6 +140,39 @@ templ VideoCard(r store.SummaryRow) {
</li>
}
// TapirSpinner is the summarizing animation: three ASCII tapir frames stacked
// and cross-faded by CSS keyframes (no JS), with a muted "Summarizing…" label.
// The art is aria-hidden — it is decorative; the label carries the meaning.
templ TapirSpinner() {
<div class="tapir-spin" aria-hidden="true">
<pre>{ tapirFrame1 }</pre>
<pre>{ tapirFrame2 }</pre>
<pre>{ tapirFrame3 }</pre>
</div>
<p class="tapir-label" role="status" aria-live="polite"><em>Summarizing…</em></p>
}
// processingCard is the in-flight summarization card. It replaces the Summarize
// button card and polls /v/{id}/status every 2s, swapping itself (outerHTML, same
// id as VideoCard) for whatever state comes back: it keeps polling while still
// processing, and the summary/queued card it is eventually replaced by carries no
// poll, so polling stops on its own when the fragment changes.
templ processingCard(r store.SummaryRow) {
<li
class="card card-processing"
id={ "video-" + r.VideoID }
hx-get={ string(statusURL(r.VideoID)) }
hx-trigger="every 2s"
hx-swap="outerHTML"
>
<div class="card-title">{ displayTitle(r) }</div>
if cardMeta(r) != "" {
<div class="card-meta">{ cardMeta(r) }</div>
}
@TapirSpinner()
</li>
}
// DetailPage is the full summary view: text, highlights, takeaways, metadata,
// and the action button group.
templ DetailPage(r store.SummaryRow) {