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

6.9 KiB
Raw Blame History

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).