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>
Minstrel
Self-hosted music server with OpenSubsonic compatibility plus an extended API for server-side smart shuffle, a dual-like model (general + contextual), session-aware radio, ListenBrainz scrobble/similarity, and Lidarr integration. Go + Postgres, packaged as a Docker container.
State and intelligence belong on the server, not the client.
Specs
Authoritative v1 scope lives under docs/:
- Server spec — current implementation focus.
- Client spec — Flutter companion app; work starts once the server reaches its Subsonic-compatible baseline.
Development workflow
- Day-to-day work happens on the
devbranch (or feature branches merged intodev). mainis protected — changes land only via pull request fromdev.- Production builds are cut by tagging a release (
v*) offmain.
CI / container image
Forgejo Actions handles:
- Tests on push to
devand on PRs intomain. - Container image build + push to the Forgejo registry on merge to
main(:main) and onv*tags (:<version>).
Task and milestone tracking: Fable (Minstrel project, id 12).
Development
Minstrel has two concurrent dev processes:
- Backend:
docker compose up— starts Postgres and Minstrel on:4533. - Frontend:
cd web && npm install && npm run dev— Vite dev server on:5173with HMR.
The Vite dev server proxies /api/* and /rest/* to the backend on :4533,
so session cookies work correctly when the SPA is loaded from the Vite origin.
Production build
docker build -t minstrel . runs the SvelteKit build inside a node stage,
copies the output into the golang stage, and //go:embeds it into the
final binary. The resulting container serves the SPA from / alongside the
API surfaces. No separate static-file server is required.