feat(web): real channel filter — multi-select of the user's channels
The free-text 'channel' filter was dead: it exact-matched SummaryRow.Channel,
which is just the provider ('youtube'), because videos never stored their source
channel. Now they do.
- migration 014: videos.channel_title (nullable; existing rows backfill on the
next discovery pass, pasted videos immediately).
- discovery (NewVideos) + paste (VideoByID) populate channel_title; UpsertVideo
persists it, preserving an existing title when an update arrives empty.
- store.DistinctChannels lists a user's channels (RLS-scoped); SummaryRow carries
ChannelTitle via the shared projection.
- Filter: single Channel -> Channels []string, matching on ChannelTitle; the feed
renders a multi-select of DistinctChannels (hidden until channels exist).
- migrate tests: 014 reversibility + fixed the relative-step counts in the 010/011
up/down tests (014 shifted the topology).
TDD throughout: channel persist + distinct, adapter channel wiring, multi-channel
filter match, handler channel filter, migration up/down.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,14 +104,14 @@ templ flashBanner(code string) {
|
||||
// #summary-list region; a non-HTMX request renders the whole page. flash carries
|
||||
// a one-shot notification (e.g. "connected", "registered") surfaced on arrival
|
||||
// after a POST→redirect.
|
||||
templ ListPage(b listBuckets, f Filter, stats PipelineStats, flash string, hasConnected bool) {
|
||||
templ ListPage(b listBuckets, f Filter, stats PipelineStats, flash string, hasConnected bool, channels []string) {
|
||||
@Layout("Tapir — Summaries") {
|
||||
@flashBanner(flash)
|
||||
if hasConnected {
|
||||
@pasteForm()
|
||||
}
|
||||
if !b.empty() || f.active() {
|
||||
@filterForm(f)
|
||||
@filterForm(f, channels)
|
||||
}
|
||||
if stats.RateLimited > 0 || stats.Pending > 0 || stats.NoText > 0 {
|
||||
@pipelineBar(stats)
|
||||
@@ -168,7 +168,7 @@ templ pasteForm() {
|
||||
<div id="paste-result"></div>
|
||||
}
|
||||
|
||||
templ filterForm(f Filter) {
|
||||
templ filterForm(f Filter, channels []string) {
|
||||
<form
|
||||
class="filters"
|
||||
method="get"
|
||||
@@ -178,7 +178,16 @@ templ filterForm(f Filter) {
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#filter-indicator"
|
||||
>
|
||||
<label>Channel <input type="text" name="channel" value={ f.Channel } placeholder="any"/></label>
|
||||
if len(channels) > 0 {
|
||||
<label>
|
||||
Channels
|
||||
<select name="channel" multiple size="4">
|
||||
for _, c := range channels {
|
||||
<option value={ c } selected?={ f.HasChannel(c) }>{ c }</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
}
|
||||
<label class="filter-check">
|
||||
<input type="checkbox" name="summarized" value="1" if f.OnlySummarized { checked }/>
|
||||
Summarized only
|
||||
|
||||
Reference in New Issue
Block a user