Files
ehoandClaude Opus 5 44dcc69811 refactor: ui/dashboard becomes ui/overzicht-secties (RD-36)
RD-03 moved the dashboard page to overzicht/ui/overzicht.page.ts and left four
sections in registratie/ui/dashboard/. The folder was named after a page that
lives in another context. A reader who opened it found four sections that are
not the dashboard.

The folder is now overzicht-secties/ — registratie's sections for the overzicht
page. The alias does not change, because the sections stay in the registratie
context. The story titles do not change, because they name the context.

Five documents cited registratie/ui/dashboard.page.ts, a file that RD-03
renamed. They now name overzicht.page.ts, or the section that owns the
behaviour they describe.

The /dashboard route keeps its path. It is a user-visible URL.

npm run ci --full passes: 67 and 45 storybook suites, 306 axe tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:21:15 +02:00

77 lines
3.9 KiB
Markdown

# Feature flags — how they're built, extended & (de)coupled
Runtime on/off switches for features. The split, deliberately mirroring stamdata: the
**catalog is config-as-code** (build-validated), the **on/off state is runtime** (SQLite,
like org-templates). Enforcement is **end-to-end** — the FE hides the surface _and_ the
backend refuses the action. Built in
[WP-47](../project/backlog/WP-47-feature-flags.md); see
[ADR-0004](architecture/0004-stamdata-as-code.md) for the catalog-vs-state reasoning.
## The one rule: hiding is not enforcing
A flag gates the UI _for feel_ and the endpoint _for real_. Flipping `inschrijving-open`
off both removes the "Inschrijven" nav/dashboard action **and** makes `POST /applications`
return 403. Never gate only the UI — a hidden button is not a closed door.
## Layered pipeline
Backend:
- `Domain/Features/FeatureFlags.cs` — the code catalog (`FeatureFlagDef(Key, Description,
DefaultEnabled)`). One flag today: `inschrijving-open` (default on).
- `Data/FeatureFlagStore.cs` — stores **only overrides** in SQLite; `All()` overlays
catalog defaults with overrides; `IsEnabled` is **fail-closed** on unknown keys;
`Set(key, …)` rejects unknown keys (→ 404).
- `Program.cs`: `GET /flags` (readable by any principal — drives FE gating),
`PUT /admin/flags/{key}` gated by `FlagsAdmin`; the **server-side enforcement** lives at
the guarded endpoint (`POST /applications` → 403 when off).
Frontend — note this feature lives in **`shared`**, not `beheer` (it's consumed app-wide):
- `shared/domain/feature-flag.ts` (`FeatureFlag` + `FLAG_INSCHRIJVING_OPEN` key constant) →
`shared/infrastructure/feature-flags.adapter.ts` (`list()`/`set()` + `parseFlags`) →
`shared/application/feature-flags.store.ts` (root singleton mirroring `AccessStore`;
loads once; `enabled(key)` is **deny-by-default** and reactive).
- `beheer/ui/feature-flags.page.ts` — the admin toggle at `/beheer/functies`, gated on
`flags:manage`.
## How to add a flag
1. Add a `FeatureFlagDef` to `FeatureFlags.cs` and a key constant to
`shared/domain/feature-flag.ts`.
2. **Enforce it server-side** at the endpoint the flag protects (return 403 when off) —
this is the non-negotiable half.
3. Gate the UI surface(s) by reading `store.enabled(KEY)`.
4. Cover it in `backend/tests/BigRegister.Tests/FeatureFlagTests.cs` (default, admin-only
toggle, unknown-key 404, off→403 / on→201).
## Coupling — the one to watch
This is the **most-coupled** of the three admin features, and the honest teaching point.
Each UI consumer injects `FeatureFlagStore`, imports the flag-key constant, and hand-writes
its own gating predicate inline:
- `shared/layout/site-header/site-header.component.ts` — filters the "Inschrijven" nav item.
- `overzicht/ui/wat-wilt-u-doen.section.ts` — hides the "Inschrijven" dashboard action.
So a second flag with a second consumer **repeats the pattern by hand** — there's no shared
"gate this thing by flag" abstraction. That's fine at one flag / two consumers (a helper for
a single case is speculative). **Recommended: when a second flag lands, extract a small
`gateByFlag(items, key)` / a structural directive** rather than growing more inline
`.filter(… || enabled(KEY))` copies. Document the intent now; don't build the abstraction
until the second case forces it.
## Wire-up gotcha
`PUT /admin/flags` must be in the `role.interceptor` **ROLE_AWARE** list or the toggle
403s silently; the public `GET /flags` needs no `X-Role`. Admin nav lives in `ADMIN_LINKS`
(`flags:manage`).
## See also
- [WP-47](../project/backlog/WP-47-feature-flags.md) — the build.
- [ADR-0004](architecture/0004-stamdata-as-code.md) — catalog-as-code vs runtime state.
- `backend/src/BigRegister.Api/Domain/Features/FeatureFlags.cs` — the catalog.
- `src/app/shared/application/feature-flags.store.ts` — the deny-by-default store.
- [Roles & access](roles-and-access.md) — `flags:manage` + admin gating.