# WebUI Phase P — File / Folder Structure & Migration Map

> **Status:** living document — first pass. Structure and increment status are verified against
> `next` as of merge `8c27024d`. Details (per-surface component inventories, exact route tables,
> test matrices) are still being fleshed out; extend the stub sections below rather than rewriting
> the verified structure.

## 1. What Phase P is

Phase P migrates the Mosaic **web UI** (`apps/web`) from the legacy **Next.js App Router** app to a
**Vite + React Router single-page app (SPA)** that the **Gateway serves same-origin** on
`:14242`. The RFC splits the work into **six increments (P1–P6)**; the P1 PR title records this as
"increment 1/6".

The migration is deliberately **incremental and non-destructive**: the new SPA is built up
_beside_ the existing Next app, sharing one `apps/web/src/lib` networking/auth layer, until the
final cutover (P5) removes the Next tree. At every point in between, **both app trees exist in the
same package** — this is intentional, not drift.

## 2. Current tree on `next` (dual-app, transitional)

```
apps/web/
├── next.config.ts                 # legacy Next.js config          (removed at P5)
├── vite.config.ts                 # SPA build + DEV proxy config    (canonical from P5)
├── package.json                   # dev/build default to NEXT today; :vite variants opt in
└── src/
    ├── main.tsx                   # ── SPA entry (Vite)
    ├── routes.tsx                 # ── SPA React Router route table
    ├── spa/                       # ── NEW SPA surfaces
    │   ├── guards.tsx             #    guest / authenticated route guards
    │   ├── pages/                 #    login, register, sso-callback (P2); chat + error boundary (P3)
    │   └── chat/                  #    P3 typed chat: use-chat-connection, commands-panel,
    │                              #    session-panel, message-transcript, tool-call-list, composer
    │
    ├── lib/                       # ── SHARED by BOTH trees (origin-relative networking + auth)
    │   ├── api.ts                 #    fetch wrapper — relative /api/...
    │   ├── socket.ts              #    Socket.IO singleton — relative /chat
    │   ├── auth-client.ts         #    BetterAuth client — relative /api/auth/...
    │   ├── auth-redirect.ts       #    post-auth redirect resolution (protocol-relative rejected)
    │   ├── chat-contract.ts       #    P3 typed chat wire contract (runtime-guarded)
    │   ├── sso.ts · types.ts · cn.ts
    │
    ├── app/                       # ══ LEGACY Next.js App Router (removed at P5)
    │   ├── (auth)/{login,register}/
    │   ├── (dashboard)/{admin,chat,projects,projects/[id],settings,tasks}/
    │   ├── auth/provider/[provider]/
    │   └── layout.tsx · page.tsx · globals.css
    │
    ├── components/                # ══ LEGACY Next component library (auth, chat, layout,
    │                              #    projects, settings, tasks, ui) — ported into spa/ across P3/P4
    └── providers/                 # ══ theme-provider (legacy; SPA equivalent under providers)
```

Legend: `──` new SPA (keep), `══` legacy Next (removed at P5), shared `lib/` in the middle.

## 3. Networking / serving model (why it's same-origin)

- The SPA speaks **origin-relative paths only**: `/api/...`, `/api/auth/...`, `/chat`. No
  `NEXT_PUBLIC_*` / `VITE_*` origin var, no hard-coded `http://localhost:14242` under
  `apps/web/src`.
- **Dev:** `vite.config.ts` runs a dev-only proxy that forwards those paths to the Gateway (so the
  SPA on its dev port and the Gateway on `:14242` behave as one origin).
- **Prod (target):** the SPA is **same-origin with the Gateway** — the Gateway serves the built
  static bundle and the API/WS on `:14242`, so no proxy and no CORS. _(The Gateway does not serve
  the web `dist` yet — adding that is the core of P5; see §5.)_

## 4. Build scripts (`apps/web/package.json`)

| Script                        | Today                                        | Notes                          |
| ----------------------------- | -------------------------------------------- | ------------------------------ |
| `dev`                         | `next dev`                                   | legacy dev server              |
| `dev:vite`                    | `vite`                                       | SPA dev server (+ dev proxy)   |
| `build`                       | `node ../../scripts/build-web.mjs`           | currently a **Next** build     |
| `build:vite`                  | `vite build`                                 | SPA production build → `dist/` |
| `lint` / `typecheck` / `test` | `eslint src` / `tsc --noEmit` / `vitest run` | tree-agnostic                  |

At **P5** the `:vite` variants become the defaults (`dev`→vite, `build`→vite build) and the Next
build path is retired.

## 5. Increment map (P1–P6)

| #      | Increment                                                                                                                                      | Branch                        | Status                    |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------- |
| **P1** | Vite + React Router skeleton beside Next (entry, router, guards, vitest)                                                                       | `feat/webui-p1-vite-skeleton` | ✅ merged — PR **#1143**  |
| **P2** | SPA data layer + same-origin auth (login/register/SSO pages, guards, relative api/socket/auth-client)                                          | `feat/webui-p2-data-auth`     | ✅ merged — PR **#1144**  |
| **P3** | Typed SPA **chat** (`spa/chat/*`, `chat-contract.ts`, chat page + error boundary)                                                              | `feat/webui-p3-chat`          | 🚧 in progress (unmerged) |
| **P4** | Port **projects / tasks / settings / admin** dashboard surfaces into the SPA                                                                   | _tbd_                         | ⏳ not started            |
| **P5** | **Cutover**: Gateway serves the Vite `dist` on `:14242`; flip `dev`/`build` to vite; **remove** the legacy Next `app/` tree + `next.config.ts` | _tbd_                         | ⏳ not started            |
| **P6** | CI / images (trails): build the SPA in CI, ship images                                                                                         | _tbd_                         | ⏳ trails                 |

Each increment follows the same delivery pipeline: brief traceable to the RFC → author →
**independent** integrator verification (build+test+typecheck+lint) → **independent** code + security
review (author ≠ reviewer) → author remediates → branch + PR to `next` → **independent** merge-gate
merges. Author self-reports are not trusted; every gate is re-derived independently.

## 6. Known dependency / blocker

- **Issue #1145 — Gateway `dist` boot is broken** (DI failure on a defaulted constructor param);
  the Gateway currently runs **dev-mode only**. This is a **hard precondition for P5**: the Gateway
  cannot serve the SPA `dist` on `:14242` until `dist` boot works. P3/P4 remain on the dev-proxy
  topology meanwhile.

## 7. Not part of Phase P (disambiguation)

`docs/plans/2026-08-09-webui-fleet-claude-bridge.md` and
`docs/scratchpads/webui-fleet-bridge-plan.md` describe a **separate** WebUI ↔ fleet/Claude bridge
effort. They are **not** the Phase P SPA migration and should not be conflated with the increments
above.

## 8. Where the detail lives (extend these)

- Per-increment working notes: `docs/scratchpads/webui-p*-*.md` (e.g. `webui-p2-data-auth.md`).
- _Stub — to flesh out:_ per-surface component inventory (which `components/*` port to which
  `spa/*`), the full SPA route table, the P5 cutover checklist, and the P6 CI/image plan.
