feat(flags): feature-flag control on the project page #5

Open
gabrielvidal wants to merge 1 commits from feature-flags into main
Owner

Summary

Adds a Feature flags card to the project page, next to the .env editor. Features
that ship behind ?flag=1 are now declared once in the project's
public/feature-flags.json, and this card flips each one's for everyone default,
mints the token link that unlocks the admin panel on the deployed site, and pushes the
JSON to that site with no rebuild.

Half of the feature lives outside this repo: the widget that resolves the flags and
renders the panel is ~/projects/feature-flags
(served at flags.dev.gabvdl.xyz/script.js), and the Traefik route that lets the panel
reach this API is in the homelab repo.

Key changes

  • backend/projectflags.py (new) — reads/writes a project's public/feature-flags.json
    (atomic replace, unknown per-flag fields preserved, a malformed file reported rather
    than silently emptied), and mints/verifies the HMAC-SHA256 admin token. The signing
    key lives at /data/flags-secret, created on first use; deleting it revokes every
    token ever issued.
  • backend/main.py — GET/PUT /api/project-flags, POST /api/project-flags/admin-link,
    POST /api/project-flags/publish (all behind Authelia), plus
    POST /api/flags/publish for the panel on the live site: token-authed, CORS-scoped to
    https://*.gabvdl.xyz.
  • sidecar/sidecar.py — POST /publish-flags: stages the one JSON file and runs
    zipgo deploy --no-delete onto the hosts the project's package.json declares.
  • frontend/src/business/projects/components/FlagsEditor.tsx (new) — the card: a row per
    flag with an everyone switch, an Admin link button, and Publish now.
  • backend/schemas.py + regenerated frontend/openapi.json / src/generated/.
  • CLAUDE.md — a Feature flags section covering the endpoints and the three
    load-bearing constraints below.

Key decisions

  • Toggle-only, no CRUD. Flags are declared in code by whoever ships the feature; the
    card refuses an unknown key rather than creating one. A flag with no code behind it is
    dead weight, and the alternative invites flags that exist only in a UI.
  • Token, not a live Authelia check. The public sites are same-site as
    lab.gabvdl.xyz, so a credentialed call would actually carry the session cookie — but
    only from the LAN/tailnet, where the lab hosts resolve. A minted token works from
    anywhere, which is what "show a colleague a feature from my phone" needs.
  • The panel's visibility is not a security boundary, and the code says so. It hides
    the panel from visitors and guards nothing secret — the flag list is public JSON the
    page already fetched. The write that affects other people is verified server-side.
  • text/plain POST, on purpose. It keeps the panel's cross-origin write a CORS
    simple request. With application/json the browser sends a preflight OPTIONS,
    which carries no cookie, which forward-auth answers with a login redirect — the request
    would never reach the token check. Same reason /api/flags needs its own Traefik
    router.
  • Publishing goes through the sidecar as a fixed operation (one named file, to hosts
    the project itself declares), not a generic exec endpoint. The container has neither
    zipgo nor the deploy key, and "run this command on the host" is not something the
    bearer token should buy.
  • Publish is best-effort on the panel path. The repo write already succeeded; a
    sidecar that is down must not read as "the toggle failed", so the response reports the
    publish error separately.

Changelog

  • Added: Feature flags card on the project page — flip a ?flag=1 feature on for
    everyone, and publish it live without a rebuild.
  • Added: Admin link button that unlocks the feature-flag panel on a deployed site.

Test notes

  • python3 -m py_compile on the changed backend/sidecar modules; npx tsc --noEmit
    clean.
  • npm test — 73 passed, 1 failed; the failure (BottomBar.test.tsx) reproduces on
    main with these changes stashed, so it is pre-existing and unrelated.
  • OpenAPI re-dumped from the service image and npm run gen:api re-run; generated tree
    committed.
  • End-to-end against a real build (eludoku dist/ + the widget, served locally):
    visiting ?ff-admin=… stored the token and showed the panel; toggling On in the
    panel made the gated nav link appear live; a plain visitor at ?archives=1 got
    panel=absent, archives=true, and an address bar with the param stripped.
    Screenshots below.

Screenshots

panel-open_20260817-003859.jpeg
toggle-on_20260817-003917.jpeg
visitor-param_20260817-003953.jpeg

