6.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Web UI for a self-hosted music service (MCMA). Role: control center — library, downloads, storage, users/permissions. Playback exists (persistent player) but is secondary to a future mobile client.
Commands
npm run dev— dev server at http://localhost:3000npm run build— production buildnpm run preview— preview production buildnpm run lint— rslint (@rslint/core, native-speed lint)npm run test— rstest (run once);npm run test:watch— watch modenpm run format— prettiernpx tsc --noEmit— typecheck (strict; this is the main correctness gate, lint does not type-check)
Single test: npx rstest tests/index.test.tsx (path or -t <name pattern>).
Stack (fixed — do not swap)
React 19 + TypeScript strict · rsbuild (not Vite/CRA) · RTK Query for all server state · Redux Toolkit slices for client state · React Router · modern-sk UI kit. React Compiler is enabled via babel-plugin-react-compiler — do not hand-add useMemo/useCallback for render-identity reasons; the compiler handles memoization.
Hard rules (architecture invariants)
- No hardcoded backend address except the env default. Runtime override is mandatory (see base-URL resolution below).
- No
fetchin components. All backend access goes through RTK Query hooks. The entire API surface is isolated insrc/api/. The one sanctioned exception isuseConnectionStatus(health ping) and the audio<audio>.srcstream URL — these are not data fetches. - No server data duplicated into Redux slices. Slices hold only client state (player, queue, ui, auth tokens). Track lists live in the RTKQ cache; the play queue stores track IDs + minimal display fields, not full records.
- UI is built only from
modern-sk. No MUI/AntD/shadcn. Missing component → compose from modern-sk primitives. Import its CSS once at the entry (modern-sk/styles.css,modern-sk/fonts.css). Wrap the app in<ThemeProvider>+<TooltipProvider>. The grained black page background comes from the.modern-sk-feltclass — applied to#rootinsrc/index.tsx; without it the page is bare white. modern-sk setsfont-familyonly on its own component classes, sosrc/styles/global.csssets the basefont-family: var(--font-sans)onbody(else plain elements fall back to browser serif).- Token names: modern-sk's real tokens are
--ink/--ink-raised,--fg-1/2/3,--steel-700/800,--lime,--hair. Our component code uses semantic aliases (--color-bg,--color-surface-1/2/3,--color-text-1/2/3,--color-accent,--color-border) defined insrc/styles/global.cssasvar()refs to the real tokens (so they follow[data-theme]). Use the--color-*aliases in app code; add a new alias there if you need another token.
- Token names: modern-sk's real tokens are
- One
<audio>element for the whole app. It is a module-level singleton inuseAudioPlayer.ts, deliberately outside the React tree so navigation never recreates it. - Every list screen needs all three states:
LoadingSkeleton,EmptyState,ErrorState(insrc/components/common/). No bare loading. - Strict TS, no
anyin API types or public module interfaces.
Backend contract
UI is backend-agnostic: it targets the /api/v1 contract, not a specific server. src/api/types.ts is the single source of truth for contract types — keep it synced with the backend plan (music-selfhost-backend-plan.md §8). Swapping/mocking the backend = change baseQuery, nothing else.
How the pieces connect
Base-URL resolution (src/config/): priority is localStorage (user-chosen via ConnectPage) > PUBLIC_API_BASE_URL env > relative /api/v1. runtime-config.ts reads/writes the localStorage override; getApiBaseUrl() is called by baseQuery, the stream-URL builder, and the health check.
API layer (src/api/): index.ts creates the single api via createApi with tagTypes (Track, Album, Artist, Playlist, Download, Like, User, Storage). Endpoints are split by domain under endpoints/ and attached with injectEndpoints — each file exports its own hooks. These files must be imported for their side effect (endpoint registration); src/index.tsx imports all of them at startup. baseQuery.ts does dynamic base URL + Authorization: Bearer injection + 401→refresh→retry reauth (falls back to logout() on refresh failure). Mutations invalidate tags to drive refetch.
Store (src/store/): configureStore wires api.reducer + api.middleware alongside slices. RootState/AppDispatch exported here; typed hooks in src/hooks/useAppDispatch.ts (useAppDispatch, useAppSelector). Slices: auth (tokens + user, persisted to localStorage key mcma_auth), player (currentTrackId/isPlaying/position/volume/repeat/shuffle/panel toggles), queue (entries + currentIndex + source), ui (sidebar/modal/context-menu).
Audio flow: useAudioPlayer syncs the singleton <audio> ↔ player slice. Queue currentIndex change → sets player.currentTrackId → effect points <audio>.src at getStreamUrl(trackId, token) (token as query param) → play/pause mirrors player.isPlaying. <audio> events (timeupdate, ended, etc.) dispatch back into the slice. MediaSession integration is a planned stub here.
Routing (src/routes/): ProtectedRoute gates on auth token + user, with requireAdmin. Unauthed → /connect. index.tsx lazy-loads non-core feature pages. Permission checks via usePermissions (role→permission map, MVP admin/user, designed to extend to granular perms — gate UI on hasPermission(...), not raw role).
Layout: AppShell = Sidebar + <Outlet/> + PersistentPlayer (bottom, persists across routes) + collapsible QueuePanel.
Project layout
src/api/ backend access · src/store/slices/ client state · src/features/<screen>/ screen-as-feature (one folder per A-screen) · src/components/{layout,player,track,common}/ reusable compositions · src/hooks/ · src/lib/ (formatters) · src/config/ · src/routes/.
Screens follow a spec (music-selfhost-screens.md, parts A1–A11). Implemented as reference: Library (A2), Album/Playlist detail (A3). Others are stub pages under src/features/ + routes — adding a screen is meant to be mechanical: fill the stub, wire RTKQ hooks, reuse common state components + TrackRow/TrackContextMenu.
Cross-cutting components (build/reuse before screens)
AvailabilityBadge (track availability: server/downloading/error/missing), TrackRow + TrackContextMenu (used in every track list), ConnectionStatus (global instance reachability), and the three list states above.
Gotcha
tests/index.test.tsx is the stale rsbuild scaffold test importing a now-deleted src/App — it will fail until rewritten. The entry point is src/index.tsx (there is no App.tsx).