T14<DiscoverResultCard> component with reserved badge slot + bottom-anchored button.
T15<StatusPill> semantic-color status indicator.
T16/discover route with debounced search, kind tabs, track-kind confirmation modal.
T17/requests user history with cancel + listen affordances, status pills, voice-rule copy.
T18/admin/* layout with hard role-gated load, sidebar, Overview landing.
T19/admin/integrations Lidarr connection panel with typed-confirm Disconnect modal.
T20/admin/requests approval queue with override + reject modals, lidarr-unreachable toast.
M5b quarantine workflow (T1–T16)
Per-user track soft-hide + admin resolution UI. Spec at docs/superpowers/specs/2026-04-30-m5b-quarantine-design.md, plan at docs/superpowers/plans/2026-04-30-m5b-quarantine.md.
Backend:
Migration 0011: lidarr_quarantine (per-user complaints, composite PK, ON DELETE CASCADE on track_id) + lidarr_quarantine_actions (admin audit log with snapshot text columns).
internal/library.DeleteTrackFile — file-first, then DB; retry-safe.
internal/lidarrquarantine.Service with Flag/Unflag/ListMine/ListAdminQueue (read paths) and Resolve/DeleteFile/DeleteViaLidarr (admin actions). FK violation on Flag translates to ErrTrackNotFound for clean 404s.
Soft-hide enforcement on user-context track-list reads: *ForUser query variants for album detail + search; quarantine clause folded into both radio loaders. /rest/* Subsonic stays unfiltered per the legacy rule.
/api/quarantine/* user-facing CRUD (3 endpoints) + /api/admin/quarantine/* (5 endpoints) wired into the existing RequireAdmin route group.
Server now constructs a real per-call Lidarr clientFn so lidarrrequests.Approve and lidarrquarantine.DeleteViaLidarr reach Lidarr when configured (was nil-stubbed since M5a).
Frontend:
<TrackMenu> overflow (kebab) component opens <FlagPopover>. Mounted in <TrackRow> and <PlayerBar> so users can flag from any list or while listening.
/library/hidden page lists the caller's quarantines with one-click un-hide.
/admin/quarantine aggregated queue with Resolve / Delete file (modal-confirm) / Delete via Lidarr (typed-confirm "DELETE"); inline play-via-radio button; lidarr-unreachable toast; dimmed Delete-via-Lidarr when lidarr_album_mbid is null.
<AdminSidebar> Quarantine promoted from placeholder to real link.
M5c suggested-additions (T1–T9)
Personalized artist suggestions on /discover when the search input is empty. Spec at docs/superpowers/specs/2026-04-30-m5c-suggested-additions-design.md, plan at docs/superpowers/plans/2026-04-30-m5c-suggested-additions.md.
Diverges from the M5a §10 carve-out (which had said /api/radio would carry the suggestions): operator redirected during brainstorming. The suggestion surface is a dedicated recommendation feed on /discover, decoupled from radio.
Backend:
Migration 0012: artist_similarity_unmatched table mirrors artist_similarity shape with no FK on the candidate side. M4b worker now persists the unmatched MBIDs it used to discard (with Name field added to the LB SimilarArtist struct).
internal/recommendation.SuggestArtists runs a single CTE per request that joins the user's likes (×5) and recency-decayed plays (exp(-age_days/half_life), half_life=30d) against artist_similarity_unmatched, filters out in-library and non-terminal-requested candidates, and returns top-N candidates with top-3 contributing seeds for attribution.
GET /api/discover/suggestions?limit=&half_life_days= — read-only, sub-50ms at household scale.
Frontend:
<DiscoverResultCard> extended with optional attribution?: string prop (italic Vellum line below the title).
<SuggestionFeed> component owns the data fetch + attribution formatting + optimistic-requested removal. Attribution copy: 1 seed → "Because you liked X.", 2 → "Because you liked X and played Y.", 3 → Oxford-comma form.
/discover swaps the initial-copy state for <SuggestionFeed>. Kind tabs hide when input is empty, return when searching.
Verification
go test -short -race ./... — all packages pass.
golangci-lint run ./... — clean.
Coverage on Lidarr + recommendation packages: lidarr 89.3%, lidarrconfig 92.3%, lidarrrequests 81.5%, lidarrquarantine 81.4%, recommendation 90.9% (combined 86.9%; all individually ≥ 80% per spec §8).
Frontend: npm run check 0 errors, npm test -- --run 323/323 pass, npm run build clean.
The pre-existing internal/library/TestScanner_Integration flake is documented at project_scanner_flake.md; not introduced by this PR (zero internal/library/ changes in the diff aside from M5b's DeleteTrackFile addition).
Test plan
M5a
Hit /discover, search Lidarr, request an artist → verify it lands at /requests and at /admin/requests.
Approve a request from /admin/requests → wait for the next library scan → confirm status flips to "Kept" at /requests.
Connect/disconnect Lidarr from /admin/integrations; confirm typed-confirm gates Disconnect.
M5b
Flag a track from the kebab on a track row; confirm it disappears from album/search/library.
Open /library/hidden, confirm row is present, un-hide → track returns to album view.
As admin, view /admin/quarantine aggregated queue (with multiple users on the same track); test Resolve, Delete file, and Delete via Lidarr (typed-confirm).
Confirm /rest/* Subsonic responses still include quarantined tracks (legacy clients unaffected).
M5c
Like an artist (or play a few tracks); wait for the M4b worker tick to populate artist_similarity_unmatched.
Open /discover with the search input empty; verify "Suggested for you" header and grid render with attribution lines.
Click Request on a suggestion; confirm it disappears (optimistic) and a row appears at /requests.
Type a search term; confirm the feed swaps out for search results and kind tabs reappear.
Clear the search input; confirm the feed comes back (cached).
For a fresh user with no signal, verify the empty-state copy.
Cross-cutting
Non-admin user redirected when navigating to /admin/*.
Bundles M5a frontend (T12–T21), M5b quarantine workflow (T1–T16), and M5c suggested-additions on `/discover` (T1–T9). 50 commits.
## M5a frontend (T12–T21)
Lands the FabledSword design system + the four M5a pages on top of the M5a backend that already shipped.
- **T12** FabledSword design tokens + Tailwind aliases (Obsidian/Iron/Slate/Pewter, Parchment/Vellum/Ash, Moss/Bronze/Oxblood, forest-teal accent; Fraunces/Inter/JetBrains Mono).
- **T13** API client modules (`lidarr.ts`, `requests.ts`, `admin.ts`) + 6 query keys.
- **T14** `<DiscoverResultCard>` component with reserved badge slot + bottom-anchored button.
- **T15** `<StatusPill>` semantic-color status indicator.
- **T16** `/discover` route with debounced search, kind tabs, track-kind confirmation modal.
- **T17** `/requests` user history with cancel + listen affordances, status pills, voice-rule copy.
- **T18** `/admin/*` layout with hard role-gated `load`, sidebar, Overview landing.
- **T19** `/admin/integrations` Lidarr connection panel with typed-confirm Disconnect modal.
- **T20** `/admin/requests` approval queue with override + reject modals, lidarr-unreachable toast.
## M5b quarantine workflow (T1–T16)
Per-user track soft-hide + admin resolution UI. Spec at `docs/superpowers/specs/2026-04-30-m5b-quarantine-design.md`, plan at `docs/superpowers/plans/2026-04-30-m5b-quarantine.md`.
**Backend:**
- Migration 0011: `lidarr_quarantine` (per-user complaints, composite PK, `ON DELETE CASCADE` on `track_id`) + `lidarr_quarantine_actions` (admin audit log with snapshot text columns).
- Lidarr client extensions: `LookupArtistByMBID`, `LookupAlbumByMBID`, `DeleteAlbum` (always with `deleteFiles=true&addImportListExclusion=true`), `ErrNotFound` sentinel.
- `internal/library.DeleteTrackFile` — file-first, then DB; retry-safe.
- `internal/lidarrquarantine.Service` with Flag/Unflag/ListMine/ListAdminQueue (read paths) and Resolve/DeleteFile/DeleteViaLidarr (admin actions). FK violation on `Flag` translates to `ErrTrackNotFound` for clean 404s.
- Soft-hide enforcement on user-context track-list reads: `*ForUser` query variants for album detail + search; quarantine clause folded into both radio loaders. `/rest/*` Subsonic stays unfiltered per the legacy rule.
- `/api/quarantine/*` user-facing CRUD (3 endpoints) + `/api/admin/quarantine/*` (5 endpoints) wired into the existing `RequireAdmin` route group.
- Server now constructs a real per-call Lidarr `clientFn` so `lidarrrequests.Approve` and `lidarrquarantine.DeleteViaLidarr` reach Lidarr when configured (was nil-stubbed since M5a).
**Frontend:**
- `<TrackMenu>` overflow (kebab) component opens `<FlagPopover>`. Mounted in `<TrackRow>` and `<PlayerBar>` so users can flag from any list or while listening.
- `/library/hidden` page lists the caller's quarantines with one-click un-hide.
- `/admin/quarantine` aggregated queue with Resolve / Delete file (modal-confirm) / Delete via Lidarr (typed-confirm "DELETE"); inline play-via-radio button; lidarr-unreachable toast; dimmed Delete-via-Lidarr when `lidarr_album_mbid` is null.
- `<AdminSidebar>` Quarantine promoted from placeholder to real link.
## M5c suggested-additions (T1–T9)
Personalized artist suggestions on `/discover` when the search input is empty. Spec at `docs/superpowers/specs/2026-04-30-m5c-suggested-additions-design.md`, plan at `docs/superpowers/plans/2026-04-30-m5c-suggested-additions.md`.
Diverges from the M5a §10 carve-out (which had said `/api/radio` would carry the suggestions): operator redirected during brainstorming. The suggestion surface is a dedicated recommendation feed on `/discover`, decoupled from radio.
**Backend:**
- Migration 0012: `artist_similarity_unmatched` table mirrors `artist_similarity` shape with no FK on the candidate side. M4b worker now persists the unmatched MBIDs it used to discard (with `Name` field added to the LB `SimilarArtist` struct).
- `internal/recommendation.SuggestArtists` runs a single CTE per request that joins the user's likes (×5) and recency-decayed plays (`exp(-age_days/half_life)`, half_life=30d) against `artist_similarity_unmatched`, filters out in-library and non-terminal-requested candidates, and returns top-N candidates with top-3 contributing seeds for attribution.
- `GET /api/discover/suggestions?limit=&half_life_days=` — read-only, sub-50ms at household scale.
**Frontend:**
- `<DiscoverResultCard>` extended with optional `attribution?: string` prop (italic Vellum line below the title).
- `<SuggestionFeed>` component owns the data fetch + attribution formatting + optimistic-requested removal. Attribution copy: 1 seed → "Because you liked X.", 2 → "Because you liked X and played Y.", 3 → Oxford-comma form.
- `/discover` swaps the initial-copy state for `<SuggestionFeed>`. Kind tabs hide when input is empty, return when searching.
## Verification
- `go test -short -race ./...` — all packages pass.
- `golangci-lint run ./...` — clean.
- Coverage on Lidarr + recommendation packages: `lidarr` 89.3%, `lidarrconfig` 92.3%, `lidarrrequests` 81.5%, `lidarrquarantine` 81.4%, `recommendation` 90.9% (combined 86.9%; all individually ≥ 80% per spec §8).
- Frontend: `npm run check` 0 errors, `npm test -- --run` 323/323 pass, `npm run build` clean.
The pre-existing `internal/library/TestScanner_Integration` flake is documented at `project_scanner_flake.md`; not introduced by this PR (zero `internal/library/` changes in the diff aside from M5b's `DeleteTrackFile` addition).
## Test plan
### M5a
- [ ] Hit `/discover`, search Lidarr, request an artist → verify it lands at `/requests` and at `/admin/requests`.
- [ ] Approve a request from `/admin/requests` → wait for the next library scan → confirm status flips to "Kept" at `/requests`.
- [ ] Connect/disconnect Lidarr from `/admin/integrations`; confirm typed-confirm gates Disconnect.
### M5b
- [ ] Flag a track from the kebab on a track row; confirm it disappears from album/search/library.
- [ ] Open `/library/hidden`, confirm row is present, un-hide → track returns to album view.
- [ ] As admin, view `/admin/quarantine` aggregated queue (with multiple users on the same track); test Resolve, Delete file, and Delete via Lidarr (typed-confirm).
- [ ] Confirm `/rest/*` Subsonic responses still include quarantined tracks (legacy clients unaffected).
### M5c
- [ ] Like an artist (or play a few tracks); wait for the M4b worker tick to populate `artist_similarity_unmatched`.
- [ ] Open `/discover` with the search input empty; verify "Suggested for you" header and grid render with attribution lines.
- [ ] Click Request on a suggestion; confirm it disappears (optimistic) and a row appears at `/requests`.
- [ ] Type a search term; confirm the feed swaps out for search results and kind tabs reappear.
- [ ] Clear the search input; confirm the feed comes back (cached).
- [ ] For a fresh user with no signal, verify the empty-state copy.
### Cross-cutting
- [ ] Non-admin user redirected when navigating to `/admin/*`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Bundles M5a frontend (T12–T21), M5b quarantine workflow (T1–T16), and M5c suggested-additions on
/discover(T1–T9). 50 commits.M5a frontend (T12–T21)
Lands the FabledSword design system + the four M5a pages on top of the M5a backend that already shipped.
lidarr.ts,requests.ts,admin.ts) + 6 query keys.<DiscoverResultCard>component with reserved badge slot + bottom-anchored button.<StatusPill>semantic-color status indicator./discoverroute with debounced search, kind tabs, track-kind confirmation modal./requestsuser history with cancel + listen affordances, status pills, voice-rule copy./admin/*layout with hard role-gatedload, sidebar, Overview landing./admin/integrationsLidarr connection panel with typed-confirm Disconnect modal./admin/requestsapproval queue with override + reject modals, lidarr-unreachable toast.M5b quarantine workflow (T1–T16)
Per-user track soft-hide + admin resolution UI. Spec at
docs/superpowers/specs/2026-04-30-m5b-quarantine-design.md, plan atdocs/superpowers/plans/2026-04-30-m5b-quarantine.md.Backend:
lidarr_quarantine(per-user complaints, composite PK,ON DELETE CASCADEontrack_id) +lidarr_quarantine_actions(admin audit log with snapshot text columns).LookupArtistByMBID,LookupAlbumByMBID,DeleteAlbum(always withdeleteFiles=true&addImportListExclusion=true),ErrNotFoundsentinel.internal/library.DeleteTrackFile— file-first, then DB; retry-safe.internal/lidarrquarantine.Servicewith Flag/Unflag/ListMine/ListAdminQueue (read paths) and Resolve/DeleteFile/DeleteViaLidarr (admin actions). FK violation onFlagtranslates toErrTrackNotFoundfor clean 404s.*ForUserquery variants for album detail + search; quarantine clause folded into both radio loaders./rest/*Subsonic stays unfiltered per the legacy rule./api/quarantine/*user-facing CRUD (3 endpoints) +/api/admin/quarantine/*(5 endpoints) wired into the existingRequireAdminroute group.clientFnsolidarrrequests.Approveandlidarrquarantine.DeleteViaLidarrreach Lidarr when configured (was nil-stubbed since M5a).Frontend:
<TrackMenu>overflow (kebab) component opens<FlagPopover>. Mounted in<TrackRow>and<PlayerBar>so users can flag from any list or while listening./library/hiddenpage lists the caller's quarantines with one-click un-hide./admin/quarantineaggregated queue with Resolve / Delete file (modal-confirm) / Delete via Lidarr (typed-confirm "DELETE"); inline play-via-radio button; lidarr-unreachable toast; dimmed Delete-via-Lidarr whenlidarr_album_mbidis null.<AdminSidebar>Quarantine promoted from placeholder to real link.M5c suggested-additions (T1–T9)
Personalized artist suggestions on
/discoverwhen the search input is empty. Spec atdocs/superpowers/specs/2026-04-30-m5c-suggested-additions-design.md, plan atdocs/superpowers/plans/2026-04-30-m5c-suggested-additions.md.Diverges from the M5a §10 carve-out (which had said
/api/radiowould carry the suggestions): operator redirected during brainstorming. The suggestion surface is a dedicated recommendation feed on/discover, decoupled from radio.Backend:
artist_similarity_unmatchedtable mirrorsartist_similarityshape with no FK on the candidate side. M4b worker now persists the unmatched MBIDs it used to discard (withNamefield added to the LBSimilarArtiststruct).internal/recommendation.SuggestArtistsruns a single CTE per request that joins the user's likes (×5) and recency-decayed plays (exp(-age_days/half_life), half_life=30d) againstartist_similarity_unmatched, filters out in-library and non-terminal-requested candidates, and returns top-N candidates with top-3 contributing seeds for attribution.GET /api/discover/suggestions?limit=&half_life_days=— read-only, sub-50ms at household scale.Frontend:
<DiscoverResultCard>extended with optionalattribution?: stringprop (italic Vellum line below the title).<SuggestionFeed>component owns the data fetch + attribution formatting + optimistic-requested removal. Attribution copy: 1 seed → "Because you liked X.", 2 → "Because you liked X and played Y.", 3 → Oxford-comma form./discoverswaps the initial-copy state for<SuggestionFeed>. Kind tabs hide when input is empty, return when searching.Verification
go test -short -race ./...— all packages pass.golangci-lint run ./...— clean.lidarr89.3%,lidarrconfig92.3%,lidarrrequests81.5%,lidarrquarantine81.4%,recommendation90.9% (combined 86.9%; all individually ≥ 80% per spec §8).npm run check0 errors,npm test -- --run323/323 pass,npm run buildclean.The pre-existing
internal/library/TestScanner_Integrationflake is documented atproject_scanner_flake.md; not introduced by this PR (zerointernal/library/changes in the diff aside from M5b'sDeleteTrackFileaddition).Test plan
M5a
/discover, search Lidarr, request an artist → verify it lands at/requestsand at/admin/requests./admin/requests→ wait for the next library scan → confirm status flips to "Kept" at/requests./admin/integrations; confirm typed-confirm gates Disconnect.M5b
/library/hidden, confirm row is present, un-hide → track returns to album view./admin/quarantineaggregated queue (with multiple users on the same track); test Resolve, Delete file, and Delete via Lidarr (typed-confirm)./rest/*Subsonic responses still include quarantined tracks (legacy clients unaffected).M5c
artist_similarity_unmatched./discoverwith the search input empty; verify "Suggested for you" header and grid render with attribution lines./requests.Cross-cutting
/admin/*.