Commit Graph
28 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 358849de77 feat: the Wanted view becomes Collection, shaped like Lidarr: an artist catalogue first, the state sections down a left rail (M502 #5577)
release / govulncheck (push) Successful in 21s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m54s
release / integration (push) Successful in 5m33s
release / android (push) Successful in 5m40s
release / Build signed APK (releases and dev) (push) Successful in 5m56s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
The section opens on a catalogue of every artist on the list, A–Z with a
letter rail, each card showing how many of their albums are held. An artist
opens on their page: every album of theirs, held or not, with its status,
search, releases and tracks. Wanted, Missing files, Activity, Following and
Blocked sit down a left rail as the sections that show state; Held is gone,
the catalogue shows it. The section moves from /wanted to /collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:14:33 -04:00
bvandeusenandClaude Opus 5.5 ebbc49b902 feat: import with verification, and a Wanted view of its own (M502 #5522)
release / govulncheck (push) Successful in 14s
release / web (push) Failing after 15s
release / go (push) Failing after 1m16s
release / integration (push) Failing after 4m49s
release / android (push) Successful in 4m59s
release / Build signed APK (releases and dev) (push) Successful in 5m14s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
A finished download is checked against MusicBrainz before anything is
filed: the files are paired with an edition's tracks by position or path,
and AcoustID, tag ids, lengths and titles say whether it is the album.
What is the album is copied into the library as
Artist/Album (Year)/Artist - Album - NN - Title, tagged with Picard's
MusicBrainz ids, and scanned; the download keeps seeding. What is not is
refused: set aside, removed from the client, and the album searched for
again. A download the check cannot settle waits for a person, who imports
it anyway or refuses it. Tracks the library already holds are skipped.

The wanted list leaves Admin for a top-level Wanted view, admin-only and
web-only, with tabs for Wanted, Activity (downloads and their imports,
the ones needing a person first), Missing files, Held, Following (with the
release-type profiles, moved off Integrations) and Blocked. The search
settings gain "File new music into" when the library has several folders.

The search pass logs why each failed search failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:36:21 -04:00
bvandeusenandClaude Opus 5.5 94a9c8cbe3 chore(deps): SvelteKit 3 with adapter-static 4 and TypeScript 6, full-tree npm audit (#5021)
Merges Renovate's kit 3 (PR #149) and adapter-static 4 (PR #148) bumps,
plus the migration they need. The mechanical part is `sv migrate
sveltekit-3`, run one task at a time and reviewed:

- svelte.config.js is gone. Its options move into sveltekit() in
  vite.config.ts, exported as kitOptions so vitest.config.ts runs the
  same kit setup, including the $test-utils alias the tests import.
- $lib becomes #lib through package.json "imports". There is no
  src/lib/index, so only the "#lib/*" entry is kept.
- tsconfig extends $app/tsconfig.
- Peer floors raised to kit 3's requirements: svelte ^5.57.1, vite
  ^8.0.12, svelte-check ^4.7.5.

By hand, from the codemod's list of non-automated tasks:

- goto's replaceState option is now replace; keepFocus becomes
  reset: false. For the search typeahead, reset: false also stops the
  scroll-to-top, which is wanted while typing.
- The test setup mocks drop pushState/replaceState and $app/paths
  base/assets, which kit 3 removed, and mock refreshAll in place of
  invalidateAll.
- The other flagged files only read page.url or goto internal routes,
  so they needed no change.

TypeScript goes to ^6, not the ^7 Renovate offers: kit 3 declares
typescript ^6 as a peer and svelte-check 4.7 accepts ^5 || ^6. Move to
7 once both accept it.

With Tailwind 4 and kit 3 in, `npm audit` on the whole tree reports 0,
so the web lane now audits every dependency rather than only what
ships to browsers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:50:39 -04:00
bvandeusenandClaude Opus 5.5 fecd030b68 chore(deps): Tailwind 4 (#5021)
release / govulncheck (push) Successful in 42s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m53s
release / integration (push) Successful in 5m37s
release / android (push) Successful in 6m54s
release / Build signed APK (releases and dev) (push) Successful in 7m27s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Failing after 34s
release / Verify release artifacts (tag releases only) (push) Skipped
Renovate's tailwindcss bump (PR #150) plus the migration it needs:

- Theme moves from tailwind.config.js into app.css as `@theme inline`,
  mapping the same FabledSword tokens. tailwind.config.js is gone.
- PostCSS runs @tailwindcss/postcss; autoprefixer is dropped, since
  Tailwind 4 prefixes through Lightning CSS.
- Class renames from @tailwindcss/upgrade 4.3.3, reviewed: outline-none
  -> outline-hidden, focus-visible:outline -> outline-solid, shadow ->
  shadow-sm, shadow-sm -> shadow-xs, flex-shrink-0 -> shrink-0. Bare
  `rounded` stays: v4 keeps it at 0.25rem, as before.
- Three v3 preflight defaults kept in a base layer so nothing changes on
  screen: gray-200 default border colour, gray-400 placeholder text and
  the pointer cursor on buttons.
- The unused class-based dark variant is not carried over; no template
  uses `dark:`.

Clears the five high and two moderate npm audit findings that came in
through Tailwind 3 (braces, chokidar, micromatch, fast-glob,
postcss-selector-parser).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:46:07 -04:00
bvandeusenandClaude Opus 5.5 956058d4a0 feat(web): notifications bell, unread badge and inbox panel in the header (#5342)
- The bell sits between search and the user menu. Its badge is
  parchment on obsidian, not the accent, which the house style keeps
  off general chrome. It counts up to 9, then shows 9+.
- The panel lists the server-rendered title, body and relative time,
  newest first, with unread rows marked. Clicking a row marks it read
  and opens its link. "Mark all read" appears while anything is unread,
  and an empty inbox says "Nothing waiting for you."
- createNotificationsQuery and createUnreadCountQuery poll every 60s
  while the tab is visible. The `notification.created` live event
  invalidates ['notifications'] so the badge and list refresh promptly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:23:32 -04:00
bvandeusenandClaude Opus 5 5593f7ce17 feat(brand): replace the M mark with the traced bard-hat logo
test-web / test (push) Successful in 44s
android / Build + lint + test (push) Successful in 5m9s
The mark is now a feathered hat with an arc of eighth notes, traced from
the operator's reference artwork at 99.74% IoU. The hat takes the text
colour and the note arc holds the accent — the same construction the M
used, and for the same reason: parchment on a light surface is invisible,
so the silhouette has to flip with its background while the accent stays
constant.

This reverses the subject-neutrality argument recorded in Minstrel's
design system, which held that depicting a bard would tell a new user the
app is for renaissance-faire music and had twice rejected a hat. The
operator commissioned this artwork and chose it with that objection on the
table; the record is updated rather than silently contradicted.

Both accent-filled alternatives were measured and rejected: #4A6B5C is
3.04:1 on obsidian and 2.80:1 on the raised iron, so an accent hat drops
under the 3:1 graphics floor as soon as it sits on a card.

tools/gen-brand-assets.py is the single source for the four copies, which
cannot share a file because each needs a different colour mechanism —
currentColor inlined, a prefers-color-scheme swap in the favicon, literal
fills in mark.svg, flat pixels in the rasters. Hand-copying 20KB of path
data four ways is how a silhouette change lands in three of them.

Two notes on the trace, both non-obvious: it runs on the original
antialiased greyscale rather than a binary mask, because tracing a
supersampled mask scores ~100% IoU by reproducing the pixel staircase
exactly — a perfect number for jagged art at 120KB of path, versus 99.74%
at 20KB. And potrace reads PBM where bit 1 is black, so the ink mask is
inverted going in; backwards, it traces the background and still emits a
plausible-looking SVG.

The header lockup moves 20px → 28px: the hat carries far more detail than
the M and does not resolve below ~32px. The 16px browser-tab favicon is
still a blob at that size and is not addressed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
2026-09-09 12:52:57 -04:00
bvandeusenandClaude Opus 5 e1e591b520 feat(brand): Minstrel mark — favicon, header lockup, Android adaptive icon
test-web / test (push) Successful in 45s
android / Build + lint + test (push) Successful in 4m14s
A Didone M whose right leg is an eighth note: stem, flag and notehead in the
accent, the letter in parchment. Traced from the operator's reference at
99.74% IoU (potrace, 26 + 22 segments), so the geometry is theirs, not an
approximation of it.

Subject-neutral on purpose. "Minstrel" pulls toward a lute or a bard, which
would tell a new user this is a renaissance-faire player rather than one for
all music. A geometric letter plus universal notation says "music" without
saying which music. The family look arrives through palette and drawing
style instead of through the subject — see the design-system discussion.

Starting state: web/static/favicon.png was a 1x1 PIXEL placeholder, so there
was effectively no favicon at all; Android had legacy bitmaps only, so modern
launchers letterboxed the square instead of masking it.

## The colour problem, and why each surface differs

Parchment on white is invisible — the operator caught this. The M therefore
has to flip with its background, while the accent note holds in both:

  - mark.svg / MinstrelMark.svelte use currentColor, so the letter takes the
    surrounding text colour and one asset covers both palettes.
  - favicon.svg bakes colours with a prefers-color-scheme swap, because a
    favicon sits on browser chrome and has no cascade to inherit from.
  - PNG fallback, apple-touch-icon and Android are PLATED. A PNG can't
    respond to scheme and iOS composites onto white regardless.

MinstrelMark is inlined rather than <img src>, because an <img> cannot
inherit currentColor and inheriting it is the entire point.

## Plate colour chosen by measurement

Obsidian (#14171A), not the raised-surface iron. The accent note only clears
the 3:1 non-text contrast threshold against the darker value: 3.04:1 vs iron's
2.70:1. My own earlier suggestion — lighten the plate — is WRONG and the
numbers say so: slate scores 2.21:1, worse, because the note is a dark colour
and lifting the plate closes the gap. Recorded in colors.xml so the reasoning
sits with the value.

## Construction

Traced as a full ink silhouette with the note painted OVER it, rather than as
two separate shapes. Separate shapes needed either a 2px seam where letter and
note touch, or an anti-aliasing fringe (2,430 misclassified pixels) around the
note. Painting over avoids both and yields a monochrome version for free — the
base layer alone is the whole mark in one colour, which is what
mipmap-anydpi-v26's <monochrome> uses for themed icons.

Android foreground sits at 61% of the 108dp canvas so it stays inside the
66dp safe zone and no launcher mask can clip it.

Paths are duplicated between the component and the two static SVGs, since one
needs currentColor and the others need literals. A comment in each names the
others.

Verified by render at 16/20/32/64/180 on obsidian, white, parchment and
plated; one optical size holds across the whole range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 16:37:25 -04:00
bvandeusen 27f1cefd55 fix(web): center nav in window + Library link + Search placeholder
test-web / test (push) Failing after 31s
Three operator-reported issues:

- Header was flex with the nav inside a flex-1 span — when the
  right side (search + user menu) grew wider than the left
  (wordmark), the nav's centered position drifted off page-center.
  Switched to grid-cols-3 with justify-self-{start,center,end} so
  the middle column pins to true window-center regardless of side
  widths.
- Library nav link pointed to /library, which 308-redirects via
  +page.server.ts. Operator reported it didn't navigate. Linked the
  nav button directly to /library/artists so SPA navigation skips
  the redirect roundtrip. matchPrefix='/library' keeps isActive
  matching every Library tab.
- SearchInput placeholder was 'Search artists, albums, tracks…' —
  shortened to 'Search' and added a Lucide Search icon inside the
  input on the left. Padding adjusted (pl-7) so the input text
  clears the icon.
2026-06-01 20:58:05 -04:00
bvandeusen a92f2c0121 feat(web): radial gradient + playlist title overlay + tile-size hierarchy
test-web / test (push) Failing after 33s
Second wave of Home visual polish (UI tasks 80/82/84):

- Shell main background gets a 1200x600 radial gradient at the top
  using color-mix on fs-iron so the page reads with subtle depth
  instead of as a flat slab.
- PlaylistCard moves the title onto the cover with a bottom-to-top
  gradient overlay. The server-generated 2x2 collage becomes mood
  texture; the Fraunces playlist name becomes the dominant element.
  Track count and refresh label stay below the art. Mirror albums
  shadow-sm/shadow-lg/ring-accent hover treatment on the art-wrap.
- Tile-size hierarchy: Playlists w-52, Recently added w-44,
  Rediscover w-40. Visual weight tapers as you scroll down the
  page so the freshest content reads as most important.
2026-06-01 19:57:05 -04:00
bvandeusen 6e6fbc7856 feat(web): top-bar centered nav + Library tab page (replace sidebar)
test-web / test (push) Successful in 32s
Operator 2026-06-01: "navigation layout and library sections are
what I'd like to have implemented as it seems better than our
current navbar solution. I think I'd like to have these nav options
moved into the top bar centered."

Top-bar restructure:
- Centered nav (replaces the 192dp left sidebar): Home / Library /
  Discover, with icons + labels. Labels collapse below sm breakpoint
  so the bar stays icon-only on small viewports.
- Right side (search input + user dropdown) unchanged.
- Hamburger button + MobileNavDrawer + the mobileNav store all
  removed - the centered nav lives at all viewport sizes.

Library page restructure (mirrors Android LibraryScreen):
- New routes/library/+layout.svelte renders a tab bar across the
  five Library sub-pages: Artists / Albums / Liked / History /
  Playlists. Active tab gets an accent underline + onSurface text.
- routes/library/+page.server.ts redirects bare /library to
  /library/artists (Android default tab).
- /playlists (list) moved to /library/playlists; old URL gets a 308
  redirect (routes/playlists/+page.server.ts) so existing bookmarks
  land on the new location. /playlists/[id] (detail) is unchanged -
  matches the server API URL shape.

Deleted: Shell's sidebar markup, MobileNavDrawer.{svelte,test.ts},
the mobileNav store, the old routes/playlists/+page.svelte. Shell
test rewritten to assert the new 3-item centered nav; playlists
test moved next to its new +page.svelte and its test-utils import
path updated.
2026-06-01 10:07:02 -04:00
bvandeusenandClaude Opus 4.7 df4cb434b2 feat(web): hamburger trigger + mobile drawer mount in Shell
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:17:35 -04:00
bvandeusen c04b288737 feat(web/m7-365): add History entry to Library nav 2026-05-04 06:59:05 -04:00
bvandeusen f31dbcc087 refactor(web/m7-363): Shell header reads appName() not literal 2026-05-03 17:32:57 -04:00
bvandeusenandClaude Opus 4.7 65651510fb feat(web): add chevron + aria affordances to user-menu button
The username was rendered as plain text with no visual cue that it
opened a dropdown. Adds a 16px Lucide ChevronDown to the right of the
name (rotates 180° when open) plus the canonical aria-haspopup='menu'
and aria-expanded={menuOpen} attributes so screen readers announce the
button as a menu opener and reflect open/closed state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:54:14 -04:00
bvandeusenandClaude Opus 4.7 7b1cd50cb9 fix(web): user-menu links navigate client-side, not via full reload
The username dropdown's container had onclick={(e) => e.stopPropagation()}
to keep the window-level close-on-outside-click handler from firing on
inside clicks. But that also blocked SvelteKit's document-level link-click
interceptor, so clicking Settings/Admin inside the menu fell through to
the browser's native navigation — a full-page reload.

Symptom: clicking Admin redirected to /. On the hard reload, the admin
guard (/admin/+layout.ts) ran before bootstrap() had settled the user
store; user.value was still null, so the guard threw redirect(302, '/').
By the time you saw / render, bootstrap had finished and the dropdown
showed Admin again, primed to repeat the cycle.

Replaces the stopPropagation-based outside-click protection with a
target-containment check in the window handler: close only when the click
landed outside both the menu button and the menu container. Drops the
redundant stopPropagation on the menu button too (no longer needed since
the window handler now ignores in-menu clicks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:51:05 -04:00
bvandeusenandClaude Opus 4.7 a5a6a10c7b fix(web): give user-menu dropdown z-50 so clicks don't fall through
The Shell's username dropdown had no z-index, so the layout's later
grid siblings (left nav, main content) painted over it. Visible
symptom: clicking Admin actually clicked the Home link underneath,
producing a quick flash followed by a redirect to /. Adding z-50 to
the popup container makes clicks land where they look like they should.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:36:03 -04:00
bvandeusenandClaude Opus 4.7 22ac86f200 feat(web): trim main nav; pair Discover+Requests; move chrome to user menu
The main nav was carrying surfaces that didn't belong on it:

- Search led to an empty page when clicked (you only ever want to land
  there from the global SearchInput typing into /search?q=...). Drop it.
- Requests is downstream of Discover (you discover, then you request).
  Lift both onto a shared horizontal tab strip; keep their URLs so
  bookmarks survive. New DiscoverTabs component used by both pages.
- Settings + Admin are operator chrome, not primary surfaces. Move them
  into the username dropdown alongside Log out, with Admin gated on
  is_admin. Users see Settings + Log out; admins see Settings + Admin
  + Log out.

Final main nav: Home / Artists / Albums / Liked / Discover / Playlists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:23:56 -04:00
bvandeusenandClaude Opus 4.7 a598ea1418 feat(web): move Hidden tracks out of main nav, surface from Settings
Hidden tracks (M5b quarantine) is a low-frequency surface — it doesn't
warrant a permanent slot in the main nav. Moves the link under a new
'Library' section card on /settings alongside ListenBrainz config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:14:01 -04:00
bvandeusenandClaude Sonnet 4.6 4412a4af0c feat(web): update nav (Home/Artists/Albums); retire ArtistRow
Add Artists and Albums nav entries, rename root label from Library to
Home. Migrate all three ArtistRow call sites (search, search/artists,
library/liked) to ArtistCard with grid wrapper; delete ArtistRow
component and its test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:27:53 -04:00
bvandeusenandClaude Opus 4.7 0d7a65cffb feat(web): /library/hidden user-facing quarantine view
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:35:11 -04:00
bvandeusenandClaude Opus 4.7 bfd48f5a02 feat(web): add /admin layout with role-gated load + sidebar
Hard route gate in admin/+layout.ts redirects non-admins to / before
the layout (or any child) renders. AdminSidebar reads the active route
from $app/state and applies a 12% accent-tinted bg + 2px forest-teal
left strip on the active item. Overview landing shows pending-request
count and Lidarr connected/unset, each linking to its sub-page. Shell
nav exposes the Admin link only to is_admin users.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:48:26 -04:00
bvandeusenandClaude Opus 4.7 ad904afaf6 feat(web): add /requests user-facing request history
Renders the caller's Lidarr requests as rows with kind pill,
StatusPill, and per-status actions: Cancel on pending (which
calls cancelRequest then invalidates qk.myRequests()), Listen
link on completed (deepest match wins: track > album > artist),
admin notes on rejected. Empty state uses the voice-rule
"Nothing requested yet." copy. Shell nav gains /requests
between /discover and /playlists, visible to all authed users
since the view is per-user.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:24:17 -04:00
bvandeusenandClaude Opus 4.7 8a12b8c571 feat(web): add /discover route with search + request flow
Local-input + 250ms debounce drives Lidarr search; tab switch refetches
with new kind. Track-kind Request opens a confirm modal explaining the
album that will be added; Confirm fires createRequest, Cancel is a no-op.
Successful requests flip the card to 'requested' optimistically.
Shell nav now exposes /discover between Search and Playlists.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:15:52 -04:00
bvandeusenandClaude Sonnet 4.6 13e12d97ae feat(web): add /settings page with ListenBrainz section
Adds api.put helper, ListenBrainz API module with query/mutation
helpers, a /settings route with token + enable/disable UI, and 5
tests. Adds Settings to the Shell nav.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:45:42 -04:00
bvandeusen de5320a7b4 feat(web): wire LikeButton into TrackRow, AlbumCard, ArtistRow, PlayerBar
Heart appears on every entity rendering surface. ArtistRow restructured
to a <div> with the link as a positioned overlay so the heart button
can nest without invalid <button>-in-<a> markup. Shell nav grows a
'Liked' entry pointing at /library/liked.
2026-04-26 16:56:07 -04:00
bvandeusen ad9c984b62 feat(web): mount SearchInput in Shell header
Header grows a flex-1 column between the brand and the user menu.
Existing Shell tests remain green (the search input has no impact on
nav/menu behavior).
2026-04-25 16:12:19 -04:00
bvandeusen ccef9be2d3 feat(web): add PlayerBar slot to Shell
Grid grows to 3 rows (auto / 1fr / auto); PlayerBar spans both columns
in the last row, rendering only when player.current is defined so the
row collapses before first play.
2026-04-24 21:42:10 -04:00
bvandeusen e1504f8e6c feat(web): add Shell component with header, sidebar, and user menu
Persistent chrome used by every authenticated route. Sidebar hides
below md:; mobile nav is a separate concern for a later plan.
2026-04-22 22:20:18 -04:00