Commit Graph

20 Commits

Author SHA1 Message Date
julian 8223a566e4 docs: import TRM design handoff + defer adoption to phase 3.8
A design handoff bundle generated by Claude Design (claude.ai/design)
on 2026-05-02. Defines the Bloomberg/F1-pit-wall aesthetic for TRM:
- ink-on-paper base + race-flag red accent (#E8412B)
- square-edged everything, sharp printed offset shadows
- mono numerics (JetBrains Mono) for any changing value
- Goldplay (real licensed font, three weights in bundle fonts/)
- four surfaces designed: dashboard / leaderboard / mobile / marketing
  (SPA scope is the first two)

The bundle is committed in-tree at TRM_Design_System-handoff/ so 3.8
has the full source material when it picks the work up. Includes:
- Top-level + project READMEs (the design spec)
- chats/chat1.md (intent + iteration history)
- colors_and_type.css (token set, drop-in for Tailwind 4 @theme)
- fonts/ (Goldplay regular/semibold/bold)
- ui_kits/ (HTML prototypes per surface)
- preview/ (per-token visual reference cards)

Updated phase-3-dogfood-readiness/README.md task 3.8 row to point at
the bundle and document the recommended approach (retheme shadcn via
CSS variable overrides + Tailwind 4 @theme, not replace).

Why deferred: foundational tokens are non-blocking for Phase 1 (login
+ placeholder home) and Phase 2 (live map without chrome). Applying
them now would either delay dogfood-blocking work or land partial
styling that gets reworked when 3.8 lands the full pass.
2026-05-02 19:11:57 +02:00
julian 79e50289fe fix(ci): generate route tree before typecheck + enforce LF line endings
Two CI gaps surfaced on first push:

1. typecheck failed because tsc -b ran before vite build, but
   src/routeTree.gen.ts is only generated by the Vite plugin during
   build. tsc has nothing to typecheck against.

   Fix: install @tanstack/router-cli and chain `tsr generate` before
   tsc in the typecheck and build scripts. Now any environment that
   runs typecheck cold (CI, fresh clone) generates the route tree
   first.

   Also added a top-level `route-tree` script so the same command is
   reusable elsewhere if needed.

2. format:check would fail on Windows working trees because
   git autocrlf (default on Windows) checks files out with CRLF, but
   .prettierrc pins endOfLine: "lf". Locally the format:check
   intermittently passed/failed depending on whether files had been
   recently auto-formatted.

   Fix: .gitattributes with `* text=auto eol=lf` enforces LF in every
   working tree. Plus explicit overrides for binary blobs (images) and
   the route tree file. `git add --renormalize .` brought the index in
   line with the new policy; no actual file content changed.

CI on the next push should now see the same green gates the local
working tree shows.
2026-05-02 18:54:59 +02:00
julian ac87a511a3 docs: mark phase 1 (foundation) as done 2026-05-02 18:49:54 +02:00
julian 4207634caf docs: mark task 1.10 done (trm/deploy 68ab08f) 2026-05-02 18:49:30 +02:00
julian 30e9aa24f5 docs: backfill task 1.9 commit SHA 2026-05-02 18:49:18 +02:00
julian 7e3808237e feat: task 1.9 gitea CI + dockerfile + nginx static serve
- Dockerfile: three-stage (deps / build / runtime). deps stage runs
  pnpm fetch with BuildKit cache mount; build stage runs vite build
  to produce dist/; runtime stage is nginx:1.27-alpine serving the
  bundle. HEALTHCHECK via wget against localhost.
- nginx.conf: gzip on text assets; /assets/ long-cache (hashed
  filenames immutable); /config.json no-cache (volume-mountable
  override in stage/prod); /index.html no-cache; SPA routing fallback
  via try_files ... /index.html.
- .dockerignore: keeps the context small (node_modules, dist, env,
  .git, .gitea, .planning, *.md except README, .claude, .vscode).
- .gitea/workflows/build.yml: matches trm/processor shape with
  format:check added between lint and test. Path filter excludes
  .planning and pure-markdown changes. Steps: checkout, Node 22,
  pnpm@latest-9, install --frozen-lockfile, typecheck, lint,
  format:check, test, buildx, registry login, build & push
  trm/spa:main, Portainer webhook.

Deviations from spec:
- Push :main tag only (not :main + per-commit SHA). Matches the
  other repos; SHA-pinning happens via *_TAG env vars in
  trm/deploy. SHA tagging is a cross-repo refactor for later.
- Pin pnpm@latest-9 (matching existing repos), not pnpm@latest
  from the spec. Reproducibility win for CI.

Smoke: typecheck/lint/format:check/build all green locally. Local
docker build not run (Docker unavailable on this machine); CI is
the gate.

Required for first deploy (1.10 covers the rest):
- REGISTRY_USERNAME / REGISTRY_PASSWORD / PORTAINER_WEBHOOK_URL
  secrets in the Gitea repo settings.
- SPA service block in trm/deploy/compose.yaml.
2026-05-02 18:49:01 +02:00
julian 8d0bc2bb1e feat(login): dev-only prefill from VITE_ADMIN_EMAIL/PASSWORD
When import.meta.env.DEV is true, the login form's email and password
fields are populated from the corresponding env vars. Shaves the manual
re-typing during dev iteration.

Production builds get empty strings regardless of build-time env values:
the prefill is gated on import.meta.env.DEV (which Vite replaces with
literal `false` at build time, so the surrounding ternary tree-shakes
and the env values can't bleed into the prod bundle even if accidentally
set in the build env).

Files:
- src/vite-env.d.ts (new): ImportMetaEnv augmentation with the four
  VITE_* vars we use (admin email/password, dev directus/processor URLs).
  Gives proper typing under strict mode.
- src/ui/pages/login.tsx: devDefaults computed once at module scope from
  import.meta.env. Form's defaultValues uses it.
- .env.example: documents VITE_ADMIN_EMAIL and VITE_ADMIN_PASSWORD with
  examples; notes the prod-ignore guarantee.
- .gitignore: adds *.env (defensive — complements the existing *.local
  pattern). .env.example stays committable (doesn't end in .env).
2026-05-02 18:48:04 +02:00
julian 152578f767 docs: backfill task 1.8 commit SHA 2026-05-02 18:47:52 +02:00
julian d3ccdded23 feat: task 1.8 logout flow + cross-tab sync + strict tsconfig
- src/auth/logout.ts: performLogout({ queryClient, navigate? })
  orchestration. Calls store.logout(), clears query cache, navigates
  to /login. Server-call failures swallowed; local state still flips
  to anonymous.
- src/auth/cross-tab-sync.ts: startCrossTabSync(). Subscribes to the
  auth store and bumps trm-auth-version in localStorage on status
  transitions; listens for storage events and re-runs initialize()
  when another tab bumps. Idempotent via module-level guard. No-ops
  if localStorage is unavailable.
- src/ui/components/logout-button.tsx: <LogoutButton> wraps shadcn
  Button. Pass-through props but locks onClick / disabled / children.
  Local isLoggingOut for "Signing out..." indicator + double-click
  guard.
- src/auth/bootstrap.tsx: <AuthBootstrap> now calls startCrossTabSync()
  alongside the existing init steps. Single mount point.
- src/auth/index.ts: re-export performLogout + startCrossTabSync.
- src/routes/_authed/index.tsx: replaced the inline sign-out button
  with <LogoutButton />. The existing useEffect on 'anonymous' status
  handles navigation when cross-tab sync fires.

Bonus: tsconfig.app.json now has "strict": true. TanStack Router emits
a conditional-type error ("strictNullChecks must be enabled") in
editors when strict is off; tsc -b was lenient. Caught in 1.7's
review. Typecheck remained green after enabling.
2026-05-02 18:46:35 +02:00
julian 8e38f69205 docs: backfill task 1.7 commit SHA 2026-05-02 18:46:19 +02:00
julian dc4e73f73a feat: task 1.7 routing skeleton
TanStack Router file-based routes:
- vite.config.ts: TanStackRouterVite plugin (target: react,
  autoCodeSplitting: true) before the React plugin.
- .gitignore: src/routeTree.gen.ts (auto-generated).
- src/lib/query-client.ts: module-level QueryClient singleton.
- src/routes/__root.tsx: root route, QueryClientProvider, Suspense-
  wrapped router + query devtools (dev-only via import.meta.env.DEV).
- src/routes/login.tsx: public route. validateSearch schema accepts
  optional redirect. LoginRoute wraps <LoginPage> with onAuthenticated
  navigating to redirect ?? '/'.
- src/routes/_authed/route.tsx: pathless protected layout. beforeLoad
  redirects to /login on 'anonymous'; falls through on 'unknown' /
  'authenticating' so hard reload doesn't flash the login page.
- src/routes/_authed/index.tsx: placeholder home with sign-out button.
  Cross-tab logout effect bounces to /login on store transition.
- src/App.tsx: replaced — now creates the router and renders
  <RouterProvider />. The interim status-branching logic from 1.6 is
  gone (the route tree handles it).
- eslint.config.js: override for src/routes/** disabling
  react-refresh/only-export-components — TanStack Router's file-based
  pattern intentionally co-exports Route alongside components.

Code-splitting working: login chunk 37KB, home + auth 17KB, card
primitive 31KB, all loaded lazily; main bundle 353KB.

Deviations:
1. No top-level useRequireAuth hook — per-page useEffect on store
   transitions is just as effective and only used in one place.
2. ESLint override for src/routes/** matches the existing pattern for
   src/ui/primitives/**.
3. Login page's auto-navigate effect from 1.6 is removed — the route
   wrapper owns navigation via onAuthenticated.
2026-05-02 18:45:55 +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 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