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