chore(api): decommission Next.js UI (#185) #209

Merged
james merged 7 commits from 185-decommission-nextjs-ui into main 2026-06-21 15:57:16 +00:00
Owner

Closes #185.

Summary

The universal client (apps/client/) now ports every screen the PWA used to render (#184 slices 1–3) and the API container serves its static bundle as a fallback (#186). This PR deletes the Next.js UI surface so the API is, in fact, just the API plus the SPA shell.

What got deleted vs. what stayed

Deleted (UI surface):

  • apps/api/app/(app)/ — every authenticated screen (account, applications, chat, experience, network, notes, profile, projects, skills, plus the layout + sidebar components + dev/ design-system playground).
  • apps/api/app/login/, apps/api/app/register/, apps/api/app/offline/ — public auth + offline shell.
  • apps/api/app/page.tsx, apps/api/app/layout.tsx, apps/api/app/providers.tsx — root surfaces.
  • apps/api/app/manifest.ts, apps/api/app/sw.ts, apps/api/app/sw-register.tsx — PWA infrastructure.
  • apps/api/app/components/ — auth-status, oauth-buttons, the ui/ primitive set.
  • apps/api/app/themes/ — CSS files + the registry init script. (Pure-data preferences moved to apps/api/lib/themes/preferences.ts.)
  • apps/api/tsconfig.sw.json — service-worker TS config.
  • apps/api/lib/i18n/next-intl request config + set-locale-action. Only the deleted UI consumed it.
  • apps/api/lib/query/client.ts — the TanStack Query factory used only by app/providers.tsx.
  • apps/api/tests/themes/registry.test.ts, apps/api/tests/i18n/resolve.test.ts — tests of deleted modules.

Stayed (API surface untouched):

  • All apps/api/app/api/**/route.ts handlers.
  • apps/api/app/[[...spa]]/route.ts (renamed from [...spa] — see the fix commit below).
  • apps/api/lib/api/, apps/api/lib/auth/, apps/api/lib/dto/, apps/api/lib/profile/, apps/api/lib/spa/, apps/api/lib/storage/ — all referenced by /api/*.
  • apps/api/lib/themes/cookie.ts, server.ts, and the new preferences.ts keep the carol_theme cookie and /api/settings honest.
  • apps/api/public/icon.svg + icon-192.png + icon-512.png — kept per the ticket; the eventual Expo PWA can reuse them.
  • packages/i18n/messages/ — the catalog the universal client consumes.
  • Every DTO under apps/api/lib/dto/ — they are all still consumed by /api/* route handlers. None turned out to be UI-only.

Wave structure

Each wave is a separate commit so reviewers can take it in chunks:

  1. chore(api): delete pwa screens, components, and themes (#185) — UI pages, shared components, theme registry, plus the preferences-only test split.
  2. chore(api): delete root layout, providers, and PWA service worker (#185) — root layout, providers, and the @serwist/next infrastructure.
  3. chore(api): drop next-intl request config and tanstack query client (#185) — cascading dead code.
  4. chore(api): drop UI deps, prune public-route allowlist, refresh docs (#185)next.config.mjs simplification, package.json dep removal, lockfile, lib/auth/public-routes.ts, vitest.config.ts, README + CLAUDE.md + docs/ci.md.
  5. fix(api): make SPA catch-all optional so / serves index.html (#185) — surfaced during smoke testing.

Config / contract changes

  • apps/api/next.config.mjs — dropped the withSerwist and withNextIntl wrappers. Kept output: "standalone", outputFileTracingRoot, serverExternalPackages, and the webpack externals.
  • apps/api/package.json — removed @serwist/next, next-intl, serwist, @carol/i18n, @tanstack/react-{query,query-devtools,form,table}, and lucide-react. Lockfile regenerated.
  • apps/api/lib/auth/public-routes.ts — removed /offline, /manifest.webmanifest, /sw.js, and the /workbox- prefix. Kept /, /login, /register, and the favicon icons so the SPA shell remains reachable and proxy.ts's redirect target doesn't loop.
  • README.md — removed DEFAULT_LOCALE and SUPPORTED_LOCALES from the Configuration table (the API no longer reads either env var; the client has its own constants).
  • CLAUDE.md — rewrote the "Themes are pluggable" and "No hardcoded user-facing strings" bullets, plus the PWA / TanStack stack-default bullets, to point at the universal client.

Catch-all rename — [...spa][[...spa]]

The required catch-all only matched paths with at least one segment, so / was falling through to Next's _not-found. The optional form matches the empty segments case, and the handler normalises params.spa ?? [] to keep the existing SPA-fallback logic in lib/spa/serve.ts unchanged. Pinned with a new test.

PWA regression callout

Deleting manifest.ts, sw.ts, and sw-register.tsx removes PWA install + offline shell until the Expo Web bundle ships its own. Tracked in #208feat(client): PWA install + offline shell via Expo. The API-side icons (apps/api/public/icon{.svg,-192.png,-512.png}) stay so the Expo follow-up can reuse them.

Test plan

All run from the repo root.

  • pnpm install --frozen-lockfile
  • pnpm -F @carol/api typecheck
  • pnpm -F @carol/api lint
  • pnpm -F @carol/api test (554 passed, 107 skipped)
  • pnpm -F @carol/api openapi:check
  • pnpm -F @carol/api openapi:coverage
  • pnpm -F @carol/api build (standalone build emits 35 routes — /api/** + [[...spa]])
  • pnpm -F @carol/api-client check
  • pnpm -F @carol/client typecheck
  • pnpm -F @carol/client lint
  • pnpm -F @carol/client test
  • pnpm -F @carol/client export:web
  • podman build -t carol:185 .
  • Container smoke:
    • GET /api/health → 200 JSON {"status":"ok",...}
    • GET / → 200 HTML (SPA shell)
    • GET /login → 200 HTML (no redirect, no 404)
    • GET /register → 200 HTML
    • GET /_expo/static/js/web/entry-<hash>.js → 200 with Cache-Control: public, max-age=31536000, immutable
    • GET /icon.svg → 200 image/svg+xml
    • GET /api/auth/me (unauth) → 401 application/problem+json (the proxy keeps API paths on JSON rails)
    • GET /notes with Sec-Fetch-Dest: document (unauth) → 302 to /login?next=%2Fnotes

🤖 Generated with Claude Code

Closes #185. ## Summary The universal client (`apps/client/`) now ports every screen the PWA used to render (#184 slices 1–3) and the API container serves its static bundle as a fallback (#186). This PR deletes the Next.js UI surface so the API is, in fact, just the API plus the SPA shell. ## What got deleted vs. what stayed **Deleted (UI surface):** - `apps/api/app/(app)/` — every authenticated screen (account, applications, chat, experience, network, notes, profile, projects, skills, plus the layout + sidebar components + `dev/` design-system playground). - `apps/api/app/login/`, `apps/api/app/register/`, `apps/api/app/offline/` — public auth + offline shell. - `apps/api/app/page.tsx`, `apps/api/app/layout.tsx`, `apps/api/app/providers.tsx` — root surfaces. - `apps/api/app/manifest.ts`, `apps/api/app/sw.ts`, `apps/api/app/sw-register.tsx` — PWA infrastructure. - `apps/api/app/components/` — auth-status, oauth-buttons, the `ui/` primitive set. - `apps/api/app/themes/` — CSS files + the registry init script. (Pure-data preferences moved to `apps/api/lib/themes/preferences.ts`.) - `apps/api/tsconfig.sw.json` — service-worker TS config. - `apps/api/lib/i18n/` — `next-intl` request config + `set-locale-action`. Only the deleted UI consumed it. - `apps/api/lib/query/client.ts` — the TanStack Query factory used only by `app/providers.tsx`. - `apps/api/tests/themes/registry.test.ts`, `apps/api/tests/i18n/resolve.test.ts` — tests of deleted modules. **Stayed (API surface untouched):** - All `apps/api/app/api/**/route.ts` handlers. - `apps/api/app/[[...spa]]/route.ts` (renamed from `[...spa]` — see the fix commit below). - `apps/api/lib/api/`, `apps/api/lib/auth/`, `apps/api/lib/dto/`, `apps/api/lib/profile/`, `apps/api/lib/spa/`, `apps/api/lib/storage/` — all referenced by `/api/*`. - `apps/api/lib/themes/` — `cookie.ts`, `server.ts`, and the new `preferences.ts` keep the carol_theme cookie and `/api/settings` honest. - `apps/api/public/icon.svg` + `icon-192.png` + `icon-512.png` — kept per the ticket; the eventual Expo PWA can reuse them. - `packages/i18n/messages/` — the catalog the universal client consumes. - Every DTO under `apps/api/lib/dto/` — they are all still consumed by `/api/*` route handlers. None turned out to be UI-only. ## Wave structure Each wave is a separate commit so reviewers can take it in chunks: 1. `chore(api): delete pwa screens, components, and themes (#185)` — UI pages, shared components, theme registry, plus the preferences-only test split. 2. `chore(api): delete root layout, providers, and PWA service worker (#185)` — root layout, providers, and the `@serwist/next` infrastructure. 3. `chore(api): drop next-intl request config and tanstack query client (#185)` — cascading dead code. 4. `chore(api): drop UI deps, prune public-route allowlist, refresh docs (#185)` — `next.config.mjs` simplification, `package.json` dep removal, lockfile, `lib/auth/public-routes.ts`, `vitest.config.ts`, README + CLAUDE.md + docs/ci.md. 5. `fix(api): make SPA catch-all optional so `/` serves index.html (#185)` — surfaced during smoke testing. ## Config / contract changes - `apps/api/next.config.mjs` — dropped the `withSerwist` and `withNextIntl` wrappers. Kept `output: "standalone"`, `outputFileTracingRoot`, `serverExternalPackages`, and the webpack externals. - `apps/api/package.json` — removed `@serwist/next`, `next-intl`, `serwist`, `@carol/i18n`, `@tanstack/react-{query,query-devtools,form,table}`, and `lucide-react`. Lockfile regenerated. - `apps/api/lib/auth/public-routes.ts` — removed `/offline`, `/manifest.webmanifest`, `/sw.js`, and the `/workbox-` prefix. Kept `/`, `/login`, `/register`, and the favicon icons so the SPA shell remains reachable and `proxy.ts`'s redirect target doesn't loop. - `README.md` — removed `DEFAULT_LOCALE` and `SUPPORTED_LOCALES` from the Configuration table (the API no longer reads either env var; the client has its own constants). - `CLAUDE.md` — rewrote the "Themes are pluggable" and "No hardcoded user-facing strings" bullets, plus the PWA / TanStack stack-default bullets, to point at the universal client. ## Catch-all rename — `[...spa]` → `[[...spa]]` The required catch-all only matched paths with at least one segment, so `/` was falling through to Next's `_not-found`. The optional form matches the empty segments case, and the handler normalises `params.spa ?? []` to keep the existing SPA-fallback logic in `lib/spa/serve.ts` unchanged. Pinned with a new test. ## PWA regression callout Deleting `manifest.ts`, `sw.ts`, and `sw-register.tsx` removes PWA install + offline shell until the Expo Web bundle ships its own. Tracked in #208 — `feat(client): PWA install + offline shell via Expo`. The API-side icons (`apps/api/public/icon{.svg,-192.png,-512.png}`) stay so the Expo follow-up can reuse them. ## Test plan All run from the repo root. - [x] `pnpm install --frozen-lockfile` - [x] `pnpm -F @carol/api typecheck` - [x] `pnpm -F @carol/api lint` - [x] `pnpm -F @carol/api test` (554 passed, 107 skipped) - [x] `pnpm -F @carol/api openapi:check` - [x] `pnpm -F @carol/api openapi:coverage` - [x] `pnpm -F @carol/api build` (standalone build emits 35 routes — `/api/**` + `[[...spa]]`) - [x] `pnpm -F @carol/api-client check` - [x] `pnpm -F @carol/client typecheck` - [x] `pnpm -F @carol/client lint` - [x] `pnpm -F @carol/client test` - [x] `pnpm -F @carol/client export:web` - [x] `podman build -t carol:185 .` - [x] Container smoke: - `GET /api/health` → 200 JSON `{"status":"ok",...}` - `GET /` → 200 HTML (SPA shell) - `GET /login` → 200 HTML (no redirect, no 404) - `GET /register` → 200 HTML - `GET /_expo/static/js/web/entry-<hash>.js` → 200 with `Cache-Control: public, max-age=31536000, immutable` - `GET /icon.svg` → 200 image/svg+xml - `GET /api/auth/me` (unauth) → 401 `application/problem+json` (the proxy keeps API paths on JSON rails) - `GET /notes` with `Sec-Fetch-Dest: document` (unauth) → 302 to `/login?next=%2Fnotes` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Tear down the Next.js UI surface introduced for the PWA: every screen
under app/(app), the public auth pages (login/register/offline), the
root home page, and the design-system playground at app/dev. The
shared app/components tree (auth-status, oauth-buttons, ui/*) goes
with them.

The app/themes/ directory is removed in favour of lib/themes/
preferences.ts so /api/settings, the carol_theme cookie, and the
SettingsDTO keep the same value vocabulary. The full CSS token surface
lives in the universal client (apps/client/lib/theme/tokens.ts).

Tests for the deleted theme init script go too; the pure-data tests
migrate to tests/themes/preferences.test.ts.

Part of #185, ADR-0027.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wave 2 of the Next.js UI teardown: the root app/layout.tsx,
app/providers.tsx (TanStack Query + next-intl wrappers), and the
@serwist/next infrastructure (app/sw.ts, app/sw-register.tsx,
app/manifest.ts, tsconfig.sw.json).

PWA install + offline shell regress with this commit — they will
return through Expo's PWA wiring in a follow-up ticket. The Next.js
build now ships /api/* routes plus the SPA catch-all from #186 and
nothing else.

Part of #185, ADR-0027.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wave 3 of the Next.js UI teardown — cascading dead code. The
lib/i18n/ directory (next-intl request config + the cookie-setting
server action) and lib/query/client.ts (the TanStack Query client
factory) had only the deleted UI as callers. packages/i18n/messages/
stays — it's the catalog the universal client consumes.

The tests/i18n/resolve.test.ts unit tests for locale negotiation go
with the runtime; the corresponding logic now lives in
apps/client/lib/i18n/locales.ts with its own test.

Part of #185.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wave 4 of the Next.js UI teardown:

- next.config.mjs sheds withSerwist + withNextIntl wrappers; keeps the
  standalone output, tracing root, and webpack externals needed for the
  Node-only DB layer.
- package.json drops the UI-only deps that became unreachable:
  @serwist/next, next-intl, @tanstack/react-{query,query-devtools,form,table},
  lucide-react, serwist, @carol/i18n. pnpm-lock.yaml regenerated.
- lib/auth/public-routes.ts loses /offline, /manifest.webmanifest,
  /sw.js, and /workbox- (PWA-specific paths deleted with the UI).
  Keeps /, /login, /register so the proxy redirect to /login still
  terminates after the catch-all takes over.
- vitest.config.ts coverage exclusions slim down — there's no UI left
  to exclude.
- CLAUDE.md (themes / i18n bullets), README.md (env-var table), and
  docs/ci.md (coverage exclusions) drop the dead-path references.

Part of #185, ADR-0027.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(api): make SPA catch-all optional so / serves index.html (#185)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 7s
PR / OSV-Scanner (pull_request) Successful in 2m2s
PR / Static analysis (pull_request) Successful in 2m29s
PR / pnpm audit (pull_request) Successful in 2m42s
PR / OpenAPI (pull_request) Successful in 2m51s
PR / Client (web export smoke) (pull_request) Successful in 3m3s
PR / Lint (pull_request) Successful in 3m38s
PR / Typecheck (pull_request) Successful in 3m50s
PR / Build (pull_request) Successful in 3m59s
PR / Test (postgres) (pull_request) Failing after 3m59s
PR / Test (sqlite) (pull_request) Successful in 3m59s
PR / Package age policy (soft) (pull_request) Successful in 1m18s
Secrets / gitleaks (pull_request) Successful in 1m12s
PR / Coverage (soft) (pull_request) Successful in 2m3s
PR / Trivy (image) (pull_request) Failing after 2m42s
bc045c025b
With the Next.js UI gone, `/` no longer matches an explicit page route.
The required catch-all `[...spa]` only matches paths with at least one
segment, so `/` was falling through to Next's not-found page.

Rename to the optional form `[[...spa]]`. The route handler now
receives `params.spa === undefined` for the root, which the resolver
normalises to "serve index.html" via the existing SPA fallback. Adds
a unit test that pins the behaviour.

Surfaced when smoke-testing the container after waves 1–4.

Part of #185, ADR-0027.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 82.9% ≥ 50%
Branches 76.0% ≥ 75%
Functions 91.3% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 82.9% ✅ | ≥ 50% | | Branches | 76.0% ✅ | ≥ 75% | | Functions | 91.3% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |
react + react-dom were exact-pinned at 19.2.3 in apps/client while the
rest of the workspace (apps/api, packages/api-client) resolves
^19.0.0 → 19.2.7. pnpm materialised both copies; the Metro bundle
ended up with two React instances, so TanStack Query's context
(identity-keyed by React's createContext) ran on one React and the
hooks looked it up on the other — surfacing as "No QueryClient set"
on every screen once the SPA started serving on its own.

Loosening to ^19.2.3 lets pnpm dedupe to the workspace's resolved
19.2.7. Single React in the bundle; provider context found.
feat(client): sidebar nav shell on the (app) layout
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 9s
PR / OSV-Scanner (pull_request) Successful in 2m10s
PR / Static analysis (pull_request) Successful in 2m27s
PR / Lint (pull_request) Successful in 2m37s
PR / pnpm audit (pull_request) Successful in 2m53s
PR / Client (web export smoke) (pull_request) Successful in 2m53s
PR / OpenAPI (pull_request) Successful in 3m15s
PR / Typecheck (pull_request) Successful in 3m36s
PR / Package age policy (soft) (pull_request) Successful in 58s
PR / Build (pull_request) Successful in 3m36s
PR / Test (postgres) (pull_request) Successful in 3m42s
PR / Test (sqlite) (pull_request) Successful in 3m47s
Secrets / gitleaks (pull_request) Successful in 56s
PR / Coverage (soft) (pull_request) Successful in 1m48s
PR / Trivy (image) (pull_request) Failing after 2m19s
a6f543cb11
The (app) layout was a bare protected gate — no nav UI. Worked while
Next.js still served its own sidebar, but #185 deleted that UI; the
Expo client is now the only surface and the user had no way to move
between screens.

Adds a 240px sidebar on web (brand + nav rows + identity + logout
footer); native gets a stacked layout for now. Active-route gets a
soft-accent background. Nav reads i18n via the per-screen `title`
key and the existing `nav.brand` / `nav.logOut` keys.

A richer nav shell (icons, collapse toggle, theme + locale switchers,
real native drawer, mobile hamburger) is tracked separately in #210.
james merged commit 983dd57679 into main 2026-06-21 15:57:16 +00:00
james deleted branch 185-decommission-nextjs-ui 2026-06-21 15:57:16 +00:00
Sign in to join this conversation.
No description provided.