feat(web): collapse older + caption-less videos in the list

Stop the un-summarized back-catalogue from burying the readable summaries
(UX review B3/B4). One feed, with a noise-collapse — not sections:

- Summarized + recent un-summarized videos lead inline as cards.
- Un-summarized videos older than the recency window collapse into a single
  "Show N older videos — summarize on demand" disclosure (they will not
  auto-fill; they are manual-only). Window comes from App.RecencyWindow
  (= cfg.AutoSummarizeWindow); 0 disables the collapse (all inline).
- Caption-less videos collapse into one honest line ("N videos have no
  captions and can't be summarized") instead of N dead terminal cards.

bucketRows is a pure classifier (cutoff-driven; undated rows never age out);
App gains RecencyWindow + an injectable clock for the cutoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 13:55:07 +02:00
co-authored by Claude Opus 4.8
parent 3df0459fed
commit 40b703e02a
6 changed files with 748 additions and 546 deletions
+42
View File
@@ -318,6 +318,48 @@ func TestListShowsSummarizeButtonForUnsummarized(t *testing.T) {
require.NotContains(t, html, "Queued", "not queued yet")
}
// TestListCollapsesOlderAndNoCaption verifies the feed IA (UX review B3/B4):
// summarized + recent un-summarized cards lead inline; older un-summarized
// videos collapse into a single disclosure; caption-less videos collapse into a
// one-line count instead of dead cards.
func TestListCollapsesOlderAndNoCaption(t *testing.T) {
ctx := context.Background()
app := newApp(t)
now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
app.RecencyWindow = 7 * 24 * time.Hour
app.Now = func() time.Time { return now }
p := rawPool(t)
resetDB(t, p)
const (
vRecent = "cccccccc-cccc-cccc-cccc-cccccccccccc" // 2d old, unsummarized → inline
vOld = "dddddddd-dddd-dddd-dddd-dddddddddddd" // 30d old, unsummarized → disclosure
vNoCap = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" // caption-less → collapsed line
)
require.NoError(t, deliver(ctx, app, videoX, "body x")) // summarized, recent
seedVideo(t, p, videoX, "Summarized X", "https://x", now.Add(-24*time.Hour))
seedVideo(t, p, vRecent, "Recent Pending", "https://r", now.Add(-2*24*time.Hour))
seedVideo(t, p, vOld, "Old Pending", "https://o", now.Add(-30*24*time.Hour))
seedVideo(t, p, vNoCap, "No Caption Vid", "https://n", now.Add(-40*24*time.Hour))
_, err := p.Exec(ctx, `UPDATE videos SET transcript_status = 'none' WHERE id = $1`, vNoCap)
require.NoError(t, err)
rec := do(t, app, httptest.NewRequest(http.MethodGet, "/", nil))
require.Equal(t, http.StatusOK, rec.Code)
html := body(t, rec)
disclosure := strings.Index(html, "Show 1 older videos")
require.GreaterOrEqual(t, disclosure, 0, "older-videos disclosure present")
// Summarized + recent un-summarized lead inline, above the disclosure.
require.Less(t, strings.Index(html, "Summarized X"), disclosure, "summarized card is inline")
require.Less(t, strings.Index(html, "Recent Pending"), disclosure, "recent pending is inline")
// The older video is hidden inside the disclosure, after its summary.
require.Greater(t, strings.Index(html, "Old Pending"), disclosure, "older video lives in the disclosure")
// Caption-less video is a one-line count, never a card.
require.Contains(t, html, "have no captions")
require.NotContains(t, html, "No Caption Vid", "caption-less video is collapsed, not a card")
}
func TestRequestSummarizeQueuesAndRendersCard(t *testing.T) {
ctx := context.Background()
app := newApp(t)