M5a frontend + M5b quarantine + M5c suggestions #30
Reference in New Issue
Block a user
Delete Branch "dev"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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/*.