Compare commits

...
11 Commits
Author SHA1 Message Date
ehoandClaude Opus 5 6330773fd5 chore: add a Taskfile facade over the existing commands
CI / changes (push) Successful in 16s
CI / lint (push) Successful in 3m0s
CI / frontend (push) Failing after 3m36s
CI / backend (push) Successful in 2m38s
CI / e2e (push) Failing after 4m14s
CI / storybook-a11y (push) Failing after 7m50s
CI / semgrep (push) Successful in 1m18s
CI / api-client-drift (push) Successful in 1m55s
`task` with no arguments lists every runnable command. The Taskfile calls the
npm scripts, dotnet and docker compose. It does not duplicate their logic.
CI does not need `task`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:56:39 +02:00
ehoandClaude Opus 5 856463b738 Merge refactor/machine-wiring-in-application — RD-30..33, RD-36, RD-37, RD-39
Closes the readable-codebase arc's phase 5. RD-37 removed the last five axe
suppressions by making the keuzelijst row the <li>. RD-30 archived the two
finished backlogs. RD-31, RD-32, RD-33 and RD-36 repaired the documents that
describe the refactored tree.

RD-34 and RD-35 stay open. Both are marked optional in the backlog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:21:30 +02:00
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
ehoandClaude Opus 5 d9aef9541f docs: teach overzicht, max-lines and the step contract (RD-33)
CLAUDE.md did not name the overzicht context that RD-03 created, and it did
not name the max-lines budget that RD-02 enforces. An agent that follows it
writes a long page into the wrong context, and meets a red build with no
warning.

