§22 M3 frontend: /p/<project>/ routing, runtime branding, directory, 308s (v0.35.0)

Implements the M3-frontend slice of the §22 multi-project track, per
docs/superpowers/specs/2026-06-03-m3-frontend-design.md (design merged in
#10). Completes the runtime-config cut 0.33.0 (M3-backend Plan A) began.

Frontend:
- DeploymentProvider boots GET /api/deployment → {name, tagline,
  defaultProjectId, projects}; brandTitle() neutral 'RFC' pre-fetch fallback.
- /p/:projectId/* routing with generic /e/<slug> segment. ProjectLayout
  fetches /api/projects/:id, applies per-project theme (reset on switch),
  provides ProjectContext, guards the corpus (served only for the default;
  others get NotServedPlaceholder — decouples this slice from Plan B).
- Directory at / (2+ projects) with N=1 redirect into the single project;
  ProjectSwitcher in deployment chrome; entry-noun by project type.
- VITE_APP_NAME hard cut: removed from vite.config + index.html; the 6 brand
  reads now use deployment.name via context; static <title>RFC</title> + JS
  document.title. Internal /rfc·/proposals links → /p/<project>/e|proposals
  via lib/entryPaths.

Backend:
- GET /api/deployment returns default_project_id (the guard contract).
- Server-side 308s: /rfc/<slug>, /rfc/<slug>/pr/<n>, /proposals/<n> →
  /p/<default>/… . nginx (testing + prod) routes /rfc/ and /proposals/ to
  the backend.

Tests: 3 new backend redirect/deployment tests (438 pass); Vitest unit for
DeploymentProvider, ProjectLayout (theme/guard/404), Directory (11 pass);
clean build with no VITE_APP_NAME. Playwright e2e deferred until Tier-1 seeds
a registry (see CHANGELOG 0.35.0 step 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-04 05:58:59 -07:00
parent 0252e40527
commit 999c4b65ef
39 changed files with 798 additions and 99 deletions
+78
View File
@@ -23,6 +23,84 @@ skip versions are the composition of each intervening adjacent
release's steps in order — no A-to-B path is pre-computed beyond
that.
## 0.35.0 — 2026-06-04
**Minor (breaking) — §22 M3 frontend: `/p/<project>/` routing, runtime
branding (the `VITE_APP_NAME` hard cut), the deployment directory + project
switcher, and server-side 308 redirects off the old corpus-root URLs.
Completes the runtime-config cut 0.33.0 began.**
Added:
- **`DeploymentProvider`** (`src/context/DeploymentProvider.jsx`) — boots
`GET /api/deployment` once and provides `{ name, tagline,
defaultProjectId, projects[], loading }` to the tree. The neutral
`brandTitle()` fallback (`'RFC'`) paints during the pre-fetch frame.
- **`/p/:projectId/*` routing** with the generic `/e/<slug>` entry segment
(§22.10). **`ProjectLayout`** (`src/components/ProjectLayout.jsx`) fetches
`GET /api/projects/:id`, applies the project's `theme` as `:root` CSS
custom-property overrides (reset on switch/unmount so accents never bleed),
provides `ProjectContext`, and sets the tab title to the project name.
- **The §4 guard** — `ProjectLayout` renders the corpus only for the
corpus-served (default) project; any other id renders a "content not yet
served" placeholder (`NotServedPlaceholder`), so per-project serving (the
next backend slice, Plan B) can land without a wrong-content footgun.
- **Deployment directory** at `/` (`Directory.jsx`) — renders the
caller-visible projects as cards when **2+** are visible; the **N=1** case
redirects straight into the single project (`/p/<id>/`), preserving OHM's
"land in the corpus" UX. A **project switcher** (`ProjectSwitcher.jsx`)
rides deployment chrome when 2+ projects are visible.
- **Entry-noun terminology** (RFC / Spec / Feature) driven by `project.type`
(§22.4a); the route segment stays the generic `/e/`.
- **`GET /api/deployment`** now returns **`default_project_id`** — the
corpus-served project the frontend guard keys on.
- **Server-side 308 redirects** (`app/api_deployment.py`): `GET /rfc/<slug>`,
`/rfc/<slug>/pr/<n>`, and `/proposals/<n>` permanently redirect to
`/p/<default>/e/<slug>[…]` / `/p/<default>/proposals/<n>` (§5/§22.10), so
external "RFC-0001"-style links and bookmarks keep working.
Breaking:
- **`VITE_APP_NAME` is removed.** The build no longer reads it and no longer
fails without it; the deployment name comes from the registry
(`deployment.name` in `projects.yaml`) served at runtime via
`GET /api/deployment`. The same build now serves any deployment. The
build-time `%VITE_APP_NAME%` HTML token and the `inject-app-name` Vite
plugin are gone; `index.html` ships a static `<title>RFC</title>` and JS
sets the real title after config loads.
- **Entry/proposal URLs moved under `/p/<project>/`.** The old SPA routes
`/rfc/:slug`, `/rfc/:slug/pr/:n`, `/proposals/:n` are removed from the SPA
and served as backend 308s instead — so nginx must route `/rfc/` and
`/proposals/` to the backend rather than the SPA `index.html`.
Scope boundary (informational, not breaking): RFC *data* is still served
unscoped for the default project this slice — per-project corpus serving is
the next backend slice (Plan B). Non-default projects show the placeholder.
Upgrade steps:
1. **MUST** update nginx: add `location /rfc/` and `location /proposals/`
blocks that `proxy_pass` to the backend, **before** the SPA `location /`
fallback. The framework's `deploy/nginx/ohm.wiggleverse.org.conf` (and
`testing/web.nginx.conf`) already carry them; a deployment running a custom
vhost MUST add them, or old corpus-root URLs 404 against the SPA instead of
redirecting.
2. **MUST** ensure the registry `projects.yaml` `deployment.name` is set — it
is now the header brand and tab title (already required since 0.33.0).
`tagline` shows on the directory.
3. **SHOULD** remove `VITE_APP_NAME` from `frontend/.env`; it is no longer
read (no error if left — simply ignored).
4. **MUST** rebuild the frontend and deploy. Verify: the header shows the
deployment name from `/api/deployment`; `/` lands in your project (N=1) or
shows the directory (2+); an old `/rfc/<slug>` URL **308**-redirects to
`/p/<default>/e/<slug>`; `/api/health` is green.
5. **MAY** note: the Tier-1 Playwright e2e for these flows lands once the
Tier-1 Docker stack seeds a registry repo + `projects.yaml` (`REGISTRY_REPO`
is unset in `testing/.env.tier1` today, so the dockerized backend can't boot
there post-0.33.0). This slice is covered by Vitest unit tests
(`DeploymentProvider`, `ProjectLayout` theme/guard, `Directory`) + the
backend redirect tests (`backend/tests/test_api_deployment.py`).
## 0.34.0 — 2026-06-04
**Minor — containerize rfc-app for per-PR preview environments (flotilla