Spec table's "Dark (current)" column now matches the actual
tokens.json values. Added Tailwind text-action-fg alias as the
mechanism for the non-flipping --fs-on-action token. Updated
audit notes to use the real Tailwind class (text-text-primary)
rather than the raw token (text-fs-parchment).
Operator decision during Task 2 implementation: routing track removal
through lidarrquarantine.DeleteViaLidarr is wrong because Lidarr is
album-granular (no per-track delete). The original spec would have
silently deleted sibling tracks.
New shape:
- Always delete file + DB row + cascade through Minstrel (os.Remove).
- Confirm dialog asks "find a replacement?": Yes (default — no Lidarr
call; monitoring re-imports on next scan) or No (call new
Lidarr.UnmonitorTrack(mbid) primitive).
- Lidarr unmonitor failure is non-fatal — sets lidarr_unmonitor_failed
on the success envelope so the UI toasts a follow-up message.
Adds a new internal/lidarr.UnmonitorTrack method (LookupTrack →
PUT /api/v1/track/monitor with monitored:false) to Task 2's scope.
Wire codes lidarr_unreachable / lidarr_unauthorized / lidarr_server_error
no longer come back from this endpoint.
9 tasks covering: SQL cascade-cleanup queries, internal/tracks
service with Lidarr-aware RemoveTrack, DELETE /api/admin/tracks/{id}
handler, frontend admin API helper, audio-store playNext addition,
TrackMenuItem + TrackMenuDivider primitives, full TrackMenu rewrite
with all 9 entries (Add to playlist… reserved as a disabled slot for
#352), and TrackRow/PlayerBar test adjustments.
Plan reflects the no-in-task-tests rule — implementers write test
files but don't run them; CI handles verification. Migration 0014
that the spec marked conditional is dropped: all relevant track_id
FKs already cascade.
Replaces the M5b-era single-entry <TrackMenu> with a 9-entry kebab
menu in 4 groups (queue / collection / navigation / lifecycle).
Reserves the "Add to playlist..." slot for #352. Lands a new
admin-only DELETE /api/admin/tracks/{id} for "Remove from library"
with cascade album -> artist tidy-up and Lidarr-aware deletion for
managed tracks.
Track metadata editing split out as #373 — different feature concern
(crosses into Lidarr's territory + scanner reconciliation).
Lays the M7 #356 first-slice scope: project scaffold, theme system fed
from a shared FabledSword tokens.json, Riverpod + dio + just_audio
stack, auth flow against existing /api/auth/* (Bearer), library browse
(home/artist/album) with shell-route PlayerBar + NowPlaying, likes
included so layout settles. Tier 1 features beyond this slice (search,
discover, requests, settings, admin) get their own spec/plan cycles.
Desktop deferred to v1.1 as a Tauri-wrapped SvelteKit SPA, not Flutter
desktop.
20 tasks covering backend SQL queries, recommendation service,
three new API endpoints (/api/home, /api/library/albums,
/api/artists/{id}/tracks), and frontend rewrite of / as the home
page composing four horizontal-scroll sections, plus new
/library/artists and /library/albums wrapping-grid pages.
ArtistRow component retired in favor of circular ArtistCard
(takes the click-target bug with it). AlbumCard gets the
FabledSword polish pass with a play-button overlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the brainstorm for the first UI polish slice (Fable #349):
new `/` home page with 4 sections (Recently added / Rediscover /
Most played / Last played) as independent horizontal-scroll rows
with arrow buttons, library list relocated to `/library/artists`
and `/library/albums` as wrapping grids with alphabetical dividers,
ArtistCard (circular) replacing ArtistRow (retires the click-target
bug), AlbumCard polish pass with play-button overlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Personalized artist suggestions on /discover (search-input-empty state).
On-demand SQL ranks out-of-library MBIDs from artist_similarity_unmatched
(new table, populated by extending the M4b similarity worker) by per-user
signal: likes weighted 5x plus recency-decayed plays (exp(-age_days/30)).
Top-12 with top-3 contributing seeds attributed per card. Reuses the M5a
DiscoverResultCard + POST /api/requests artist-add path.
M5a is the foundation slice of M5 (Lidarr integration + quarantine).
Decomposed into M5a/M5b/M5c during brainstorming on 2026-04-29:
- M5a (this spec) — Lidarr connection + search/add + admin shell
- M5b — Quarantine workflow (per-user soft-hide, admin resolution)
- M5c — Radio suggested-additions (out-of-library MBIDs surfaced)
Each ships as its own PR with its own brainstorm/spec/plan cycle.
Key decisions captured:
- Permissions: search-all, add-admin via request queue
- Config: DB-only, Settings UI is the only entry point (no YAML)
- Settings shape: dedicated /admin/* route group, hard route gate
- Search UX: standalone /discover route, all three granularities
- Lifecycle: library scan as source of truth + 5-min reconciler worker
- Quality profile/root folder: default + per-add admin override
- UI lands at FabledSword design system bar (forest-teal accent;
Moss/Bronze/Oxblood action buttons; per project_design_system memory)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Third and final M4 sub-plan (Fable #347) — closes M4. Replaces M3's
whole-library candidate pool with a 5-way SQL UNION (LB-similar tracks /
tracks by similar artists / MB-tag overlap / likes-overlap / random
fill). Adds a new SimilarityScore × SimilarityWeight term to M3's Score()
formula. Web client auto-refreshes the queue when 80% consumed via a new
?exclude= query param. No lazy LB fetch (M4b's worker + random
augmentation handle sparse data). Fallback to M3 LoadCandidates on any
similarity-pool error.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second M4 sub-plan (Fable #346). Periodic worker pulls track-track and
artist-artist similarity edges from LB's public /explore/* endpoints,
filters to the local library, stores top-20 per source track in two new
tables (track_similarity, artist_similarity). Hourly tick, batch=5,
weekly re-fetch cap per row, passive retry via timer. No auth (public
endpoints). Discovery within library handled by LB's collaborative-
filtering response naturally surfacing unplayed library tracks; spec
notes M4c will add a serendipity floor + lazy fetch + sparse-fallback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First M4 sub-plan (Fable #345). Outbound scrobble worker with batching +
exponential-backoff retry. Per-user token, plaintext storage, pull-based
30s timer worker, patient backoff (1m → 5m → 30m → 2h → 6h, 5 attempts).
New scrobble_queue table; sent rows deleted (canonical record stays in
play_events.scrobbled_at). Minimal /settings page in M4a as scaffold for
M6's full Settings sub-plan to extend.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eight-task TDD-shaped plan covering pure Similarity + ContextualMatchScore
functions, Score extension with ContextWeight=2.0, two new sqlc queries
(ListActiveContextualLikesForUser, GetCurrentSessionVectorForUser),
LoadCandidates signature change to accept currentVector, radio handler
wiring, end-to-end contextual ranking test, and final verification.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes M3 — adds the `contextual_match_score` term to the scoring formula
via weighted-Jaccard similarity (tags 0.7, artists 0.3) over the user's
contextual_likes. Reads the current session vector from the most recent
open play_event (populated by sub-plan #2). Cold-start paths collapse to
zero contribution, preserving v1 behavior.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sub-plan #2 of 3 in M3 (Fable #341). Two write-path additions on top
of M2's events + likes infra:
1. play_started: compute session vector from prior 5 tracks in
current play_session, write to play_events.session_vector_at_play.
2. like a track: if open play_event has populated vector, snapshot
into contextual_likes with vector + session_id. Soft-delete on
unlike (deleted_at column gates engine queries).
Includes new migration 0007_contextual_likes — the table was missing
from migration 0005 despite being mentioned in its comments. Adds
GIN index for sub-plan #3's similarity queries.
Backend-only slice; no UI changes.
Replaces the M2 stub /api/radio with real weighted-shuffle scoring:
LikeBoost + recency_decay - skip_ratio penalty + jitter, hard
suppression of last-hour plays. Wide candidate pool (whole library)
for v1; M4 adds similarity-based pool refinement. Pure scoring
function in internal/recommendation; live-DB candidate loader; HTTP
handler is a thin shim. No web changes — existing playRadio action
already calls /api/radio.
Sub-plan #1 of 3 in M3 (Fable #340). Session vectors and contextual
match score land in the next two sub-plans.
Pages that render TrackRow/AlbumCard/ArtistRow/PlayerBar now indirectly
mount LikeButton, which calls useQueryClient(). Route tests need the
same mocks the component-level tests have.
- Added createLikedIdsQuery mock to 7 route test files
- Added useQueryClient mock to all affected route tests
- Added readable store import where needed
Three new tables (general_likes, general_likes_albums,
general_likes_artists), seven native /api/likes/* endpoints, four
Subsonic handlers (star/unstar/getStarred/getStarred2), and web UI
heart buttons on TrackRow/AlbumCard/ArtistRow/PlayerBar plus a new
/library/liked page. Closes M2 (with the events foundation already
in main).
Liked state is computed client-side via O(1) Set lookups against a
single createLikedIdsQuery cache; server stays simple per request
(no joins added to entity reads). Optimistic updates with rollback.
Specifies POST /api/events with start+end semantics, session service
(30-min rolling), play/skip classification (spec §6 boundaries),
sendBeacon + auto-close-on-next-start for abandoned plays, and
Subsonic /rest/scrobble integration that writes synthetic completed
plays so third-party clients feed the recommendation pipeline.
Schema migration ships the full M2 set (play_events, skip_events,
sessions) including M3-only nullable columns so M3 doesn't need a
follow-up migration. General likes deferred to a separate M2
sub-plan; this slice covers spec §13 step 5 only.
Specifies header search bar (debounced, URL-synced ?q=), /search page
with three sections of top-10 reusing library components, three
overflow routes (/search/artists, /search/albums, /search/tracks)
backed by createInfiniteQuery. Track click triggers playRadio with
seed via a new /api/radio stub that returns the seed alone (M4 fills
in real similarity-based selection later, response shape final). Adds
+queue button on TrackRow and AlbumCard via new enqueueTrack /
enqueueTracks player-store actions.
8 TDD tasks: player rune store (2 tasks covering basic actions then
shuffle/repeat/audio-reporting), MediaSession glue, click-to-play
wiring in TrackRow + album page, PlayerBar component, Shell 3rd-row
integration, audio element wiring in root layout, and a final
verification + branch-finish step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Queue-based playback on top of HTMLAudioElement: rune store + pure
action functions, single <audio> mounted in the root layout driven
via $effect, MediaSession metadata/action handlers, and a persistent
bottom bar with shuffle/repeat/volume. Non-goals: server-side listen
history (awaiting /api/events), cross-refresh persistence, keyboard
shortcuts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
page.params.id is string|undefined in SvelteKit types; use ?? ''.
TanStack Query's error is typed as Error; cast through unknown to ApiError.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TanStack Query 5's Svelte adapter returns a Readable<QueryObserverResult>
store, not a plain object. Pages need to destructure with $-subscription:
const queryStore = $derived(createXQuery(id));
const query = $derived($queryStore);
Applied to artist + album detail page templates. Task 11 already
landed with this fix (and readable-wrapped mocks in test-utils).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rune-using test files need the .svelte. infix so vite-plugin-svelte
processes them. Also adds the flushSync() after $effect.root in the
'source already true' test so fake-timer advances fire the scheduled
timeout.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers the three browse routes (/, /artists/:id, /albums/:id) on top
of the existing /api/artists, /api/artists/{id}, /api/albums/{id},
and /api/albums/{id}/cover endpoints. Uses TanStack Query's infinite
query for the artists list with Load-more pagination, reusable card
and row components, and a shared delayed-skeleton pattern for
cache-hit navigation feel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renamed references from +page.test.ts to login.test.ts throughout,
matching the fix in 8896159. Keeps the plan accurate for re-runs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
vitest's toMatchObject doesn't accept a type parameter. Removed the
annotation in Task 3 test code (and the unused ApiError import) so
re-runs of the plan don't reintroduce the type error fixed in 8a6a496.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
12 TDD tasks: deps, api client, query client, auth store, 401
interceptor, Shell, placeholder pages, login page, root layout
wiring, and final verification + branch finish. Each task writes
the failing test first, then minimum implementation, then commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers the login flow, session store, fetch wrapper, persistent Shell,
and route guarding for the first frontend feature on top of the scaffold.
Commits to TanStack Query as the data layer going forward.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four-task TDD plan for /api/albums/{id}/cover and /api/tracks/{id}/stream:
scaffold media.go helpers (Task 1), cover handler (Task 2), stream handler
(Task 3), production Mount wiring + route-registration regression (Task 4).
Uses seedTrackWithFile helper to materialize real bytes on disk so Range /
If-Modified-Since tests hit http.ServeContent's real code path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Self-review fixups: make "first track" explicit (by disc/track order) and
replace the awkward "so existing tests continue to compile" phrasing with a
clearer description of why newLibraryRouter needs extending.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>