# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Web client for **TLE Community** (The Lacuna Expanse), a community-run continuation of the Lacuna Expanse game. This repo talks to a separate backend server (`tlecommunity/server`) over JSON-RPC; it does not run the game logic itself. The project has gone through a couple of names over the years (originally "Lacuna", later "Kantigen"/"KA") — current naming is `tlecommunity` / "TLE Community", so avoid introducing old "Kantigen", "KA", "ka-web", or "ka-server" naming into new code, comments, or docs. ## Commands - `npm start` — start Vite dev server against a real backend (needs `LACUNA_SERVER_URL` etc. set, see `.env.local`) - `npm run start:stubbed` — run the local stubbed server (`stubs/server.ts`, port 3005) and Vite (`--mode stubbed`, loading `.env.stubbed`) together via `concurrently`. The stub server runs under `tsx watch`. `npm run serve:stubs` runs just the stub server. - `npm run lint:check` — Oxlint (config in `oxlint.config.ts`); `npm run lint:fix` applies autofixes - `npm run format:check` — Prettier check; `npm run format:fix` writes in place - `npm run types:check` — `tsc --noEmit` - `npm test` — Jest; `npm run test:watch` for watch mode, `npm run test:coverage` for coverage - Single file: `npx jest app/path/to/file.test.ts` - Single test: `npx jest -t "test name"` - `npm run build` — production build via `vite build` Node >=24 and npm >=10 are required (`engines` in `package.json`). The `@tlecommunity/client` dependency is a private scoped package resolved from a self-hosted Gitea npm registry (see `.npmrc`), not the public npm registry. ## Architecture The codebase is mid-migration from a legacy YUI2 client to React/TypeScript, so two UI layers currently coexist: - **`app/yui/**`** — legacy YUI2 + jQuery code, being incrementally eliminated. Oxlint ignores this directory entirely; don't hold it to current lint/style standards. - **`app/components/**`** — current React/TypeScript UI. New feature work belongs here. Backend data lives in **TanStack Query**; MobX holds only what a query cannot own. `docs/` (see below) predates both and is inaccurate on this point. **Data fetching belongs in `app/queries/*`** — one file per API module, exporting `useQuery`/`useMutation` hooks. Components must not call `lacuna.*` directly; add a hook instead. The rules: - Every query key comes from `app/queryKeys.ts`. **Never inline a key at a call site** — a key and the invalidation that busts it have to move together. Keys are hierarchical tuples, so `queryKeys.building.all` invalidates every building view while `queryKeys.building.view(url, id)` targets one. - Every `queryFn`/`mutationFn` wraps its call in `unwrap()` (`app/queries/unwrap.ts`). The client never rejects — it resolves to `{ result, error }` — and React Query needs a rejection to see a failure. `unwrap` throws a `LacunaError`; the user-facing alert is still the global `onResponse` handler in `app/lacuna.ts`. - When a write makes a read stale, invalidate it in the mutation's `onSuccess`. Prefer `mutate` with callbacks over `await mutateAsync()`, so a server error cannot escape as an unhandled rejection. - `app/components/queryState.tsx` renders the loading and error branches, so components do not each grow their own spinner. - `app/queryClient.ts` is a module-level singleton, which is how non-React code (the legacy YUI layer) invalidates. Stores live under `app/stores/`: - `app/stores/rpc/*` — the `server`, `empire` and `body` "status blocks". These stay MobX on purpose: they are push-based, arriving on _any_ response via `splitStatus()` (`app/server.ts`), and are extrapolated every second by `app/stores/ticker.ts`. No single query owns them. Anything that is the response to one specific call belongs in `app/queries/` instead. - `app/stores/windows.ts` + `app/stores/window/*` — the window manager. `WindowsStore` holds a z-index-ordered stack of `Window` objects, one per `WindowType` (enumerated in `app/interfaces/window.ts`); adding a window of an already-open type replaces it rather than stacking a duplicate. Rendered by `app/components/menu/gameWindow`. - `app/stores/menu.ts`, `session.ts`, `ticker.ts` — global UI/session/clock state. Server communication flow: `app/lacuna.ts` exports the configured `@tlecommunity/client` singleton — typed endpoints as properties (`lacuna.empire.viewBoosts()`, `lacuna.body.getBuildings({...})`, `lacuna.buildingFromUrl(url).view({...})`), each resolving to `{ result, error }` rather than throwing. It also registers the one global `onResponse` handler, which routes status blocks into MobX, alerts on errors, and owns the captcha (1016), empire-not-founded (1100) and session-expired flows. It deliberately does not enable the client's `enableRpcLimitHandler()` auto-retry: that suits unattended scripts, whereas here the user initiated the action and should see the RPC limit (1010) error, wait it out, and decide what to do next. `app/server.ts` is now just `splitStatus`. There is no `app/services/` layer any more; `app/queries/*` replaced it. Other structural pieces: - `app/interfaces/*` — TS types for API params/responses and domain models (building, body, empire, window, tabber). - `app/hocs/*` — React HOCs, e.g. `withBuildingData`, which runs the building `view` query and injects the result as a prop. It is the single fetch point behind all six building windows. - `app/shims/*` — do not `import $ from 'jquery'` directly; import the shim at `app/shims/jquery` instead (it combines jQuery plugins used by legacy code into one object). - Env vars use a `LACUNA_` prefix (Vite `envPrefix`), typed in `app/env.d.ts`. `.env.local`/`.env.stubbed`/`.env.production` select which backend/assets URLs are used. ### `docs/` folder `docs/` exists but its own `docs/README.md` warns "most of the information in here is rather out of date." It documents a Reflux/Gulp-era architecture (Actions → Stores → Components via Reflux, `gulp dev`, `js/` source paths) that predates the current Vite/MobX/`app/` setup. Treat it as historical background only — do not follow `docs/guides/implementing-a-building.md` or `docs/add-buildings.md` as literal instructions for adding a building; use an existing building implementation under `app/components/building` and `app/queries` as the template instead. ## Testing - Jest is configured with `clearMocks: true` (`jest.config.js`), so mocks are cleared automatically between tests — do not call `jest.clearAllMocks()` in `beforeEach`/`afterEach`. - Prefer Testing Library's `userEvent` (e.g. `userEvent.click()`) over `fireEvent` — it dispatches the fuller sequence of events a real browser/user interaction produces, so it more closely mirrors actual user behavior. Reach for `fireEvent` only when testing a specific low-level event that `userEvent` doesn't model. ## Code style - Oxlint config defined in `oxlint.config.ts` - Prettier: single quotes, semicolons, trailing commas (`es5`), 100 print width, 2-space indent. - camelCase file names; no `index.ts`/`index.tsx` barrel files.