Roadmap item #11. Ships the non-modal bottom-of-page cookie consent banner, the default /privacy and /cookies policy pages, the `cookie_consent` table + two §17 endpoints for server-side persistence, the localStorage fallback for anonymous viewers, the /settings "Privacy & cookies" tab for revisiting the choice, and the `frontend/src/lib/consent.js` helper that roadmap item #13's analytics SDK (v0.15.0) will gate against. No analytics SDK ships in this release — the consent infrastructure goes in first so the gate is already in place. Adds SPEC §14.5 / §14.6, lists two new endpoints in §17, names the new table in §5, and surfaces four §19.2 candidates (content-repo file vs env-var policy, GPC / DNT headers, i18n, item-#13 dependency). Two new optional env vars (`VITE_PRIVACY_POLICY_URL`, `VITE_COOKIES_POLICY_URL`) — defaults render the framework's stub pages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
35 KiB
Changelog
The binding policy for what each kind of version bump means and what
the changelog has to carry is in SPEC.md §20. The
practical recipe downstream deployments follow when reading this file
is in docs/DEPLOYMENTS.md.
The canonical version is the VERSION file at the repo root;
frontend/package.json#version mirrors it. Deployments pin to a
specific framework version in their own .rfc-app-version file
(per §20.5).
While the framework is pre-1.0, minor-version bumps may introduce breaking changes; the entry below each such bump documents the upgrade steps a deployment must apply.
Upgrade-steps blocks use the RFC 2119
/ RFC 8174 normative-
language convention per SPEC.md §20.4. MUST / SHALL steps
are required; SHOULD steps are recommended (the framework's
default and tested path); MAY steps are optional. Upgrades that
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.13.0 — 2026-05-28
Minor — schema migration required; new optional env vars. This
release ships the cookie / privacy consent surface (roadmap item #11,
SPEC §14.5 / §14.6). Every viewer — authenticated and anonymous alike —
now sees a non-modal bottom-of-page banner on first visit asking which
categories of cookies they allow (essential / essential + analytics /
essential + analytics + other). The choice persists in localStorage
for anonymous viewers and in a new cookie_consent table for
authenticated viewers, with server-side overriding local on sign-in.
The framework also ships default /privacy and /cookies policy pages
that deployments can layer their own policy URL on top of via two new
optional env vars. No analytics SDK ships in this release — the
consent infrastructure is wired so item #13 (v0.15.0) can read from
frontend/src/lib/consent.js when the SDK lands.
Added
- Cookie consent banner (
frontend/src/components/CookieConsentBanner.jsx). Non-modal, bottom of viewport. Three single-select choices with inline descriptions. Visible until the user makes a choice; hides thereafter. Reachable for revision via the settings surface. - Consent helper (
frontend/src/lib/consent.js). ExportsgetConsent(),hasChosen(),onConsentChange(cb),setConsent(),hydrateFromServer(),clearLocal(). Cross-tab sync via thestorageevent. Item #13's analytics SDK reads consent here before importing. - Privacy and cookies policy pages
(
frontend/src/pages/Privacy.jsx,frontend/src/pages/Cookies.jsx). Default minimal policies that describe the framework's stance and list the cookies the framework sets. Deployments override via the two new env vars below; the framework's stub always renders above the link so the framework-level contract stays visible. - "Privacy & cookies" tab in
/settings/notificationsshowing the current consent choice, the recorded-at stamp, and a "Change" button that re-opens the banner via a custom DOM event. §17endpoints —GET /api/users/me/cookie-consent— read the current consent record.PUT /api/users/me/cookie-consent— write a new consent record. Upserts a single row per user, stampsrecorded_atto now, acceptsessentialfor symmetry but always persists it as true.
- Schema migration
013_cookie_consent.sql— newcookie_consenttable keyed byuser_id, three flags (essential,analytics,other_cookies), andrecorded_at. (Renumbered from012_*during driver integration because v0.7.0 also added a012_otc.sqlmigration that landed in the integration order before this one.) - SPEC
§14.5Cookie / privacy consent — settles the banner shape, the three-category single-select, the storage shape (local for anon, server row for authenticated), the precedence rule on sign-in, and theconsent.jshelper surface for downstream callers including item #13. - SPEC
§14.6Privacy and cookies policy pages — settles the/privacyand/cookiesroutes, the framework's stub content, and theVITE_PRIVACY_POLICY_URL/VITE_COOKIES_POLICY_URLoverride shape. - SPEC
§5— names thecookie_consenttable in the canonical app-tables list. - SPEC
§17— lists the two new cookie-consent endpoints. - SPEC
§19.2— surfaces four candidates: policy content via content-repo file vs env var, GPC / DNT headers, multi-language consent text, and the item #13 analytics-SDK gating dependency.
Changed
frontend/.env.example— documents the two new optional env varsVITE_PRIVACY_POLICY_URLandVITE_COOKIES_POLICY_URL. Unset is supported; defaults render the framework's stub.backend/app/api_notifications.py— module docstring grew two endpoint lines; the new endpoints sit alongside the existing/api/users/me/*neighbors.frontend/src/App.jsx— registers/privacyand/cookiesroutes (anonymous-reachable), wires<CookieConsentBanner>into the global chrome, and listens for arfc-app:cookie-consent-reopencustom event to re-open the banner from the settings surface.
Upgrade steps (from 0.7.0)
- You MUST rebuild the frontend and restart the backend after
upgrading.
frontend/package.json#versionandVERSIONboth move to0.13.0and the build embeds the new env-var contract. - You MUST apply schema migration
013_cookie_consent.sql. The migration creates a single new table keyed byuser_idwith three flag columns and arecorded_atstamp. The framework runs migrations automatically at process start; no manual step is required beyond restarting the backend so the migration runner picks the file up. - You MAY set
VITE_PRIVACY_POLICY_URLto an http(s) URL that points at your deployment's full privacy policy. The framework's/privacypage renders its built-in stub above a link to the configured URL. Unset is supported — the stub is sufficient for a default-config deployment. - You MAY set
VITE_COOKIES_POLICY_URLto an http(s) URL that points at your deployment's full cookies policy. Same shape as the privacy URL. - You MAY announce the new consent banner to your users. Existing
authenticated users will see the banner on their next visit
(because their
cookie_consentrow does not yet exist); their current sessions remain valid.
0.7.0 — 2026-05-28
Minor — schema migration required; new auth path is additive.
This release lands email + one-time-code sign-in (roadmap item #5,
SPEC §6.2) as the primary human-auth gesture. Users sign in by
typing their email, receiving a six-digit code via email, and
entering it. The Gitea OAuth callback (/auth/callback) remains
functional during migration — the new UI no longer points at it
primarily, but a "Sign in with Gitea (fallback)" link survives on
the new login surface so users with active OAuth sessions or older
invite paths still have a way in. A future release retires the
OAuth path entirely once every active user has signed in at least
once via OTC.
The migration path for existing users: first OTC sign-in matches by
users.email (case-insensitive) to the OAuth-era row and reuses
that row's id and gitea_id. New users provisioned via OTC carry
gitea_id = NULL and gitea_login = NULL. The gitea_id linker
remains the canonical handle for grandfathered users; email
becomes the identity key for everything provisioned after v0.7.0.
The Gitea bot user + token are still required (server-side git operations — repo reads, PR creation — still flow through it). Only the operator-facing sign-in surface moves.
Upgrade steps (from 0.6.0)
- MUST restart the backend so migration
012_otc.sqlruns. The migration rebuilds theuserstable (SQLite cannot ALTER COLUMN); existing rows pass through unchanged, but the new schema relaxesgitea_id/gitea_loginto nullable (with partial unique indexes that ignore NULL) and adds a partial unique index onemail. A newotc_codestable is created. - MUST confirm the SMTP overlay is set (
SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASSWORD,SMTP_STARTTLS,EMAIL_FROM,EMAIL_FROM_NAME). The OHM overlay already carries these as of v0.5.0; deployments without them fall back to logging the code to stdout (dev-only path — production visitors will not receive their codes). - SHOULD announce the new email-based sign-in to existing users. Wording suggestion: "You can now sign in by entering your email and a one-time code we'll send you. Your old account is linked automatically the first time you sign in."
- MAY keep the existing OAuth callback as a fallback path. The new login UI surfaces a small "Sign in with Gitea (fallback)" link beneath the primary email/code form; a deployment that prefers to hide it can override the Login component in a future framework release that exposes the link behind a feature flag. For v0.7.0, the link is hard-coded.
New environment variables (all optional with defaults)
OTC_TTL_MINUTES(default10) — how long a one-time code is valid after issuance. Re-requesting invalidates the prior code immediately regardless of TTL.OTC_REQUEST_COOLDOWN_SECONDS(default60) — per-email cooldown between successive/auth/otc/requestcalls. The endpoint returns HTTP 429 when the cooldown blocks a request (the loud-failure shape; the abuse path is visible rather than swallowed).
No new secrets are required. The existing SECRET_KEY continues to
sign session cookies; OTC codes are bcrypt-hashed at rest using a
per-row salt the library generates.
Added
POST /auth/otc/request— body{email}. Generates a six-digit code, stores its bcrypt hash with an expiry, and dispatches a plain text email via the existing SMTP layer. Returns HTTP 200 ({ok:true}) uniformly so allowlist state is not leaked. Returns HTTP 429 when the per-email cooldown blocks the request.POST /auth/otc/verify— body{email, code}. Validates the bcrypt hash against the most-recent unconsumed non-expired row, marks the row consumed, provisions or links theusersrow by email, and stores the session cookie. Returns HTTP 200 on success, HTTP 400 on any failure (expired, consumed, wrong, unknown).backend/migrations/012_otc.sql— createsotc_codesand rebuildsuserswith nullablegitea_id/gitea_loginplus a partial unique index onemail.backend/app/otc.py— the OTC request/verify state machine and theprovision_or_link_userlinker.backend/app/email_otc.py— outbound OTC mail composition. Reuses the SMTP plumbing fromemail.py(EmailConfig.from_env()) and the test buffer (_SENT) but writes its own envelope (no unsubscribe footer, no quiet-hours hold — OTC mail carries a credential and ignores notification preferences).frontend/src/components/Login.jsx— two-step sign-in surface at/login. Step 1: enter email → request code. Step 2: enter six-digit code → verify. Cmd/Ctrl+Enter on the code field submits. The previous header "Sign in" link and theWelcomecomponent's inline link now route to/logininstead of jumping straight to the Gitea OAuth dance.- SPEC
§6.1/§6.2/§14.1/§17/§19.2corrections per §19.3 rule-2 — see below. backend/tests/test_otc_vertical.py— 11 new tests covering the happy path, expired/consumed/wrong codes, the per-email rate limit (and its per-email isolation), the allowlist gate, the migration link to OAuth-era users, fresh provisioning, and the prior-code-invalidation behavior on re-request.
Changed
backend/app/auth.py#current_user— coerces NULLgitea_idand NULLgitea_loginto0/""so theSessionUsershape stays stable for OTC-only users. The DB remains the source of truth for "is this user OAuth-linked" (gitea_id IS NOT NULL).backend/requirements.txt— addsbcrypt>=4.2for OTC code hashing. Pure-Python wheels are available on every platform the deployment matrix targets;pip install -r backend/requirements.txtpicks it up.frontend/src/App.jsx— adds the/loginroute and replaces the header "Sign in"<a href="/auth/login">with<Link to="/login">. TheWelcomecomponent's inline sign-in link follows suit.frontend/src/components/Landing.jsx— the/welcomepage's primary action moves from "Sign in with Gitea" to "Sign in" pointing at/login.
Deferred to later releases
Per the v0.7.0 scope discipline (the foundation for items #6, #8, #9, #10), several adjacent capabilities are intentionally not in this release and surface as §19.2 candidates:
- First-OTC profile capture (first name, last name, "why") — item #6, expected v0.8.0.
- Open beta-access request flow replacing the allowlist gate — also item #6, v0.8.0.
- Passcodes (a long-term reauth token alternative) — item #8, expected v0.10.0.
- Device-trust 30-day skip — item #9, expected v0.11.0.
- Cloudflare Turnstile on
/auth/otc/request— item #10, expected v0.12.0. - Removing the Gitea OAuth
/auth/callbackroute entirely — a later release after every active user has signed in via OTC.
0.6.0 — 2026-05-28
Minor — no operator action required. A sweep-the-edges hardening
release (roadmap item #4, "anon discuss + contribute off-limits") that
audits every write-shaped backend endpoint and asserts each one
enforces an explicit auth.require_contributor (or stricter) gate
before doing any state-changing work. v0.3.0 hid write affordances
behind a sign-in CTA on the frontend; v0.5.0 added the PR-less
discussion surface with its own write gate; v0.6.0 sweeps the rest
and adds a regression test net so future endpoints can't quietly
ship without a gate. No schema migration, no env-var changes, no
new dependencies. The only behavioural change is one tightening: the
GET /api/rfcs/<slug>/graduate/progress SSE now requires
auth.require_user since it surfaces operator-visible step detail
(repo name, PR number, rollback steps) not part of the v0.3.0
anonymous-read contract for catalog/RFC bodies.
Changed
backend/app/api_graduation.py—GET /graduate/progressnow callsauth.require_user(request)as its first line. Anonymous callers receive 401 instead of being able to subscribe to a graduation's SSE. The floor isrequire_user(notrequire_contributor) so a write-muted operator can still observe the progress of a graduation they kicked off before being muted.- SPEC
§6.1(SPEC.md) — the Anonymous role's bullet now documents the v0.6.0 audit: every write-shaped endpoint in §17 enforces an explicit gate; anonymous writes refuse 401. The list of audited write families is recorded in-line. - SPEC
§10.10— the discussion-vs-contribution section now records that the v0.5.0 write gates have a regression test net (test_anon_offlimits_vertical.py) added in v0.6.0. - SPEC
§17— theGET /api/rfcs/<slug>/graduate/progressbullet now documents therequire_usergate added in v0.6.0, with the rationale.
Added (tests)
backend/tests/test_anon_offlimits_vertical.py— twelve new tests asserting that every write-shaped endpoint surveyed in the v0.6.0 audit refuses anonymous callers with 401, and that the five anonymous-read surfaces (health, philosophy, auth/me, catalog, RFC view, discussion threads, proposals) stay reachable. Sixty-six assertions in total, covering: propose; proposal merge / decline / withdraw; branch promote-to-branch / start-edit-branch / metadata / manual-flush / visibility / grants (POST + DELETE) / threads (POST) / messages (POST) / resolve / chat-seen / changes (accept / decline / reask) / chat-stream; super-draft start-edit-branch + metadata; PR pr-draft / open / seen / review / merge / withdraw / description / resolution-branch; discussion thread create + message post + resolve; admin role / mute / allowlist (POST + DELETE) plus the admin reads; notification preferences / quiet-hours / watch / mark-read / user-mute (POST + DELETE); funder credentials (POST + DELETE) + consent (POST + DELETE); graduation kickoff + claim + progress SSE; PR review page anonymous-readable.
Anonymous-writeable allowlist
Two endpoints are intentionally anonymous-by-design. They are not audit findings; they are documented here so the contract is explicit:
GET /auth/loginandGET /auth/callback— the OAuth round-trip. Anonymous-by-design because they ARE the sign-in entrypoint.POST /api/webhooks/giteaandPOST /api/webhooks/email-bounce— anonymous in the session sense but authenticated by HMAC shared secret (GITEA_WEBHOOK_SECRETandWEBHOOK_EMAIL_BOUNCE_SECRETrespectively). The webhook receiver is the wrong place for an authenticated session; the shared-secret shape is correct.
§19.2 candidates surfaced
- None unique to this release. The two pre-existing candidates the
audit touched — anonymous-read polish for the discussion surface
(carried from v0.5.0) and the operator-visibility floor on
graduation progress — were settled here as
require_useron/graduate/progressrather than deferred.
Upgrade steps (from 0.5.0)
- The deployment MUST rebuild the frontend (
npm install && npm run build) so the frontend bundle's reported version matches the backend's. No new env vars; existingfrontend/.envis sufficient. - The deployment MUST restart the backend so the new
require_usergate on/graduate/progressis enforced. No schema migration runs. - The deployment MAY announce the audit completion to
operators: every write-shaped backend endpoint now enforces an
explicit gate, and the
test_anon_offlimits_vertical.pytest net asserts the contract on every CI run. The audited surfaces are listed in theAdded (tests)section above and inSPEC.md§6.1. - The deployment MUST NOT assume any new envelope behaviour: v0.6.0 is purely a hardening release. No schema, no env, no dependency changes; the operator's role is reduced to rebuild + restart.
0.5.0 — 2026-05-27
Minor — no operator action required. This release wires the
PR-less per-RFC discussion surface (roadmap item #3, SPEC §10.10).
An RFC's main view now carries a discussion panel distinct from PR
comments and branch chat; contribution — proposing edits the document
will land — still requires opening a PR via the §10.1 affordance.
The substrate is the existing threads / thread_messages tables;
rows with threads.branch_name IS NULL scope to the RFC's main view.
No schema migration is required.
Added
- PR-less discussion endpoints (
backend/app/api_discussion.py) mounted at/api/rfcs/<slug>/discussion/...:GET /api/rfcs/<slug>/discussion/threads— list threads wherethreads.branch_name IS NULL. Anonymous-readable per the v0.3.0 contract; the default whole-doc chat thread is materialized lazily on first read.POST /api/rfcs/<slug>/discussion/threads— open a new discussion thread (thread_kind='chat',anchor_kind='whole-doc',branch_name=NULL). Body: optionallabel, optional firstmessage. Requires contributor role.GET /api/rfcs/<slug>/discussion/threads/<thread_id>/messages— read messages on a discussion thread. Anonymous-readable.POST /api/rfcs/<slug>/discussion/threads/<thread_id>/messages— post a message. Body:text, optionalquote. Requires contributor role.POST /api/rfcs/<slug>/discussion/threads/<thread_id>/resolve— resolve a discussion thread. Permission per §10.10: thread creator, RFC owner / arbiter, or app admin / owner.
RFCDiscussionPanel.jsx— the right-column surface on the RFC view when the viewer is onmain. Composer requires sign-in; Cmd/Ctrl+Enter sends. Multiple threads surface as pill-shaped tabs above the message feed. A "New thread" affordance opens a fresh thread on the same RFC.- SPEC
§10.10PR-less discussion vs. contribution — settles the distinction between discussion (RFC-scoped, no PR, both anonymous-readable and contributor-writeable) and contribution (still requires a PR via §10.1). Also extends§5'sthreadstable commentary so the null-branch interpretation is documented as actively used rather than reserved scaffold. - SPEC
§17— lists the five newdiscussion/...endpoints in the illustrative table.
Changed
backend/app/chat.py—_fan_out_chatnow passesbranch_namestraight through to the notify chokepoint instead of coercingNoneto"main". The notifications row carries the null through, which preserves the §15.7 reconciler's keying on(rfc_slug, branch_name)for the eventual discussion-side chat-seen advance (deferred to a §19.2 candidate). Existing branch-scoped chat continues to pass non-null branch names; the change is invisible to that path.backend/app/notify.py—fan_out_chat_message'sbranch_nameparameter is now typedstr | Noneto match the v0.5.0 PR-less shape. Routing rules are unchanged; the inbox prose renders identically whether the chat lives on a branch or on the RFC's discussion surface, which is the right honest signal.RFCView.jsx— the right-column panel is now conditional: whenbranchParam === 'main', renderRFCDiscussionPanel(the new PR-less surface); otherwise render the existingChatPanel(branch chat unchanged).
§19.2 candidates surfaced
- PR-less discussion: range / paragraph anchors (the data model permits them; the UI is the deferred part).
- PR-less discussion: distinct notification
event_kinds (open_rfc_discussion_thread, etc.) if usage shows contributors want to filter discussion-vs-branch in the §15.2 inbox. - PR-less discussion: chat-seen cursor closing the §15.7 reconciliation loop for the new surface.
- PR-less discussion: AI participant invocation (discussion-only,
no
<change>block side-effects). - PR-less discussion: anonymous-read polish to match the v0.6.0 write-gate hardening (item #4).
Upgrade steps (from 0.4.0)
- The deployment MUST rebuild the frontend (
npm install && npm run build) soRFCDiscussionPanel.jsxships in the bundle. No new env vars; existingfrontend/.envis sufficient. - The deployment MUST restart the backend so the new
api_discussionrouter mounts. No schema migration runs — thethreadstable already supportsbranch_name IS NULLper §5, and the v0.5.0 build is the first to write rows in that shape. - The deployment MAY announce the new surface to its contributors: the RFC view's main page now carries a discussion panel below the document. Existing branch chat and PR review surfaces are unchanged.
- The deployment SHOULD NOT expect a hardening of the anonymous-write gate in v0.5.0 — that lands in v0.6.0 (item #4). v0.5.0's write paths already refuse anonymous posts, so no pre-emptive operator action is needed.
0.4.0 — 2026-05-27
Minor — no operator action required beyond rebuild + restart. The
proposer of a new RFC is now the implicit first owner of its super-
draft entry, set automatically at propose time from the session user.
The §13.1 claim flow remains available for additional owners. No
schema changes, no env-var changes, no API-shape changes; only newly
proposed RFCs receive the auto-owner — existing super-drafts whose
owners: is empty are unaffected and can still be claimed via §13.1
as before. This is a §19.3 rule-2 spec correction: SPEC.md §9.1,
§9.2, and §13.1 are updated to reflect the new shape.
Changed
POST /api/rfcs/propose(backend/app/api.py) now setsEntry.owners = [user.gitea_login]when constructing the new super-draft entry, instead ofowners=[]. The session'sgitea_loginis the canonical source; the endpoint never accepted an owner field from the request payload and still doesn't.SPEC.md§9.1 narrowed: the "no proposed-owner or working- group fields" sentence becomes a proposer-owner-auto / working- group-deferred split, with a §19.3 rule-2 note.SPEC.md§9.2 frontmatter shape:owners: []→owners: [<proposer.gitea_login>], with a §19.3 rule-2 note.SPEC.md§13.1 reframed: claim flow is now a graduation-time broadening for additional owners, not a precondition for the proposer's own RFC. The §13.1 / §13.2 / §13.3 graduation pipeline itself is unchanged — the "at least one owner" precondition for graduation still holds and is now satisfied by default.
Upgrade steps (from 0.3.0)
- The deployment MUST rebuild and restart per the routine
deploy steps. No
.envchanges, no schema/migration changes, no API-shape changes. - Operators SHOULD note that newly proposed RFCs after the
upgrade carry the proposer in
owners:automatically. Existing super-drafts with emptyowners:are not migrated; they remain claimable via the §13.1 flow exactly as before. No deployment- side data action is required. - Deployments MAY communicate the UX shift to active proposers (the "Claim ownership" affordance no longer applies to your own newly proposed RFC), but the affordance simply hides on RFCs the viewer already owns, so no operator-side action is required.
0.3.0 — 2026-05-27
Minor — operator action required if a deployment wants to enable the private-beta gate; no action required to stay open. This release adds an email allowlist that, when populated, restricts OAuth sign-in to the listed emails while keeping all read paths public. Anonymous visitors now see the full app (catalog, RFC bodies, public branch conversations) in read-only mode instead of the §14.1 landing-page wall.
Added
allowed_emailstable (backend/migrations/011_allowlist.sql). Empty list = gate off (any successful OAuth provisions a user, as before). Any rows present = gate on (only listed emails, plus users already grandfathered bygitea_id, may sign in).- Admin → Allowlist tab at
/admin/allowlist. Add/remove emails, see who added each row and when. Status banner shows whether the gate is currently active. /beta-pendingpage shown after a rejected OAuth callback. Free- text invite-contact line is configurable via the newVITE_BETA_CONTACTenv var (optional; falls back to a generic line).- Beta chips next to the Discuss/Contribute mode toggle, the Sign in link, and the header Sign-in button so anonymous viewers see immediately what is gated.
- Anonymous read mode in the React app: the §14.1 Landing page is
preserved at
/welcomefor deployments that want to link to it, but the default route now renders the full app shell with write affordances hidden behind a sign-in CTA.
Changed
/auth/callbacknow consultsauth.is_allowed_sign_in()after fetching the Gitea profile. Rejected sign-ins clear the OAuth state and redirect to/beta-pending; the session is not populated.Catalogreceives aviewerprop. Anonymous viewers see "Sign in to propose (Beta)" instead of "+ Propose New RFC".PhilosophyWithSidebarnow readsauthenticatedfrom the current viewer instead of hardcodedtrue.
Fixed
- Single-finger scroll on the
/philosophypage (and any other.chrome-pane-hosted view:/admin/*,/settings/notifications) was broken on iOS Safari. The.appcontainer usedheight: 100vh, which on iOS measures the URL-bar-hidden ("largest") viewport — so.appoverflowed what's actually visible. Combined with thebody { overflow: hidden }inindex.css, this meant single-finger touches on the visible area were consumed by the (blocked) page- level scroll attempt rather than reaching the nested.chrome-panescroll. Two-finger touches bypassed the page-level layer and one-finger then worked once the URL bar had collapsed. Switched.apptoheight: 100dvh(dynamic viewport — adjusts as the URL bar shows/hides), with100vhretained as a fallback for browsers predating iOS 15.4 / Chrome 108.
Upgrade steps (from 0.2.3)
- The deployment MUST rebuild the frontend with the new
VITE_BETA_CONTACTenv var optionally set infrontend/.env(it is OK to leave it blank — the/beta-pendingpage falls back to a generic line). - The deployment MUST restart the backend so migration
011_allowlist.sqlruns. No data loss; the new table starts empty, which keeps the gate off and preserves existing behavior. - To enable the private-beta gate, the deployment operator
SHOULD sign in once (so their
usersrow exists and they grandfather in bygitea_id), then open/admin/allowlistand add the first invited email. The first row added turns the gate on for any user not yet inusers. - To stay open, do nothing — leave
allowed_emailsempty and the deployment behaves exactly as 0.2.3. - The deployment MAY customise its
/beta-pendingcontact line by settingVITE_BETA_CONTACT(an email, a URL, or a short instruction) before the frontend build. Unset is fine.
0.2.3 — 2026-05-26
Patch — no operator action required. Rebuild and restart per the
routine deploy steps; no .env changes, no schema changes, no
behavior changes a deployment would notice in steady state beyond
the new endpoint below.
Added
GET /api/health— an unauthenticated probe returning JSON{version, status}for ops tooling.versionis the running framework version (the contents ofVERSIONper §20.1, read at process start and cached as a module-level constant inbackend/app/health.py);statusis"ok"with HTTP 200 in v1. The"degraded"/ HTTP 503 path stays reserved in the response shape so a later release can wire real degradation conditions without breaking the contract. The version-match check is the structural value — a deploy-control-panel polling the endpoint aftersystemctl restartcatches the failure mode where a restart did not pick up the new code. SeeSPEC.md§17 and the §19.2 settlement.
Upgrade steps (from 0.2.2)
- The deployment MAY configure its monitoring (Pingdom,
Healthchecks.io, the flotilla deploy control panel, etc.) to
probe
/api/healthand compare the returnedversionagainst the tag last deployed. The endpoint is unauthenticated by design — no PII in the payload, no session required.
0.2.2 — 2026-05-26
Patch — no operator action required. Rebuild the frontend and
restart per the routine deploy steps; no .env changes, no schema
changes, no behavior changes a deployment would notice in steady
state beyond the fix below.
Fixed
- Mermaid blocks rendered as raw code on
/philosophy.frontend/src/components/Philosophy.jsxparsed PHILOSOPHY.md with the baremarkedimport anddangerouslySetInnerHTML, so```mermaidfences fell through as<pre>blocks rather than rendered diagrams. Swapped toMarkdownPreview, which already carries the lazy mermaid loader + SVG render path used by the RFC body view, so the philosophy surface now renders mermaid the same way RFC bodies do. Pre-existing gap, surfaced when a deployment authored a mermaid block in itsPHILOSOPHY_PATHoverride.
0.2.1 — 2026-05-26
Patch — no operator action required. Rebuild the frontend and
restart per the routine deploy steps; no .env changes, no schema
changes, no behavior changes a deployment would notice in steady
state.
Fixed
- PR view blank page (React #310).
frontend/src/components/PRView.jsxdeclared itsthreadsByKinduseMemoafter the early-return guard for the loading state (if (!pr) return …). On first mountprwas null so the early return fired and 14 hooks were called; on the second renderprwas populated and execution reached theuseMemo, calling 15 hooks — violating the Rules of Hooks and unmounting the page subtree (blank screen). TheuseMemois now declared above the early returns with optional-chaining onpr, keeping the hook count stable between the loading and loaded renders. Pre-existing bug in the post-Contribute-rewrite PR view; surfaced in production by 0.2.0's graduation merge race fix enabling the workflow that opens this code path.
0.2.0 — 2026-05-26
Breaking config change. The frontend now requires
VITE_APP_NAME to be set at build time. npm run build fails with a
clear message if it is missing. There is no default; every deployment
names itself.
Upgrade steps (from 0.1.0)
- The deployment MUST create a
frontend/.envfile before building. Seefrontend/.env.examplefor the contract. - The deployment MUST set
VITE_APP_NAMEinfrontend/.envto the user-visible name it wants to ship — the string used as the browser tab title, the header brand, and the landing H1. The build will fail loudly if this is unset or blank. - The deployment MUST rebuild the frontend (
npm install && npm run build) and redeploy the resulting bundle. The previous bundle does not readVITE_APP_NAME. - The deployment SHOULD verify the name appears correctly in
the browser tab and the header after redeploy before bumping its
.rfc-app-versionpin to0.2.0. - The deployment MAY, in the same upgrade, take the opportunity to retire any local overrides it was using to brand the pre-0.2.0 hardcoded strings — those overrides are now obsolete.
- The deployment MUST NOT continue to expect graduation step 4
to fail on Gitea's "Please try again later" race; that path is
now handled by
wait_for_mergeable+ bounded retry. Any local workaround retrying graduation at the deployment level is now redundant and SHOULD be removed.
Fixed
- Graduation merge race. §13.3 step 4 (
merge_pr) used to call Gitea's merge endpoint the instant step 3 returned, which races Gitea's background mergeability computation and produces the405 "Please try again later"response. Step 4 now waits for themergeablefield to become non-null (bounded by a 30s timeout) and retries the merge call up to three times on the transient response. Seebackend/app/gitea.py#wait_for_mergeableandbackend/app/bot.py#_merge_with_retry.
Changed
frontend/src/App.jsxheader brand readsVITE_APP_NAMEinstead of a hardcoded string.frontend/src/components/Landing.jsxH1 readsVITE_APP_NAMEinstead of a hardcoded string. The pitch / deck / attribution copy in this file is still deployment-specific and tracked as a follow-up for extraction into deployment config.frontend/index.html<title>is rewritten at build time via Vite's HTML transform.
Added
frontend/.env.exampledocumenting the new required variable.- Top-level
VERSIONfile and thisCHANGELOG.mdas the canonical release log. SPEC.md§20 (versioning and downstream deployments) as the binding policy for the framework/deployment relationship.docs/DEPLOYMENTS.mdas the practical guide for building on rfc-app and upgrading existing deployments to new versions.CLAUDE.mdat the repo root capturing the separation-of-concerns rule for working sessions.
0.1.0 — v1 build
Initial release. See docs/DEV.md for the slicing plan and build
history.