## Summary Adds a **Feature flags** card to the project page, next to the `.env` editor. Features that ship behind `?flag=1` are now declared once in the project's `public/feature-flags.json`, and this card flips each one's *for everyone* default, mints the token link that unlocks the admin panel on the deployed site, and pushes the JSON to that site with no rebuild. Half of the feature lives outside this repo: the widget that resolves the flags and renders the panel is [`~/projects/feature-flags`](https://git.gabvdl.xyz/gabrielvidal/feature-flags) (served at `flags.dev.gabvdl.xyz/script.js`), and the Traefik route that lets the panel reach this API is in the homelab repo. ## Key changes - `backend/projectflags.py` (new) — reads/writes a project's `public/feature-flags.json` (atomic replace, unknown per-flag fields preserved, a malformed file reported rather than silently emptied), and mints/verifies the HMAC-SHA256 admin token. The signing key lives at `/data/flags-secret`, created on first use; deleting it revokes every token ever issued. - `backend/main.py` — `GET`/`PUT /api/project-flags`, `POST /api/project-flags/admin-link`, `POST /api/project-flags/publish` (all behind Authelia), plus `POST /api/flags/publish` for the panel on the live site: token-authed, CORS-scoped to `https://*.gabvdl.xyz`. - `sidecar/sidecar.py` — `POST /publish-flags`: stages the one JSON file and runs `zipgo deploy --no-delete` onto the hosts the project's `package.json` declares. - `frontend/src/business/projects/components/FlagsEditor.tsx` (new) — the card: a row per flag with an *everyone* switch, an **Admin link** button, and **Publish now**. - `backend/schemas.py` + regenerated `frontend/openapi.json` / `src/generated/`. - `CLAUDE.md` — a *Feature flags* section covering the endpoints and the three load-bearing constraints below. ## Key decisions - **Toggle-only, no CRUD.** Flags are declared in code by whoever ships the feature; the card refuses an unknown key rather than creating one. A flag with no code behind it is dead weight, and the alternative invites flags that exist only in a UI. - **Token, not a live Authelia check.** The public sites are same-*site* as `lab.gabvdl.xyz`, so a credentialed call would actually carry the session cookie — but only from the LAN/tailnet, where the lab hosts resolve. A minted token works from anywhere, which is what "show a colleague a feature from my phone" needs. - **The panel's visibility is not a security boundary**, and the code says so. It hides the panel from visitors and guards nothing secret — the flag list is public JSON the page already fetched. The write that affects other people is verified server-side. - **`text/plain` POST, on purpose.** It keeps the panel's cross-origin write a CORS *simple request*. With `application/json` the browser sends a preflight `OPTIONS`, which carries no cookie, which forward-auth answers with a login redirect — the request would never reach the token check. Same reason `/api/flags` needs its own Traefik router. - **Publishing goes through the sidecar as a fixed operation** (one named file, to hosts the project itself declares), not a generic exec endpoint. The container has neither zipgo nor the deploy key, and "run this command on the host" is not something the bearer token should buy. - **Publish is best-effort on the panel path.** The repo write already succeeded; a sidecar that is down must not read as "the toggle failed", so the response reports the publish error separately. ## Changelog - Added: Feature flags card on the project page — flip a `?flag=1` feature on for everyone, and publish it live without a rebuild. - Added: Admin link button that unlocks the feature-flag panel on a deployed site. ## Test notes - `python3 -m py_compile` on the changed backend/sidecar modules; `npx tsc --noEmit` clean. - `npm test` — 73 passed, 1 failed; the failure (`BottomBar.test.tsx`) reproduces on `main` with these changes stashed, so it is pre-existing and unrelated. - OpenAPI re-dumped from the service image and `npm run gen:api` re-run; generated tree committed. - End-to-end against a real build (eludoku `dist/` + the widget, served locally): visiting `?ff-admin=…` stored the token and showed the panel; toggling *On* in the panel made the gated nav link appear live; a plain visitor at `?archives=1` got `panel=absent`, `archives=true`, and an address bar with the param stripped. Screenshots below. ## Screenshots ![panel-open_20260817-003859.jpeg](https://git.gabvdl.xyz/attachments/79ba4935-1267-42b5-a04c-18869f4cd893) ![toggle-on_20260817-003917.jpeg](https://git.gabvdl.xyz/attachments/41a06786-71c6-4719-a830-e4b868371f8f) ![visitor-param_20260817-003953.jpeg](https://git.gabvdl.xyz/attachments/b99fa8a2-ac52-4bb4-9459-0c7c55c8a176)
gabrielvidal self-assigned this 2026-08-17 00:42:22 +02:00
gabrielvidal added 1 commit 2026-08-17 00:42:23 +02:00
Adds a Feature flags card next to the .env editor: it flips each declared
flag's for-everyone default in the project's public/feature-flags.json, mints
the token link that unlocks the admin panel on the deployed site, and publishes
the file to that site with no rebuild.

Toggle-only by design — flags are declared in code by whoever ships the
feature. The panel on the live site has no Authelia session, so its write comes
back to /api/flags/publish with a minted HMAC token, as a text/plain POST so
the browser fires no preflight for forward-auth to bounce. Publishing runs on
the host sidecar (the container has neither zipgo nor the deploy key) as a
fixed one-file operation, never a generic exec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This pull request has changes conflicting with the target branch.
  • frontend/src/generated/api.ts
  • frontend/src/generated/model/index.ts
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature-flags:feature-flags
git checkout feature-flags
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gabrielvidal/ai-agent#5