main
20
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1dd8bd4e1b |
S-24/#25 · Helm chart + Kubernetes deployment, and Caddy for the portals (#166) (#167)
## What & why Two changes, made and verified together on a real cluster. **S-24 / #25 — a Helm chart for the platform.** One chart, `infra/helm/big-reference`, whose `values.yaml` is a near-literal transcription of `infra/docker-compose.yml`, rendered by three generic templates (Deployment, Job, Service) over a `workloads` map. Adding a service is a values edit. `make k8s-lint` renders and schema-checks the whole stack without a cluster. The issue asked for a *sketch*; this is deployed and verified end to end (see below), which is more than it asked for — the part it asked for that is **not** here is the production-posture write-up (HA, secrets, backup), see Known gaps. **#166 — Caddy replaces nginx in the portals.** nginx resolves a variable `proxy_pass` upstream itself, using only the `resolver` directive and never `/etc/resolv.conf`'s search domains. That had cost two workarounds in one script: rewriting the resolver address for rootless podman, and injecting a full FQDN so the bare `bff` name could resolve on Kubernetes. Caddy dials per request through the system resolver, so `reverse_proxy bff:8080` works on every engine unchanged; `apps/portal-nginx-resolver.sh` and the chart's `BFF_HOST` env are deleted. Closes #25 Closes #166 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation — twice: the Caddyfile contract test before the Caddyfiles, `make k8s-lint` before the chart. - [x] Implementation makes the test pass. - [x] Conventional Commits referencing the issues (`refs #25` / `refs #166`). - [ ] CI green — awaiting the run on this PR (`make k8s-lint`, `dotnet format` and the new unit self-check pass locally; the compose e2e and mutation lanes are CI's). - [ ] `docker compose up` from a fresh clone reaches green health checks within 3 minutes — the portal images were rebuilt and verified standalone, but a full `make up` run has not been done on this branch. Please confirm in review or let CI's smoke test speak. - [x] Docs updated — `docs/runbooks/kubernetes-talos.md` (new), `frontend-decisions.md`, `demo-script.md`, and the docs that named nginx. - [x] ADR added — ADR-0033 (chart) and ADR-0034 (Caddy). - [ ] Demo note in `docs/demo-script.md` — not added: the deployment target is not a user-visible slice, and the Caddy swap is invisible to the demo script beyond the wording fix included here. ## How it was verified Brought up from scratch on a single-node Talos v1.14.0 VM (6 vCPU / 10 GB, virtio disk) under virt-manager: **29 pods ready and four bootstrap Jobs complete in under three minutes, zero restarts**, using ~4.4 GB of the VM's 10 GB. - Full Common Ground path: portal Caddy → BFF → domain → Flowable → ACL → OpenZaak + Objecten → NRC → event-subscriber → projection → public register (`INGEDIEND`, reference matching the submitted registration). - Werkbak read with an MFA'd medewerker token → 200. - The browser flow driven with Playwright against `http://localhost:30140`: secure context, `crypto.subtle` present, Keycloak form reached, login completed, **no console errors**. - Routing checked against a stub BFF: SPA fallback serves deep links, each portal proxies its own groups, and a portal does *not* proxy a neighbour's group. ## Notes for reviewers Three bugs this shook out, each fixed at the cause rather than the symptom: 1. **`command` vs `args`.** Compose's `command:` replaces the image CMD; Kubernetes' replaces the ENTRYPOINT. Transcribing one to the other broke every upstream image that relies on its entrypoint — postgres refused to run as root, Keycloak tried to exec `start-dev`. The chart now `fail`s at render time on `command`. 2. **Concurrent migrations.** Both `/setup_configuration.sh` and `/start.sh` run `manage.py migrate`; compose serialises them with `depends_on`, Kubernetes has no such edge, so the init Job and its web pod raced (`relation "zgw_consumers_service" already exists`). The four Django services now do both steps in order in the web pod — which also deletes four workloads. 3. **`emptyDir` databases are wiped by any pod-template change.** `make k8s-reseed` now also restarts `event-subscriber` and `projection-api`, which create the projection schema on start and otherwise keep writing to a schema-less database. Known gaps / follow-ups: - **Secrets.** `values.yaml` carries the dev credentials in plain text (`admin/admin`, the ZGW client secret, the two Objecten tokens) and the chart has no `Secret` objects. Fine for a laptop demo, and exactly what #25's "production posture" ADR should address — I suggest a follow-up issue rather than stretching this PR. - **No CI gate for the chart yet.** `make k8s-lint` exists but is not wired into `.gitea/workflows/ci.yaml`, and nothing enforces that the chart and the compose file stay in step. Worth a small follow-up. - **This is two slices in one PR.** They were built and verified together and the diff is entangled (the chart was written against Caddy from the start), so splitting now would mean re-creating an nginx-shaped chart to throw away. Happy to split if you'd rather. - **Rebased onto #161** (merged as #165) rather than merged, to keep the history linear. One conflict, in the `unit:` target where both branches add a self-check line — resolved by keeping both. #161's `infra/host-browser.yml` arrived with `/usr/share/nginx/html/config.json` and is fixed to `/usr/share/caddy/` inside the `feat(portals)` commit, so no commit on this branch leaves that overlay pointing at a path the images no longer have.Reviewed-on: #167 |
||
|
|
d6b3f9764f |
fix(e2e): bound the Playwright run and make a failed login say why (closes #161) (#165)
## What & why #161 is really two defects, and the second one is why the first was undiagnosable. **A wedged suite consumed the job, and took the post-mortem with it.** Nothing bounded the Playwright run, so CI stopped the job mid-suite — and `if: always()` does not survive that. Run 739's job metadata shows every step after the e2e as a **0-second failure** stamped at the kill: ``` 14 failure 09:48:17 -> 10:14:54 Self-service e2e (Playwright …) 15 failure 10:14:54 -> 10:14:54 verify-stack check summary ← if: always() 16 failure 10:14:54 -> 10:14:54 e2e spec summary ← if: always() 17 failure 10:14:54 -> 10:14:54 Dump container logs on failure ← if: failure() 18 failure 10:14:54 -> 10:14:54 Tear down ← if: always() ``` So the per-spec summary, the container-log dump and the teardown never ran, and the log lost whatever the killed process had buffered — leaving the single `✘` line the issue was filed from. `globalTimeout` now makes Playwright stop and *report*: the JSON report is written and those steps still get their turn. (A `timeout-minutes` on the job would have reproduced the same failure, so there isn't one.) The "~24-minute gap" is that kill, not necessarily a hang — note run 739 shows `run_attempt: 2`, and `concurrency.cancel-in-progress` kills an in-flight run on any re-run or push. **A login that never got its form ate the 90-second test timeout.** Playwright actions auto-wait until the *test* timeout, not `expect.timeout` — so a portal that serves its page but never bootstraps (its `config.json` fetch or the OIDC discovery behind `authorize()` failed; `main.ts` only `console.error`s) spent 90s to report `locator.fill: Test timeout of 90000ms exceeded`: the symptom, not the cause. That is catalogus.spec's 1.8 minutes. Both Keycloak forms are now asserted visible first, with a 20s budget and a message naming the step that never happened. Verified against a real blank-bootstrap portal — the beheer image served with a `config.json` that is not JSON — which fails in **20.2s** with *"the Keycloak login form never appeared — the portal did not reach Keycloak (check its config.json fetch and the OIDC discovery …)"*. **And the summary now says why.** The per-spec table (#136) rendered a verdict icon and nothing else, so even a surviving summary cost a log dive. Failing specs now carry their first error, flattened for a table cell (ANSI stripped, newlines collapsed, `|` escaped, clipped) — shape verified against a real @playwright/test 1.61 failing report, with a stdlib assert self-check on `make unit`. Closes #161 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation. - [x] Implementation makes the test pass; refactor commit follows (login helper dedup). - [x] Conventional Commits referencing the issue (`refs #161`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (untouched). - [x] Docs updated — `docs/runbooks/gitea-actions-gotchas.md` §9. - [x] ADR — not needed: no boundary, dependency or coupling rule touched (test/CI infra only). - [x] Demo note — not applicable: nothing user-visible. ## Notes for reviewers **What this does not do: identify why the beheerder login failed that once.** The evidence to do that was destroyed by defect 2, which is what this PR fixes. The suite ran green here five times today (catalogus.spec 1.1–5.3s each) — but a local box is not the loaded CI runner, so that is weak evidence and I am not claiming the flake is gone. What changes is that the next occurrence is bounded and self-describing: it fails in 20s naming the failing step, the JSON report survives, and the summary prints the error. Please keep #161 in mind rather than treating this as proof. **Two follow-ups I did not pull into this PR:** - *All four portals show a permanently blank page if their startup fetch fails* — `main.ts` does `fetch('config.json').then(bootstrap).catch(console.error)`, one shot, no UI and no recovery. That is a real product gap (the deliberately-broken portal above is exactly what a user would see) and wants its own slice, not a test-infra PR. - `retries: 1` is untouched. CLAUDE.md §15 says flaky tests are fixed rather than retried, but removing retries while a real flake is unexplained would trade a rare red for a frequent one. Worth revisiting once #161 recurs (or doesn't) with the new diagnostics. The login-helper rename (`medewerker-login.ts` → `keycloak-login.ts`, citizen logins routed through `loginBurger`) is its own no-behaviour-change commit: the three citizen specs each duplicated the same three-line login, so guarding the login path once meant routing them through it first.Reviewed-on: #165 |
||
|
|
8b206a005f |
S-26/#162 · Werkbak refreshes itself when a registration is ready for beoordeling (#164)
## What & why The behandel werkbak now **refreshes itself** while it is open, so a registration that reaches beoordeling after the behandelaar opened the page shows up on its own — no reload. `interval(WERKBAK_REFRESH_MS)` (5 s) re-reads the existing BFF endpoint, scoped to the page with `takeUntilDestroyed()`. A *background* read leaves the rows and states on screen alone until it has an answer, so a tick never flashes the loading state over rows being read and one failed poll never swaps the list for the error alert; a read that comes back also clears an earlier failure, so the view recovers on its own rather than needing the very reload this slice removes. No new endpoint, dependency or server-side state, and no service boundary moves — rxjs and `GET /behandel/werkbak` are both already here. **ADR-0032** records why polling rather than a pushed stream: nothing notifies the BFF either, so SSE/WebSockets would poll the domain *inside* the BFF for the same freshness, plus connection lifecycle, nginx buffering and a stateful BFF. Proposal: #163. Closes #162 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation. - [x] Implementation makes the test pass; refactor commit if structure improved. - [x] Conventional Commits referencing the issue (`refs #162`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (unchanged; only the behandel bundle differs). - [x] Docs updated if behaviour, contracts, or operations changed. - [x] ADR added in `docs/architecture/` (ADR-0032). - [x] Demo note in `docs/demo-script.md` (user-visible). ## Notes for reviewers **The e2e is the real acceptance test, and it took two goes to make it one.** Simply dropping the `staff.reload()` from the happy path proved nothing: the werkbak was visited *after* the documents were supplied, so the row was already there at page load. The spec now logs the behandelaar in **first**, asserts the row is not there yet, and only then has the citizen supply the documents that route it to Beoordelen — so the row can only reach that already-open, never-reloaded page via the refresh. Verified both ways against a live stack: with the interval stubbed out it fails at `Goedkeuren <ref> … element(s) not found` after 30 s; with it, the behandel nginx logs the poll that delivers the row. The page is foregrounded before the assertion because Chromium throttles timers in a hidden tab. **Ceiling (named in the ADR):** a fixed 5 s interval, per open page, that keeps polling in a background tab; each tick costs one Flowable task query plus a store read per open task. Upgrade path: publish task events from the domain, then swap the `interval` for a stream — the endpoint contract and the rendering stay put. Gate on `document.visibilityState` first if request volume is the concern. **Two housekeeping notes, neither blocking:** - #162 is on **no milestone** (DoD item 1). It is portal UX, so it fits neither *Data Governance* nor *Production Posture* cleanly — your call where it lands. - The issue titles itself **S-26**, which already belongs to the self-service resume slice (#111, `BACKLOG.md`). Everything here references **#162**; worth renumbering the title if the S-ids are meant to stay unique. `BACKLOG.md` is untouched for the same reason (it mirrors the active milestone, and this slice is on none).Reviewed-on: #164 |
||
|
|
d0fb2b3e8c |
S-15c · Enforce MFA on the medewerker (Keycloak) realm (#158)
Closes #132.
Staff logins (behandel + beheer portals) now need a second factor; the citizen realms are unchanged.
**How:** every seeded medewerker carries a TOTP credential, which activates Keycloak's stock *conditional OTP* step in both the browser flow and the direct grant — no custom browser-flow JSON in the export. `CONFIGURE_TOTP` is a default required action so a medewerker added later must enrol first. ADR-0031 records the choice and, explicitly, that the shared fixture secret is a demo posture only.
**Tests (red first,
|
||
|
|
94742a261f |
feat: read projection sourced from the register in Objecten (closes #153) (#155)
## What & why S-19b-2, closing out ADR-0028's stated direction: **the read projection is now derived from the `RegisterRecord` in Objecten, not from ZGW zaak events.** Until now the subscriber listened on `zaken` and *inferred* register state from case events — a `zaak/create` meant INGEDIEND, and any `status/create` was assumed to be the approval (it may not read OpenZaak, so it could not tell statustypen apart). The reference wasn't in the notification at all, so every projection made a second hop to the ACL. The register — a fact about a person — was being reconstructed by guessing at the lifecycle of the case that produced it. - The subscriber's abonnement moves to the `objecten` kanaal (S-19b-1 made it publish). - An Objecten notification carries **no record data**, only the object URL, so the record is read back through the ACL (`POST /register-records/read`) — §8.1 applies to Objecten exactly as ADR-0028 established. - The record carries `id`, `status` and `reference`, so the row *is* the record: `IsZaakCreated`, `IsZaakStatusSet`, `ZaakUrl`, `ZaakId` and `ToEntry`'s `Resource == "status"` inference are all gone, and so is the ACL enrichment hop. - **The ACL now writes an INGEDIEND record on submit.** Without it, re-sourcing would silently drop every submitted registration from the public register, since only approval wrote a record. - `processed_notifications` holds the projected row (`register_id`, `status`, `reference`) instead of the ZGW event, so a rebuild is a replay with no mapping rules and no upstream reads at all. **ADR-0030** records it. ADR-0028's open caveat — record written but not yet read, "the two must agree" — is closed: there is one source now. Closes #153 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing tests committed before the implementation — two red/green pairs, ACL side ( |
||
|
|
0cd70ae8c3 |
S-19a · ACL writes the RegisterRecord to Objecten on approval (closes #149) (#151)
Closes #149. **Outcome:** approving a registration now writes the canonical register record to the **Objecten** API as a `RegisterRecord` object, alongside the ZGW eindstatus. OpenZaak holds the process, Objecten holds the register (ADR-0028). The write goes through the ACL (§8.1) and is idempotent on the zaak id, so a replayed approval updates the existing object rather than creating a second one. S-19 (#20) was split first (CLAUDE.md §13) — it bundled this with re-sourcing the read projection, which is now #150. ### What landed - `IRegisterRecordGateway` + `RegisterRecord` in `Acl.Application`; `ObjectenGateway` in `Acl.Infrastructure` (static Token auth, CRS headers, objecttype resolved by name to its highest **published** version). - `AclService.ApproveZaakAsync` writes the record after the eindstatus, keyed on the zaak UUID with the zaak's identificatie as reference. - Compose wiring for both stacks; `ADR-0028`; demo note; PRD §15 out-of-scope line retired. ### Three things only a live stack found Running the gateway against a real Objecten + Objecttypen pair while writing this turned up blockers CI would have hit after the fact: 1. **Objecten rejects an objecttype it has not been configured with**, by UUID — assigned at seed time by a one-shot that runs *after* Objecten's static setup_configuration. The UUID is now pinned on both sides. 2. **Objecten 500s on every write when its Notificaties config is absent** (`notifications_api_common` raises rather than skipping). Objecten → NRC has no broker, worker, kanaal or abonnement, so notifications are **disabled** rather than wired to drop every message; #150 turns them on for real. 3. **Objecttypen echoes the request Host into the objecttype `url`**, and Objecten only accepts the one matching its configured `api_root` — so the ACL must read Objecttypen at `http://objecttypen:8000`. This is why the new integration test only passes inside the compose network. All three are recorded in ADR-0028. ### Verification - `ObjectenGatewayIntegrationTests` (verify-acl, in-network): two writes for one id leave exactly one object with the second write's status. **Passing locally against live Objecten.** - The **Playwright happy path** asserts, after the behandelaar approves, that Objecten holds exactly one `RegisterRecord` for *that* reference — missing, duplicated, or non-public-safe all fail. - ACL mutation score **92.23%** (baseline 91.37%, break 90). - `make lint` / `make unit` green locally; full-stack `make verify` runs in CI. ## Definition of Done - [x] A linked Gitea issue exists (#149). - [x] Failing test written and committed first. - [x] Implementation makes the test pass. - [x] Refactor commit follows if structure improved. - [x] Conventional Commit messages referencing the issue (`refs #149`). - [x] All Gitea Actions CI jobs green (run 684). - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (verify-stack step 1). - [x] Docs touched — ADR-0028, demo note, PRD §15, BACKLOG. - [x] ADR added: `docs/architecture/adr-0028-objecten-holds-the-register.md`. - [x] Demo note appended to `docs/demo-script.md`. - [x] Closed by the merging PR (`closes #149`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #151 |
||
|
|
0494730223 |
feat(portal-beheer): ACL default-fill configuration editor (closes #131) (#138)
## What & why S-15b, second of the S-15 (#16) split, on top of S-15a (#133). A beheerder edits the ACL's ZGW **default-fill** values from the beheer portal, and the next zaak is stamped with the new values — no restart. Closes #131 ### The vertical portal → BFF `GET/PUT /beheer/default-fill` (medewerker realm + `beheerder` role) → ACL `GET/PUT /default-fill` → a runtime-mutable in-memory store the ACL reads **per zaak**. - **ACL**: `IDefaultFillStore` / `InMemoryDefaultFillStore` (thread-safe, seeded from `Acl:Defaults`); `AclService` reads `fill.Current` per zaak (not cached at construction); `GET`/`PUT /default-fill` with required-field validation. - **BFF**: `IAclClient` gains `GetDefaultFillAsync`/`UpdateDefaultFillAsync`; `GET`/`PUT /beheer/default-fill` behind the `beheerder` policy. OpenAPI + generated client regenerated. - **Frontend**: a *Default-fill* editor page in the beheer app (load → edit → save, with saved/failure states) + nav between Catalogus and Default-fill. ### Scope decision → ADR-0026 Only the **three ZGW fill fields** (bronorganisatie, verantwoordelijke organisatie, vertrouwelijkheidaanduiding) are editable. The S-27 catalog-resolution keys stay **static config** — editing them would desync the zaaktype-URL cache (ADR-0021), and they're catalogus wiring, not "default fill". The store is **in-memory** (seeded from config): an edit reverts on restart. That's the reference-app-appropriate ceiling (no DB added to the stateless ACL); upgrade path documented. Recorded in **ADR-0026**. ## Verified locally lint (`dotnet format`) ✓ · .NET unit — acl 60 / bff 45 / domain 152 / event-subscriber 19 / acceptance 17 ✓ · frontend lint+test (8 projects) ✓ · beheer build ✓. Clean full-solution build (caught + fixed the acceptance `AclService` ctor drift). TDD red→green per layer (ACL store, ACL endpoints, BFF, frontend). ## Definition of Done - [x] Failing test committed before each implementation (red→green per layer). - [x] Conventional Commits referencing #131. - [ ] CI green — see note below. - [x] Docs: ADR-0026 + S-15b demo note. - [x] Demo note in `docs/demo-script.md`. ## Note on CI The bulk validates in the fast jobs (lint/build/unit/frontend/mutation). The **verify-stack e2e** (incl. the new `default-fill.spec.ts`) can't go green until the pre-existing **verify-stack bring-up failure on the 1.27/2.0.0 runner** is resolved (that fails on plain `main` too — unrelated to this PR). Additive change; no existing e2e touched. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #138 |
||
|
|
fff88ca23d |
ci: richer step reports via Gitea 1.27 job summaries (closes #136) (#137)
## What & why Use the standard `$GITHUB_STEP_SUMMARY` (Gitea 1.27 + act_runner 2.0.0) to surface on the run page what was previously buried in logs or download-only artifacts. All five quick wins from #136, **reporting-only** — no job's pass/fail gating changes. Closes #136 ### Items 1. **Mutation scores** — added the `markdown` reporter to each `stryker-config.json`; the `mutation` job concatenates each service's `mutation-report.md` into the summary (`if: always()`). Also reveals where `make mutation` stopped on a ratchet break. 2. **Per-frontend tests** — the 4 apps' `test` targets emit vitest JSON to `test-output/{projectName}.json` (Nx token interpolation); `infra/vitest-summary.py` renders a per-frontend table. 3. **Per-service unit tests** — `make unit` now writes TRX; `infra/trx-summary.py` renders a per-service table (service name derived from the `services/<name>/` path, so `domain` shows, not `big.tests`). 4. **e2e per-spec results** — Playwright writes `playwright-report.json`; `run-e2e-check.sh` copies it out of the container (capturing the exit code first); `infra/playwright-summary.py` renders a per-spec table. Turns a red e2e into a one-glance "which spec". 5. **verify-stack check table** — each live-stack check has an `id`; a final `if: always()` step tabulates each check's ✅/❌/⏭️. Docs: `gitea-actions-gotchas.md` §8 (version requirement + `$GITHUB_STEP_SUMMARY` guard + step-level `always()` note). ### Notes - Every summary write is guarded with `[ -n "${GITHUB_STEP_SUMMARY:-}" ]`, so it no-ops on an unsupported runner / locally. - New helper scripts are stdlib-only Python, matching the existing `infra/*.py` check scripts (no new dependency — a few lines of parsing rather than a test-logger package). - `TestResults/` and `test-output/` gitignored. - This is also the first PR-run exercising the #135 verify-stack fix end to end. ## Verified locally `make unit` (TRX) ✓ · 4 apps' vitest JSON ✓ · ACL Stryker markdown report ✓ · all four parsers + the two summary shell blocks ✓ · `ci.yaml` + `run-e2e-check.sh` syntax ✓. The rendered summaries themselves only appear on the run page — this PR's CI run is the end-to-end check. ## Definition of Done - [x] Each item writes to `$GITHUB_STEP_SUMMARY` (guarded), renders on the run page. - [x] No change to any job's pass/fail gating. - [x] Conventional Commits referencing #136 (one per item + docs). - [ ] CI green; summaries visible on the run. - [x] Runbook note (gotchas §8). 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #137 |
||
|
|
4698c869f3 |
feat(portal-beheer): beheer portal + read-only catalogus viewer (closes #130) (#133)
## What & why S-15a, the first of the S-15 (#16) split. A new **beheer** portal (medewerker realm, like behandel) shows the ZTC catalogus — the published zaaktypen — **read-only**. A beheerder logs in and sees the seeded BIG-REGISTRATIE zaaktype. Closes #130 ### The vertical portal → BFF `GET /beheer/catalogi/zaaktypen` (medewerker realm + `beheerder` role) → ACL `GET /catalogi/zaaktypen` → ZGW Catalogi API. - **ACL**: new read-only `GET /catalogi/zaaktypen` listing published zaaktypen (reuses the ADR-0021 Catalogi client; public-safe `identificatie`/`omschrijving`). - **BFF**: new typed `IAclClient` + `Downstream:Acl:BaseUrl`, and `GET /beheer/catalogi/zaaktypen` behind a new `beheerder` policy (reuses the medewerker bearer scheme + realm-role lifting). OpenAPI spec + generated Angular client regenerated. - **Keycloak**: `beheerder` realm role + `bram-beheerder` test user in the medewerker realm. - **Frontend**: new `apps/beheer` Angular app (copied from behandel) with a read-only catalogus page; `SECURE_API_ROUTES=['/beheer/']`. - **Infra**: `beheer` compose service (port 8143), added to `WAIT_SVCS` + CI log-dump; a Playwright e2e (beheerder login → catalogus shows BIG-REGISTRATIE). ### New boundary → ADR-0025 The BFF now reaches the **ACL directly** for the catalogus read — a new service-to-service edge (§14). The catalogus is neither a domain nor a projection concern, and §8.1 means only the ACL may read ZGW; routing through the domain would pollute it with a non-domain passthrough. §8.1/§8.3 stay intact. Recorded in **ADR-0025**. ## Definition of Done - [x] Failing test committed before each implementation (red→green per layer: ACL, BFF, frontend). - [x] Conventional Commits referencing #130. - [ ] CI green — pending Gitea Actions run. - [x] `docker compose up` brings up `beheer` (health-gated in `WAIT_SVCS`). - [x] Docs — ADR-0025 + demo-script S-15a note. - [x] Demo note in `docs/demo-script.md`. ## Verified locally lint (`dotnet format`) ✓ · .NET unit (Acl 57 / Big 152 / EventSubscriber 19 / Bff 40) ✓ · frontend lint+test (8 projects) ✓ · frontend build (4 apps) ✓. Mutation ratchet: added a gateway unit test for the new `ListZaaktypenAsync` mapping so the ACL score holds. verify-stack (compose smoke + e2e) runs in CI. ## Notes for reviewers - The BFF drops the ZGW URL from `BeheerZaaktype` (public-safe: identificatie + omschrijving only). - The catalogus e2e asserts on the stable seeded `BIG-REGISTRATIE` (not a per-test reference), safe on the shared verify stack. - Follow-ups: **S-15b** (#131) default-fill CRUD, **S-15c** (#132) medewerker-realm MFA. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #133 |
||
|
|
5f8ab4dbcd |
feat: self-service resume of an existing registration after refresh (S-26, closes #111) (#119)
## What & why After submitting, the self-service portal held the registration only in in-memory signals, so a **page refresh stranded an in-flight registration** — the reference and its "Documenten aanleveren" / "Trek aanvraag in" actions were lost, with no way back (the reference wasn't in the URL and there was no read endpoint). This is the gap a citizen hit in testing. Now the portal **resumes on load**: - **Domain:** `IRegistrationStore.FindOpenByBsnAsync` (the citizen's non-terminal INGEDIEND/IN_BEHANDELING registration) + `GET /registrations/current?bsn=`. - **BFF:** owner-scoped `GET /self-service/registrations` (bsn from the DigiD token) → the current registration, or **204** when none. Regenerated `services/bff/openapi.json`. - **Frontend:** `registration-page` calls it on init and restores the submitted view (reference + actions); 204 shows the submit form as before. api-client regenerated (orval). Closes #111 ## Definition of Done - [x] Linked issue (#111). - [x] TDD — store `FindOpenByBsnAsync` tests, BFF endpoint tests, an Angular component test (resume-on-load), a Playwright e2e (submit → reload → restored). - [x] Conventional Commits referencing #111. - [ ] CI green — validated locally (below); runner CI running. - [x] `docker compose up` reaches green health — fresh stack + full e2e (3 specs) green. - [x] Docs — `docs/synthetic-data.md` (new e2e users). - [ ] ADR — N/A (follows existing BFF/domain patterns; no boundary change). - [ ] Demo note — the flow is unchanged for the demo; no new demo-script section (happy to add one if wanted). ## Verified locally - Unit: Big 141 (+7 store tests), Bff 36 (+3 endpoint tests), all suites green. - Frontend: 12 self-service component tests (incl. resume-on-load); lint + build green. - **e2e (fresh CI stack): all 3 specs pass** — `registration`, `resume`, `withdrawal` (29.5s, single worker). - Mutation: domain **91.04%**, bff **100%** (break 90%). `make lint` clean. ## Notes for reviewers - **Shared-stack isolation:** resume-on-load restores any open registration for the logged-in bsn, so the self-service e2e specs can no longer share `jan-burger` (the verify-* API checks submit as `jan-burger`/`123456782` before the e2e). Each spec now has its own DigiD citizen (`emma`/`sanne`/`lars`-burger); `jan-burger` stays the documented citizen for the verify checks. This is the fix for the two intermittent e2e failures seen during development. - **Scope:** resumes the current **in-flight** registration only (terminal ones aren't resumed), per the issue's out-of-scope note. Reviewed-on: #119 |
||
|
|
d5e5fa254c |
fix(e2e): run Playwright single-worker to stop OOM page-crash in verify-stack (closes #115) (#116)
## What & why `verify-stack` was failing intermittently on the Playwright e2e with `Page crashed` mid-action (`locator.fill`) and 90s timeouts — the run logged **"2 workers"**, i.e. two full `channel: 'chromium'` browsers running alongside the entire compose stack on the 8 GB self-hosted runner. The renderer gets OOM-killed. Tests passed only when a retry happened to run alone. Fix: pin `workers: 1` in `tests/e2e/playwright.config.ts` (there are only two long-running happy-path specs, so serial costs little) and add `--disable-dev-shm-usage`. This removes the memory contention at the source rather than leaning on `retries` (CLAUDE.md §15 — flaky tests are fixed, not retried). Closes #115 ## Definition of Done - [x] Linked Gitea issue (#115). - [ ] Failing test committed first — N/A: the "red" is the observed `verify-stack` e2e crash (`Page crashed`, 2 workers); this changes test-harness config to fix it. Verified green by re-running the e2e (see notes). - [x] Conventional Commit referencing the issue (`refs #115`). - [ ] CI green — the point of the change; `verify-stack` e2e should stop OOM-crashing. - [x] Docs — none needed (test-config only; rationale in an inline comment). - [ ] ADR — N/A. ## Notes for reviewers - One-line-of-behaviour change: `workers: 1` + `--disable-dev-shm-usage`; no product or spec changes. - `Page crashed` is a renderer OOM, not a product defect — the happy path passes when a browser runs alone (the flaky retries already showed this). Single-worker makes that the normal case. - Independent of #110 (that PR fixes `docker-compose.local.yml`; this fixes the CI `verify-stack` e2e). Landing this first unblocks #110's `verify-stack`. Reviewed-on: #116 |
||
|
|
0904df8db0 |
feat(acl): diploma upload stored in the ZGW Documenten API (S-10b, closes #103) (#108)
## What & why S-10b: the self-service **diploma upload** is now real. After submitting, the citizen picks a PDF and uploads it; the portal base64-encodes it client-side → BFF → domain → **ACL**, which stores it in the ZGW **Documenten (DRC) API** as an `enkelvoudiginformatieobject` and relates it to the zaak, then the `WachtOpDocumenten` wait completes and the case advances to beoordeling. Per §8.1 only the ACL talks to ZGW. Closes #103 Mechanism in **ADR-0018** (proposal #107). Builds on S-10a (#102). The zaak-close-on-expiry item is carved to **#106 (S-10c)**. ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation (red→green per layer). - [x] Conventional Commits referencing the issue (`refs #103`). - [ ] CI green — all Gitea Actions jobs (pending on this PR). - [x] `docker compose up` health unaffected (ACL boots on a placeholder informatieobjecttype URL; the real one is injected by verify-domain). - [x] Docs updated (ADR-0018, demo-script, BACKLOG + S-10c). - [x] ADR added (`docs/architecture/adr-0018-diploma-upload-via-acl-documenten.md`). - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - **ACL** (`OpenZaakGateway.StoreDocumentAsync` + `AclService.StoreDiplomaAsync` + `POST /documenten`) reuses the existing gateway patterns (ZGW Bearer, buffered non-chunked body, **no CRS** — Documenten isn't geo). Unit-tested via the stub handler; an **integration test** stores a real document against live OpenZaak (verify-acl). - **Transport:** base64 JSON on every hop (portal encodes client-side) — I deviated from proposal #107's multipart to keep one contract shape and avoid `IFormFile`/antiforgery/multipart-client plumbing; fine at diploma size (ADR-0018 §Alternatives). - **Infra:** `seed_catalogus.py` seeds + publishes a "Diploma" `informatieobjecttype` and relates it to the zaaktype (while both concept); `verify-domain` injects its URL into the ACL. No new ZGW scopes (seed applicatie has `heeft_alle_autorisaties`). - **e2e:** uploads a real PDF (`setInputFiles`) after the openbaar INGEDIEND row confirms the zaak is open (so storage doesn't race the OpenZaak worker). - **Scope boundary:** the ZGW zaak is not set to a cancellation status on 30-day expiry — that's #106 (S-10c). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #108 |
||
|
|
4777ff2b1d |
feat(workflow): document-wait task + 30-day timeout cancellation (S-10a, closes #102) (#105)
## What & why S-10a, the **workflow/timeout spine** of the (split) document-upload slice: the registratie process now parks at a **`WachtOpDocumenten`** user task with an **interrupting `P30D` boundary timer**. When the documents arrive the task completes and the process continues into the diploma routing (S-13) → Beoordelen; if the 30 days lapse, the timer cancels the wait, runs a `RegistratieVerlopen` external-worker task, and the domain expires the aggregate to a new terminal status **`Verlopen`**. Backend only — the real upload trigger (portal → BFF → ACL → Documenten API) is S-10b (#103). Closes #102 Mechanism recorded in **ADR-0017**; opened as proposal #104. Mirrors the S-14 escalation (boundary-timer + external-worker) and S-11 withdrawal (interrupting cancel) patterns. ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation (red→green pairs per layer). - [x] Implementation makes the test pass. - [x] Conventional Commits referencing the issue (`refs #102`). - [ ] CI green — all Gitea Actions jobs (pending on this PR). - [x] `docker compose up` health unaffected (no new services; deploy path unchanged). - [x] Docs updated (ADR-0017, demo-script, BACKLOG split). - [x] ADR added (`docs/architecture/adr-0017-document-wait-timeout-cancellation.md`). - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - **Domain** (`Registration.Expire()` + `Verlopen`), **application** (`ExpireRegistrationWorker`), **infra** (`RegistratieVerlopenProcessor`/`Pump`, `IRegistratieVerlopenClient`, Flowable acquire/complete + `CompleteDocumentWaitAsync`) — the timeout counterpart to the OpenZaak/escalation worker trios; idempotent per §8.6. - **BPMN** verified live against a `flowable-rest` probe: complete `WachtOpDocumenten` → routes to Beoordelen; fire the P30D timer → `RegistratieVerlopen` job (carrying `registrationId`) + the wait task cancelled. `verify-domain` exercises both branches in-stack (completes the wait in every existing block; fires the timer and asserts `Verlopen` in a new block). - **Scope boundary:** on expiry the aggregate goes `Verlopen` and the process ends, but the ZGW *zaak* is not yet set to a cancellation status — that needs a new ACL method + statustype seeding and is folded into S-10b (noted in ADR-0017). - `CompleteDocumentWaitAsync` is built and HTTP-tested here but not yet called from a domain endpoint; S-10b wires the upload trigger to it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #105 |
||
|
|
94699f3603 |
feat(self-service): trek aanvraag in — withdrawal action (S-11c-2, closes #12) (#93)
## What & why Final sub-slice of **S-11 · Withdrawal (Flow 3)** — the user-facing "trek aanvraag in" action, which **closes #12**. - **self-service portal**: the submit confirmation gains a **"Trek aanvraag in"** button. It withdraws the just-submitted registration via `postSelfServiceRegistrationsIdWithdraw(reference)`; success shows an *ingetrokken* confirmation, a failure is surfaced (`role="alert"`) and the action stays available — same confirm-and-surface pattern as submit. - **acceptance**: `Een registratie intrekken` — owner withdraws → INGETROKKEN + workflow cancelled; a different bsn is reported not-found. - **e2e**: `withdrawal.spec.ts` — DigiD submit → trek aanvraag in → the portal confirms ingetrokken. - **docs**: demo-script + frontend-decisions. Together with S-11a (#88), S-11b (#89), S-11c-1 (#90), this completes the flow: citizen withdraws → domain INGETROKKEN → BPMN message event cancels the process → the case leaves the behandelaar's werkbak. Closes #12 ## Definition of Done - [x] Linked Gitea issue (#12). - [x] Failing tests committed before the implementation. - [x] Implementation makes the tests pass. - [x] Conventional Commits referencing the issue (`refs #12`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` unaffected. - [x] Docs updated (demo-script + frontend-decisions). - [x] ADR — ADR-0014 (from S-11b) covers the cancellation decision; nothing new here. ## Notes for reviewers - Full local gate run before pushing: `dotnet format --verify-no-changes` clean; `make unit` green (Acceptance **11** incl. the 2 new withdrawal scenarios, Big 95, BFF 30, Acl 27, EventSubscriber 19); self-service lint/test/build green (9 tests, incl. the 2 new withdraw tests). - `withdrawal.spec.ts` waits on the *ingetrokken* confirmation (which only renders after the withdraw POST returns), so it can't cancel the request early (the 499 lesson from #87). Live-validated by verify-stack. Reviewed-on: #93 |
||
|
|
3abf8f7ccf |
feat(behandel): behandel-portal — werkbak + beoordeling (closes #13) (#87)
## What & why
Finishes **S-12 · Behandel-portal — werkbak + beoordeling**. The backend sub-slices (S-12a/b/c-1/c-2) were merged, but the slice's stated outcome — a behandel *portal* with medewerker login, a werkbak, and decide — had no frontend. This adds it.
- **`libs/auth`**: `MedewerkerAuthService` + `provideMedewerkerAuth` (Keycloak `medewerker` realm), a `roles`/`hasRole` surface on the shared `AuthService`, and a realm-roles protocol mapper so the SPA can read `behandelaar`/`teamlead` from the token. The BFF remains the security boundary (ADR-0013).
- **`apps/behandel`**: a new Nx Angular app mirroring self-service — medewerker OIDC login and a **werkbak** page listing registrations awaiting beoordeling (`GET /behandel/werkbak`) with per-row **Goedkeuren/Afwijzen** actions (`POST /behandel/registrations/{id}/decide`) that refresh the list. NL DS/Utrecht, standalone + signals.
- **e2e**: the walking-skeleton happy path now approves through the real portal (behandelaar logs in, finds the row by reference, clicks Goedkeuren) instead of the temporary admin endpoint.
- **infra/docs**: behandel service in compose (`:8142`, depends on Keycloak); added to the smoke `WAIT_SVCS` + CI log dump; `frontend-decisions.md` and `demo-script.md` updated.
Closes #13
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass; refactor commit if structure improved.
- [x] Conventional Commits referencing the issue (`refs #13`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(behandel image + container verified locally; full stack gated in CI.)*
- [x] Docs updated if behaviour, contracts, or operations changed.
- [x] ADR added — ADR-0013 (merged with the backend sub-slices) already covers the wiring; no new decision here.
- [x] Demo note in `docs/demo-script.md`.
## Notes for reviewers
- Verified locally: auth + behandel + all frontend projects pass lint & unit tests (incl. axe WCAG 2.1 AA); production build green; the behandel Docker image builds and serves with the correct baked `medewerker` config + SPA fallback.
- The full compose-up smoke, e2e, and mutation are CI-gated (known local full-stack verify limits).
- **Follow-ups (not in scope):** the `WerkbakItem` contract has no citizen name (werkbak shows the BSN) — adding one is a BFF+domain contract change; and the domain's temporary admin `approve` endpoint is now unused by the e2e and could be removed.
Reviewed-on: #87
|
||
|
|
9997da8beb |
feat(#78): one citizen reference across self-service and the openbaar register (#79)
## What & why Before this change the self-service confirmation and the openbaar register showed **different** identifiers, so a citizen could not look their registration back up (#78). Now both surface the same **reference**: - **domain → ACL (write):** the domain `registrationId` is set as the zaak's `identificatie` on `POST /zaken`. - **event-subscriber → ACL (read):** the subscriber reads the zaak's `identificatie` back through the ACL (§8.1 — only the ACL talks to ZGW) via a new `POST /zaken/reference`, and stores it on the projection row **and** the `processed_notifications` replay log. - **BFF + openbaar:** the public view exposes `id/status/reference` (never bsn/naam) and searches by id or reference; the register's "Referentie" column shows the reference. Storing the reference in the replay log keeps ADR-0008's **rebuild-is-log-only** invariant intact — `/admin/rebuild` reproduces the reference without re-reading the ACL. Decision recorded in **ADR-0012**. ## Definition of Done - [x] Linked issue: #78 - [x] Tests written first; red → green per layer - [x] Unit + acceptance green (`make unit`): domain 49, acl 27, bff 20, event-subscriber 19, acceptance 7 - [x] Frontend lint + test green (`nx run-many -t lint test`) - [x] Mutation ≥ break(90): acl 100%, event-subscriber 100%, bff 100%, domain 98.41% (pre-existing FlowableWorkflowClient baseline, untouched) - [x] e2e extended: confirmation reference == register reference - [x] openapi.json + api-client regenerated (drift guard green) - [x] ADR-0012 added; demo-script note appended - [x] `Acl__BaseUrl` wired for the subscriber in compose closes #78 Reviewed-on: #79 |
||
|
|
1c185e6686 |
S-09b: Approval flow — temp admin endpoint + status transition to projection (#77)
## What & why S-09b (#75, split from #10) — the **approval flow** that completes the walking skeleton. A behandelaar can now approve a submitted registration; the entry flips from `INGEDIEND` to `INGESCHREVEN` in the public register. Flow: `POST /registrations/{id}/approve` (domain) → ACL sets the zaak eindstatus (ZGW `/statussen`) → OpenZaak → NRC → event-subscriber → projection → openbaar. ## Changes (bottom-up, each red→green TDD) - **Domain** — `RegistrationStatus.Ingeschreven` + `Registration.Approve()` (guards: opened zaak, only from INGEDIEND); `ApproveRegistration` use case (idempotent) + temp `POST /registrations/{id}/approve` endpoint; `IAclClient.ApproveZaakAsync`. - **ACL** — resolves the zaaktype's **eindstatus** from the catalogus (`isEindstatus` / highest volgnummer) and POSTs a ZGW status; exposed as `POST /statussen`. Unit + real-OpenZaak integration test. - **Event-subscriber** — binds NRC `hoofdObject`, projects a `status`/`create` as `INGESCHREVEN` keyed on the zaak (updates the existing row), **without reading OpenZaak** (§8.1). Retains the ZGW `resource` in the log (new column + EF migration) so a rebuild reproduces the status. - **e2e** — extended: submit → public INGEDIEND → approve → public INGESCHREVEN. - **Docs** — ADR-0011 (the two non-obvious decisions + the walking-skeleton assumption) + demo note. ## Key decisions (see ADR-0011) - **ACL discovers the eindstatus** (chosen over injecting a statustype URL): no new config/seed plumbing, domain stays ZGW-ignorant. - **Any post-creation status-set ⇒ INGESCHREVEN**: in the walking skeleton the only status ever set after creation is the approval, and the subscriber may not read ZGW — documented to tighten when more transitions arrive (S-12+). ## Verification - All .NET unit suites green locally (domain 47, acl 11, event-subscriber 14, bff 16, acceptance 7); Release build + `dotnet format` clean. - No new compose config (the eindstatus-discovery approach avoided it). - The real-OpenZaak integration test (ACL status-set) and the full submit→approve→visible e2e run in CI `verify-stack` (live NRC→projection + selectielijst egress, not reproducible locally). closes #75 Reviewed-on: #77 |
||
|
|
bc9831c113 |
S-09: Openbaar Register portal — public lookup (#76)
Anonymous openbaar portal completing the walking skeleton (submit → projection → public visibility). closes #10 |
||
|
|
39923e0e68 |
fix(e2e): treat the http portal origin as secure so DigiD PKCE login works (refs #68)
CI / lint (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 53s
CI / unit (pull_request) Successful in 1m3s
CI / frontend (pull_request) Successful in 1m47s
CI / mutation (pull_request) Successful in 4m2s
CI / verify-stack (pull_request) Failing after 7m51s
The walking-skeleton e2e timed out waiting for the Keycloak login form (`#username`). Root cause: in the compose network the portal is served over plain HTTP on a non-localhost origin (http://self-service), which is not a secure context, so Web Crypto (`crypto.subtle`) is undefined. angular-auth- oidc-client needs SubtleCrypto to build the PKCE code challenge, so `authorize()` threw ("Cannot read properties of undefined (reading 'digest')") and the login redirect never fired. Production serves the portal over HTTPS, where this works. Instead of terminating TLS in the throwaway e2e stack, tell Chromium to treat the origin as secure via --unsafely-treat-insecure-origin-as-secure. The flag is only honoured by the full Chromium build (new headless), not Playwright's default headless-shell, so pin channel: 'chromium'. Verified against a minimal in-network stack (keycloak + self-service): login redirect now reaches the Keycloak form, and the full login → token exchange → authenticated portal renders with no console errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
490e7347b0 |
test(e2e): walking-skeleton Playwright happy path + verify-e2e lane (refs #68)
tests/e2e Playwright spec drives DigiD login (jan-burger/test123) → submit → confirmation against the compose-served portal. run-e2e-check.sh runs it inside the compose network (node container, browser installed at runtime) so the token issuer (keycloak:8080) matches the BFF authority (ADR-0010). Wired as verify-e2e (Makefile + verify chain + a verify-stack CI step). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |