Commit Graph
13 Commits
Author SHA1 Message Date
not 9eb51b8b3e feat(bff): GET /beheer/catalogi/zaaktypen proxies the ACL for beheerders (refs #130) 2026-07-24 10:51:22 +02:00
not d5dfbdc0b2 feat(obs): Prometheus metrics on /metrics + golden-signal Grafana dashboard (closes #124) (#129)
CI / build (push) Successful in 1m47s
CI / lint (push) Successful in 1m59s
CI / unit (push) Successful in 1m57s
CI / frontend (push) Successful in 3m58s
CI / mutation (push) Successful in 6m57s
CI / verify-stack (push) Successful in 8m29s
## What & why

S-16c, the last of the S-16 (#17) split, on top of the backplane (#122) and distributed tracing (#123). The five .NET services now expose OpenTelemetry **metrics** in Prometheus format at `/metrics`; Prometheus scrapes each (one job per service); and Grafana ships a pre-built **Request path — golden signals** dashboard (traffic / errors / latency / saturation), split by service.

Closes #124

### How

- Each service adds `.WithMetrics(AddAspNetCoreInstrumentation + AddHttpClientInstrumentation + AddMeter("System.Runtime") + AddPrometheusExporter)` and maps `/metrics`. Same shape as the S-16b tracing wiring already in these `Program.cs` files.
- `infra/observability/prometheus/prometheus.yml`: one scrape job per service (`acl`, `domain`, `bff`, `event-subscriber`, `projection-api`), reached by compose service name.
- `infra/observability/grafana/provisioning/dashboards/`: dashboard provider + `golden-signals.json` (baked into the Grafana image by the existing `COPY provisioning/`).
- `verify-metrics` (new CI verify-stack step + Makefile target): generates BFF traffic and asserts Prometheus scraped the golden-signal metric from every service. Mirrors `verify-tracing`.

### Dependency (CLAUDE.md §13/§14)

Adds `OpenTelemetry.Exporter.Prometheus.AspNetCore` `1.17.0-beta.1` (matched to the `1.17.0` core already in use). It gives the OTel-native `/metrics` pull endpoint; replacing it would mean hand-rolling Prometheus exposition over a `MeterListener`; the risk is that it is a **prerelease** package (the whole OTel .NET Prometheus line is `-beta`) — pinned, wired only in `Program.cs`, and gated by `verify-metrics`. Recorded in **ADR-0024**.

## Definition of Done

- [x] Linked Gitea issue (#124).
- [x] Failing test committed before the implementation (`test(bff): /metrics exposes http-server request duration`).
- [x] Implementation makes the test pass.
- [ ] CI green — pending Gitea Actions run.
- [x] `docker compose up` reaches green health within 3 min (backplane images unchanged in shape; not on the health gate, ADR-0023).
- [x] Docs updated — demo-script S-16c entry.
- [x] ADR added — ADR-0024.
- [x] Demo note in `docs/demo-script.md`.

## Notes for reviewers

- `/health` polls are counted as traffic (metrics aren't path-filtered, unlike traces). Fine for a demo dashboard and honest — real load stacks on top.
- `projection-api` has no Stryker config (unchanged); the four mutated services carry the metrics wiring in `Program.cs`, same as the merged S-16b tracing code.
- Metric names verified against a live service: `http_server_request_duration_seconds{,_bucket,_count}`, label `http_response_status_code`, `dotnet_process_cpu_time_seconds_total`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #129
2026-07-24 08:31:41 +00:00
not 88338396f6 feat(obs): distributed traces across the .NET services (S-16b, closes #123) (#126)
CI / verify-stack (push) Successful in 12m13s
CI / build (push) Successful in 1m50s
CI / lint (push) Successful in 1m58s
CI / unit (push) Successful in 2m8s
CI / frontend (push) Successful in 4m29s
CI / mutation (push) Successful in 11m51s
## What & why

S-16b, second of the S-16 split, on top of the #125 backplane. The five .NET services now emit OpenTelemetry traces so a request is **one connected trace** across them.

- Each host wires `AddOpenTelemetry().WithTracing(...)` with `AddAspNetCoreInstrumentation` (incoming) + `AddHttpClientInstrumentation` (outgoing) + `AddOtlpExporter` to **Tempo**.
- Because every cross-service call already goes through a typed `HttpClient` (§8 boundaries), the W3C `traceparent` propagates with no manual code — bff → domain → acl → openzaak and bff → projection-api stitch into a single trace.
- Service name + OTLP endpoint come from `OTEL_*` env set per app service in compose. `/health` is filtered out so liveness polls don't flood the traces.

No new ADR — ADR-0023 already records the stack + the two documented gaps (browser-side tracing is out of scope, so the trace begins at the BFF; the async Flowable-poll boundary is a separate trace).

Closes #123

## Definition of Done

- [x] Failing test committed first (`verify-tracing` fails with no instrumentation).
- [x] Implementation makes it pass — **validated locally end to end**: a real connected trace spanning `bff` + `projection-api` was found in Tempo (BFF→projection→db + Tempo subset, no OpenZaak/egress).
- [x] Conventional Commits referencing the issue (`refs #123`).
- [ ] CI green — awaiting Gitea Actions (verify-tracing added to verify-stack after verify-bff).
- [x] `docker compose up` health unaffected — services boot healthy even when Tempo is unreachable (exporter no-ops; verified).
- [x] Docs — demo-script + BACKLOG.
- [x] ADR — none needed (covered by ADR-0023).

## Notes for reviewers

- **Per-service wiring, no shared lib:** the block is duplicated across the five hosts by design — services don't share code across boundaries here (§8), same as the duplicated typed clients.
- **Packages:** OpenTelemetry.Extensions.Hosting / Instrumentation.AspNetCore / Instrumentation.Http / Exporter.OpenTelemetryProtocol, all 1.17.0, pinned per-csproj (no central props file).
- **The check** generates anonymous BFF→projection traffic (no auth, no OpenZaak), then queries Tempo (TraceQL search → fetch trace → assert both service.names present) from a python:3-slim container in-network — same idiom as run-projection-check.sh.
- **Next:** #124 (S-16c) adds `/metrics` + Prometheus scrape targets + golden-signal Grafana dashboards.

Reviewed-on: #126
2026-07-23 14:38:26 +00:00
not 5f8ab4dbcd feat: self-service resume of an existing registration after refresh (S-26, closes #111) (#119)
CI / lint (push) Successful in 1m19s
CI / build (push) Successful in 1m7s
CI / unit (push) Successful in 1m16s
CI / frontend (push) Successful in 2m41s
CI / mutation (push) Successful in 5m57s
CI / verify-stack (push) Successful in 8m24s
## 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
2026-07-23 07:22:08 +00:00
not 0904df8db0 feat(acl): diploma upload stored in the ZGW Documenten API (S-10b, closes #103) (#108)
CI / lint (push) Successful in 1m21s
CI / build (push) Successful in 1m4s
CI / unit (push) Successful in 1m12s
CI / frontend (push) Successful in 2m40s
CI / mutation (push) Successful in 5m31s
CI / verify-stack (push) Successful in 7m56s
## 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
2026-07-21 12:15:33 +00:00
not 4777ff2b1d feat(workflow): document-wait task + 30-day timeout cancellation (S-10a, closes #102) (#105)
CI / build (push) Successful in 1m1s
CI / unit (push) Successful in 1m11s
CI / frontend (push) Successful in 2m33s
CI / mutation (push) Successful in 5m14s
CI / verify-stack (push) Successful in 7m37s
CI / lint (push) Successful in 1m17s
## 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
2026-07-20 09:42:02 +00:00
not 2397d9196a feat(bff): owner-scoped self-service withdraw endpoint (S-11c-1, refs #12) (#90)
CI / build (push) Has been cancelled
CI / unit (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / mutation (push) Has been cancelled
CI / verify-stack (push) Has been cancelled
CI / lint (push) Has been cancelled
## What & why

Third sub-slice of **S-11 · Withdrawal (Flow 3)** (#12) — the **owner-scoped BFF withdraw endpoint** (backend). S-11a/b made a withdrawal transition the aggregate and cancel the workflow; this adds the citizen-facing entry point through the BFF, gated to the registration's owner.

- **Domain**: `WithdrawRegistrationCommand` carries the caller's `bsn`; the handler returns a `WithdrawOutcome` and refuses a bsn that doesn't own the registration. Unknown and not-owned are **both 404** (indistinguishable — ownership isn't revealed). `POST /registrations/{id}/withdraw` takes `{bsn}` and maps the outcome (204/404).
- **BFF**: `POST /self-service/registrations/{id}/withdraw` (DigiD-authenticated) forwards the token's `bsn` to the domain and relays 204/404. The BFF authenticates; the domain owner-scopes (an aggregate invariant, not the domain doing auth).
- OpenAPI spec + Angular client regenerated for the new endpoint.
- `run-domain-check.sh` withdrawal step now sends the owner `bsn` (verify-stack).

Refs #12 — the self-service "trek aanvraag in" button + e2e (S-11c-2) closes it.

## 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] No ADR needed (owner-scoping is an aggregate invariant; no boundary change).
- [x] Docs — the user-visible demo note lands with S-11c-2.

## Notes for reviewers

- **Full local gate run before pushing this time** (lessons from #89): `dotnet format --verify-no-changes` clean; `make unit` green — Acl 27, EventSubscriber 19, BFF 30, Acceptance 9, Big 95; `api-client` lint+test green.
- Owner mismatch returns 404 (not 403) so the portal can't be used to probe which references exist.

Reviewed-on: #90
2026-07-16 12:20:43 +00:00
not d226b6402d feat(#13): S-12c-2 — behandel decide → domain + complete workflow task (#86)
CI / lint (push) Successful in 1m17s
CI / build (push) Successful in 1m0s
CI / unit (push) Successful in 1m8s
CI / frontend (push) Successful in 2m20s
CI / mutation (push) Successful in 4m57s
CI / verify-stack (push) Successful in 7m3s
## What & why

Second half of **S-12c** (behandel-portal backend), completing the decision path per **ADR-0013**:

- **Domain:** `BeoordeelRegistratie` now, after applying the decision (aggregate + ACL for approval), **completes the open Flowable `Beoordelen` task** for that registration (found by registrationId) with the besluit, so the workflow advances. No open task → the decision still stands (completes nothing); idempotent.
- **BFF:** `POST /behandel/registrations/{id}/decide` behind the medewerker/`behandelaar` policy, forwarding `goedkeuren`/`afwijzen` to the domain. Validates the besluit vocabulary (400 on unknown) without troubling the domain.

Behavior: decide is **401** without a token, **403** without the role, **400** for an unknown besluit, **204** (forwarded) for a behandelaar.

This completes the behandel backend. **S-12d** (the Angular behandel-portal + Playwright e2e) closes umbrella #13 and retires the temporary `/approve`.

## Definition of Done

- [x] Linked issue: #13 (umbrella, `refs`)
- [x] Tests first; red → green per layer
- [x] Unit + acceptance green (`make unit`): domain 79, bff 27, acceptance 9 (acl/event-subscriber unaffected)
- [x] Beoordeling acceptance scenario asserts task completion (goedkeuren + afwijzen)
- [x] openapi.json + api-client regenerated (drift guard passes)
- [x] Mutation ≥ break(90): **domain 100%, bff 100%**
- [ ] CI green (pending)

Part of #13.

Reviewed-on: #86
2026-07-16 06:53:52 +00:00
not 9c3da48d8e feat(#13): S-12c-1 — behandel BFF auth + werkbak (ADR-0013) (#85)
CI / lint (push) Successful in 1m26s
CI / build (push) Successful in 1m19s
CI / unit (push) Successful in 1m14s
CI / frontend (push) Successful in 2m37s
CI / mutation (push) Successful in 5m54s
CI / verify-stack (push) Successful in 7m18s
## What & why

First half of **S-12c** (behandel-portal backend), per **ADR-0013** (decisions recorded in #84):

- **BFF multi-realm auth.** A second JWT bearer scheme (`medewerker`) alongside the default `digid` scheme. On validation it lifts Keycloak's `realm_access.roles` onto the principal, and a `behandelaar` policy (medewerker scheme + `behandelaar` role) gates `/behandel/*`. Self-service keeps the digid scheme.
- **Werkbak = Flowable tasks.** The domain `Werkbak` query reads the open `Beoordelen` tasks (§8.2, S-12b's `IUserTaskClient`) and enriches each with its aggregate's bsn + status; `GET /behandel/werkbak` (domain) is proxied by the BFF `GET /behandel/werkbak` behind the behandelaar policy. The read projection stays the anonymous openbaar model (no premature `IN_BEHANDELING`/personal-data plumbing — deferred in ADR-0008).

Behavior: `/behandel/werkbak` is **401** without a token, **403** for a medewerker lacking the role, **200 + werkbak** for a behandelaar.

**S-12c-2** (next): `POST /behandel/registrations/{id}/decide` → domain decision + complete the Flowable task.

## Definition of Done

- [x] Linked issue: #13 (umbrella, `refs`); closes the adr-proposal #84
- [x] Tests first; red → green per layer
- [x] Unit + acceptance green (`make unit`): domain 78, bff 23, acceptance 9 (+ acl/event-subscriber unaffected)
- [x] api-client `test` green; openapi.json regenerated (drift guard passes)
- [x] Mutation ≥ break(90): **domain 100%, bff 100%**
- [x] ADR-0013 added; `Keycloak__MedewerkerAuthority` wired into compose
- [ ] CI green (pending)

Part of #13. closes #84

Reviewed-on: #85
2026-07-15 09:54:01 +00:00
notandClaude Opus 4.8 69d6e80378 feat(bff): committed OpenAPI contract + drift guard (refs #8)
Document typed responses (202 SubmitAccepted / 400 / 401 on self-service; 200
OpenbaarEntry[] on openbaar) so the generated spec carries real schemas for S-08's
client. A document transformer clears the auto-populated servers block so the spec
is host-independent and deterministic. Commit services/bff/openapi.json and add a
test asserting it matches the served /openapi/v1.json (fails on drift).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:07:55 +02:00
notandClaude Opus 4.8 d767430ad7 feat(bff): implement self-service submit and openbaar lookup (refs #8)
POST /self-service/registrations requires a valid digid JWT, reads the bsn claim
and forwards it to the domain, returning 202. GET /openbaar/register is anonymous
and returns OpenbaarProjection.PublicView — rows filtered by q and mapped to the
public-safe id+status only (bsn/naam never exposed). JwtBearer validates
signature/issuer/expiry against the Keycloak digid authority (§8.3, ADR-0010).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:00:56 +02:00
notandClaude Opus 4.8 751ca006a7 test(bff): endpoints, JWT auth and public-safe projection (refs #8)
Failing tests for the BFF walking-skeleton endpoints:
- POST /self-service/registrations rejects missing/malformed/wrong-key/expired
  tokens (401) and, with a valid digid token, forwards the bsn to the domain and
  returns 202 (WebApplicationFactory + a local test signing key, ADR-0010).
- GET /openbaar/register serves public-safe rows anonymously (never the bsn) and
  filters by q.
- OpenbaarProjection.PublicView (pure) filters by id and maps to id+status only.

Endpoints and PublicView are stubs so the tests compile and fail on their
assertions; the green commit implements them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:59:55 +02:00
eho dfbaf7640a feat(bff): placeholder BFF + /health endpoint (closes #28) (#34) 2026-06-03 11:38:08 +00:00