CLAUDE.md now names the context, the @overzicht/* alias, the
overzicht -> registratie arrow, the 250-line budget and its glob, and the
step-component contract. layers.mdx gains the same arrow.

atomic-design.mdx credited eslint.config.mjs with the layer rules.
dependency-cruiser enforces them. Each tool is now named for what it does.
The page also gains the layer-folder table and the step contract.

Four paths were pre-monorepo: three example paths in the ui-component skill,
and two citations of libs/shared/src/ui/async, which RD-27 moved to
libs/shared/src/ui/molecules/async.

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:14:56 +02:00
ehoandClaude Opus 5 682db41344 docs: repoint fp-tea paths at the monorepo tree (RD-32)
The learning guide fp-tea-atomic-design.md still cited the pre-monorepo
tree. Every path started with src/app/, which no directory has used
since WP-67 split the app into apps/ssp and libs/shared.

Rewrite the 11 stale paths per the ticket's mapping rules: a context
path becomes apps/ssp/src/app/<context>/..., a shared path becomes
libs/shared/src/.... Fix the broken anchor at Part 5b to match the
current ARCHITECTURE.md §1 heading. Fix the "Where" guidance in
Recipe A to name the atomic layer folder under libs/shared/src/ui/.

The teaching prose is unchanged; only addresses moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:16:26 +02:00
ehoandClaude Opus 5 241fe1b57b docs: cite symbols, not lines, in ARCHITECTURE 2g
RD-31 applied the symbols-not-lines rule to section 6a only. Section 2g has
the same rot. Four of its five citation groups point at the wrong code:
herregistratie.machine.ts L138-142 lands on `resolve`, not `setField`;
draft-sync.ts L34 is a blank line; the wizard's L78 is an input attribute;
brief.store.ts L157 is a comment.

One link was also dead. text-input.component.ts moved to
libs/shared/src/ui/atoms/ in RD-27.

Every link in the document now resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:12:56 +02:00
ehoandClaude Opus 5 d31e054504 docs: cite symbols, not lines, in ARCHITECTURE 6a (RD-31)
Section 6a cited line numbers and two paths from before the monorepo
split (WP-67). A line number goes stale on the next edit; a symbol
survives it. Replace every L<n> citation in the section with the
named symbol, fix the two dead paths (environment.ts now lives under
libs/shared, proxy.conf.json is now one file per app), and point the
read walkthrough at mijn-registratie.section.ts, where the RD-03
split moved the dashboard's <app-async> block.

Mark RD-31 done and update its README row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:11:03 +02:00
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00
ehoandClaude Opus 5 097e8468e0 fix: keuzelijst rows become the <li> (RD-37)
app-choice-link rendered a component host between the keuzelijst <ul>
and its <li>. This broke the axe list/listitem rule for assistive
technology. Five story suppressions named WP-11 as the fix, but WP-11
closed with no open ticket left to own the defect.

choice-link now uses selector: 'li[app-choice-link]', the same
attribute-host pattern as application-link. The host carries the
keuzelijst__list-item class; the template drops its own <li>.
Position: relative stays on .keuzelijst__link so the stretched-link
overlay still resolves against the card, not the host.

aanvraag-block needed no component change: it renders a CIBG melding,
never an <li>. Only its story wrapped it in a <ul>, which is what axe
rejected. The wrapper is removed, and the four non-Concept stories are
deleted — the component's template only renders for status Concept, so
they rendered nothing.

All five a11y: { disable: true } suppressions are gone, with no
replacement. atomic-design.mdx now records that both molecules are the
<li>, kept separate for the vendored CSS they bind, not for list
semantics.

npm run ci --full passes, axe included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 22:51:04 +02:00
ehoandClaude Opus 5 2aa343f255 chore: cap container memory in docker compose
The API container used server garbage collection. Server GC makes one heap per
CPU, which is 22 heaps on this host. The two frontend containers sized the Node
heap from host RAM (16 GB), because compose sets no cgroup limit.

Set DOTNET_gcServer=0 and NODE_OPTIONS=--max-old-space-size=2048. This bounds
the memory that `docker compose up` needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 22:39:40 +02:00
ehoandClaude Opus 5 f3e5745145 fix: the wizards' seed input never arrived (RD-39)
All three wizard containers read `this.seed()` in the constructor. Angular
binds component inputs after the constructor runs, so the value was always the
`initial` default, `seeded !== initial` was always false, and every mount took
the `draftSync.resume()` branch. The `seed` input was dead code.

The two single-step forms built on the same idiom read the input inside the
microtask and work correctly. That contrast is the diagnosis.

Impact: 21 seeded wizard stories rendered step 1 instead of the state they
asked for. Storybook is this repo's UI test surface, so the states with no
other coverage were exactly the ones not rendering — Submitting, Submitted,
Failed, Ingediend, Mislukt. The a11y runner checks that whatever rendered is
accessible, never that the right thing rendered, so nothing caught it.
Production was unaffected: no route binds `seed`.

Read the input inside the microtask, matching the two forms. Turn the spec's
old `componentInstance.dispatch(...)` workaround into a real regression test
through `componentRef.setInput('seed', ...)`.

Verified: with the intake fix reverted the two spec cases fail; with it, 319
pass. `npm run ci --full` is green, and the newly rendered markup produced no
axe violations. A browser check of seven seeded stories across all three
wizards asserts text only reachable from a seed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:53:03 +02:00
196 changed files with 1097 additions and 216 deletions
+3 -3
View File
@@ -20,11 +20,11 @@ The repo splits docs by kind (`docs/README.md`). Pick one — don't write all th
_decision/pattern_ (a tradeoff, a "we chose X over Y"). One format, no drift:
`# ADR-NNNN — Title`, then `Status: Accepted · Date: YYYY-MM-DD`, then
Problem / Options / Decision / Consequences.
- **WP** — `docs/project/backlog/WP-NN-<slug>.md`. Tracked work; use the template at the
bottom of `docs/project/backlog/README.md`.
- **WP** — `docs/project/archive/backlog/WP-NN-<slug>.md`. Tracked work; use the template at the
bottom of `docs/project/archive/backlog/README.md`.
Then **add the index row**: `docs/README.md` table for a reference doc/ADR,
`docs/project/backlog/README.md` for a WP. A doc with no index row is invisible.
`docs/project/archive/backlog/README.md` for a WP. A doc with no index row is invisible.
## Adjusted feature → update what already describes it
+3 -3
View File
@@ -62,7 +62,7 @@ Work through that checklist, keeping the GREEN gate below passing at every step.
re-target it at an arbitrary new context, so it's deleted alongside `registratie`, along
with its three wiring lines in `shell.component.ts` (import, `imports:` entry, template tag).
- The `dashboard` route is **not** deleted even though it currently imports
`@registratie/ui/dashboard.page` — too much else hardcodes `/dashboard` (login's post-auth
`@overzicht/ui/overzicht.page` — too much else hardcodes `/dashboard` (login's post-auth
redirect, `authGuard`'s fallback, header nav/logo, breadcrumb trail, several stories/specs).
The script rewrites its `loadComponent` to point at the freshly scaffolded `--context` page
instead (a `TODO(create-frontend)` stopgap landing page, not a real overview).
@@ -85,8 +85,8 @@ Work through that checklist, keeping the GREEN gate below passing at every step.
placeholder path and creates an empty `public/<name>-huisstijl/` — it cannot generate a
real house style. Vendor your CSS there, then re-point the `--rhc-*` bridge in
`src/styles.scss` (ADR-0003 pattern: bridge, don't rewrite tokens), then `npm run check:tokens`.
- `docs/project/backlog/` WPs, PRDs, and memory-specific docs — new portal, new backlog
(keep `docs/project/backlog/README.md`'s WP process/template if you like the workflow).
- `docs/project/archive/backlog/` WPs, PRDs, and memory-specific docs — new portal, new backlog
(keep `docs/project/archive/backlog/README.md`'s WP process/template if you like the workflow).
`docs/reference/scaffolding.md` also names `BigRegister.Api` in prose — update by hand.
- `e2e/*.spec.ts` (`smoke.spec.ts`, `brief-v2.spec.ts`, `error-state.spec.ts`): full
BIG-register user-flow tests (BSN login → registration wizard → submission assertions).
+10 -8
View File
@@ -5,13 +5,13 @@ description: Add a shared UI building block (atom, molecule, organism) with its
# UI component (atom / molecule / organism)
First: check `shared/ui/` and `shared/layout/` — a new page should be composition of
existing blocks. Only add a block when nothing fits.
First: check `libs/shared/src/ui/` and `libs/shared/src/layout/` — a new page should be
composition of existing blocks. Only add a block when nothing fits.
## Rules
- **Folder = atomic layer**: `shared/ui/` atoms → molecules → organisms;
`shared/layout/` templates. Each level only uses levels below.
- **Folder = atomic layer**: `libs/shared/src/ui/` atoms → molecules → organisms;
`libs/shared/src/layout/` templates. Each level only uses levels below.
- Standalone component, **English name** (shared = language-agnostic), signal
`input()`s only, `inject()` over constructor DI.
- **Atoms are thin wrappers over CIBG Huisstijl (Bootstrap 5.2) classes** (`btn`,
@@ -21,7 +21,8 @@ existing blocks. Only add a block when nothing fits.
- **Tokens only** — `var(--rhc-*)` / `var(--app-*)`, never hardcoded colors
(`npm run check:tokens` fails the build; escape hatch: `token-ok` marker + reason).
- **No hardcoded Dutch** in shared components — expose copy as `input()`s with
`$localize` defaults; the domain caller supplies the text (see `shared/ui/async`).
`$localize` defaults; the domain caller supplies the text (see
`libs/shared/src/ui/molecules/async`).
- Components with content-projected slots export a spread constant so callers import
one thing: `export const ASYNC = [AsyncComponent, AsyncLoadedDirective, …] as const;`
@@ -41,9 +42,10 @@ UI test surface.
## Worked examples
- Atom: `src/app/shared/ui/button/` — typed variant API over `btn` classes.
- Molecule: `src/app/shared/ui/async/` — slot directives, localizable input defaults, spread constant.
- Template: `src/app/shared/layout/wizard-shell/` — the canonical wizard outline.
- Atom: `libs/shared/src/ui/atoms/button/` — typed variant API over `btn` classes.
- Molecule: `libs/shared/src/ui/molecules/async/` — slot directives, localizable input
defaults, spread constant.
- Template: `libs/shared/src/layout/wizard-shell/` — the canonical wizard outline.
## Verify
+2 -2
View File
@@ -28,5 +28,5 @@ plop-templates/
backend/
# Agent prompts — their exact wording is the input, reflowing markdown edits the prompt
docs/project/refactor-backlog-setup/agents/
docs/project/refactor-backlog-setup/refactor-backlog/final-prompts/
docs/project/archive/refactor-backlog-setup/agents/
docs/project/archive/refactor-backlog-setup/refactor-backlog/final-prompts/
+31 -13
View File
@@ -12,7 +12,7 @@ signals. Auth is faked; **data and business rules are served by a minimal ASP.NE
Core backend** (`backend/`, see its README) and consumed through an NSwag-generated
typed client. The FE renders the backend's decisions. Reference data mimicking
BRP/DUO (`Data/SeedData.cs`) is in-memory; applications, documents and the brief
persist to a SQLite file via EF Core (WP-22) — `docs/project/backlog/WP-22-durable-persistence.md`.
persist to a SQLite file via EF Core (WP-22) — `docs/project/archive/backlog/WP-22-durable-persistence.md`.
**Monorepo (WP-67):** two Angular projects share one backend + one shared library —
`apps/ssp` (Zorgverlener self-service, this doc's main subject) and `apps/behandelportal`
@@ -34,6 +34,7 @@ npm run gen:api # regenerate the ONE typed client (libs/shared) f
npm run ci # run the CI gate locally BEFORE pushing (mirrors ci.yml); `npm run ci --full` adds storybook-a11y
docker compose up # run both FE apps + backend together (Swagger at :5000/swagger)
cd backend && dotnet test # backend rule + endpoint tests
task # list every task (a thin facade over the commands above)
```
**Two Storybook instances, not one:** `apps/ssp` and `apps/behandelportal` each have their own
@@ -72,7 +73,7 @@ so this doesn't depend on a human remembering to run `/model` at the right momen
`git status`/`grep`, verifying a file exists. No Edit/Write access.
Delegate to the matching agent only when the _current_ session isn't already on that
model — don't add indirection for its own sake. `docs/project/backlog/README.md`'s
model — don't add indirection for its own sake. `docs/project/archive/backlog/README.md`'s
session protocol is the worked example of this in practice.
## The decisions (non-negotiable working agreements)
@@ -81,10 +82,11 @@ session protocol is the worked example of this in practice.
`apps/<app>/src/app/<context>/<layer>/` for an app-local context; `libs/<lib>/src/<layer>/`
for a cross-app library (WP-67). Two apps today: `apps/ssp` (Zorgverlener self-service —
contexts `auth`, `registratie`, `herregistratie`, `brief` (letter-composition teaching
slice), `showcase` (teaching page, not a feature; **sanctioned** to read every context in
its own app — nothing imports it)) and `apps/behandelportal` (Behandelaar backoffice,
ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
contexts `auth`, `overzicht` (the portal home; composes `registratie`'s dashboard sections
plus its own cross-context nav sections), `registratie`, `herregistratie`, `brief`
(letter-composition teaching slice), `showcase` (teaching page, not a feature; **sanctioned**
to read every context in its own app — nothing imports it)) and `apps/behandelportal`
(Behandelaar backoffice, ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
design system + kernel + generated API client — no business logic) and `libs/beheer` (the
admin/stamdata context, identical for both apps today — WP-67 folded a silently-diverging
duplicate copy back into one). `auth` is deliberately **not** shared even though today it's
@@ -106,10 +108,11 @@ depend on `libs/beheer` either — it stays the base). `ui`/`layout` never impor
lint-enforced (per app, since each app is cruised against its own tsconfig — WP-67's
`.dependency-cruiser.base.js` + one thin `.dependency-cruiser.<app>.js` per app). An app
may not import the other app's source directly. Cross-context only
`herregistratie → registratie → libs/shared|beheer`, `auth → libs/shared|beheer`,
`brief → libs/shared|beheer` (ssp); `behandeling → libs/shared|beheer`, `auth →
libs/shared|beheer` (behandelportal). Imports use aliases as direction statements:
`@shared/* @beheer/* @auth/* @registratie/* @herregistratie/* @brief/*` (ssp) —
`overzicht → registratie → libs/shared|beheer`, `herregistratie → registratie →
libs/shared|beheer`, `auth → libs/shared|beheer`, `brief → libs/shared|beheer` (ssp);
`behandeling → libs/shared|beheer`, `auth → libs/shared|beheer` (behandelportal). Imports use
aliases as direction statements:
`@shared/* @beheer/* @auth/* @overzicht/* @registratie/* @herregistratie/* @brief/*` (ssp) —
`@shared/* @beheer/* @auth/* @behandeling/*` (behandelportal); each app's own
`tsconfig.json` declares its full map (the root `tsconfig.json` intentionally has no
`paths` — see its comment). `domain/` imports nothing from Angular.
@@ -129,6 +132,18 @@ the design system does the visuals. (Where CIBG lacks a class — e.g. `skeleton
`// CIBG-GAP EXTENSION:` marker; see ADR-0003. `alert` is **not** such a case: it wraps the
vendored `.feedback feedback-*` classes.)
**The step-component contract.** A wizard step follows the same rule as
`address-fields.component.ts`: values in, events out, no internal state. Three clauses:
1. **Inputs down.** A step reads its data only from `input()`s the container passes it.
2. **One narrow output up.** A step emits one specific event, not the container's whole
`dispatch`.
3. **`dispatch` is never passed down.** The container owns the Model and decides what a
step's event means; a step never calls `dispatch` itself.
Corollary: a step gets **no** story of its own. The wizard's own story already mounts every
step, because it seeds the machine.
### 3. State: make illegal states unrepresentable
Default reflex — **if you're about to add a second/third boolean to track state,
@@ -136,7 +151,7 @@ model a discriminated union instead.** Three tools, all in `libs/shared/src/appl
- **`RemoteData<E,T>`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`.
Combine sources with `map`/`map2`/`andThen` (Failure > Loading > Success).
Render it via the `<app-async>` molecule (`libs/shared/src/ui/async`) — one of four
Render it via the `<app-async>` molecule (`libs/shared/src/ui/molecules/async`) — one of four
templates, mutually exclusive by construction. Default loading spinner/skeleton
is delay-gated (~250ms) so fast connections don't flash.
- **Elm-style store** (`store.ts``createStore(initial, reduce)`) — all state in
@@ -242,7 +257,7 @@ organism doesn't get its own `Organisms/` bucket).
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
translation file, not a code change (the seam). Shared/English components must **not**
hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller
supplies the text (see `libs/shared/src/ui/async`). Format-validation messages in
supplies the text (see `libs/shared/src/ui/molecules/async`). Format-validation messages in
`domain/value-objects/` stay co-located but are still `$localize`-wrapped.
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
@@ -272,7 +287,10 @@ organism doesn't get its own `Organisms/` bucket).
- Prettier; `.editorconfig`. tsconfig: `noImplicitReturns`,
`noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`.
- **Enforced, not just hoped-for:** `npm run lint` (`eslint.config.mjs`, scoped to
`{apps,libs}/**`) fails the build on `any`; `npm run dep:check`
`{apps,libs}/**`) fails the build on `any`; the same config's `max-lines` rule caps every
`{apps,libs}/**/*.{page,component,section,step}.ts` file at 250 lines
(`skipBlankLines: true`, `skipComments: true`). 250 is reachable, not a style-guide
default — the dashboard page lands at 42 lines. `npm run dep:check`
(`.dependency-cruiser.base.js` + one `.dependency-cruiser.<app>.js` per app, WP-67) fails
on illegal imports — `domain/` importing Angular, a context importing "upward" (the
`herregistratie → registratie → shared`, `auth → shared` direction), an app importing the
+1 -1
View File
@@ -66,7 +66,7 @@ Admin pages (`/beheer/*`, `/brief/huisstijl`) need the `admin` role — see
| know _why_ a decision was made | [the ADRs](docs/reference/architecture/) — BFF-lite, contexts, huisstijl, stamdata, ZGW, test data |
| work on the backend / BFF | [backend/README.md](backend/README.md) |
| run OpenZaak locally | [backend/openzaak/README.md](backend/openzaak/README.md) |
| see what shipped, or pick up work | [docs/project/backlog/README.md](docs/project/backlog/README.md) |
| see what shipped, or pick up work | [docs/project/archive/backlog/README.md](docs/project/archive/backlog/README.md) |
| build a feature the house way | [`.claude/skills/`](.claude/skills/) — invocable recipes (`new-feature`, `form-machine`, …) |
| know the import rules | [dependencies.md](docs/reference/architecture/dependencies.md) — enforced by `dep:check` |
| work on this repo as an AI agent | [CLAUDE.md](CLAUDE.md) |
+93
View File
@@ -0,0 +1,93 @@
# ponytail: a thin façade over the npm scripts + dotnet + docker, NOT a second copy of
# package.json. `task` (no args) lists everything runnable; the logic stays where it is —
# `task ci` shells to scripts/ci-local.sh, which remains the gate. npm keeps working
# exactly as before, and CI does not depend on `task` being installed.
version: '3'
tasks:
default:
silent: true
cmd: task --list
deps:
internal: true
status:
- test -d node_modules
cmd: npm ci
dev:
desc: Serve the SSP on :4200 (proxies /api to the backend)
deps: [deps]
cmd: npm start
dev:bp:
desc: Serve the behandelportal on :4201
deps: [deps]
cmd: npm run start:behandelportal
api:
desc: Run the backend on :5000 (Swagger at /swagger)
dir: backend
cmd: dotnet run --project src/BigRegister.Api
test:
desc: Run the frontend tests (both apps + both shared libraries)
deps: [deps]
cmd: npm test
test:api:
desc: Run the backend rule + endpoint tests (skips the OpenZaak integration tests)
dir: backend
# Same filter as scripts/ci-local.sh: the Integration tests need a running OpenZaak
# container (scripts/openzaak-ui-up.sh), so they are not part of the default gate.
cmd: dotnet test BigRegister.slnx --filter "Category!=Integration"
lint:
desc: Lint the frontend (no `any`, import/layer boundaries)
deps: [deps]
cmd: npm run lint
fmt:
desc: Format the frontend with prettier and the backend with dotnet format
deps: [deps]
cmds:
- npm run format
- dotnet format backend/BigRegister.slnx
build:
desc: Build both Angular apps
deps: [deps]
cmd: npm run build
ci:
desc: Run the CI gate locally before pushing (scripts/ci-local.sh)
deps: [deps]
cmd: npm run ci
e2e:
desc: Run the Playwright end-to-end tests (starts its own servers)
deps: [deps]
cmd: npm run e2e
sb:
desc: Run the SSP Storybook instance
deps: [deps]
cmd: npm run storybook
sb:bp:
desc: Run the behandelportal Storybook instance
deps: [deps]
cmd: npm run storybook:behandelportal
gen:api:
desc: Regenerate the typed API client from the backend OpenAPI document
deps: [deps]
cmd: npm run gen:api
up:
desc: Start both apps + the backend with docker compose
cmd: docker compose up
down:
desc: Stop the docker compose stack
cmd: docker compose down
@@ -263,10 +263,13 @@ export class HerregistratieWizardComponent {
constructor() {
// An explicit seed (stories/tests) wins; otherwise resume the backend draft
// (`?aanvraag=<id>`) or start fresh. Persistence is the draftSync controller's job.
const seeded = this.seed();
queueMicrotask(() =>
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
);
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
// runs, so an eager read here always returns the `initial` default.
queueMicrotask(() => {
const seeded = this.seed();
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
else void this.draftSync.resume();
});
}
/** Reset the wizard to a fresh, empty start. */
@@ -26,14 +26,30 @@ const buitenlandJa: IntakeState = {
scholingThreshold: 1000,
};
/** Mount with a seed and let the constructor's microtask apply it. */
async function mountSeeded(state: IntakeState) {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideApiClient()],
});
const fixture = TestBed.createComponent(IntakeWizardComponent);
fixture.componentRef.setInput('seed', state);
await Promise.resolve(); // the seed is applied in a queueMicrotask
fixture.detectChanges();
return fixture;
}
describe('IntakeWizardComponent', () => {
it('renders each field group as its own grey <fieldset>', () => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideApiClient()],
});
const fixture = TestBed.createComponent(IntakeWizardComponent);
fixture.componentInstance.dispatch({ tag: 'Seed', state: buitenlandJa });
fixture.detectChanges();
// Regression: the constructor must read `seed()` INSIDE its microtask. Angular binds
// inputs after the constructor runs, so an eager read silently yields the `initial`
// default and every seeded story renders step 1 instead of the state it asked for.
it('honours the seed input', async () => {
const fixture = await mountSeeded(buitenlandJa);
expect(fixture.componentInstance.state()).toEqual(buitenlandJa);
});
it('renders each field group as its own grey <fieldset>', async () => {
const fixture = await mountSeeded(buitenlandJa);
const fieldsets: HTMLElement[] = Array.from(
fixture.nativeElement.querySelectorAll('form.form-horizontal fieldset'),
@@ -212,10 +212,13 @@ export class IntakeWizardComponent {
constructor() {
// An explicit seed (stories/tests) wins; otherwise resume the backend draft
// (`?aanvraag=<id>`) or start fresh. Persistence is the draftSync controller's job.
const seeded = this.seed();
queueMicrotask(() =>
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
);
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
// runs, so an eager read here always returns the `initial` default.
queueMicrotask(() => {
const seeded = this.seed();
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
else void this.draftSync.resume();
});
// Apply the server-owned threshold into machine state as it arrives. Track
// only the policy value; untrack the dispatch (it reads the state signal
// internally, which would otherwise make this effect loop on its own write).
@@ -1,15 +1,15 @@
import { Component } from '@angular/core';
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
import { MijnAanvragenSection } from '@registratie/ui/dashboard/mijn-aanvragen.section';
import { WatMoetIkRegelenSection } from '@registratie/ui/dashboard/wat-moet-ik-regelen.section';
import { MijnRegistratieSection } from '@registratie/ui/dashboard/mijn-registratie.section';
import { SpecialismenSection } from '@registratie/ui/dashboard/specialismen.section';
import { MijnAanvragenSection } from '@registratie/ui/overzicht-secties/mijn-aanvragen.section';
import { WatMoetIkRegelenSection } from '@registratie/ui/overzicht-secties/wat-moet-ik-regelen.section';
import { MijnRegistratieSection } from '@registratie/ui/overzicht-secties/mijn-registratie.section';
import { SpecialismenSection } from '@registratie/ui/overzicht-secties/specialismen.section';
import { WatWiltUDoenSection } from './wat-wilt-u-doen.section';
import { BeheerLinksSection } from './beheer-links.section';
/** Page: "Mijn overzicht" — the portal home, following the NL Design System "Mijn
omgeving" pattern. Composition only: each section below answers its own data
question (own store, own async state) — four sections stay in `registratie/ui/dashboard/`
question (own store, own async state) — four sections stay in `registratie/ui/overzicht-secties/`
(they render registratie data), two live here (cross-context navigation). */
@Component({
selector: 'app-overzicht-page',
@@ -17,57 +17,17 @@ const meta: Meta<AanvraagBlockComponent> = {
title: 'Domein/Registratie/Aanvraag Block',
component: AanvraagBlockComponent,
decorators: [applicationConfig({ providers: [provideRouter([])] })],
render: (args) => ({
props: args,
// A row is an <li> — the keuzelijst styling needs the real list context.
template: `<ul class="keuzelijst__list"><app-aanvraag-block [aanvraag]="aanvraag" /></ul>`,
}),
parameters: {
// Structural: app-aanvraag-block's host sits between the keuzelijst <ul> and its <li>
// — axe's list/listitem rule needs them adjacent regardless of `display:contents`.
// WP-11 (CIBG markup fidelity) reworks this markup; see docs/project/backlog/WP-11-markup-fidelity.md.
a11y: { disable: true },
},
// A Concept renders as a CIBG melding (block element), not a keuzelijst <li> — no <ul>
// wrapper. Production agrees: mijn-aanvragen.section.ts renders this block for concepten
// only, outside any list.
render: (args) => ({ props: args, template: `<app-aanvraag-block [aanvraag]="aanvraag" />` }),
};
export default meta;
type Story = StoryObj<AanvraagBlockComponent>;
// One story per status variant; the block renders its own body + actions.
// A Concept renders as a CIBG melding (block element), not a keuzelijst <li> — no <ul> wrapper.
// The whole template sits inside `@if (aanvraag().status.tag === 'Concept')`, so this is
// the only status that renders anything. Submitted/resolved aanvragen render through
// application-link, which has its own stories.
export const Concept: Story = {
args: { aanvraag: { ...base, status: { tag: 'Concept', stepIndex: 1, stepCount: 3 } } },
render: (args) => ({ props: args, template: `<app-aanvraag-block [aanvraag]="aanvraag" />` }),
};
export const InBehandelingAuto: Story = {
args: {
aanvraag: {
...base,
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: false },
},
},
};
export const InBehandelingManual: Story = {
args: {
aanvraag: {
...base,
type: 'registratie',
status: { tag: 'InBehandeling', referentie: 'BIG-2026-456789', manual: true },
},
},
};
export const Goedgekeurd: Story = {
args: { aanvraag: { ...base, status: { tag: 'Goedgekeurd', referentie: 'BIG-2026-456789' } } },
};
export const Afgewezen: Story = {
args: {
aanvraag: {
...base,
type: 'herregistratie',
status: {
tag: 'Afgewezen',
referentie: 'BIG-2026-456789',
reden: 'Aanvraag afgewezen: geen gewerkte uren geregistreerd.',
},
},
},
};
@@ -66,14 +66,6 @@ export const MetTaken: Story = {
],
}),
],
parameters: {
// Structural: app-choice-link's host sits between the keuzelijst <ul> and its <li>
// — axe's list/listitem rule needs them adjacent regardless of `display:contents`.
// Same pre-existing gap as task-list.stories.ts and choice-list.stories.ts. WP-11
// (CIBG markup fidelity) reworks this markup; see
// docs/project/backlog/WP-11-markup-fidelity.md.
a11y: { disable: true },
},
};
export const NietsOpenstaand: Story = {
decorators: [
@@ -233,10 +233,13 @@ export class RegistratieWizardComponent {
constructor() {
// An explicit seed (stories/tests) wins; otherwise resume from the backend draft
// (`?aanvraag=<id>`), or start fresh. Persistence is the draftSync controller's job.
const seeded = this.seed();
queueMicrotask(() =>
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
);
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
// runs, so an eager read here always returns the `initial` default.
queueMicrotask(() => {
const seeded = this.seed();
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
else void this.draftSync.resume();
});
// Prefill the address from the BRP lookup as it arrives. Track only the facade's
// parsed prefill signal; untrack the dispatch (it reads the state signal, which
// would otherwise make this effect loop on its own write). Don't clobber
+1 -1
View File
@@ -15,7 +15,7 @@ status codes and error envelope are production-shaped.
covers it, see `docker-compose.yml`) does **not** lose data. Delete the file to
reset demo data back to empty, the same state a fresh clone starts from. This is
a deliberate, right-sized choice for a POC (SQLite, no external DB service) — see
`docs/project/backlog/WP-22-durable-persistence.md`.
`docs/project/archive/backlog/WP-22-durable-persistence.md`.
## Run
@@ -152,7 +152,7 @@ public static class LetterHtml
// Walks up from the running assembly's own directory (NOT the process cwd, which
// varies by how `dotnet run`/docker/tests invoke it — see
// docs/project/backlog/WP-25-letter-preview-html.md) until it finds `public/letter.css`. docker-compose.yml bind-mounts `./public` under the
// docs/project/archive/backlog/WP-25-letter-preview-html.md) until it finds `public/letter.css`. docker-compose.yml bind-mounts `./public` under the
// api container's `/src` for exactly this walk to resolve there too.
private static string FindLetterCss()
{
+6
View File
@@ -12,6 +12,8 @@ services:
command: dotnet run --project src/BigRegister.Api --urls http://+:5000
environment:
- ASPNETCORE_ENVIRONMENT=Development
# ponytail: Server GC makes one heap per CPU (22 here); workstation GC makes one.
- DOTNET_gcServer=0
volumes:
# ':z' relabels for SELinux (Fedora/RHEL); harmless on other hosts.
# WP-22: no separate volume needed for the SQLite file — `dotnet run` sets
@@ -41,6 +43,8 @@ services:
# switcher actually switches. ponytail: `--no-fund --loglevel=error` silences npm 11 noise.
command: sh -c "npm ci --no-fund --loglevel=error && npx ng build ssp --configuration development --localize && node scripts/serve-i18n.mjs"
environment:
# ponytail: without a cgroup limit Node sizes its heap from host RAM (16 GB).
- NODE_OPTIONS=--max-old-space-size=2048
- PORT=4200
- API_PROXY_TARGET=http://api:5000
- APP_DIST_ROOT=dist/ssp/browser
@@ -57,6 +61,8 @@ services:
working_dir: /app
command: sh -c "npm ci --no-fund --loglevel=error && npx ng build behandelportal --configuration development --localize && node scripts/serve-i18n.mjs"
environment:
# ponytail: without a cgroup limit Node sizes its heap from host RAM (16 GB).
- NODE_OPTIONS=--max-old-space-size=2048
- PORT=4201
- API_PROXY_TARGET=http://api:5000
- APP_DIST_ROOT=dist/behandelportal/browser
+11
View File
@@ -0,0 +1,11 @@
# Archive — finished backlogs
The trees in this directory are historical. Every ticket in `backlog/` and
`refactor-backlog-setup/` carries `Status: done`. `SHOWCASE-ROADMAP.md` was superseded before
either arc started.
Git holds the rest — `git log --follow` on any file here shows its full history, including the
work that produced it.
The live plan is `docs/project/readable-codebase/`. Product reference material is
`docs/project/prd/`.
@@ -1,6 +1,6 @@
# Showcase roadmap — superseded
**This roadmap is superseded by [`docs/project/backlog/`](backlog/README.md)** (2026-07-02).
**This roadmap is superseded by [`docs/project/archive/backlog/`](backlog/README.md)** (2026-07-02).
The backlog absorbs and corrects this document: its Storybook-as-curriculum track became
WP-14/15 (+ per-invariant MDX pages in WP-05/07/08/13/17), its enforcement track became
@@ -59,7 +59,7 @@ The review's remaining findings are listed under "Follow-ups" and are **not** th
## Read first
- `CLAUDE.md` §"The decisions" #3 (make illegal states unrepresentable) and #4 (BFF-lite)
- [ADR-0001 — BFF-lite + decision DTOs](../../reference/architecture/0001-bff-lite-decision-dtos.md)
- [ADR-0001 — BFF-lite + decision DTOs](../../../reference/architecture/0001-bff-lite-decision-dtos.md)
- `backend/src/BigRegister.Api/Data/ApplicationStore.cs` (the `Aanvraag` entity, the store's
lock discipline, `AanvraagStatusTag`, `RecordBesluit`)
- `backend/src/BigRegister.Api/Contracts/Mappers.cs` (`ToStatusDto` — the logic to move)
@@ -39,7 +39,7 @@ re-validates as authority) is unenforced for the one rule it was written to illu
- `backend/src/BigRegister.Api/Domain/Intake/IntakePolicy.cs` (the corrected doc-comment,
WP-68)
- [ADR-0001 — BFF-lite + decision DTOs](../../reference/architecture/0001-bff-lite-decision-dtos.md)
- [ADR-0001 — BFF-lite + decision DTOs](../../../reference/architecture/0001-bff-lite-decision-dtos.md)
§"config value"
- `apps/ssp/src/app/herregistratie/domain/intake.machine.ts` (`lageUren`, `scholingGevolgd`,
`punten` — the wizard's existing FE-side rule and its answers)

Some files were not shown because too many files have changed in this diff Show More