feat(runner): bound auto-summarize to a recency window

In automatic mode the scheduler now only summarizes videos published within
TAPIR_AUTO_SUMMARIZE_WINDOW (default ~7d). Older videos are still discovered
and listed — they keep the manual "Summarize" affordance — but are not
auto-processed, so a large back-catalogue (the maintainer's ~256-deep queue)
stops self-inflicting 429s against the per-IP caption gate each cycle (UX
review B1, recency design).

- runner.WithAutoWindow + Stats.SkippedTooOld; tooOld() treats a zero window
  as disabled and an undated video as never-aged-out (processed, not stranded).
- An explicit manual request bypasses the bound even in auto mode (requested
  videos are loaded in auto mode when a window is active).
- Wired through cmdRun, the scheduler's per-user runner, sumStats, and pass
  logging. config: TAPIR_AUTO_SUMMARIZE_WINDOW (default 168h), .env.example.
- Account copy (A7) updated to match: "Automatic summarizes new videos from
  about the last week; older videos stay browsable — summarize on demand."

The rate gate is untouched; the manual path still serialises through it. This
bounds auto LOAD, it does not fetch harder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 13:48:04 +02:00
co-authored by Claude Opus 4.8
parent 4a0a56e152
commit 2384c47b81
9 changed files with 199 additions and 42 deletions
+4 -3
View File
@@ -411,9 +411,10 @@ templ AccountPage(displayName, email string, conns []store.Connection, autoSumma
<section>
<h2>Summarization</h2>
<p class="muted">
Automatic summarizes every new video as it is discovered. Manual lets you
pick which videos to summarize new videos appear in your list with a
Summarize button.
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.
</p>
@summarizeModeControl(autoSummarize)
</section>
+18 -18
View File
@@ -1521,7 +1521,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</dl><section><h2>Summarization</h2><p class=\"muted\">Automatic summarizes every new video as it is discovered. Manual lets you pick which videos to summarize — new videos appear in your list with a Summarize button.</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</dl><section><h2>Summarization</h2><p class=\"muted\">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.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1541,7 +1541,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
var templ_7745c5c3_Var66 string
templ_7745c5c3_Var66, 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: 424, Col: 100}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 425, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66))
if templ_7745c5c3_Err != nil {
@@ -1559,7 +1559,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
var templ_7745c5c3_Var67 string
templ_7745c5c3_Var67, 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: 430, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 431, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
if templ_7745c5c3_Err != nil {
@@ -1572,7 +1572,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
var templ_7745c5c3_Var68 string
templ_7745c5c3_Var68, 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: 432, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 433, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
if templ_7745c5c3_Err != nil {
@@ -1610,7 +1610,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
var templ_7745c5c3_Var69 string
templ_7745c5c3_Var69, 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: 447, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 448, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
if templ_7745c5c3_Err != nil {
@@ -1628,7 +1628,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
var templ_7745c5c3_Var70 string
templ_7745c5c3_Var70, 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: 449, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 450, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70))
if templ_7745c5c3_Err != nil {
@@ -1646,7 +1646,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
var templ_7745c5c3_Var71 string
templ_7745c5c3_Var71, 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: 451, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 452, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71))
if templ_7745c5c3_Err != nil {
@@ -1659,7 +1659,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
var templ_7745c5c3_Var72 string
templ_7745c5c3_Var72, 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: 453, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 454, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72))
if templ_7745c5c3_Err != nil {
@@ -1672,7 +1672,7 @@ func AccountPage(displayName, email string, conns []store.Connection, autoSummar
var templ_7745c5c3_Var73 templ.SafeURL
templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(disconnectURL(c.Provider))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 454, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 455, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73))
if templ_7745c5c3_Err != nil {
@@ -1740,7 +1740,7 @@ func summarizeModeControl(auto bool) templ.Component {
var templ_7745c5c3_Var75 string
templ_7745c5c3_Var75, 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: 491, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 492, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75))
if templ_7745c5c3_Err != nil {
@@ -1753,7 +1753,7 @@ func summarizeModeControl(auto bool) templ.Component {
var templ_7745c5c3_Var76 string
templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolStr(!auto))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 499, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 500, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var76)
if templ_7745c5c3_Err != nil {
@@ -1766,7 +1766,7 @@ func summarizeModeControl(auto bool) templ.Component {
var templ_7745c5c3_Var77 string
templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinStringErrs(summarizeModeToggleLabel(auto))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 500, Col: 79}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 501, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77))
if templ_7745c5c3_Err != nil {
@@ -1812,7 +1812,7 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
var templ_7745c5c3_Var79 templ.SafeURL
templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinURLErrs(actionURL(videoID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 514, Col: 29}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 515, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79))
if templ_7745c5c3_Err != nil {
@@ -1825,7 +1825,7 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
var templ_7745c5c3_Var80 string
templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(string(actionURL(videoID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 515, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 516, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80)
if templ_7745c5c3_Err != nil {
@@ -1848,7 +1848,7 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
var templ_7745c5c3_Var82 string
templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(v)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 523, Col: 13}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 524, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82)
if templ_7745c5c3_Err != nil {
@@ -1874,7 +1874,7 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
var templ_7745c5c3_Var84 string
templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.ResolveAttributeValue(ariaPressed(active[v]))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 525, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 526, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var84)
if templ_7745c5c3_Err != nil {
@@ -1888,7 +1888,7 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
var templ_7745c5c3_Var85 string
templ_7745c5c3_Var85, templ_7745c5c3_Err = templ.JoinStringErrs("✓ " + actionLabel(v))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 528, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 529, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var85))
if templ_7745c5c3_Err != nil {
@@ -1898,7 +1898,7 @@ func ActionButtons(videoID string, active map[string]bool) templ.Component {
var templ_7745c5c3_Var86 string
templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(actionLabel(v))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 530, Col: 21}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views.templ`, Line: 531, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86))
if templ_7745c5c3_Err != nil {