Commit Graph

540 Commits

Author SHA1 Message Date
bvandeusen f8684f0034 fix(server,web): update test fixtures for new New/Handler signatures 2026-05-03 18:44:34 -04:00
bvandeusen ca82862346 chore(config): rename SMARTMUSIC_ env prefix to MINSTREL_; default admin user
The legacy SMARTMUSIC_ prefix dates from when the project was specced
as "smart-music" before being renamed to Minstrel. Hard cutover to
MINSTREL_* across config.go, tests, config.example.yaml, README, and
docker-compose. Also renames SMARTMUSIC_CONFIG in cmd/minstrel/main.go
which was missed in the original audit.

Also seeds Auth.AdminBootstrap.Username = "admin" in Default() so
MINSTREL_DATABASE_URL is the only env var required for a fresh prod
deployment — the bootstrap auto-generates a password and prints it
to stderr. Drops the now-redundant MINSTREL_AUTH_ADMIN_USERNAME=admin
line from the README quickstart and docker-compose.

M1 follow-up from the #363 final review: guards
MINSTREL_BRANDING_APP_NAME and _DESCRIPTION against empty-string
overrides — empty would have shipped <title></title> to share-preview
crawlers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:40:59 -04:00
bvandeusen 841b4cd8df fix(docs/m7-363): README env vars + first-run instructions match reality 2026-05-03 17:54:03 -04:00
bvandeusen 874fe17191 docs(m7-363): operator-first README rewrite 2026-05-03 17:50:17 -04:00
bvandeusen 574bf30504 feat(web/m7-363): OG share-image placeholder + one-off generator 2026-05-03 17:47:05 -04:00
bvandeusen af6f006242 feat(web/m7-363): theme-color meta follows resolved theme 2026-05-03 17:44:01 -04:00
bvandeusen 07b228dd4e feat(web/m7-363): per-route svelte:head titles for dynamic detail pages 2026-05-03 17:40:08 -04:00
bvandeusen 9ad2e0343a feat(web/m7-363): per-route svelte:head titles for static routes
Add browser tab titles to all 18 static page routes using the pageTitle()
helper from $lib/branding. Titles follow the Minstrel · Section format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:37:13 -04:00
bvandeusen f31dbcc087 refactor(web/m7-363): Shell header reads appName() not literal 2026-05-03 17:32:57 -04:00
bvandeusen 6c23dba51b feat(web/m7-363): branding helper + window.__MINSTREL__ consumer 2026-05-03 17:30:31 -04:00
bvandeusen a4a7225542 fix(web/m7-363): vite plugin fails build if anchors drift 2026-05-03 17:29:08 -04:00
bvandeusen 9803657087 feat(web/m7-363): vite plugin injects Go template tokens at build 2026-05-03 17:26:26 -04:00
bvandeusen 98caa7ea84 test(server/m7-363): script-context XSS test asserts </script> count 2026-05-03 17:24:42 -04:00
bvandeusen 004d935512 feat(server/m7-363): branding-aware index handler with html/template 2026-05-03 17:21:30 -04:00
bvandeusen 46ea9a761a feat(server/m7-363): thread BrandingConfig through server.New 2026-05-03 17:17:52 -04:00
bvandeusen cd50ffd77d feat(config/m7-363): branding config field + defaults 2026-05-03 17:14:59 -04:00
bvandeusen ec1b3a3397 chore: untrack docs/superpowers, ignore going forward
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.
2026-05-03 15:32:43 -04:00
bvandeusen d56edadf0e docs(m7-363): branding polish design spec
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.
2026-05-03 15:27:39 -04:00
bvandeusen 7d82be864d fix(flutter): handle nested colors split in tokens.json codegen
Web tokens.json was reshaped for the SPA theme toggle (#362) with
colors split into dark/light/flat blocks. gen_tokens.dart still
expected a flat string map and threw _Map<String,dynamic> cast
errors in the flutter CI workflow. Read colors.dark + colors.flat
and merge into the existing flat namespace; light variants will be
emitted when the Flutter client gains a theme toggle as part of
#356 parity work. Adds hyphen→camelCase normalization so the new
on-action token surfaces as onAction.
2026-05-03 14:46:27 -04:00
bvandeusen 7faa7b4e1f fix(web): hide Play next / Add to queue on PlayerBar's track menu
Those actions don't have meaningful semantics for the track you're
already listening to. New TrackMenu prop hideQueueActions gates the
pair; PlayerBar passes it true. Other mount sites (track rows in
lists) keep the full 8-entry menu.
2026-05-03 14:08:29 -04:00
bvandeusen 4b101481fc fix(web): drop-up Add-to-playlist submenu, hide horizontal scroll row scrollbar
- AddToPlaylistMenu: anchor submenu bottom to parent bottom so it
  grows upward (top-0 → bottom-0) — kebab row is usually near the
  bottom of the track menu and the submenu kept extending off-screen.
- HorizontalScrollRow: scrollbar-width: none + ::-webkit-scrollbar
  display:none. Page arrows already drive the scroll; the scrollbar
  was just consuming vertical real estate.
2026-05-03 14:04:07 -04:00
bvandeusen 16a692e81a test(web/m7-362): Settings Appearance card render + selection test 2026-05-03 13:53:47 -04:00
bvandeusen dd50ce09fd test(web/m7-362): theme store unit test (default/persist/matchMedia) 2026-05-03 13:53:03 -04:00
bvandeusen 0f83b2d6ae test(web/m7-362): tokens-to-css generator unit test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:52:13 -04:00
bvandeusen e40fa0d252 feat(web/m7-362): Settings → Appearance card with dark/light/system selector 2026-05-03 13:51:25 -04:00
bvandeusen 4a04ec64f8 feat(web/m7-362): pre-paint theme application via inline FOUC script 2026-05-03 13:50:40 -04:00
bvandeusen ffed19f736 feat(web/m7-362): theme preference + resolved-theme rune store 2026-05-03 13:43:28 -04:00
bvandeusen f0bfac2555 refactor(web/m7-362): action-button labels use non-flipping text-action-fg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:42:42 -04:00
bvandeusen 5c5f9f3ec1 feat(web/m7-362): add Tailwind text-action-fg alias for non-flipping label color 2026-05-03 13:40:20 -04:00
bvandeusen 9b7dd8272e feat(web/m7-362): tokens.json dark/light/flat split + dual-block CSS generator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:39:54 -04:00
bvandeusen a9fc0c6fe8 docs(m7-362): theme toggle implementation plan (9 tasks)
Tokens schema split + dual-block generator, Tailwind text-action-fg
alias, action-button audit, theme rune store, FOUC script, Settings
Appearance card. Plus generator/store/UI tests for CI. No in-task
test runs per memory rule.
2026-05-03 13:37:33 -04:00
bvandeusen 1d6a71a825 docs(m7-362): correct dark hex values + Tailwind alias
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).
2026-05-03 13:34:40 -04:00
bvandeusen 1dcce4e304 docs(m7-362): theme toggle design spec
Web SPA dark/light/system theme. Token strategy: dual :root +
[data-theme="light"] blocks in tokens.generated.css; Tailwind
unchanged. Light palette is parchment/aged-paper. Adds
non-flipping --fs-on-action for action button labels. FOUC
prevented via inline app.html head script.
2026-05-03 13:31:18 -04:00
bvandeusen 0ce78035aa test(web): PlaylistCard cover-img assertion uses DOM query
The <img alt=""> production element renders as role="presentation"
(decorative — playlist name is in sibling text). screen.getByRole('img')
doesn't match presentation-role elements. Switch to
container.querySelector('img') so the test doesn't fight the
correct a11y choice. Production code unchanged.
2026-05-03 12:28:21 -04:00
bvandeusen e0a4d13c45 fix(web): /playlists/{id} page uses $app/state, not legacy $app/stores
svelte-check failed with 8 type errors: $app/stores' page.params.id
is typed `string | undefined`, and the page passed `id` raw to
helpers that need `string`. Two changes:

