diff --git a/DECISIONS.md b/DECISIONS.md index 0b1a50d..9b19462 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -923,6 +923,41 @@ summaries. --- +## ADR-025 — Honest, state-aware foreground summarization status + +**Status:** Accepted (2026-06-10). **Pillar B of the manual-mode UX work** (Pillar A, foreground +fetch priority, is a separate follow-up). Builds on ADR-014 (the rate limit the UX must make +legible). + +**Context.** Clicking "Summarize" spawned a background goroutine and polled `/status`, which +returned only two states: the spinner (in-flight) or the normal card (done). But the web +`ProcessVideo` only recorded an outcome on *success* — a 429'd or caption-less click left +`transcript_status` unset, so the next poll silently reverted to the "Summarize" button. The +user saw either an endless spinner or a button that did nothing useful when clicked again. The +binding constraint (YouTube's caption rate limit) was completely invisible. + +**Decision.** +1. **Record every outcome on the web path**, mirroring the runner: `ProcessVideo` stamps + `rate_limited` / `none` / `fetched`. A rate-limited video keeps its requested flag so the + background sweep retries it; `none` and `fetched` are terminal. +2. **`/status` is state-aware**: summarized → summary card; in-flight → working spinner; + `rate_limited` → a calm "waiting, will retry" card that keeps polling (every 30s) so the + summary appears on its own when the retry lands — the user never clicks again; + `none` → a terminal "no captions" card with no poll and no dead-end button. +3. **Charm status text** (Claude-Code / Crush inspired): the working spinner cycles playful, + tapir-themed gerunds ("Chewing the cud…", "Munching leaves…", "Distilling the gist…") via + CSS only — no JS, keeping the HTMX/no-JS ethos. Decorative (aria-hidden) with a stable + `role=status` line for assistive tech. + +**Principle.** When the system cannot be fast (throttled IP), it is at least honest, and it +self-resolves without making the user retry. Honesty is the load-bearing half — Pillar A's +priority lane only improves the odds of a fast slot; it cannot beat an already-hot IP. + +**Reversibility.** Pure transport-layer + view change over the unchanged engine/ports. No +schema change (reuses `transcript_status` from migration 007). + +--- + ## Rejected alternatives Approaches considered during the 2026-06-02 planning + grill session and **deliberately not diff --git a/cmd/tapir/processor.go b/cmd/tapir/processor.go index 4e222fe..a61359f 100644 --- a/cmd/tapir/processor.go +++ b/cmd/tapir/processor.go @@ -132,7 +132,28 @@ func (p *engineProcessor) ProcessVideo(ctx context.Context, userID, videoID stri if err != nil { return fmt.Errorf("process video %q: %w", videoID, err) } - if res.Summary != nil { + + // Record the outcome so the status endpoint can show honest state (ADR-025): + // a 429'd or caption-less click used to leave transcript_status unset, so the + // poll silently reverted to the "Summarize" button. Mirror the runner: stamp + // rate_limited / none / fetched. A rate-limited video keeps its requested flag + // so the background sweep retries it; none and fetched are terminal here. + switch { + case res.Skipped && res.TranscriptSource == string(domain.SourceRateLimited): + if err := p.store.SetTranscriptStatus(ctx, userID, videoID, "rate_limited"); err != nil { + return fmt.Errorf("set rate_limited status %q: %w", videoID, err) + } + case res.Skipped: + if err := p.store.SetTranscriptStatus(ctx, userID, videoID, "none"); err != nil { + return fmt.Errorf("set none status %q: %w", videoID, err) + } + if err := p.store.ClearSummarizeRequested(ctx, userID, videoID); err != nil { + return fmt.Errorf("clear summarize flag %q: %w", videoID, err) + } + case res.Summary != nil: + if err := p.store.SetTranscriptStatus(ctx, userID, videoID, "fetched"); err != nil { + return fmt.Errorf("set fetched status %q: %w", videoID, err) + } if err := p.store.ClearSummarizeRequested(ctx, userID, videoID); err != nil { return fmt.Errorf("clear summarize flag %q: %w", videoID, err) } diff --git a/internal/web/handlers.go b/internal/web/handlers.go index 29ec5b1..48041e6 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -495,11 +495,22 @@ func (a *App) handleStatus(w http.ResponseWriter, r *http.Request) { return } - if row.Summarized || !a.Processing.Has(processingKey(userID, videoID)) { + // Honest, state-aware status (ADR-025). Order matters: a finished summary wins; + // an in-flight goroutine shows the working spinner; a recorded rate-limit shows + // the calm "waiting, will retry" card that keeps polling; a recorded "none" is + // terminal; anything else falls back to the normal card. + switch { + case row.Summarized: + a.render(w, r, VideoCard(*row)) + case a.Processing.Has(processingKey(userID, videoID)): + a.render(w, r, processingCard(*row)) + case row.TranscriptStatus == "rate_limited": + a.render(w, r, waitingCard(*row)) + case row.TranscriptStatus == "none": + a.render(w, r, noCaptionsCard(*row)) + default: a.render(w, r, VideoCard(*row)) - return } - a.render(w, r, processingCard(*row)) } // handleSummarizeMode toggles the user's auto/manual summarization mode. The form diff --git a/internal/web/processing_handler_test.go b/internal/web/processing_handler_test.go index 456b3ff..601bdf1 100644 --- a/internal/web/processing_handler_test.go +++ b/internal/web/processing_handler_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" "gitea.d-ma.be/mathias/tapir/internal/web" @@ -129,3 +130,42 @@ func getStatus(t *testing.T, app *web.App, videoID string) *httptest.ResponseRec app.Router().ServeHTTP(rec, req) return rec } + +// setTranscriptStatus stamps videos.transcript_status directly (bypassing RLS via +// the super pool) so a test can drive the status endpoint into a given state. +func setTranscriptStatus(t *testing.T, p *pgxpool.Pool, videoID, status string) { + t.Helper() + _, err := p.Exec(context.Background(), + `UPDATE videos SET transcript_status = $2 WHERE id = $1`, videoID, status) + require.NoError(t, err) +} + +// TestStatusRateLimitedShowsWaitingCard: a click that hit YouTube's rate limit +// must surface the honest "waiting, will retry" card that keeps polling — not a +// silent revert to the Summarize button. +func TestStatusRateLimitedShowsWaitingCard(t *testing.T) { + app := newApp(t) + p := rawPool(t) + resetDB(t, p) + seedVideo(t, p, videoX, "Throttled Title", "https://x", time.Time{}) + setTranscriptStatus(t, p, videoX, "rate_limited") + + html := body(t, getStatus(t, app, videoX)) + require.Contains(t, html, "Waiting on YouTube rate limits", "honest rate-limit copy") + require.Contains(t, html, `hx-trigger="every 30s"`, "waiting card keeps polling so it self-resolves") + require.NotContains(t, html, "Summarize this video", "must not revert to the Summarize button") +} + +// TestStatusNoCaptionsTerminal: a no-captions outcome is terminal — an honest +// message, no poll, no button to click back into the same dead end. +func TestStatusNoCaptionsTerminal(t *testing.T) { + app := newApp(t) + p := rawPool(t) + resetDB(t, p) + seedVideo(t, p, videoX, "Silent Title", "https://x", time.Time{}) + setTranscriptStatus(t, p, videoX, "none") + + html := body(t, getStatus(t, app, videoX)) + require.Contains(t, html, "No captions available", "honest terminal copy") + require.NotContains(t, html, "hx-trigger", "terminal card must stop polling") +} diff --git a/internal/web/view.go b/internal/web/view.go index d415ebd..e053c46 100644 --- a/internal/web/view.go +++ b/internal/web/view.go @@ -658,11 +658,30 @@ a.btn, a.btn:visited { color: var(--accent-fg); } .tapir-bar-fill { animation: tapir-fill 8s linear infinite; text-shadow: 0 0 6px rgba(14, 249, 182, .7); } @keyframes tapir-fill { 0% { clip-path: inset(0 100% 0 0); } 100% { clip-path: inset(0 0 0 0); } } .tapir-label { color: var(--muted); font-size: .9rem; margin: 0; } +/* Cycling status verbs (Claude-Code / Crush style): five gerunds stacked, each + visible 1/5 of a 6s loop, cross-faded. The container reserves one line height + so the layout does not jump as verbs swap. */ +.tapir-verbs { position: relative; height: 1.3em; margin: .2em 0 0; color: var(--muted); font-size: .9rem; } +.tapir-verbs span { position: absolute; left: 0; top: 0; white-space: nowrap; opacity: 0; animation: tapir-verb 6s steps(1, end) infinite; } +.tapir-verbs .tv1 { animation-delay: 0s; } +.tapir-verbs .tv2 { animation-delay: 1.2s; } +.tapir-verbs .tv3 { animation-delay: 2.4s; } +.tapir-verbs .tv4 { animation-delay: 3.6s; } +.tapir-verbs .tv5 { animation-delay: 4.8s; } +@keyframes tapir-verb { 0%, 19.99% { opacity: 1; } 20%, 100% { opacity: 0; } } +/* Resting tapir for the rate-limit waiting state: the panel, one still frame, no + animation — calm, not busy, signalling "parked, not stuck". */ +.tapir-resting pre { position: relative; opacity: 1; animation: none; } +.card-waiting { border-style: dashed; opacity: .92; } +.card-no-captions .card-state { font-style: italic; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } @media (prefers-reduced-motion: reduce) { .tapir-charm pre { animation: none; } .tapir-charm .tapir-f2, .tapir-charm .tapir-f3 { display: none; } .tapir-charm .tapir-f1 { opacity: 1; } .tapir-bar-fill { animation: none; clip-path: inset(0 35% 0 0); } + .tapir-verbs span { animation: none; } + .tapir-verbs .tv1 { opacity: 1; } } /* summarization mode toggle on the account page */ diff --git a/internal/web/views.templ b/internal/web/views.templ index e8d9631..4d82cd6 100644 --- a/internal/web/views.templ +++ b/internal/web/views.templ @@ -339,7 +339,17 @@ templ TapirSpinner() {
@templ.Raw(tapirFrameHTML3)
{ tapirBarFill }
-

