Commit Graph

17 Commits

Author SHA1 Message Date
bvandeusen 4e3bd46d69 fix(server,web/m7-353): post-review fixes (test compilation, AlbumRef field, bulk handler, design system)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:21:24 -04:00
bvandeusen 28617df5bd feat(web/m7-353): admin cover-refetch UI (per-album + bulk)
Adds cover_art_source to AlbumRef, two admin API helpers (refetchAlbumCover,
refetchMissingCovers), a per-album retry button gated on is_admin in the album
detail page, and a bulk-refetch section in the admin overview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:15:26 -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 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 c3be0f3e6e refactor(web): consolidate error-copy table to JSON source of truth
Three admin pages had divergent inline error-code → user-copy switch
tables (with wording drift, including a stale "Settings → Integrations"
that should have been Admin). Consolidate into web/src/lib/styles/error-copy.json,
expose via web/src/lib/api/error-copy.ts (ERROR_COPY + copyForCode),
and refactor the three pages to import the helper.

Fixes the three-way drift; the only user-visible behavior change is
the quarantine page now correctly says "Admin → Integrations".

Sets up the JSON for the M7 Flutter client (#356) to consume the same
table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:01:46 -04:00
bvandeusen f77245bc41 feat(requests): show ingest progress while requests are in flight
After Lidarr accepts a request the local reconciler imports albums and
tracks as they arrive — but until the request hit 'completed' the
operator had no way to see "is anything happening?" The /requests and
/admin/requests rows now surface a live progress line whenever the
request's matched entity has children in our library.

Backend:
- New CountAlbumsByArtist + CountTracksByArtist sqlc queries.
- requestView gains imported_album_count and imported_track_count.
- New fillProgress helper computes them from the matched entity:
  - kind=artist  → counts albums + tracks under matched_artist_id
  - kind=album   → counts tracks under matched_album_id
  - kind=track   → 1 once matched_track_id is set
  N+1 in the list endpoints; acceptable at admin scale.
- handleListRequests, handleGetRequest, and handleListAdminRequests
  populate the new fields on every response.

Frontend:
- LidarrRequest TS type extended with the two counters.
- Both the operator's /requests page and the admin /admin/requests
  page render an accent-colored line under the row meta when at
  least one counter is non-zero, e.g.:
    Artist:  "5 albums · 47 tracks ingested"
    Album:   "12 tracks ingested"
    Track:   "Track ingested"
- Updated test fixtures to include the new required fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:04:38 -04:00
bvandeusen 7b91718fc9 feat(admin): expand /admin overview into an actionable workbench
The admin overview was just two stat cards. Promotes it to a proper
workbench by surfacing the top-5 of each actionable queue with inline
handlers, while keeping the dedicated /admin/requests and
/admin/quarantine pages as the deep-action surfaces.

Changes:

- Top stats grow from 2 cards to 3 (requests / quarantine / lidarr).
- New "Pending requests" preview list — top-5 rows with inline Approve
  and Reject buttons that call the same approveRequest/rejectRequest
  endpoints used on the dedicated page (default profiles, no override
  modal — that flow stays on /admin/requests for power users).
- New "Quarantine" preview list — top-5 rows with three inline actions
  matching the dedicated page: Resolve (single click), Delete file
  (two-click confirm with inline "Confirm?" state), Delete via Lidarr
  (two-click confirm). The two-click confirm avoids modal duplication
  on the overview while still gating the irreversible operations.
- "View all <N> →" link surfaces beneath each preview only when the
  full queue exceeds the preview limit, pointing to the dedicated page.
- Shared toast surface with errorCopy mapping for the same lidarr_*
  codes the dedicated requests page handles, so failed actions surface
  meaningful text instead of "unknown".

Test fixture migrated from per-test dynamic imports to top-level
imports — the previous pattern was producing stale-mock symptoms
where mockReturnValue was set after the page bound against the
original mocks. 12 tests cover stat counts, empty states, links,
each handler binding, the two-click confirm pattern, and the
"View all" surfacing rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:51:57 -04:00
bvandeusen c265b871c3 feat(admin): metadata-profile picker on integrations page
Persists the operator's metadata-profile choice alongside quality
profile + root folder. Defaults to the first profile Lidarr returns
(usually 'Standard' on a vanilla install) so the common case is
zero-click; operators with custom profiles like 'Singles only' can pick
explicitly.

Backend:
- Migration 0013: adds nullable default_metadata_profile_id to
  lidarr_config. Existing rows get NULL and the service falls back to
  fetch-and-pick-first until they save.
- Updated lidarr_config queries + sqlc + lidarrconfig.Config + admin
  view/put body to round-trip the new field.
- handlePutLidarrConfig requires it (along with QP and root folder)
  when enabled=true — matches the existing missing_defaults gate.
- New GET /api/admin/lidarr/metadata-profiles handler + lidarr.Client
  ListMetadataProfiles (GET /api/v1/metadataprofile, same shape as
  the quality-profile endpoint).
- lidarrrequests.Approve prefers cfg.DefaultMetadataProfileID; falls
  back to the fetch-list path only when 0 (back-compat for upgraders).

Frontend:
- LidarrConfig type + LidarrMetadataProfile type + qk.lidarrMetadataProfiles.
- listMetadataProfiles + createMetadataProfilesQuery client helpers.
- Integrations page: third <select> picker, auto-defaults to first
  profile when the saved value is 0, sends the new field on save and
  clears it on disconnect.
- Updated test fixtures + the duplicate 'Standard' option string in
  the dropdown-populates assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:09:13 -04:00
bvandeusen 7d33166f23 fix(lidarr): include artistName + metadataProfileId on add-artist/add-album
The 'lidarr_rejected' the operator was hitting on Approve was Lidarr 4xx-ing
the POST with two field-validation errors:

  'Metadata Profile Id' must be greater than '0'.
  'Artist Name' must not be empty.

Our payload was missing both. Lidarr's metadata profile is a separate
concept from quality profile (it controls which release types are
tracked: albums, singles, EPs, etc.) and is required by /api/v1/artist
and /api/v1/album. The 'already exists' line in the toast copy was an
incorrect guess at the cause; the real issue was an invalid payload.

Changes:

- internal/lidarr/types.go: AddArtistParams + AddAlbumParams gain
  ArtistName and MetadataProfileID. New MetadataProfile struct mirroring
  QualityProfile.
- internal/lidarr/client.go: AddArtist + AddAlbum payloads include both
  new fields. New ListMetadataProfiles fetches GET /api/v1/metadataprofile.
- internal/lidarrrequests/service.go: Approve fetches the metadata
  profile list and uses the first as a default (Lidarr installs ship
  'Standard' at id=1 OOB, so this works for vanilla setups). Picker
  on /admin/integrations is a follow-up. ArtistName flows from the
  request row.
- web/src/routes/admin/requests/+page.svelte: drop the
  speculative 'usually means already in library' copy on lidarr_rejected;
  point operators at server logs for the field-level reason instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:57:39 -04:00
bvandeusen ab8235dd0b fix(admin): approve flow surfaces real errors; auto-default Lidarr picks
The "I can't approve a request, the toast just says unknown" bug was
four problems compounded:

1. Lidarr config let enabled=true save with default_quality_profile_id=0
   and default_root_folder_path=''. Approve then sent invalid POST bodies
   to Lidarr, which 5xx'd.
2. lidarr.client.post() discarded Lidarr's response body on error, so
   we couldn't tell why Lidarr 5xx'd from server logs.
3. handleApproveRequest's error switch didn't map ErrServerError or
   ErrLookupFailed — both fell through to a generic 500 server_error.
4. apiFetch only parsed {error: {code, message}} envelopes, but admin
   endpoints write {error: 'code_string'}. Every admin error toast
   rendered as 'unknown'.

Fixes:

- internal/lidarr/client.go: capture up to 512 bytes of Lidarr's response
  body when it returns 4xx/5xx; include in the wrapped error so server
  logs show what Lidarr actually said instead of just the status bucket.
- internal/lidarrrequests/service.go: new ErrDefaultsIncomplete fires
  before the Lidarr call when QP=0 or root_folder=''. Stops the bad
  POST entirely.
- internal/api/admin_requests.go: handleApproveRequest now maps
  ErrDefaultsIncomplete -> 'lidarr_defaults_incomplete' (400),
  ErrServerError -> 'lidarr_server_error' (502),
  ErrLookupFailed -> 'lidarr_rejected' (502).
- internal/api/admin_lidarr.go: handlePutLidarrConfig now requires
  QP + root folder to be set whenever enabled=true.
- web/src/lib/api/client.ts: apiFetch handles both error envelope shapes
  so admin error codes propagate to toasts.
- web/src/routes/admin/integrations/+page.svelte: auto-default to the
  first quality profile and first root folder Lidarr returns when the
  operator hasn't picked one yet — saves a click for typical
  one-profile/one-folder home setups.
- web/src/routes/admin/requests/+page.svelte: friendly toast copy for
  the new error codes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:37:17 -04:00
bvandeusen 14c1895beb feat(web): flatten admin shell — horizontal tabs, drop redundant banner
The /admin layout had three nested concentric shells: the global Shell
header + main nav, then a per-admin header banner with a Sword icon, then
a left-aligned admin sub-nav (AdminSidebar) — two competing left-edge
navigation columns plus a redundant 'Admin' label that the URL and active
nav already conveyed.

Replaces the side sub-nav with a horizontal tab strip across the top of
the admin content (matches the discover-page tab pattern), drops the
duplicate header banner, and removes the disabled Users/Library
placeholder items so the tab strip only shows surfaces that actually
ship today. The component is renamed AdminSidebar -> AdminTabs to match
its new shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:14:39 -04:00
bvandeusen 3bfec944c7 feat(web): /admin/quarantine aggregated queue with resolution actions
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:34 -04:00
bvandeusen ab9be40bb2 feat(web): add /admin/requests approval queue with override modal
Pending/Approved/Completed/Rejected tabs (active count only). Override modal
collapsed by default; reject modal with notes textarea. Lidarr-unreachable
toast surfaces sync-Approve failures. Cancel buttons in both modals are
Pewter ghost; Reject/Confirm reject use Bronze (the design system reserves
Oxblood for irreversible actions like Disconnect).
2026-04-29 23:31:18 -04:00
bvandeusen b895e9ef7e feat(web): add /admin/requests approval queue with override modal
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:24:34 -04:00
bvandeusen b1b50187b3 fix(web): integrations panel review fixes
- Trim whitespace before checking the typed-confirm value (paste-with-spaces
  was silently keeping the button disabled).
- Wrap onConfirmDisconnect in try/catch and surface disconnect_failed inline
  in the modal — operators on flaky networks now see why nothing happened.
- Invalidate quality-profile + root-folder caches alongside config on both
  save and disconnect, so a base_url change refetches the lists immediately.
2026-04-29 23:11:32 -04:00
bvandeusen c281c8b5dd feat(web): add /admin/integrations Lidarr connection panel
Connection management for Lidarr — base URL, API key, default quality
profile, default root folder. Empty api_key on save preserves the
masked-saved key per backend semantics. Disconnect requires typed
"DISCONNECT" confirmation. Includes a placeholder MusicBrainz overrides
section for future integrations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:06:19 -04:00
bvandeusen 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