- Switch to `$app/state` (the project canonical for new pages — see
  routes/albums/[id]/+page.svelte). page.params.id is a direct
  property read, not a store subscription. Fall back to "" so id is
  always a string; SvelteKit won't actually render the page without
  a populated id, but the type-checker doesn't know that.
- Drop the now-unused `writable` import in the test file and update
  the mock to expose the new state-shape (object with .params, not a
  writable store).

The remaining a11y warnings (tabindex on role=menu/dialog, click
handlers without keyboard events, draggable div without role,
autofocus) are svelte-check warnings — they don't block the build.
Some are pre-existing in FlagPopover; others are tech debt from
slice 1's drag-and-drop and modal patterns. Address as a follow-up
polish task.
2026-05-03 11:58:10 -04:00
bvandeusen d32e1505c5 docs(config): document storage.data_dir in the example yaml
Forgot to include this in the previous commit (6d1709c). The new
config field's example block needs to be in the canonical yaml so
operators copying it in get the doc.
2026-05-03 11:38:05 -04:00
bvandeusen 6d1709caff fix(config): wire DataDir from yaml/env through to playlists service
Slice 1 of M7 #352 added Server.DataDir but left it un-populated —
the playlist cover-collage writer would have used "" as the relative
path root in production, writing to the current working directory.