Summarizing…

+ // Claude-Code / Crush-style status: playful gerunds cycle in place (CSS only, + // no JS). Decorative — aria-hidden — with one stable status line below for + // assistive tech. + +

Summarizing…

} // processingCard is the in-flight summarization card. It replaces the Summarize @@ -363,6 +373,43 @@ templ processingCard(r store.SummaryRow) { } +// waitingCard is the honest rate-limited state: the click landed but YouTube is +// throttling the caption fetch, so the tapir rests and the card keeps polling +// (gently, every 30s) until the background retry lands the summary — the user +// never has to click again. Replaces the old silent revert to a Summarize button. +templ waitingCard(r store.SummaryRow) { +
  • +
    { displayTitle(r) }
    + if cardMeta(r) != "" { +
    { cardMeta(r) }
    + } + +

    + Waiting on YouTube rate limits. Tapir keeps trying, slowly and politely, and the summary will appear here on its own. +

    +
  • +} + +// noCaptionsCard is the terminal no-captions state: nothing to summarize, so the +// card stops (no poll, no button to click again into the same dead end). +templ noCaptionsCard(r store.SummaryRow) { +
  • +
    { displayTitle(r) }
    + if cardMeta(r) != "" { +
    { cardMeta(r) }
    + } +

    No captions available, so Tapir cannot summarize this one.

    +
  • +} + // DetailPage is the full summary view: text, highlights, takeaways, metadata, // and the action button group. templ DetailPage(r store.SummaryRow) { diff --git a/internal/web/views_templ.go b/internal/web/views_templ.go index b6ddd3e..54d1027 100644 --- a/internal/web/views_templ.go +++ b/internal/web/views_templ.go @@ -1161,7 +1161,7 @@ func TapirSpinner() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "

    Summarizing…

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "

    Fetching captions… Chewing the cud… Munching leaves… Distilling the gist… Summarizing…

    Summarizing…

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1202,7 +1202,7 @@ func processingCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var46 string templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue("video-" + r.VideoID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 353, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 363, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46) if templ_7745c5c3_Err != nil { @@ -1215,7 +1215,7 @@ func processingCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var47 string templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(string(statusURL(r.VideoID))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 354, Col: 39} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 364, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47) if templ_7745c5c3_Err != nil { @@ -1228,7 +1228,7 @@ func processingCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var48 string templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 358, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 368, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { @@ -1246,7 +1246,7 @@ func processingCard(r store.SummaryRow) templ.Component { var templ_7745c5c3_Var49 string templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 360, Col: 39} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 370, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { @@ -1269,6 +1269,189 @@ func processingCard(r store.SummaryRow) templ.Component { }) } +// waitingCard is the honest rate-limited state: the click landed but YouTube is +// throttling the caption fetch, so the tapir rests and the card keeps polling +// (gently, every 30s) until the background retry lands the summary — the user +// never has to click again. Replaces the old silent revert to a Summarize button. +func waitingCard(r store.SummaryRow) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var50 := templ.GetChildren(ctx) + if templ_7745c5c3_Var50 == nil { + templ_7745c5c3_Var50 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var53 string + templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 388, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if cardMeta(r) != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var54 string + templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 390, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "
    ")
    +		if templ_7745c5c3_Err != nil {
    +			return templ_7745c5c3_Err
    +		}
    +		templ_7745c5c3_Err = templ.Raw(tapirFrameHTML2).Render(ctx, templ_7745c5c3_Buffer)
    +		if templ_7745c5c3_Err != nil {
    +			return templ_7745c5c3_Err
    +		}
    +		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "

    Waiting on YouTube rate limits. Tapir keeps trying, slowly and politely, and the summary will appear here on its own.

  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// noCaptionsCard is the terminal no-captions state: nothing to summarize, so the +// card stops (no poll, no button to click again into the same dead end). +func noCaptionsCard(r store.SummaryRow) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var55 := templ.GetChildren(ctx) + if templ_7745c5c3_Var55 == nil { + templ_7745c5c3_Var55 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var57 string + templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 405, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if cardMeta(r) != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var58 string + templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(cardMeta(r)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 407, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "

    No captions available, so Tapir cannot summarize this one.

  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + // DetailPage is the full summary view: text, highlights, takeaways, metadata, // and the action button group. func DetailPage(r store.SummaryRow) templ.Component { @@ -1287,12 +1470,12 @@ func DetailPage(r store.SummaryRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var50 := templ.GetChildren(ctx) - if templ_7745c5c3_Var50 == nil { - templ_7745c5c3_Var50 = templ.NopComponent + templ_7745c5c3_Var59 := templ.GetChildren(ctx) + if templ_7745c5c3_Var59 == nil { + templ_7745c5c3_Var59 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var51 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var60 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -1304,99 +1487,99 @@ func DetailPage(r store.SummaryRow) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "

    ← Summaries

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "

    ← Summaries

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var52 string - templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) + var templ_7745c5c3_Var61 string + templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(displayTitle(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 372, Col: 24} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 419, Col: 24} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if detailMeta(r) != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var53 string - templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(detailMeta(r)) + var templ_7745c5c3_Var62 string + templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(detailMeta(r)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 375, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 422, Col: 26} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.FallbackUsed { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "fallback") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "fallback") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if url, ok := embedURL(r.ProviderVideoID); ok { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "\" loading=\"lazy\" referrerpolicy=\"strict-origin-when-cross-origin\" allow=\"accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.URL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "

    watch on source ↗

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "\" rel=\"noopener noreferrer\">watch on source ↗

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1406,83 +1589,83 @@ func DetailPage(r store.SummaryRow) templ.Component { return templ_7745c5c3_Err } if len(r.Takeaways) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "

    Takeaways

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(r.Highlights) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "

    Highlights

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "

    Summary

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "

    Summary

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var59 string - templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary) + var templ_7745c5c3_Var68 string + templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(r.Summary) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 423, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 470, Col: 31} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — "+displayTitle(r)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var51), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — "+displayTitle(r)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var60), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1509,12 +1692,12 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var60 := templ.GetChildren(ctx) - if templ_7745c5c3_Var60 == nil { - templ_7745c5c3_Var60 = templ.NopComponent + templ_7745c5c3_Var69 := templ.GetChildren(ctx) + if templ_7745c5c3_Var69 == nil { + templ_7745c5c3_Var69 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var61 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var70 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -1526,59 +1709,59 @@ func RegisterPage(email, errMsg string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "

    Complete your registration

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "

    Complete your registration

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if email != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "

    Signed in as ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "

    Signed in as ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var62 string - templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(email) + var templ_7745c5c3_Var71 string + templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 437, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 484, Col: 40} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, ".

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, ".

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "

    Choose a display name to finish setting up your Tapir account.

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "

    Choose a display name to finish setting up your Tapir account.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if errMsg != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var63 string - templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) + var templ_7745c5c3_Var72 string + templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 441, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 488, Col: 42} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var61), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var70), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1606,12 +1789,12 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var64 := templ.GetChildren(ctx) - if templ_7745c5c3_Var64 == nil { - templ_7745c5c3_Var64 = templ.NopComponent + templ_7745c5c3_Var73 := templ.GetChildren(ctx) + if templ_7745c5c3_Var73 == nil { + templ_7745c5c3_Var73 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var65 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var74 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -1627,43 +1810,43 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "

    Account

    Display name
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "

    Account

    Display name
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var66 string - templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(displayNameOr(displayName)) + var templ_7745c5c3_Var75 string + templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(displayNameOr(displayName)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 465, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 512, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if email != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "
    Signed in as
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "
    Signed in as
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var67 string - templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(email) + var templ_7745c5c3_Var76 string + templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.JoinStringErrs(email) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 468, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 515, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var76)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "

    Summarization

    Automatic summarizes new videos from about the last week as they are discovered. Older videos stay browsable — summarize them on demand. Manual lets you pick which videos to summarize — every new video appears in your list with a Summarize button.

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, "

    Summarization

    Automatic summarizes new videos from about the last week as they are discovered. Older videos stay browsable — summarize them on demand. Manual lets you pick which videos to summarize — every new video appears in your list with a Summarize button.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1671,178 +1854,178 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(channelErrors) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "

    Unavailable channels

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "

    Unavailable channels

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var68 string - templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d channel(s) returned errors on the last discovery pass.", len(channelErrors))) + var templ_7745c5c3_Var77 string + templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d channel(s) returned errors on the last discovery pass.", len(channelErrors))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 485, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 532, Col: 100} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, " These may have been deleted or made private on YouTube.

      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, " These may have been deleted or made private on YouTube.

        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, ce := range channelErrors { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var69 string - templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(ce.ChannelName) + var templ_7745c5c3_Var78 string + templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.JoinStringErrs(ce.ChannelName) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 491, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 538, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var78)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, " unavailable since ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, " unavailable since ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var70 string - templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(ce.FirstSeen.Format("2006-01-02")) + var templ_7745c5c3_Var79 string + templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinStringErrs(ce.FirstSeen.Format("2006-01-02")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 493, Col: 89} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 540, Col: 89} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "

    Connected accounts

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "

    Connected accounts

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(conns) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "

    No connected video accounts yet.

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "

    No connected video accounts yet.

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "
        ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, c := range conns { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "
      • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var71 string - templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(providerLabel(c.Provider)) + var templ_7745c5c3_Var80 string + templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.JoinStringErrs(providerLabel(c.Provider)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 508, Col: 64} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 555, Col: 64} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var80)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if c.ProviderAccount != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var72 string - templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.ProviderAccount) + var templ_7745c5c3_Var81 string + templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.JoinStringErrs(c.ProviderAccount) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 510, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 557, Col: 49} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var81)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var73 string - templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinStringErrs(c.Status) + var templ_7745c5c3_Var82 string + templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.JoinStringErrs(c.Status) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 512, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 559, Col: 38} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var82)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "
        connected ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "
        connected ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var74 string - templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.JoinStringErrs(c.ConnectedAt.Format("2006-01-02")) + var templ_7745c5c3_Var83 string + templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.JoinStringErrs(c.ConnectedAt.Format("2006-01-02")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 514, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 561, Col: 83} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var74)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var83)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, "
      ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if !hasYouTube(conns) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "

    Connect YouTube

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "

    Connect YouTube

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "

    Delete account

    Permanently remove your Tapir account and all of its data — summaries, watch/skip/save actions, and connected accounts. This cannot be undone.

    Delete account…

    This permanently deletes your account and all data. Are you sure?

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "

    Delete account

    Permanently remove your Tapir account and all of its data — summaries, watch/skip/save actions, and connected accounts. This cannot be undone.

    Delete account…

    This permanently deletes your account and all data. Are you sure?

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = Layout("Tapir — Account").Render(templ.WithChildren(ctx, templ_7745c5c3_Var65), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout("Tapir — Account").Render(templ.WithChildren(ctx, templ_7745c5c3_Var74), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1870,51 +2053,51 @@ func summarizeModeControl(auto bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var76 := templ.GetChildren(ctx) - if templ_7745c5c3_Var76 == nil { - templ_7745c5c3_Var76 = templ.NopComponent + templ_7745c5c3_Var85 := templ.GetChildren(ctx) + if templ_7745c5c3_Var85 == nil { + templ_7745c5c3_Var85 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "

    Current mode: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "

    Current mode: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var77 string - templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(summarizeModeLabel(auto)) + var templ_7745c5c3_Var86 string + templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(summarizeModeLabel(auto)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 552, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 599, Col: 53} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1942,38 +2125,38 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var80 := templ.GetChildren(ctx) - if templ_7745c5c3_Var80 == nil { - templ_7745c5c3_Var80 = templ.NopComponent + templ_7745c5c3_Var89 := templ.GetChildren(ctx) + if templ_7745c5c3_Var89 == nil { + templ_7745c5c3_Var89 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "\" hx-target=\"#action-buttons\" hx-swap=\"outerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1985,7 +2168,7 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1993,7 +2176,7 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -2019,81 +2202,81 @@ func actionButton(verb string, isActive bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var83 := templ.GetChildren(ctx) - if templ_7745c5c3_Var83 == nil { - templ_7745c5c3_Var83 = templ.NopComponent + templ_7745c5c3_Var92 := templ.GetChildren(ctx) + if templ_7745c5c3_Var92 == nil { + templ_7745c5c3_Var92 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var84 = []any{"action", templ.KV("active", isActive)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var84...) + var templ_7745c5c3_Var93 = []any{"action", templ.KV("active", isActive)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var93...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }