Files
register-referentie/docs/architecture/adr-0032-werkbak-live-refresh.md
T
not 8b206a005f
CI / build (push) Successful in 1m8s
CI / lint (push) Successful in 1m23s
CI / unit (push) Successful in 1m27s
CI / frontend (push) Successful in 3m8s
CI / mutation (push) Successful in 6m13s
CI / verify-stack (push) Successful in 10m12s
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
2026-09-04 09:34:14 +00:00

3.9 KiB

ADR-0032: The werkbak refreshes itself by polling, not by a pushed stream

  • Status: Accepted
  • Date: 2026-09-04
  • Deciders: Respellion engineering
  • Slice: #162 (proposal #163). The issue titles it S-26; that id already belongs to the self-service resume slice (#111), so #162 is the identifier that counts.

Context

The werkbak (S-12) is a read of the open Flowable Beoordelen tasks: portal → BFF GET /behandel/werkbak → domain Werkbak query → workflow engine, each task enriched from its aggregate. A registration reaches Beoordelen asynchronously, only once the citizen supplies its documents and the DMN routes it (S-10a) — so it appears in a werkbak that is already open, and until now a behandelaar had to reload the page to see it.

Three forces shape the mechanism:

  • Nothing notifies anyone. The trigger lives in Flowable. The domain does not publish task events, and there is no bus between the domain and the BFF.
  • The BFF is stateless and sits behind each portal's nginx.
  • This is the repo's first live-updating view, so the choice sets a precedent.

Decision

The werkbak page re-reads the existing BFF endpoint on a fixed interval (WERKBAK_REFRESH_MS, 5 s) while it is open. No new endpoint, dependency or server-side state.

The refresh is a background read: it leaves the rows and the loading/failure states untouched until it has an answer, so a tick never flashes a spinner over rows a behandelaar is reading and a single 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 — the same reload this slice set out to remove would otherwise be needed to escape a transient error. Only a foreground read (on open, after a decision) speaks for whether the werkbak is readable at all.

Why not SSE or WebSockets

Neither buys freshness here, because nothing notifies the BFF either:

  • SSE (text/event-stream) would mean a new streaming endpoint whose handler polls the domain and forwards diffs — the same latency, plus connection lifecycle, nginx buffering, and auth on a long-lived connection.
  • WebSocket/SignalR adds a dependency (CLAUDE.md §13) and makes the BFF stateful and sticky-session-bound. A genuine push path would also need the domain to publish task events. Warranted by high-frequency, bidirectional or fan-out-heavy traffic; the werkbak is none of those.

Polling meets the acceptance ("a registration can be seen in the werkbak once it is ready for review") in a handful of lines inside one component.

  • ponytail ceiling: 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 component's interval for a stream. The endpoint contract and the component's rendering stay as they are; gate on document.visibilityState first if request volume is the concern.

Consequences

Positive

  • The outcome is delivered with no new endpoint, dependency, or server-side state, and no service boundary moves.
  • Self-healing: a transient read failure no longer strands the view until a manual reload.
  • The e2e got simpler — the happy path waits for the werkbak row without reloading the page, which is itself the live-refresh assertion.

Negative / costs

  • Staleness is bounded by one interval (≤5 s) rather than instant.
  • One GET /behandel/werkbak per open werkbak per interval, including in hidden tabs.
  • The precedent is polling; a future view with genuinely high-frequency updates will have to revisit this (see the upgrade path above).

Coupling rules touched (CLAUDE.md §8)

None. The poll reuses the existing portal → BFF → domain read path: §8.3 (portals talk only to the BFF) and §8.2 (only the Workflow Client talks to Flowable) are unchanged.