Project started 🥂

This commit is contained in:
2026-06-02 01:13:22 +03:00
commit 612d0f0125
146 changed files with 15242 additions and 0 deletions
+66
View File
@@ -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 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`).