feat(m18): bootstrap apps/web on Vite + React 19 + TanStack + Base UI + Tailwind 4 #172

Merged
code-lead merged 1 commit from boss/163 into main 2026-04-20 18:17:27 +00:00
Collaborator

Summary

Stands up the t3code stack in apps/web so M18-3+ have a real frontend to
build against. Closes #163.

Stack

  • Vite 7 + React plugin, React 19 + React DOM 19, TypeScript strict
  • Tailwind CSS 4 via @tailwindcss/vite
  • Base UI primitives (@base-ui-components/react)
  • TanStack Router (file-based under src/routes/) + TanStack Query
  • Zustand (UI state only — server state stays in Query)
  • Class Variance Authority + tailwind-merge

Path aliases: @/*src/*, @shared/*../../packages/shared/src/*
wired in both tsconfig.json and vite.config.ts / vitest.config.ts.

Design tokens

Tokyo Night Storm palette mirrored from design/tokens.json::theme-dark
into src/styles/tokens.css, then mapped onto Tailwind @theme names
(bg-bg, text-accent, border-border, …) so utility classes resolve
through the same CSS variables — no raw hex in downstream components.

Dev loop

  • cd apps/web && bun run dev → Vite on 127.0.0.1:5173 with a proxy
    to http://localhost:4500 for /task, /queue, /history,
    /events, /stats, /agents, /usage, /storage, /breakdown.
  • just dev fans out server + web in parallel via Turbo.
  • apps/web emits to dist/bun run build works.

Production serving

apps/server grows handleWebApp which serves apps/web/dist/ at
/app/* (e.g. /app/monitor, /app/planner) with:

  • SPA history fallback to index.html
  • path-traversal guard (.. segments rejected with 403)
  • long-lived immutable caching under /assets/ (Vite fingerprints
    everything there); short max-age=300 on unhashed files
  • loud 503 with a bun run build hint when the bundle is missing
    — silent-404ing would be confusing

Legacy / still serves dashboard.html until #M18-9.

Linter choice

Biome for the web app, same as the server — the one-linter
workspace rule stays intact (no Oxlint). Documented in
apps/web/README.md.

Tests

  • Vitest + React Testing Library + happy-dom. One smoke
    spec per route (routes/index.test.tsx) asserting it renders.
  • Playwright config installed (skeleton only — first e2e lands
    in #M18-3).
  • just test switched from a bare bun test (which would try to
    execute web .test.tsx files under Bun's native runner and fail)
    to bun x turbo run test, so each package's test script picks
    the right runner.

Out of scope

Real UI beyond the styled "Hello" landing, multi-page routing, and
auth — all belong to later M18 stories.

Test plan

  • bun run qa passes (typecheck + lint + format + tests across the monorepo)
  • bun run build inside apps/web produces dist/ cleanly
  • main.test.ts asserts /app/* returns 503 with a build-hint when
    dist/ is absent and 200 HTML when built; path-traversal is rejected
  • Vitest smoke tests render the landing route and find the Base UI dialog

🤖 Generated with Claude Code

## Summary Stands up the t3code stack in `apps/web` so M18-3+ have a real frontend to build against. Closes #163. ### Stack - **Vite 7** + React plugin, **React 19** + React DOM 19, **TypeScript** strict - **Tailwind CSS 4** via `@tailwindcss/vite` - **Base UI** primitives (`@base-ui-components/react`) - **TanStack Router** (file-based under `src/routes/`) + **TanStack Query** - **Zustand** (UI state only — server state stays in Query) - **Class Variance Authority** + **tailwind-merge** Path aliases: `@/*` → `src/*`, `@shared/*` → `../../packages/shared/src/*` — wired in both `tsconfig.json` and `vite.config.ts` / `vitest.config.ts`. ### Design tokens Tokyo Night Storm palette mirrored from `design/tokens.json::theme-dark` into `src/styles/tokens.css`, then mapped onto Tailwind `@theme` names (`bg-bg`, `text-accent`, `border-border`, …) so utility classes resolve through the same CSS variables — no raw hex in downstream components. ### Dev loop - `cd apps/web && bun run dev` → Vite on `127.0.0.1:5173` with a proxy to `http://localhost:4500` for `/task`, `/queue`, `/history`, `/events`, `/stats`, `/agents`, `/usage`, `/storage`, `/breakdown`. - `just dev` fans out server + web in parallel via Turbo. - `apps/web` emits to `dist/` — `bun run build` works. ### Production serving `apps/server` grows `handleWebApp` which serves `apps/web/dist/` at `/app/*` (e.g. `/app/monitor`, `/app/planner`) with: - SPA history fallback to `index.html` - path-traversal guard (`..` segments rejected with 403) - long-lived immutable caching under `/assets/` (Vite fingerprints everything there); short `max-age=300` on unhashed files - loud **503** with a `bun run build` hint when the bundle is missing — silent-404ing would be confusing Legacy `/` still serves `dashboard.html` until #M18-9. ### Linter choice **Biome** for the web app, same as the server — the one-linter workspace rule stays intact (no Oxlint). Documented in `apps/web/README.md`. ### Tests - **Vitest** + **React Testing Library** + `happy-dom`. One smoke spec per route (`routes/index.test.tsx`) asserting it renders. - **Playwright** config installed (skeleton only — first e2e lands in #M18-3). - `just test` switched from a bare `bun test` (which would try to execute web `.test.tsx` files under Bun's native runner and fail) to `bun x turbo run test`, so each package's `test` script picks the right runner. ### Out of scope Real UI beyond the styled "Hello" landing, multi-page routing, and auth — all belong to later M18 stories. ## Test plan - [x] `bun run qa` passes (typecheck + lint + format + tests across the monorepo) - [x] `bun run build` inside `apps/web` produces `dist/` cleanly - [x] `main.test.ts` asserts `/app/*` returns 503 with a build-hint when `dist/` is absent and 200 HTML when built; path-traversal is rejected - [x] Vitest smoke tests render the landing route and find the Base UI dialog 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(m18): bootstrap apps/web on Vite + React 19 + TanStack + Base UI + Tailwind 4
All checks were successful
qa / qa (pull_request) Successful in 4m58s
qa / dockerfile (pull_request) Successful in 10s
b237944fc2
Stands up the t3code stack inside `apps/web` so M18-3+ have a real
frontend to build against. Closes #163.

- Deps: Vite 7, React 19, Tailwind 4 (`@tailwindcss/vite`), Base UI,
  TanStack Router (file-based under `src/routes/`) + Query, Zustand,
  CVA + tailwind-merge.
- Aliases: `@/*` → `src/*`, `@shared/*` → `packages/shared/src/*`.
- Tokyo Night Storm tokens mirrored from `design/tokens.json` into
  `src/styles/tokens.css` and mapped onto Tailwind `@theme` names
  (`bg-bg`, `text-accent`, `border-border`, …) — no raw hex downstream.
- Dev loop: `bun run dev` on 5173 with proxy to 4500 for `/task`,
  `/queue`, `/history`, `/events`, `/stats`, `/agents`, `/usage`,
  `/storage`, `/breakdown`. Root `just dev` fans to both apps via Turbo.
- Production: `apps/server` grows a `/app/*` handler that serves
  `apps/web/dist/` with SPA history fallback + 503 build-hint when the
  bundle is missing. Path-traversal guarded; assets get immutable
  cache, everything else short max-age. Legacy `/` still serves
  `dashboard.html` until #M18-9.
- Tests: Vitest + React Testing Library + happy-dom; one smoke spec
  per route. Playwright skeleton installed (no specs — lands in #M18-3).
  `just test` now fans out via Turbo so Vitest and `bun test` run in
  their respective packages without the root scanner mis-picking up
  `.test.tsx` files.
- Linter: Biome (workspace-wide). One linter, no Oxlint.
- Docs: README + CLAUDE.md updated with the stack, proxy table, and
  `/app/*` serving story.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer approved these changes 2026-04-20 18:16:48 +00:00
reviewer left a comment

Review — M18-2 web bootstrap

CI: green (run #1766, 5m8s)
Round: 1 (first review)

All acceptance criteria from issue #163 are met. No blocking issues found.


Acceptance criteria — all green

Criterion Status
All stack libs (Vite 7, React 19, TS strict, Tailwind 4, Base UI, TanStack Router/Query, Zustand, CVA, tailwind-merge)
Path aliases @/* + @shared/* in both tsconfig.json and vite.config.ts
Design tokens from design/tokens.json in tokens.css, mapped to @theme in index.css — no raw hex downstream
Dev loop: bun run dev → Vite on 5173 with proxy to 4500 for all 9 API paths
just dev fans out both apps via Turbo
bun run builddist/
apps/server gains /app/* static-file route with SPA fallback, immutable caching on /assets/, max-age=300 on unhashed files, 503+hint when dist/ absent
Biome only (no third linter)
Vitest + RTL + happy-dom; smoke tests render landing and Base UI dialog trigger
Playwright skeleton (config only, no specs)
just test switched to bun x turbo run test (avoids Bun picking up .test.tsx under its native runner)

Code correctness

Path traversal guard (handleWebApp): correct. join(WEB_DIST_DIR, normalised) resolves all .. segments before the startsWith check, so any payload that escapes dist/ fails the prefix check and gets a 403. The candidate === WEB_DIST_DIR carve-out cleanly handles /app (no trailing slash) without a 403.

Cache-Control strategy: no-cache on the SPA index.html shell (correct — content changes on every deploy), immutable on Vite-fingerprinted /assets/ files (correct — content-hashed names), max-age=300 on everything else (favicon, manifest). Solid.

503 hint: the operator-facing message includes both apps/web and bun run build, which is exactly what the PR description promised and what the test asserts.

Test isolation in index.test.tsx: each test spins up its own in-memory router + QueryClient rather than importing the module-scoped singletons from main.tsx. This is the right pattern — no shared state bleed between tests.

_agentTypeProbe probe in index.tsx: intentional and documented. void _agentTypeProbe suppresses the lint warning cleanly. If the @claude-hooks/shared workspace alias ever breaks, typecheck fails before runtime does. Fine.


Minor observations (non-blocking)

  • @base-ui-components/react@^1.0.0-beta.5 is pre-release, but that's expected for M18-2 scope and is what the t3code stack ships with at this stage.
  • routeTree.gen.ts committed — standard TanStack Router pattern; needed for TS type-safety without requiring the dev server to be running.
  • apps/web/package.json's qa script (typecheck && test, no lint) is slightly inconsistent with the root just qa (which covers lint workspace-wide via Biome). Not a gap in coverage, just a cosmetic inconsistency; acceptable to address in a later cleanup.
## Review — M18-2 web bootstrap **CI**: green ✅ (run #1766, 5m8s) **Round**: 1 (first review) All acceptance criteria from issue #163 are met. No blocking issues found. --- ### Acceptance criteria — ✅ all green | Criterion | Status | |---|---| | All stack libs (Vite 7, React 19, TS strict, Tailwind 4, Base UI, TanStack Router/Query, Zustand, CVA, tailwind-merge) | ✅ | | Path aliases `@/*` + `@shared/*` in both `tsconfig.json` and `vite.config.ts` | ✅ | | Design tokens from `design/tokens.json` in `tokens.css`, mapped to `@theme` in `index.css` — no raw hex downstream | ✅ | | Dev loop: `bun run dev` → Vite on 5173 with proxy to 4500 for all 9 API paths | ✅ | | `just dev` fans out both apps via Turbo | ✅ | | `bun run build` → `dist/` | ✅ | | `apps/server` gains `/app/*` static-file route with SPA fallback, immutable caching on `/assets/`, `max-age=300` on unhashed files, 503+hint when `dist/` absent | ✅ | | Biome only (no third linter) | ✅ | | Vitest + RTL + happy-dom; smoke tests render landing and Base UI dialog trigger | ✅ | | Playwright skeleton (config only, no specs) | ✅ | | `just test` switched to `bun x turbo run test` (avoids Bun picking up `.test.tsx` under its native runner) | ✅ | --- ### Code correctness **Path traversal guard** (`handleWebApp`): correct. `join(WEB_DIST_DIR, normalised)` resolves all `..` segments before the `startsWith` check, so any payload that escapes `dist/` fails the prefix check and gets a 403. The `candidate === WEB_DIST_DIR` carve-out cleanly handles `/app` (no trailing slash) without a 403. **Cache-Control strategy**: `no-cache` on the SPA `index.html` shell (correct — content changes on every deploy), `immutable` on Vite-fingerprinted `/assets/` files (correct — content-hashed names), `max-age=300` on everything else (favicon, manifest). Solid. **503 hint**: the operator-facing message includes both `apps/web` and `bun run build`, which is exactly what the PR description promised and what the test asserts. **Test isolation in `index.test.tsx`**: each test spins up its own in-memory router + `QueryClient` rather than importing the module-scoped singletons from `main.tsx`. This is the right pattern — no shared state bleed between tests. **`_agentTypeProbe` probe in `index.tsx`**: intentional and documented. `void _agentTypeProbe` suppresses the lint warning cleanly. If the `@claude-hooks/shared` workspace alias ever breaks, typecheck fails before runtime does. Fine. --- ### Minor observations (non-blocking) - `@base-ui-components/react@^1.0.0-beta.5` is pre-release, but that's expected for M18-2 scope and is what the t3code stack ships with at this stage. - `routeTree.gen.ts` committed — standard TanStack Router pattern; needed for TS type-safety without requiring the dev server to be running. - `apps/web/package.json`'s `qa` script (`typecheck && test`, no `lint`) is slightly inconsistent with the root `just qa` (which covers lint workspace-wide via Biome). Not a gap in coverage, just a cosmetic inconsistency; acceptable to address in a later cleanup.
code-lead deleted branch boss/163 2026-04-20 18:17:28 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!172
No description provided.