The /admin left-rail NavLinks used relative targets (to="users", etc.). Under
the /admin/* nested route a relative link resolves against the current URL, so
each click appended a segment — Users→Allowlist→Graduation yielded
/admin/users/allowlist/graduation instead of /admin/graduation. Use absolute
targets (/admin/<tab>) + `end` for exact active matching.
Patch 0.52.1 → 0.52.2; CHANGELOG updated. Caught by the operator on PPE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The §9 deployed-environment E2E harness (0.52.0), run against a PPE host
with per-collection-isolated content, surfaced a latent multi-collection
bug: RFCView computed the collection id from the route but called
getRFC(pid, slug) without it, so a named-collection entry was always
fetched via the project default-collection route — which 404s for an entry
that exists only in a named collection ("Error: Not found"; metadata panel
absent). Local/Tier-1 stacks masked it (same slug also reachable via the
default collection). Thread cid through all three getRFC call sites; re-run
the load effect on collection change.
Harness/test-infra (not in the deployed artifact):
- e2e: pre-record cookie consent via addInitScript (lib/fixtures.js) so the
bottom-fixed consent banner can't intercept catalog row-select clicks on
the slower deployed edge.
- testing/seed-ppe.sh: fail loudly on any non-2xx Gitea response (a
swallowed 403 org-repo create had reached the deploy as a 502).
- testing/ppe-deploy-and-test.sh: seed via the Keychain admin token
(write:organization needed to create the PPE repos); store the E2E secret
newline-free; read EXPECT_VERSION from VERSION.
Patch bump 0.52.0 → 0.52.1; CHANGELOG updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runs the whole §9 PPE stage non-interactively after the operator's gcloud
reauth: bot-token-seed the PPE repos -> ensure E2E secret -> deploy ->
wait for health=0.52.0 + bdd-collection sync -> run metadata.spec.js
against the deployed host. Idempotent; secrets fetched from SM, never
echoed. Test/ops infra (not in the deployed artifact).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The §9 pipeline's PPE+E2E stage was unreachable: e2e/metadata.spec.js was
bound to Tier-1-only scaffolding (docker-seeded faceted collection,
SQLite-injected owner, Mailpit OTC sink). This makes the same suite run
against a deployed host.
- backend: POST /auth/test/login — fail-closed, secret-gated, single-
identity owner test-login (404 unless E2E_TEST_AUTH_SECRET +
E2E_TEST_AUTH_EMAIL both set; constant-time compare; loud startup warn).
6 vertical tests; backend 665 green. Documented in backend/.env.example.
- e2e/lib/auth.js: branch on E2E_TEST_AUTH_SECRET (deployed test-login vs
local Mailpit OTC); OWNER_EMAIL from E2E_OWNER_EMAIL. Spec unchanged so
the localhost Tier-1 path keeps working.
- testing/seed-ppe.sh: seed a dedicated, prod-untouching PPE registry +
content repo (faceted bdd collection) — real OHM content never touched.
- docs/design/2026-06-07-deployed-env-e2e-harness.md; CHANGELOG; VERSION
+ frontend/package.json -> 0.52.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Modernize the Tier-1 stack to the three-tier app and add browser coverage for
the §22.4a metadata UI, closing the E2E gap deferred across SLICE-3/4/5:
- seed-gitea.sh: create a REGISTRY_REPO with projects.yaml + a faceted named
collection (.collection.yaml fields: priority enum + tags) seeded with three
metadata-bearing entries; register content+registry webhooks; self-guarding
(skip if a prior token still works) so a dependency-triggered re-run can't
remint and invalidate the backend's token.
- .env.tier1: REGISTRY_REPO/DEFAULT_PROJECT_ID; disable OTC cooldown + lift the
per-IP auth limiter for the single-IP test runner.
- docker-compose: pin backend image; backend-seed inserts a granted owner the
OTC path can sign in as (write paths need contributor+).
- Makefile: two-phase tier1-up (seed to completion, then create backend so it
reads the populated token env); robust down; e2e-fresh = down+up+e2e (the
canonical run, since the edit/bulk specs mutate the seeded corpus).
- metadata.spec.js: SLICE-3 faceted filter (anon), SLICE-4 edit panel (owner),
SLICE-5 bulk bar (owner). 4 passed against a fresh stack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Wires the four-service Tier-1 integration stack and brings it up green.
Fixes found during live bring-up:
- gitea-admin-init: run as user 'git'; the gitea CLI refuses to run as
root and our custom entrypoint bypasses the image's s6 init that
normally drops privileges.
- backend.Dockerfile: COPY VERSION to /VERSION; health.py reads the
canonical VERSION file from the repo root (parents[2]) at import, so
it must exist in the image or the backend crashes on startup.
env_file cold-start: generated/.env.tier1.generated is created as an
empty placeholder (gitignored) so the backend env_file path always
exists; the seeder overwrites it before backend starts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>