Files
julian 26e059fc20 feat: planning structure + task 1.2 stack rounding-out
Add .planning/ scaffolding:
- ROADMAP.md (4 phases, 8 non-negotiable design rules)
- phase-1-foundation/ README + 9 task files (1.2-1.10)
- phase-2-live-map / phase-3-dogfood-readiness / phase-4-future README placeholders

Task 1.2 — stack rounding-out:
- Tailwind 4 via @tailwindcss/vite + src/styles/globals.css
- shadcn/ui (slate, new-york) primitives in src/ui/primitives/:
  button, input, label, form, card, alert
- TanStack Router 1.169 + Query 5.100 (devtools + plugin in devDeps)
- Zustand 5, @directus/sdk 21, zod 4, react-hook-form 7 + resolvers
- Prettier 3 + eslint-config-prettier + eslint-plugin-prettier
- ESLint override disabling react-refresh/only-export-components for
  src/ui/primitives/** (intentional dual-exports in shadcn primitives)
- Path alias @/* -> ./src/* in tsconfig.json + tsconfig.app.json
  (TS 6 deprecates baseUrl; paths now resolve relative to config file).
  Pulled forward from 1.3 because shadcn add CLI needs it resolvable.
- Scripts: dev, build, preview, lint, typecheck, format, format:check,
  test (placeholder)
- App.tsx Tailwind smoke test (centred card + shadcn Button)
- README.md rewritten with stack/scripts/shadcn-add docs

All four gates green: typecheck, lint, format:check, build (222KB / 70KB gz).
2026-05-02 18:41:54 +02:00

116 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 1 — Foundation
The deployable shell. Scaffold + stack + Directus cookie auth + protected routes + Gitea CI + compose deploy block. After Phase 1 the SPA is _infrastructurally complete_: an operator can hit the public URL, log in, see a placeholder home page, log out. Live map and operator features are layered on in subsequent phases.
## Outcome statement
When Phase 1 is done:
- A user with a valid Directus credential can browse to `https://stage.trmtracking.org` (or whatever subdomain the proxy is configured for), be redirected to `/login`, submit credentials, get a session, and land on a placeholder home page.
- Same-domain reverse proxy serves the SPA bundle, Directus REST, Directus business-plane WS, and (when Phase 1.5 of [[processor]] lands) the live-position WS — all under one origin so the auth cookie flows naturally to all three.
- Refresh-on-401 is automatic via Directus's `/auth/refresh`. The access token never touches `localStorage`.
- Route guards check `user.role` in shape, even though everyone's effectively admin until [[directus]] Phase 4.
- Logout clears in-memory state + signs out via Directus + redirects to `/login`.
- `pnpm typecheck`, `pnpm lint`, `pnpm test` are green; Gitea CI builds and pushes a Docker image; Portainer redeploys the stack from `trm/deploy`'s `compose.yaml`.
## Sequencing
```
1.1 Scaffold (done)
└─→ 1.2 Stack rounding-out
├─→ 1.3 Vite dev proxy
│ └─→ 1.4 Runtime config endpoint
│ └─→ 1.5 Directus auth client
│ ├─→ 1.6 Login page
│ │ └─→ 1.7 Routing skeleton ───→ 1.8 Logout flow
│ │
│ └─→ (1.7 also depends on 1.5)
└─→ 1.9 Gitea CI + Dockerfile ───→ 1.10 Deploy compose block
```
1.9 + 1.10 (deploy plumbing) can be developed in parallel with 1.41.8 (auth flow) once 1.3 lands. They merge cleanly because they touch different parts of the repo.
## Files modified
Phase 1 produces this layout in `spa/`:
```
spa/
├── .gitea/workflows/build.yml
├── public/
│ ├── config.json # runtime config; replaced by proxy in stage/prod
│ └── (favicon, etc.)
├── src/
│ ├── main.tsx # already exists
│ ├── App.tsx # rewritten to host the router
│ ├── routes/ # TanStack Router file-based routes
│ │ ├── __root.tsx
│ │ ├── index.tsx # placeholder home
│ │ ├── login.tsx
│ │ └── _authed/ # protected route group
│ │ └── route.tsx
│ ├── auth/
│ │ ├── client.ts # Directus SDK client + auth helpers
│ │ ├── store.ts # Zustand auth store (user, status)
│ │ └── guard.ts # protected-route guard hook
│ ├── config/
│ │ ├── load.ts # fetch /config.json + zod-validate
│ │ ├── schema.ts # zod schema for runtime config
│ │ └── context.tsx # React context provider
│ ├── ui/
│ │ ├── primitives/ # shadcn/ui components live here
│ │ └── pages/
│ │ ├── login.tsx
│ │ └── home.tsx
│ ├── lib/
│ │ └── query-client.ts # TanStack Query singleton
│ └── styles/
│ └── globals.css # Tailwind directives
├── Dockerfile
├── nginx.conf # static-serve config
├── compose.dev.yaml # local dev with Directus + Processor stubs (optional)
├── package.json # extended with the Phase 1 deps
├── tailwind.config.ts
├── postcss.config.js
├── components.json # shadcn/ui config
├── .prettierrc
├── eslint.config.js # extended for Tailwind + Prettier integration
└── README.md
```
## Tech stack additions to the existing scaffold
The Phase 1.1 scaffold already includes Vite 8, React 19, TypeScript 6, ESLint 10, and `@vitejs/plugin-react`. Phase 1 adds:
- **`tailwindcss` + `@tailwindcss/vite`** — utility CSS. Tailwind 4 (the Vite-plugin-based generation, not PostCSS) for cleanliness.
- **`shadcn/ui`** — copy-in component primitives. Not a runtime dep; the `components.json` config + `npx shadcn add ...` populates `src/ui/primitives/`.
- **`@tanstack/react-router`** + **`@tanstack/router-plugin`** — file-based routing, type-safe.
- **`@tanstack/react-query`** — server state cache for Directus calls.
- **`zustand`** — high-frequency client state (auth store now; live-position store in Phase 2).
- **`@directus/sdk`** — typed Directus client.
- **`zod`** — runtime validation (config schema, form validation).
- **`react-hook-form`** + **`@hookform/resolvers`** — form library.
- **`prettier`** + **`eslint-config-prettier`** + **`eslint-plugin-prettier`** — formatting.
Test infra (Vitest + React Testing Library) deferred to Phase 3 — Phase 1's tests are minimal and unit-testable with no DOM.
## Non-negotiable design rules (phase-specific)
These are in addition to the global rules in the ROADMAP.
1. **No build-time secrets.** No env vars baked into the Vite build that change between environments. Everything env-specific is in `/config.json`.
2. **Auth state lives in Zustand, not React Context.** Context cascades re-renders on every change; Zustand selectors don't. The auth store is small but it's read in many places.
3. **Directus SDK over hand-rolled `fetch`.** Use `@directus/sdk`'s typed client for REST; only fall back to bare `fetch` for the runtime-config endpoint (which is served by the proxy, not Directus).
4. **One `QueryClient` instance.** Module-level singleton, imported wherever needed. No provider re-creating it on render.
## Acceptance for the phase as a whole
- [ ] All 9 task files (1.21.10) done.
- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm dev` clean and runnable locally.
- [ ] `pnpm build` produces a deployable static bundle in `dist/`.
- [ ] Gitea CI builds and pushes the Docker image; Portainer redeploy puts the SPA at the public URL.
- [ ] Manual smoke against stage: navigate to public URL → redirected to `/login` → submit valid Directus credentials → see placeholder home page → click logout → back to `/login`. End-to-end happy path works.
- [ ] Wrong credentials show an error on the login form, not a stack trace.
- [ ] Refresh-on-401 works without re-prompting login (verifiable by waiting for the access token to expire while idle on the home page; SPA silently refreshes).