Project started 🥂
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# 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 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`).
|
||||
Reference in New Issue
Block a user