26e059fc20
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).
116 lines
6.7 KiB
Markdown
116 lines
6.7 KiB
Markdown
# 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.4–1.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.2–1.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).
|