Typed fetch layer (lib/api/client.ts): apiFetch + api.{get,post,del} with typed ApiError{code,message,status} and auto-logout on 401 via dynamic import (breaks the apiFetch ↔ auth/store cycle).
Rune-based auth store (lib/auth/store.svelte.ts): user, bootstrap, login, logout — bootstrapped once in +layout.ts so refresh never flashes the login screen.
TanStack Query installed and wired via <QueryClientProvider> at the root layout; queryClient is cleared on logout so the next user doesn't see stale data.
Persistent Shell (lib/components/Shell.svelte): header with user-menu logout + sidebar nav (Library / Search / Playlists). Sidebar hides below md:; mobile nav is a later plan.
Login page (/login): typed error UX — "Invalid username or password" vs "Sign-in unavailable" — with returnTo support that blocks open-redirect vectors (//evil.com, http://evil.com, /login, ../admin).
Route guard in +layout.svelte via $effect — unauthed users are sent to /login?returnTo=…, authed users on /login go to /.
Placeholder pages for /, /search, /playlists so the sidebar works end-to-end before the real feature plans land.
Dev infra: Testing Library + jest-dom wired into Vitest; renamed one route-colocated test to login.test.ts to avoid the +page.* SvelteKit route-walker collision.
Test Plan
go test -short -race ./... passes (unchanged; backend untouched)
golangci-lint run ./... clean
cd web && npm run check → 0 errors / 0 warnings
cd web && npm test → 29 tests across 5 files pass
cd web && npm run build succeeds (adapter-static emits web/build/)
docker build . builds; binary is -x in the final image
Manual: docker compose up -d → POST /api/auth/login returns 200 + Set-Cookie: minstrel_session=... (verified with seeded admin user)
Forgejo CI green on this PR
## Summary
- **Typed fetch layer** (`lib/api/client.ts`): `apiFetch` + `api.{get,post,del}` with typed `ApiError{code,message,status}` and auto-logout on 401 via dynamic import (breaks the `apiFetch ↔ auth/store` cycle).
- **Rune-based auth store** (`lib/auth/store.svelte.ts`): `user`, `bootstrap`, `login`, `logout` — bootstrapped once in `+layout.ts` so refresh never flashes the login screen.
- **TanStack Query** installed and wired via `<QueryClientProvider>` at the root layout; queryClient is cleared on logout so the next user doesn't see stale data.
- **Persistent Shell** (`lib/components/Shell.svelte`): header with user-menu logout + sidebar nav (Library / Search / Playlists). Sidebar hides below `md:`; mobile nav is a later plan.
- **Login page** (`/login`): typed error UX — "Invalid username or password" vs "Sign-in unavailable" — with `returnTo` support that blocks open-redirect vectors (`//evil.com`, `http://evil.com`, `/login`, `../admin`).
- **Route guard** in `+layout.svelte` via `$effect` — unauthed users are sent to `/login?returnTo=…`, authed users on `/login` go to `/`.
- **Placeholder pages** for `/`, `/search`, `/playlists` so the sidebar works end-to-end before the real feature plans land.
- **Dev infra**: Testing Library + jest-dom wired into Vitest; renamed one route-colocated test to `login.test.ts` to avoid the `+page.*` SvelteKit route-walker collision.
## Test Plan
- [x] `go test -short -race ./...` passes (unchanged; backend untouched)
- [x] `golangci-lint run ./...` clean
- [x] `cd web && npm run check` → 0 errors / 0 warnings
- [x] `cd web && npm test` → 29 tests across 5 files pass
- [x] `cd web && npm run build` succeeds (adapter-static emits `web/build/`)
- [x] `docker build .` builds; binary is `-x` in the final image
- [x] Manual: `docker compose up -d` → `POST /api/auth/login` returns 200 + `Set-Cookie: minstrel_session=...` (verified with seeded admin user)
- [ ] Forgejo CI green on this PR
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
lib/api/client.ts):apiFetch+api.{get,post,del}with typedApiError{code,message,status}and auto-logout on 401 via dynamic import (breaks theapiFetch ↔ auth/storecycle).lib/auth/store.svelte.ts):user,bootstrap,login,logout— bootstrapped once in+layout.tsso refresh never flashes the login screen.<QueryClientProvider>at the root layout; queryClient is cleared on logout so the next user doesn't see stale data.lib/components/Shell.svelte): header with user-menu logout + sidebar nav (Library / Search / Playlists). Sidebar hides belowmd:; mobile nav is a later plan./login): typed error UX — "Invalid username or password" vs "Sign-in unavailable" — withreturnTosupport that blocks open-redirect vectors (//evil.com,http://evil.com,/login,../admin).+layout.sveltevia$effect— unauthed users are sent to/login?returnTo=…, authed users on/logingo to/./,/search,/playlistsso the sidebar works end-to-end before the real feature plans land.login.test.tsto avoid the+page.*SvelteKit route-walker collision.Test Plan
go test -short -race ./...passes (unchanged; backend untouched)golangci-lint run ./...cleancd web && npm run check→ 0 errors / 0 warningscd web && npm test→ 29 tests across 5 files passcd web && npm run buildsucceeds (adapter-static emitsweb/build/)docker build .builds; binary is-xin the final imagedocker compose up -d→POST /api/auth/loginreturns 200 +Set-Cookie: minstrel_session=...(verified with seeded admin user)