Commit Graph

13 Commits

Author SHA1 Message Date
julian 4ac5ed4eca fix(auth): switch directus sdk to 'session' mode
'cookie' mode keeps the session alive via an in-memory access token
that's refreshed from a refresh cookie when the SDK is alive. After a
hard reload the SDK has no access token in memory, so /users/me 401s
before autoRefresh can kick in (or the refresh cookie's surface doesn't
cover plain reads cleanly). Net effect: every reload bounces back to
login even when the cookie is still valid.

'session' mode puts the actual session in the cookie. Browser sends it
automatically on every request, the SDK doesn't need to manage tokens,
and reload survives cleanly because /users/me with the session cookie
just works.

Reordered .with() calls to match the working pattern from a prior
project: rest() before authentication() (cosmetic; SDK accepts either
order).
2026-05-02 18:45:31 +02:00
julian 1fcdf289b0 docs: backfill task 1.6 commit SHA 2026-05-02 18:45:16 +02:00
julian ef18222edf feat: task 1.6 login page
src/ui/pages/login.tsx — LoginPage component:
- Centred card on muted background.
- shadcn Form + FormField with react-hook-form + zodResolver.
  FormMessage renders field-level zod errors automatically.
- Email + password with proper autocomplete attrs for password managers.
  autoFocus on email.
- Submit calls useAuthStore.getState().login(); errors render in a
  destructive Alert above the form.
- In-flight via the auth store's 'authenticating' status (cross-tab safe).
- onAuthenticated callback fires on store transition to authenticated;
  1.7 wires this to a router redirect.

src/App.tsx branches on auth status:
- unknown / authenticating -> "Signing you in..." placeholder
  (avoids flashing the login page on hard refresh while the boot probe
  runs)
- anonymous -> <LoginPage />
- authenticated -> home placeholder (1.7 replaces with router shell)

Deviation: skipped src/routes/login.tsx — requires the router plugin
to have generated routeTree.gen.ts, which is 1.7's territory. The page
works standalone via App.tsx's status branch.

Search-param redirect (?redirect=...) also deferred to 1.7 — no router
yet to expose useSearch. onAuthenticated callback is the seam.
2026-05-02 18:44:57 +02:00
julian a65ad428e6 fix(auth): resolve relative directus URL against window origin
The runtime config supports relative paths like /api so the SPA can run
same-origin in dev (Vite proxy) and stage/prod (Traefik). createDirectus
from @directus/sdk calls new URL(...) internally, which throws on
relative URLs. Resolve in toAbsoluteUrl() before handing to the SDK:

  '/api' -> 'http://localhost:5173/api' (dev)
  'https://api.example.com' -> passes through unchanged

Caught at runtime; typecheck/build don't surface this since the SDK
treats its url arg as a string.
2026-05-02 18:44:37 +02:00
julian 3917119446 docs: backfill task 1.5 commit SHA 2026-05-02 18:44:12 +02:00
julian 1ba0648cf3 feat: task 1.5 directus auth client + zustand auth store
Six files under src/auth/:
- client.ts: createDirectus<Schema>().with(authentication('cookie', ...))
  .with(rest(...)). Lazy singleton via initDirectusClient(url) +
  getDirectus() + useDirectus() (React-side helper). DirectusUser type;
  empty Schema (grows in Phase 2+).
- store.ts: Zustand store with discriminated AuthState
  (unknown / anonymous / authenticating / authenticated). Actions:
  initialize, login, logout, setUser. humanizeAuthError maps Directus
  error codes (INVALID_CREDENTIALS / INVALID_OTP / USER_SUSPENDED) to
  user-facing strings.
- guard.ts: useRequireAuth (redirect to /login on anonymous) and
  useRequireRole (bounce to / on role mismatch). Uses TanStack Router's
  useNavigate; effective once 1.7 wires the router.
- bootstrap.tsx: <AuthBootstrap> component runs initDirectusClient +
  initialize() once after the runtime config is loaded.
- index.ts: barrel re-exports.

main.tsx wraps App in <RuntimeConfigProvider><AuthBootstrap>...
App.tsx shows the auth status as a smoke indicator before 1.6/1.7.

Deviations:
1. Split getDirectus into initDirectusClient(url) + getDirectus()
   because the runtime config is a React context (per 1.4), not a
   Zustand store. <AuthBootstrap> bridges from React land to the non-
   React getter.
2. Added <AuthBootstrap> as a discrete component rather than putting
   the bootstrap effect in App.tsx — App.tsx gets replaced by the
   router in 1.7, but bootstrap needs to keep running.
2026-05-02 18:43:51 +02:00
julian d2026ee83d docs: backfill task 1.4 commit SHA 2026-05-02 18:43:32 +02:00
julian 309333b2d3 feat: task 1.4 runtime config endpoint
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.
2026-05-02 18:43:08 +02:00
julian 90c35e0ad5 docs: backfill task 1.3 commit SHA 2026-05-02 18:42:43 +02:00
julian 3c7033c3f3 feat: task 1.3 vite dev proxy + tsconfig hardening
Same-origin dev proxy via Vite's server.proxy:
- /api/*       -> ${VITE_DEV_DIRECTUS_URL}/*   (Directus REST + GraphQL)
- /ws-business -> ws://.../websocket           (Directus business-plane WS)
- /ws-live     -> ${VITE_DEV_PROCESSOR_WS_URL} (Processor live WS, Phase 1.5)

WS proxy targets derive ws:// scheme from the http(s) Directus URL.
loadEnv reads VITE_DEV_* overrides; sensible localhost defaults.

tsconfig.app.json: noUncheckedIndexedAccess + noImplicitOverride.

.env.example documents the two override vars; .env.local is gitignored
via the existing *.local pattern.

README "Local dev" section: proxy table + override instructions + port
collision workaround.

Deviation: spec called for .env.dev.example/.env.dev.local but Vite's
dev mode is "development" not "dev", so mode-specific files would be
.env.development.* and Vite wouldn't auto-load .env.dev.local. Switched
to the standard Vite .env.example/.env.local convention. Documented in
the task's Done section.

Plus: backfill 1.2 commit SHA (9918418) in its Done section.
2026-05-02 18:42:22 +02:00
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
julian c3c83b53f6 Clean up 2026-05-02 16:56:15 +02:00
julian e76c70bfe4 Init 2026-05-02 16:53:12 +02:00