Files
mcma-webui/CLAUDE.md
2026-06-02 01:13:22 +03:00

67 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:3000
- `npm run build` — production build
- `npm run preview` — preview production build
- `npm run lint` — rslint (`@rslint/core`, native-speed lint)
- `npm run test` — rstest (run once); `npm run test:watch` — watch mode
- `npm run format` — prettier
- `npx 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)
1. **No hardcoded backend address** except the env default. Runtime override is mandatory (see base-URL resolution below).
2. **No `fetch` in components.** All backend access goes through RTK Query hooks. The entire API surface is isolated in `src/api/`. The one sanctioned exception is `useConnectionStatus` (health ping) and the audio `<audio>.src` stream URL — these are not data fetches.
3. **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.
4. **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-felt` class — applied to `#root` in `src/index.tsx`; without it the page is bare white. modern-sk sets `font-family` only on its own component classes, so `src/styles/global.css` sets the base `font-family: var(--font-sans)` on `body` (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 in `src/styles/global.css` as `var()` 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.
5. **One `<audio>` element for the whole app.** It is a module-level singleton in `useAudioPlayer.ts`, deliberately outside the React tree so navigation never recreates it.
6. **Every list screen needs all three states**: `LoadingSkeleton`, `EmptyState`, `ErrorState` (in `src/components/common/`). No bare loading.
7. **Strict TS, no `any`** in 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 A1A11). 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`).