309333b2d3
Three-file config module under src/config/: - schema.ts: RuntimeConfigSchema (directusUrl, liveWsUrl, businessWsUrl, optional googleMapsKey, env enum). Custom UrlOrAbsolutePath validator accepts http(s)/ws(s) URLs plus paths starting with /. - load.ts: loadConfig() fetches /config.json with cache: 'no-store', safeParse, throws ConfigValidationError on schema failure. - context.ts: RuntimeConfigContext + useRuntimeConfig() hook (no JSX). - provider.tsx: RuntimeConfigProvider — loading / ready / error states. Single retry after 500ms on network failure; renders zod issues on validation failure for debuggability. public/config.json: committed dev defaults (relative paths matching the Vite proxy from 1.3). src/main.tsx wraps App in <RuntimeConfigProvider>. README gains a Runtime config section with the schema table. Deviation: spec sketched provider+hook in one context.tsx. Split into context.ts (hook only) and provider.tsx (component only) to satisfy react-refresh/only-export-components without adding an eslint override.
79 lines
4.9 KiB
Markdown
79 lines
4.9 KiB
Markdown
# trm/spa
|
|
|
|
End-user single-page app for the TRM platform: race directors, marshals, timekeepers, and (post-Phase 4 of `trm/directus`) public-facing participants. Talks to `trm/directus` for REST/GraphQL + business-plane WebSocket and to `trm/processor` for the live-position WebSocket firehose.
|
|
|
|
Architectural anchors live in `trm/docs/wiki/`:
|
|
|
|
- `wiki/entities/react-spa.md` — this service.
|
|
- `wiki/concepts/maps-architecture.md` — map subsystem (Phase 2).
|
|
- `wiki/synthesis/processor-ws-contract.md` — live-position wire spec (Phase 2).
|
|
- `wiki/synthesis/directus-schema-draft.md` — schema this consumes.
|
|
|
|
Implementation planning lives in `.planning/` — see `.planning/ROADMAP.md`.
|
|
|
|
## Stack
|
|
|
|
- **Vite 8 + React 19 + TypeScript 6** — SPA build, no SSR.
|
|
- **Tailwind CSS 4** via `@tailwindcss/vite`.
|
|
- **shadcn/ui** primitives (slate base, new-york style) — copy-in components owned by us, in `src/ui/primitives/`.
|
|
- **TanStack Router + Query** — file-based routes, server-state cache.
|
|
- **Zustand** — high-frequency client state (auth, live positions in Phase 2).
|
|
- **`@directus/sdk`** — typed Directus client (REST + WS).
|
|
- **`zod`** — runtime validation (config, forms, WS protocol).
|
|
- **`react-hook-form` + `@hookform/resolvers`** — forms.
|
|
|
|
## Common scripts
|
|
|
|
| Command | Purpose |
|
|
| ------------------- | ------------------------------------------- |
|
|
| `pnpm dev` | Start the Vite dev server on `:5173`. |
|
|
| `pnpm build` | Type-check + production build into `dist/`. |
|
|
| `pnpm preview` | Serve the built `dist/` for smoke testing. |
|
|
| `pnpm typecheck` | Type-check only (no build). |
|
|
| `pnpm lint` | ESLint over the repo. |
|
|
| `pnpm format` | Prettier auto-format. |
|
|
| `pnpm format:check` | Prettier check only (CI gate). |
|
|
| `pnpm test` | Test runner (Phase 3 wires Vitest). |
|
|
|
|
## Adding a shadcn primitive
|
|
|
|
```sh
|
|
pnpm dlx shadcn@latest add <component>
|
|
```
|
|
|
|
Components land in `src/ui/primitives/` per `components.json`'s alias config. Edit them freely — they're our code from the moment they land.
|
|
|
|
## Local dev
|
|
|
|
The Vite dev server (`pnpm dev` on `:5173`) proxies three path namespaces to local services so everything runs same-origin — the same topology stage/prod gets via Traefik, no CORS workarounds needed.
|
|
|
|
| Path | Forwards to | Notes |
|
|
| -------------- | ------------------------------------ | ----------------------------------------------------------------------------- |
|
|
| `/api/*` | `${VITE_DEV_DIRECTUS_URL}/*` | Directus REST + GraphQL. Default `http://localhost:8055`. |
|
|
| `/ws-business` | `${VITE_DEV_DIRECTUS_URL}/websocket` | Directus business-plane WebSocket (auto-derived ws:// scheme). |
|
|
| `/ws-live` | `${VITE_DEV_PROCESSOR_WS_URL}/` | Processor live-position WebSocket. Default `ws://localhost:8081` (Phase 1.5). |
|
|
|
|
Override per-developer by copying `.env.example` to `.env.local` and editing — Vite auto-loads `.env.local` in any mode and the values flow into `vite.config.ts` via `loadEnv`.
|
|
|
|
If the dev port `5173` collides with another Vite app, run with `VITE_PORT=5174 pnpm dev` (or pin a different port in `vite.config.ts`).
|
|
|
|
## Runtime config
|
|
|
|
`/config.json` is fetched at app boot, validated with zod, and held in a React context (`useRuntimeConfig()` hook). One image, multiple environments — the deploy stack overrides the file via a Docker volume mount; the SPA never rebuilds for an env-only change.
|
|
|
|
| Field | Type | Notes |
|
|
| --------------- | -------- | ---------------------------------------------------------------------------- |
|
|
| `directusUrl` | URL/path | Directus REST + GraphQL base. Same-origin path or absolute URL. |
|
|
| `liveWsUrl` | URL/path | Processor live-position WebSocket URL. |
|
|
| `businessWsUrl` | URL/path | Directus business-plane WebSocket URL. |
|
|
| `googleMapsKey` | string? | Optional. If absent, Google tile sources are hidden in the UI. |
|
|
| `env` | enum | `dev` / `stage` / `prod` — used in log labels and feature-flag conditionals. |
|
|
|
|
URLs may be absolute (`http(s)://`, `ws(s)://`) or absolute paths starting with `/`. Relative paths work because the SPA is always same-origin to its backends. The committed `public/config.json` uses path-only defaults that match the dev proxy.
|
|
|
|
In stage/prod the deploy stack mounts an override file at `/usr/share/nginx/html/config.json`; see `trm/deploy/README.md` (lands in Phase 1.10).
|
|
|
|
## CI
|
|
|
|
`.gitea/workflows/build.yml` (lands in Phase 1.9) runs the four gates — typecheck, lint, format:check, build — and publishes a Docker image on push to `main`.
|