Frontend half of §22 M3, paired with the M3-backend spec. /p/<project>/ routing
(BrowserRouter + nested Routes + DeploymentProvider/ProjectLayout), N=1 redirect,
the VITE_APP_NAME->runtime hard cut (brandTitle fallback), per-project theme
overlay, real server-side 308 redirects, deployment directory + project switcher.
Scope boundary: corpus calls stay unscoped (default project only) with a guard
placeholder for non-served projects; second-project content is gated on Plan B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds two read endpoints that replace the build-time VITE_APP_NAME with a
runtime-config surface: /api/deployment returns deployment name/tagline plus
the projects visible to the caller (gated filtered by membership, unlisted
omitted from enumeration per §22.5); /api/projects/:id returns one project's
config and optional theme overlay, gated behind the §22.5 read gate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The first end-to-end smoke spec, run live against the Tier-1 stack
(§22 M3-0 Task 7). Drives the §6.2 email OTC sign-in: loads the app,
requests a code, reads it from Mailpit, verifies it, and asserts the
verify returns ok:true and sets the rfc_session cookie.
Supporting harness fixes:
- Makefile: add .PHONY so `make e2e` actually runs (the e2e/ directory
was shadowing the target, making it a no-op).
- mailpit.js: guard the per-message detail fetch (if !full.ok continue)
and scan the plain-text part before HTML so the \d{6} match can't
latch onto a stray number.
- spec uses a unique per-run email to avoid the per-email request
cooldown (OTC_REQUEST_COOLDOWN_SECONDS) on re-runs.
Co-Authored-By: Claude Opus 4.8 <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>
Adds the bite-sized TDD implementation plan for Plan A (registry mirror +
runtime-config APIs + initial_state/unreviewed semantics; additive only).
Refines the spec with two planning discoveries: the re-stamp must be a
Python startup step (pure SQL can't read DEFAULT_PROJECT_ID), and the
PK-rebuild blast radius justifies splitting M3-backend into Plan A (this)
and Plan B (rebuild + project_id threading + re-stamp, before M4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bite-sized TDD plan for the Tier-1 local-Docker test foundation: Vitest
frontend-unit setup, a four-service docker compose stack (seeded real
disposable Gitea + backend + nginx SPA + Mailpit), and a Playwright e2e
harness (BASE_URL + Mailpit mail-sink) with one OTC-login smoke spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bite-sized TDD plan for the Tier-1 local-Docker test foundation: Vitest
frontend-unit setup, a four-service docker compose stack (seeded real
disposable Gitea + backend + nginx SPA + Mailpit), and a Playwright e2e
harness (BASE_URL + Mailpit mail-sink) with one OTC-login smoke spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brainstormed design for the backend half of §22 slice M3, split at the
backend/frontend seam. Covers migration 027 (type/initial_state columns,
the 12-table PK rebuilds, default→slug re-stamp), the app/registry.py
mirror over REGISTRY_REPO, GET /api/deployment + /api/projects/:id, and
the initial_state/unreviewed review semantics. Routing, runtime branding,
directory, and switcher are deferred to a later M3-frontend spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brainstormed design for §22 slice M3 (registry mirror + routing + runtime
branding). Decomposes M3 into a §10.3 Tier-1 test/local-env foundation
(M3-0) plus four sequential feature sub-plans (M3a migration+restamp, M3b
registry mirror, M3c routing+redirects+branding, M3d landing-state/review).
Records resolved decisions: hard-cut branding, §4.1 reconciler pattern,
Stage-1 forward-only migration latitude with a future expand/contract note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrates the multi-project work so far — the §22 design drafts, M1 (the
schema spine), and M2 (project-scoped authorization + the §22.7 resolver) —
on top of the v0.32.0 retire/§13 changes that landed on main meanwhile.
Integration decisions:
- Renumbered the M1 projects migration 025_projects.sql -> 026_projects.sql.
v0.32.0 shipped 025_retired_state.sql, which rebuilds cached_rfcs and
predates the project_id column; running projects *after* the retire rebuild
is required so project_id survives on a fresh database (the runner applies
*.sql in filename order). Comment/doc references bumped to match.
- Resolved the get_rfc conflict in api.py by composing both gates: compute the
viewer once, apply the §22.5 visibility gate, then v0.32.0's §13.7
retired-entry owner-only check.
- api_graduation.py / api_discussion.py auto-merged cleanly (M2's threaded
viewer/visibility gate coexists with the new retire endpoints + retired
state).
Full suite 401 passed on a fresh DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds the three-tier authorization resolver on M1's schema spine: the
most-permissive union of deployment role (§6.1), project role (§22.6), and
per-RFC authority (§6.3/§12), with the §22.5 visibility gate subtractive on
top (§22.7). Pure app-layer — no migration (M1 shipped the tables), no
behavior change on the public default project. Verifiable on the single
default project by flipping its visibility and granting/revoking roles.
- app/auth.py: the §22.7 resolver primitives — project_visibility,
project_member_role, project_of_rfc, is_project_superuser, can_read_project,
effective write/discuss standing (can_contribute_in_project /
can_discuss_in_project), require_project_readable, visible_project_ids.
The three per-RFC capability helpers (can_discuss_rfc / can_contribute_to_rfc
/ can_invite_to_rfc) now compose all three tiers, so the ~20 call sites
inherit M2 unchanged.
- Read pass: the §22.5 visibility gate (404 to non-members) threaded into
every RFC-resolution helper across api.py / api_branches / api_prs /
api_graduation / api_discussion / api_contributions / api_invitations, plus
the catalog + proposals listings filtered by visible_project_ids.
- Write pass: the deep gates (branch read/contribute/owner, PR merge/withdraw/
edit, graduation, discussion resolve) fold in project_admin via
is_project_superuser; the "any-contributor" branch mode routes through
can_contribute_in_project; propose + claim gate on project contribute
standing. Deployment-level surfaces (admin idea-PR merge/decline, account
notification-mute) stay deployment-scoped.
- Operator decisions: implicit-on-public (a granted deployment contributor
keeps its pre-M2 write baseline on a public project, no membership row, so
the N=1 case stays whole) and preserve-curation (that baseline does not
override per-RFC owner curation — only an explicit project grant or a
deployment owner/admin does), keeping the v0.16.0 per-RFC invite contract
intact on public.
- tests: test_multi_project_authz_vertical.py — 9 vertical assertions on the
resolver tiers, public-unchanged regression, the gated 404 read gate, the
gated contribution gate, union + subtractive visibility, and revocation.
Full suite 390 passed.
- docs: mark Part C M2 "(landed)" with the two operator decisions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Default project id: config-derived slug (DEFAULT_PROJECT_ID > slug of
deployment name > 'default'); M1's 'default' bootstrap is re-stamped in M3
before any /p/ URL is public, so it's meaningful (/p/ohm/) and never renamed
live. §22.13.
- Entry-noun in URLs: generic /p/<project>/e/<slug> for every type; the noun
(RFC/Spec/Feature) is a type-driven UI label, not in the path. §22.10.
- Existing RFC-NNNN: kept as a frozen, read-only legacy display label in
frontmatter id (preserves citations); never used for lookup, never assigned
to new entries. §22.4, §2.3 amended.
Threads through §22.4/22.10/22.13, the §2.3 amendment, and the M3 slice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the §22 multi-project draft along four axes decided this session:
- Project type — each project declares document | specification | bdd
(immutable, registry-set). One shared propose→branch→PR→graduate engine;
type only layers entry schema, terminology, and type-specific surfaces
(spec release-planning, BDD scenarios/coverage). §22.4a.
- Slug-only identity — an entry is (project_id, slug); the per-project
RFC-NNNN numbering and its allocator are retired. §22.4, §2.3/§13 amended.
- initial_state — per-project landing state for a new entry: super-draft
(document/spec default) or active (bdd default). §22.4b.
- unreviewed flag — entries that skip straight to active land unreviewed;
graduation implies review so the normal path is never flagged. Owner
mark-reviewed clears it; §7 catalog gains an unreviewed filter; mirrored
into cached_rfcs. §22.4c.
Slicing plan re-sliced six→seven: type/initial_state/unreviewed config ride
M3, type surfaces become M5, membership→M6, hardening→M7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restructure the ohm vhost from the pre-certbot template into an explicit
80->443 redirect + hand-managed 443 TLS block, pointed at the wildcard cert
installed on the VM (/etc/ssl/certs/wiggleverse-wildcard.crt + the key under
/etc/ssl/private). Adds modern ssl_protocols/session settings that previously
came from certbot's managed snippet. All CSP/HSTS/security headers, root, and
locations preserved verbatim. Enables flipping ohm to the Cloudflare orange
cloud (SSL mode Full strict). Cert files must be installed before reload;
retire the old cert with `certbot delete` once cut over.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Optional number (§13.2/§13.3): GraduateBody.rfc_id is now optional.
A blank/absent id graduates to `active` with `id: null`, leaving the
slug as the canonical identifier (§2.3). /graduate/check treats a blank
id as valid (ok:true); /graduate only validates the RFC-NNNN regex +
collision check when a number is supplied. The Graduate dialog allows an
empty number and renders number-less entries by slug (no RFC-undefined).
Retire (§3, §3.1, §13.7): new `retired` soft-delete state. An RFC's own
owners (frontmatter) and site `owner`-role holders — not app admins —
retire via POST /api/rfcs/<slug>/retire, an auto-merged meta-repo flip
PR (graduation machinery reused). Retired entries leave every browsing
surface: catalog, get_rfc (404 except site owner), discussion/branch
reads. Un-retire is site-owner-only (POST .../unretire), discoverable
via the owner-gated GET /api/admin/retired-rfcs ("Retired" admin tab).
Migration 025 widens the cached_rfcs.state CHECK to include 'retired'
(table rebuild, all columns preserved).
Tests: graduation no-number cases (active+null id by slug; check accepts
blank) added to test_graduation_vertical.py; new test_retire_vertical.py
covers perms (owner/site-owner allowed, admin/contributor 403),
catalog/read exclusion, and a graduate→retire→unretire round-trip. Full
backend suite 386 passing; frontend builds clean. SPEC §3/§3.1/§13
updated; CHANGELOG + VERSION → 0.33.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lays the additive foundation for hosting N projects per deployment, with
today's single corpus as the N=1 case. No behavior change: the app runs
exactly as before, single project, with the spine underneath.
- migration 025: `projects` + `project_members` tables; seed the `default`
project (visibility=public, preserving open-by-default); thread
`project_id NOT NULL DEFAULT 'default'` onto all 19 slug-bearing tables,
backfilling existing rows. Additive — no table rebuilds; the slug-keyed
uniqueness/PK rework is enumerated in the migration header and deferred to
the slice that activates project #2.
- app/projects.py: §22.13 startup backfill of the default project's
content_repo from META_REPO (idempotent — never clobbers a value the
future registry mirror sets).
- config: REGISTRY_REPO wired (optional through M1; consumed by the M3
mirror), documented in .env.example as META_REPO's successor.
- tests: 6 vertical assertions on the spine (seed, backfill, column shape,
role CHECK, idempotency, config). Full suite 381 passed.
- docs: align Part C M1/M3 boundaries with the landed code (registry mirror
+ redirect move to M3).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design for hosting N projects (corpora) per deployment, with today's
single-corpus deployment as the N=1 case. Captures the locked decisions
(git registry, gated-by-default visibility, per-project RFC numbering),
the three-tier role model (deployment / project / per-RFC), and the
forced runtime-branding shift off VITE_APP_NAME.
- multi-project.md: rationale, decisions, operator (flotilla) integration.
- multi-project-spec.md: draft binding §22, in-place amendments to
§§1,2,5,6,7,8,13,14,17,18,20, and a six-slice (M1-M6) build plan.
Registry lives in a deployment-side repo the framework reads via a new
REGISTRY_REPO env var (META_REPO's multi-project successor); confirmed
against the ohm-rfc-app-flotilla spec — no operator-tooling change needed.
Not yet merged into SPEC.md or versioned; folds in when M1 lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The deploy docs predated the GCP name-alignment and described
infrastructure that no longer exists. Discovered during the v0.31.4
deploy. Corrections:
- Project wiggleverse-rfc -> wiggleverse-ohm; VM rfc-app -> ohm-rfc-app;
install path /opt/rfc-app -> /opt/ohm-rfc-app; system user + service
rfc-app -> ohm-rfc-app; external IP 34.132.29.41 -> 136.116.40.66.
- SSH is now IAP-only (direct port 22 times out): document
--tunnel-through-iap on every gcloud compute ssh.
- Meta repo wiggleverse/meta -> wiggleverse/ohm-content.
- Two-remote reality: the VM's git origin is git.benstull.org/benstull/
rfc-app, a SEPARATE Gitea from the release one (git.wiggleverse.org)
that does not auto-mirror — so a release must be pushed to the
benstull remote before the VM can fetch it. The VM tracks a detached
release TAG, not a branch.
- Frontend-only changes are live once dist/ is rebuilt (nginx serves it
directly); pip install only when requirements.txt changed.
Docs-only; no version bump (cf. the docs-only commit after v0.31.3).
The First-Time Deployment blocks keep the structural commands with a
substitution note rather than unvalidated rewrites.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`.btn-link` was a dark-header utility (white text on translucent white)
reused on light surfaces — the RFC breadcrumb action bar, PR diff toggle,
modals, discussion panel — where it rendered white-on-white and looked
like missing buttons (reported: "Metadata"/"Claim ownership"/"Invitations"
absent on a super-draft header). Root-cause fix:
- Base .btn-link is now a light-surface secondary button (white fill,
hairline border, dark label); the dark-header "Sign out" keeps the
translucent-on-dark treatment via an .app-header .btn-link scope.
- .breadcrumb-actions normalizes the toggle, filled CTAs, and secondary
buttons to one height/radius/type as a single control group, and
flex-wraps instead of clipping off the right edge.
- Diff-mode active toggle now reads as clearly selected (filled ink).
CSS-only; patch release, plain frontend rebuild applies it. No upgrade
steps. Driver session 0059.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The provisioning docstring claimed the invitee row gets
`last_seen_at = NULL` as the "not yet arrived" discriminator. It does
not: the column is NOT NULL and the INSERT omits it, so it defaults to
datetime('now') — the longer note below already explained this, but the
bullet contradicted it. Rewrite the bullet to state the real behavior
(both timestamps default to now; the pending-invite state lives in the
unclaimed user_invite_tokens row) and note that consumers must treat a
pending-invite row as never-seen. Comment-only; no runtime change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An admin-created invite row showed a Last-seen timestamp identical to
Signed-up, implying the invitee had visited. users.last_seen_at is
NOT NULL DEFAULT (datetime('now')) and the invite INSERT sets neither
timestamp, so both default to row-creation time; last_seen_at only
advances on real authentication. An unclaimed invite has provably never
authenticated (the unclaimed state drives the PENDING INVITE badge), so
the Users tab now renders "Never" for the Last-seen cell of a pending
row. Signed-up (invite-created date) unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Visual-only patch. The "/" welcome read-view was jammed against the
catalog divider with no top offset and loose paragraph rhythm: the
.main-pane §8 override (padding:0; display:flex) shadows the padded
read-view rule, so the pane gives no padding, and .welcome (max-width
only) never compensated. The welcome surface now owns its breathing
room — 56px top / 48px side gutters, a 680px measure, a text-3xl hero,
and even --space-8 paragraph spacing at --leading-relaxed. Covers both
the signed-out and signed-in welcome.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Visual-only patch atop v0.31.0. CSS plus markup/structure in Admin.jsx;
no API, schema, config, overlay, or secret change — a plain frontend
rebuild applies it.
Two latent CSS defects fixed:
- .invite-badge had no rule, so "(pending invite)" rendered as bare
parenthetical text; it's now a quiet amber pill.
- .btn-link-quiet never reset native <button> chrome, so the admin
Revoke/Grant/Remove buttons, the modal close ×, and Login/BetaPending
link-buttons kept the browser's grey button box. The reset the
.otc-login scope already carried is folded into the base rule.
Users tab: table headers no longer wrap (WRITE-MUTED), timestamps
render as a date-over-time stack, the duplicated subline email is
de-duped, the Create-user-+-invite action moves flush-right beside the
title, and intro DB-column refs read as quiet chips.
Header: the Inbox (§15.2) trigger was styled for a light surface
(gray-200 border, gray-50 hover) and rendered as a pale box that went
white-on-white on hover; restyled to the nav-link vocabulary
(borderless, gray-300 icon → white on a faint translucent hover).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>