feat(store,runner,web): channel unavailability notice (migration 013)
CI / Lint / Test / Vet (push) Successful in 26s
CI / Build & Import (push) Successful in 11s

YouTube channels that 404 on playlist discovery (deleted/private) are now:
1. Wrapped in domain.ErrChannelUnavailable by the YouTube adapter (instead of
   a generic error), so the runner can identify them without string-matching.
2. Stored per-user in channel_errors (migration 013, RLS-guarded) via runner's
   new UpsertChannelError path — removed from the generic Errors counter,
   counted separately as ChannelUnavailable.
3. Shown on the account page under "Unavailable channels" with name, chip-warn
   badge, and first-seen date, so users know why some subscribed channels
   produce no videos.
This commit is contained in:
2026-06-06 10:09:52 +02:00
parent 940f80899a
commit f1e9739900
13 changed files with 380 additions and 161 deletions
@@ -0,0 +1 @@
DROP TABLE IF EXISTS channel_errors;
@@ -0,0 +1,22 @@
-- channel_errors: channels that returned HTTP 404 (deleted/private) on the most
-- recent scheduler pass. Surfaced on the account page so users know why some
-- subscribed channels produce no videos. Upserted per-pass; cleared when the
-- channel starts returning results again (runner calls UpsertChannelError only
-- on 404, so a recovered channel simply stops appearing after its row ages out
-- or the user takes action). ON DELETE CASCADE keeps rows tidy on account deletion.
CREATE TABLE channel_errors (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
channel_id TEXT NOT NULL,
channel_name TEXT NOT NULL,
first_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, channel_id)
);
CREATE INDEX idx_channel_errors_user_id ON channel_errors(user_id);
ALTER TABLE channel_errors ENABLE ROW LEVEL SECURITY;
ALTER TABLE channel_errors FORCE ROW LEVEL SECURITY;
CREATE POLICY channel_errors_isolation ON channel_errors
FOR ALL
USING (user_id = current_setting('tapir.current_user_id', true)::uuid);