- Add Storage.DataDir to Config (yaml: storage.data_dir, env:
  SMARTMUSIC_STORAGE_DATA_DIR), defaulting to "./data".
- server.New takes the data dir as a parameter; main.go threads it.
- main.go MkdirAll's the directory at startup so the playlist cover
  writer doesn't fail on first use with a missing parent.
- config.example.yaml documents the new section.

Existing internal/server/server_test.go constructs Server directly
without server.New, so no test fixture changes needed for that file.
2026-05-03 11:37:41 -04:00
bvandeusen b0a928c554 feat(web): /playlists/{id} detail page with drag-reorder (M7 #352)
Header shows collage, name, description, public/private chip, track
count, owner attribution (when not owner). Edit + delete buttons
visible to the owner only. Track list uses PlaylistTrackRow with
HTML5 drag-and-drop; drop fires PUT /tracks with the new ordered
positions and TanStack Query invalidates the playlist + index cache.

Toast surface is a placeholder browser alert in slice 1 — a real
toast is a polish task whenever it lands.
2026-05-03 11:34:05 -04:00
bvandeusen 80a6861ded feat(web): /playlists index page (M7 #352 slice 1)
Replaces the placeholder route. Two sections: "Your playlists" (owned)
and "From other users" (public). Inline create form in the header
with Enter-to-submit / Esc-to-cancel. Empty-state copy when the
operator has nothing yet. Routes to /playlists/{id} on card click via
PlaylistCard.
2026-05-03 11:22:25 -04:00
bvandeusen 71dbaaede5 feat(web): AddToPlaylistMenu submenu wired into TrackMenu (M7 #352)
The "Add to playlist…" entry that #372 reserved as a disabled slot
is now active. Submenu lists the operator's own playlists
alphabetically; "New playlist…" toggles an inline create form that
makes the playlist + appends the track in two API calls.

TrackMenu's existing test asserts the entry is enabled and opens the
submenu instead of the previous "is disabled with tooltip" check.
2026-05-03 11:20:14 -04:00
bvandeusen 4067be04a6 feat(web): PlaylistTrackRow component for M7 #352 slice 1
Variant of TrackRow specialised for playlist detail. Drag handle
visible to owner only; remove button (X) visible to owner only;
greyed-out + strikethrough when track_id is null (upstream track
removed from library). Reuses the existing LikeButton and TrackMenu
when the track is still alive.
2026-05-03 11:16:35 -04:00
bvandeusen 0eb346e0c6 feat(web): PlaylistCard component for M7 #352 slice 1
Square card with cover (or "No tracks yet" glyph fallback), name,
track count, and owner attribution when the playlist isn't the
current user's. Click navigates to /playlists/{id}.
2026-05-03 11:14:30 -04:00
bvandeusen b9830cc9cc feat(web/api): playlists helper + types + query keys (M7 #352 slice 1)
Wire shapes mirror the server's snake_case envelope. URL helpers use
the existing apiFetch + ApiError surface so error codes (not_found,
not_authorized, bad_request, server_error) propagate via the same
copyForCode chain as everything else.
2026-05-03 11:12:46 -04:00
bvandeusen c331168d3b feat(api): /api/playlists* handlers for M7 #352 slice 1
9 handlers covering create / get / list / update / delete / append /
remove / reorder / cover. Permissions enforced in the service layer
(ErrForbidden -> 403 not_authorized) so the handler is a thin
HTTP-shape adapter. /cover serves the cached collage from disk via
http.ServeFile; 404 when cover_path is nil.

Server gained a DataDir field so the playlists service can find the
collage cache; api.Mount picks up two new params (playlistsSvc and
dataDir). The stale TestRoutesRegisteredInMount Mount() call in
library_test.go was missing the tracks.Service argument added by an
earlier slice — fixed in passing.

Wire codes follow the project's existing nested errorBody envelope:
not_found, not_authorized, unauthenticated, bad_request, server_error.
The plan called for a flat envelope, but the api package's writeErr
already produces {"error":{"code":"...","message":"..."}} and
deviating here would break every existing client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:10:21 -04:00
bvandeusen 79bab14b30 feat(playlists): track operations + cover-collage generator
AppendTracks / RemoveTrack / Reorder run in single transactions and
update the denormalized rollups (track_count, duration_sec). Reorder
uses a +10000 offset to bump every row out of the position range
before writing the new positions, sidestepping PK conflicts during
rewrite.

GenerateCollage composes a 600x600 JPEG from the first 4 album
covers, with a slate-tinted fallback for missing cells. Synchronous,
called inline after every mutating operation. SVG-rasterized fallback
is a follow-up — slice 1 ships with a solid placeholder.
2026-05-03 10:25:20 -04:00
bvandeusen 5c61c10b63 feat(playlists): Service with CRUD methods (M7 #352 slice 1)
Create / Get / List / Update / Delete with the visibility model from
the spec: private by default, owner-only mutations, public read for
non-owners. Update uses sqlc's CASE-WHEN-flag pattern for PATCH-style
partial updates without writing N variants.

Delete cleans up the cached cover file from disk best-effort. Track
operations (Append/Remove/Reorder) and the collage generator land
in subsequent tasks.

Also adds playlists + playlist_tracks to dbtest.ResetDB's truncate
list so integration tests in this package start from a clean state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:17:08 -04:00
bvandeusen 1226cb7583 feat(db): playlists schema for M7 #352 slice 1
Migration 0014 adds playlists + playlist_tracks. track_id is nullable
with ON DELETE SET NULL — tracks can be removed from the library
without silently dropping playlist entries; the denormalized snapshot
(title/artist/album/duration) keeps the row legible afterwards. UI
renders such rows greyed-out.

Indexes: playlists by (user_id, updated_at DESC) and a partial public
index for cross-user discovery; playlist_tracks partial index on
track_id to support the FK SET NULL lookup.

Queries provide CRUD + rollup recompute (track_count, duration_sec)
+ append/remove primitives. Reorder is service-layer orchestrated via
raw tx.Exec; no SQL primitive needed.
2026-05-03 10:03:36 -04:00
bvandeusen e056c8b8be docs(m7): implementation plan for #352 playlists CRUD slice 1
11 tasks: migration 0014 + sqlc queries (T1), Service CRUD methods
(T2), track operations (T3), cover-collage generator (T4), 9 HTTP
handlers + service wiring (T5), frontend api helper + types + qk (T6),
PlaylistCard (T7), PlaylistTrackRow with drag handle + soft-mark
strikethrough (T8), AddToPlaylistMenu wired into TrackMenu (T9),
/playlists index page (T10), /playlists/[id] detail with drag-reorder
(T11).

Soft-mark cascade preserved end-to-end: schema (track_id nullable +
ON DELETE SET NULL with denormalized snapshot columns), service (no
cascade on track delete; rows persist), UI (greyed-out + strikethrough
for null track_id rows). Cover collage is synchronous inline 2x2
JPEG via image/jpeg stdlib; SVG fallback rasterization is a follow-up.
2026-05-03 00:59:21 -04:00
bvandeusen 65bb4e6dfd docs(m7): spec for #352 playlists CRUD (slice 1 of 3)
Manual playlists only — schema (migration 0014), backend service +
collage generator, /api/playlists* endpoints, /playlists list +
detail pages, AddToPlaylistMenu wired into TrackMenu's reserved slot.

10 locked decisions captured: private-by-default, manual-only,
api-only (no Subsonic), 2×2 collage always (glyph fills missing
cells), soft-mark cascade with denormalized snapshot, full-list
reorder, inline collage gen, composite PK on (playlist_id, position),
denormalized track_count + duration_sec rollups.

Slices 2 (system-generated daily mixes) and 3 (home-page playlists
row) are sibling cycles; per v1=full-product all three ship before
tag.
2026-05-03 00:28:34 -04:00
bvandeusen 1cf58b18e8 ci(web): exclude #374-blocked tests from vitest module discovery
describe.skip inside the file doesn't help — the SvelteKit
`notifiable_store is not a function` failure happens at module-load,
before vitest evaluates describe blocks. Add the two paths to
vitest's exclude list so the files aren't loaded at all. The skip
markers stay inside the files as a triage signal for M7 #374.
2026-05-03 00:13:04 -04:00