feat(openzaak): one-command UI-OpenZaak bridge, real BSN fix, flake mitigation
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 55s
CI / frontend (push) Successful in 1m33s
CI / backend (push) Successful in 1m46s
CI / e2e (push) Successful in 3m14s
CI / storybook-a11y (push) Successful in 6m50s
CI / semgrep (push) Successful in 1m13s
CI / api-client-drift (push) Successful in 1m47s

scripts/openzaak-ui-up.sh brings up the root app and the OpenZaak harness
together, wires them onto one docker network, seeds the catalogus, grants
the container-alias zaaktype scope, and verifies a real aanvraag submitted
through the UI lands in OpenZaak.

Along the way: DocumentStore.DemoOwner was reusing the seeded doctor's
11-digit BIG-nummer as a stand-in BSN, which isn't a valid 9-digit BSN shape
— OpenZaak rejects it, breaking both submit's rol-creation step and the
citizen's own applications list under Zgw:Enabled=true. Fixed to a real
elfproef-valid BSN.

Also adds mitigation for a still-unexplained per-container flake (every
outbound ZGW POST fails as if the body were empty, for that container's
whole lifetime) that correlates with host memory pressure: the script now
warns when host swap is heavily used, and an opt-in ZgwDiagnosticHandler
(ZGW_DEBUG_HTTP=1) logs Content-Length vs. actual bytes sent so the next
reproduction can confirm or rule out client-side body corruption.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-31 08:23:01 +02:00
co-authored by Claude Sonnet 5
parent 3ff80c124f
commit ba24784586
8 changed files with 348 additions and 4 deletions
+36
View File
@@ -80,6 +80,42 @@ BFF_AUTH='<the same secret>' ./verify-notificatie.sh # proves a real delivery,
To go back to the fast, no-notifications default: `docker compose -f docker-compose.openzaak.yml
up -d --remove-orphans` (drops the celery worker, restores `NOTIFICATIONS_DISABLED: 'true'`).
## Testing the Angular UI against this harness
The base harness above and the app's own root `docker-compose.yml` are independent projects on
purpose (see the top of this file) — this is the opt-in bridge between them, for when you want
to click through the real UI and see an aanvraag land in a real OpenZaak instead of just
running `dotnet test`. One command from the repo root:
```bash
scripts/openzaak-ui-up.sh
```
It brings up the root app (so its docker network exists), brings up this harness plus
`docker-compose.openzaak.bff.yml` (gives this harness's `web` service a dotted alias,
`openzaak.local`, on its own network — the root project's `api` container joins THIS network,
in the opposite direction from the notifications overlay below, to avoid a real alias
collision: the root project's frontend service is also called `web`. The alias needs a dot
because Django's URLValidator rejects a bare hostname in a URL field; this environment's
rootless Podman also can't route container→host-port traffic through `host.docker.internal`,
so container-to-container is the only reliable path either way — see that file's header
comment for the full, empirically-confirmed reasoning), seeds the catalogus, additively
replaces the zrc authorization grant to match the alias (ZGW authorization is scoped by the
*exact* zaaktype URL string, not just the resource; see `scripts/openzaak-ui-up.sh`'s own
comment for why this is a replace, not an add), and brings the root app back up pointed at
OpenZaak (`docker-compose.openzaak.yml` at the repo root). A final self-check submits a
throwaway aanvraag and confirms it actually lands in OpenZaak, restarting `api` (up to 5
times) if not — see that script for a caveat about an intermittent per-container networking
flake this environment can hit under memory pressure (the script now warns if host swap is
already high going in; `ZGW_DEBUG_HTTP=1` on `api`, see `docker-compose.openzaak.yml`, logs
diagnostics to help nail the cause next time it reproduces). Prints the URLs to check
afterward and the teardown commands.
Two caveats, both non-fatal (WP-60 catches and flags rather than surfacing an error):
**only `herregistratie` has a seeded zaaktype** here, so submit that wizard to prove a real
write; and **no Documenten content is seeded**, so a document upload's ZGW half no-ops (pick
"per post" in the wizard's document step, or ignore it).
## Tear down
```bash
@@ -0,0 +1,38 @@
# Opt-in overlay, layered ON TOP of docker-compose.openzaak.yml (never alone):
#
# docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.bff.yml up -d
#
# Gives this harness's `web` (the OpenZaak API) an extra, dotted hostname alias
# (`openzaak.local`) on its OWN network, so the root project's `api` container (joined in via
# `docker-compose.openzaak.yml` at the repo root, as an EXTERNAL network) can reach it. See
# `scripts/openzaak-ui-up.sh` for the one-command version that brings both projects up
# together, seeds the catalogus, and grants the extra authorization scope this alias needs.
#
# Three real things this works around, each discovered empirically (curl against the
# running containers), not guessed:
#
# 1. Why container-to-container instead of `http://localhost:8000`: this dev environment's
# rootless Podman drops container→host-port traffic through `host.docker.internal`
# (confirmed for the WP-58 notifications overlay's celery worker — DNS resolves it, every
# TCP connect times out).
#
# 2. Why the ROOT project's `api` joins INTO this project's network (below), not the other way
# around: the root project's frontend service is also called `web`. Docker Compose always
# adds a service's own name as a network alias on every network it joins — so if THIS `web`
# joined the root project's network, "web" would resolve to two different containers there.
# Only `api` crosses into this network, under its own already-unique name.
#
# 3. Why the alias has a dot in it (`openzaak.local`, not e.g. `openzaak`): Django's built-in
# URLValidator rejects a bare, dotless hostname in a URL field (it special-cases exactly
# "localhost"; anything else needs a dot or to be a valid IP). OpenZaak's `zaaktype` field
# (and others) run through this validator — confirmed with a POST referencing
# `http://<dotless-alias>:8000/...` failing "Voer een geldige URL in" (enter a valid URL)
# before any authorization check even runs.
services:
web:
environment:
# Django rejects any request whose Host header isn't in ALLOWED_HOSTS.
ALLOWED_HOSTS: localhost,127.0.0.1,web,openzaak.local
networks:
default:
aliases: [openzaak.local]