Make a fresh install usable out of the box and document the first-run flow
for the public-facing repo.
- Default scan_on_startup to true (Default() + config.example.yaml, which is
the live config baked into the image). Previously false, so a fresh stack
came up with an empty library and no hint to scan. Scans are incremental
(mtime skip), so the per-restart cost is just a directory walk. Re-point
the env-override test to exercise the override against the new default.
- README: add a "First run" walkthrough (register -> scan -> integrations ->
install Android app -> invite users), each grounded in a real route.
- Add docs/screenshots/ with six captures, referenced via width-constrained
<img> wrapped in a link (shrink inline + click to open full size).
API token and invite token were cropped/redacted out of the captures
before commit so no live credential lands in the public history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Agent-authored design specs and implementation plans are kept local
on the working machine; the canonical record lives in commit messages
and the running code. The 48 historical files remain in git history
and can be retrieved via git checkout <sha> -- docs/superpowers/ if
ever needed; they just stop accruing on the dev tip.
Per-route titles, MINSTREL_APP_NAME via Go template injection of the
embedded index.html, OG/Twitter meta + theme-color follow-through for
the dark/light toggle, and an operator-first README rewrite. PWA /
service worker / install icons explicitly out of scope (Flutter #356
+ Tauri post-v1 cover mobile and desktop). OG image ships as a
generated placeholder; hand-designed artwork drops in later.
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>