Files
rfc-app/CHANGELOG.md
Ben Stull 3ddeb5c1cd Release 0.13.0: cookie/privacy consent banner + policy pages (instrumentation prep)
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>
2026-05-28 00:53:21 -07:00

480 lines
23 KiB
Markdown

# Changelog
The binding policy for what each kind of version bump means and what
the changelog has to carry is in [`SPEC.md` §20](./SPEC.md). The
practical recipe downstream deployments follow when reading this file
is in [`docs/DEPLOYMENTS.md`](./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](https://www.rfc-editor.org/rfc/rfc2119)
/ [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) 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`). Exports
`getConsent()`, `hasChosen()`, `onConsentChange(cb)`, `setConsent()`,
`hydrateFromServer()`, `clearLocal()`. Cross-tab sync via the
`storage` event. 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/notifications` showing
the current consent choice, the recorded-at stamp, and a "Change"
button that re-opens the banner via a custom DOM event.
- **`§17` endpoints** —
- `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, stamps `recorded_at` to now,
accepts `essential` for symmetry but always persists it as true.
- **Schema migration** `012_cookie_consent.sql` — new
`cookie_consent` table keyed by `user_id`, three flags
(`essential`, `analytics`, `other_cookies`), and `recorded_at`.
- **SPEC `§14.5` Cookie / 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 the `consent.js` helper surface for downstream
callers including item #13.
- **SPEC `§14.6` Privacy and cookies policy pages** — settles the
`/privacy` and `/cookies` routes, the framework's stub content, and
the `VITE_PRIVACY_POLICY_URL` / `VITE_COOKIES_POLICY_URL` override
shape.
- **SPEC `§5`** — names the `cookie_consent` table 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
vars `VITE_PRIVACY_POLICY_URL` and `VITE_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 `/privacy` and `/cookies`
routes (anonymous-reachable), wires `<CookieConsentBanner>` into
the global chrome, and listens for a `rfc-app:cookie-consent-reopen`
custom 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#version` and `VERSION` both move
to `0.13.0` and the build embeds the new env-var contract.
- You **MUST** apply schema migration `012_cookie_consent.sql`. The
migration creates a single new table keyed by `user_id` with three
flag columns and a `recorded_at` stamp. 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_URL` to an http(s) URL that
points at your deployment's full privacy policy. The framework's
`/privacy` page 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_URL` to 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_consent` row does not yet exist); their
current sessions remain valid.
## 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 where
`threads.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: optional `label`, optional first
`message`. 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`, optional `quote`. 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 on `main`. 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.10` PR-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`'s `threads`
table commentary so the null-branch interpretation is documented
as actively used rather than reserved scaffold.
- **SPEC `§17`** — lists the five new `discussion/...` endpoints
in the illustrative table.
### Changed
- **`backend/app/chat.py`** — `_fan_out_chat` now passes
`branch_name` straight through to the notify chokepoint instead of
coercing `None` to `"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`'s
`branch_name` parameter is now typed `str | None` to 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:
when `branchParam === 'main'`, render `RFCDiscussionPanel`
(the new PR-less surface); otherwise render the existing
`ChatPanel` (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_kind`s
(`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)
1. The deployment **MUST** rebuild the frontend (`npm install &&
npm run build`) so `RFCDiscussionPanel.jsx` ships in the bundle.
No new env vars; existing `frontend/.env` is sufficient.
2. The deployment **MUST** restart the backend so the new
`api_discussion` router mounts. No schema migration runs — the
`threads` table already supports `branch_name IS NULL` per §5,
and the v0.5.0 build is the first to write rows in that shape.
3. 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.
4. 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 sets
`Entry.owners = [user.gitea_login]` when constructing the new
super-draft entry, instead of `owners=[]`. The session's
`gitea_login` is 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)
1. The deployment **MUST** rebuild and restart per the routine
deploy steps. No `.env` changes, no schema/migration changes, no
API-shape changes.
2. Operators **SHOULD** note that newly proposed RFCs after the
upgrade carry the proposer in `owners:` automatically. Existing
super-drafts with empty `owners:` are not migrated; they remain
claimable via the §13.1 flow exactly as before. No deployment-
side data action is required.
3. 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_emails` table** (`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 by `gitea_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-pending` page** shown after a rejected OAuth callback. Free-
text invite-contact line is configurable via the new
`VITE_BETA_CONTACT` env 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 `/welcome` for 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/callback`** now consults `auth.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.
- **`Catalog`** receives a `viewer` prop. Anonymous viewers see "Sign
in to propose (Beta)" instead of "+ Propose New RFC".
- **`PhilosophyWithSidebar`** now reads `authenticated` from the
current viewer instead of hardcoded `true`.
### Fixed
- **Single-finger scroll on the `/philosophy` page** (and any other
`.chrome-pane`-hosted view: `/admin/*`, `/settings/notifications`)
was broken on iOS Safari. The `.app` container used `height: 100vh`,
which on iOS measures the URL-bar-hidden ("largest") viewport — so
`.app` overflowed what's actually visible. Combined with the
`body { overflow: hidden }` in `index.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-pane`
scroll. Two-finger touches bypassed the page-level layer and
one-finger then worked once the URL bar had collapsed. Switched
`.app` to `height: 100dvh` (dynamic viewport — adjusts as the URL
bar shows/hides), with `100vh` retained as a fallback for browsers
predating iOS 15.4 / Chrome 108.
### Upgrade steps (from 0.2.3)
1. The deployment **MUST** rebuild the frontend with the new
`VITE_BETA_CONTACT` env var optionally set in `frontend/.env` (it
is OK to leave it blank — the `/beta-pending` page falls back to
a generic line).
2. The deployment **MUST** restart the backend so migration
`011_allowlist.sql` runs. No data loss; the new table starts
empty, which keeps the gate off and preserves existing behavior.
3. To **enable** the private-beta gate, the deployment operator
**SHOULD** sign in once (so their `users` row exists and they
grandfather in by `gitea_id`), then open `/admin/allowlist` and
add the first invited email. The first row added turns the gate
on for any user not yet in `users`.
4. To **stay open**, do nothing — leave `allowed_emails` empty and
the deployment behaves exactly as 0.2.3.
5. The deployment **MAY** customise its `/beta-pending` contact line
by setting `VITE_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. `version` is the running
framework version (the contents of `VERSION` per §20.1, read at
process start and cached as a module-level constant in
`backend/app/health.py`); `status` is `"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 after `systemctl restart` catches the failure mode
where a restart did not pick up the new code. See `SPEC.md` §17
and the §19.2 settlement.
### Upgrade steps (from 0.2.2)
1. The deployment **MAY** configure its monitoring (Pingdom,
Healthchecks.io, the flotilla deploy control panel, etc.) to
probe `/api/health` and compare the returned `version` against
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.jsx` parsed PHILOSOPHY.md with
the bare `marked` import and `dangerouslySetInnerHTML`, so
```` ```mermaid ```` fences fell through as `<pre>` blocks rather
than rendered diagrams. Swapped to `MarkdownPreview`, 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 its `PHILOSOPHY_PATH` override.
## 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.jsx`
declared its `threadsByKind` `useMemo` *after* the early-return
guard for the loading state (`if (!pr) return …`). On first mount
`pr` was null so the early return fired and 14 hooks were called;
on the second render `pr` was populated and execution reached the
`useMemo`, calling 15 hooks — violating the Rules of Hooks and
unmounting the page subtree (blank screen). The `useMemo` is now
declared above the early returns with optional-chaining on `pr`,
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)
1. The deployment **MUST** create a `frontend/.env` file before
building. See `frontend/.env.example` for the contract.
2. The deployment **MUST** set `VITE_APP_NAME` in `frontend/.env` to
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.
3. The deployment **MUST** rebuild the frontend (`npm install && npm
run build`) and redeploy the resulting bundle. The previous
bundle does not read `VITE_APP_NAME`.
4. The deployment **SHOULD** verify the name appears correctly in
the browser tab and the header after redeploy before bumping its
`.rfc-app-version` pin to `0.2.0`.
5. 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.
6. 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 the
`405 "Please try again later"` response. Step 4 now waits for the
`mergeable` field to become non-null (bounded by a 30s timeout) and
retries the merge call up to three times on the transient response.
See `backend/app/gitea.py#wait_for_mergeable` and
`backend/app/bot.py#_merge_with_retry`.
### Changed
- `frontend/src/App.jsx` header brand reads `VITE_APP_NAME` instead of
a hardcoded string.
- `frontend/src/components/Landing.jsx` H1 reads `VITE_APP_NAME`
instead 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.example` documenting the new required variable.
- Top-level `VERSION` file and this `CHANGELOG.md` as the canonical
release log.
- `SPEC.md` §20 (versioning and downstream deployments) as the
binding policy for the framework/deployment relationship.
- `docs/DEPLOYMENTS.md` as the practical guide for building on
rfc-app and upgrading existing deployments to new versions.
- `CLAUDE.md` at 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.