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
+6
View File
@@ -503,6 +503,7 @@ a.btn, a.btn:visited { color: var(--accent-fg); }
/* passive "retrying later" chip: dim/grey (CharmDim), not the accent — it is a
status, not an action the user can take. */
.chip-retry { background: rgba(108, 108, 108, .16); color: #6c6c6c; }
.chip-warn { background: rgba(255, 110, 156, .15); color: #FF6E9C; }
.card-state { color: var(--muted); font-size: .8rem; }
.badge { display: inline-block; padding: .15rem .55rem; border-radius: 999px; background: var(--badge-bg); color: var(--badge-fg); font-size: .72rem; font-weight: 600; }
@@ -591,6 +592,11 @@ a.btn, a.btn:visited { color: var(--accent-fg); }
.account-meta { display: grid; grid-template-columns: max-content 1fr; gap: var(--s1) var(--s3); margin: 0; }
.account-meta dt { color: var(--muted); font-size: .85rem; }
.account-meta dd { margin: 0; }
.channel-errors { }
.channel-error-list { list-style: none; margin: 0 0 var(--s3); padding: 0; display: grid; gap: var(--s1); }
.channel-error-list li { display: flex; align-items: center; gap: var(--s2); }
.channel-error-name { font-weight: 500; }
.channel-error-since { font-size: .8rem; }
.conn-list { list-style: none; margin: 0 0 var(--s3); padding: 0; display: grid; gap: var(--s2); }
.conn { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--s3); display: flex; flex-direction: column; gap: var(--s1); }
.conn-main { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }