Commit Graph
89 Commits
Author SHA1 Message Date
eho 0d623f90e8 feat(fp): WP-08 — one store idiom + machine naming + TEA MDX
Rename change-request.machine.ts's bare State/Msg to ChangeRequestState/
ChangeRequestMsg (the last machine not context-prefixed), document the
createStore-is-the-idiom + naming convention in CLAUDE.md §3, and add the
Foundations/State Machines (TEA) curriculum page. The wizard pages already
wired createStore (confirmed by reading each and by git log) -- the WP's
"hand-wired signal(model)" premise was stale; recorded as a deviation.
2026-07-03 21:50:53 +02:00
eho e3cd908f4f feat(fp): WP-07 — brief on the shared idioms + RemoteData MDX
Collapse brief.store's busy signal + nullable lastError into one Idle |
Busy | Failed union (saveState gets matching tag-object style), and route
brief.page's load through RemoteData + <app-async> instead of a hand-rolled
@switch, via a BriefStore.remoteData projection of the machine's existing
loading/failed tags -- the machine keeps owning the letter's own status
lifecycle untouched. New brief.store.spec.ts covers the Busy->Idle/Failed
transitions; new Foundations/RemoteData & Async MDX page documents the
pattern and the WP-06 typed-loaded-slot fallback. Deviation from the
original plan recorded in the WP file.
2026-07-03 21:39:29 +02:00
eho 199cbe1f8c feat(fp): WP-06 — kill $any() in templates (18x)
Make AsyncLoadedDirective generic with a static ngTemplateContextGuard for
AsyncComponent's own internal typing. That can't propagate to consumer
`<ng-template appAsyncLoaded let-p>` sites though -- Angular only infers a
structural directive's type parameter from an input bound on that same
node, not from a sibling input on the parent component -- so the ~9
root-cause consumers (dashboard, registration-detail, aanvraag-detail,
registratie-wizard) instead unwrap the RemoteData Success value via a
typed computed() and narrow it locally with `@if (x(); as p)`. The
remaining union-narrowing casts (registration-summary, showcase concepts
page) are replaced with a stable @let binding and a direct resource read,
respectively. Documented as a deviation in WP-06's backlog file.
2026-07-03 21:27:01 +02:00
eho 34d34512b3 feat(fp): WP-05 — parse-don't-validate closure + MDX
Close the three remaining unvalidated `as <DomainType>` casts at the wire
boundary (intake-policy, big-register aantekening type, brief passage scope),
each replaced by a Result-returning parser with a rejection-case spec, plus
the Foundations/Parse, don't validate curriculum page.
2026-07-03 21:02:15 +02:00
ehoandClaude Sonnet 5 5d6a78d4ec docs(backlog): record WP-18 commit hash
CI / frontend (push) Successful in 1m22s
CI / backend (push) Successful in 1m3s
CI / api-client-drift (push) Successful in 1m36s
CI / storybook-a11y (push) Successful in 4m4s
CI / codeql (csharp) (push) Failing after 1m45s
CI / codeql (javascript-typescript) (push) Failing after 1m25s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 20:32:03 +02:00
ehoandClaude Sonnet 5 7ec13d8b59 feat(brief): WP-18 — ABAC capability spine (PRD-0002 phase P1)
Replace the FE-computed authorization anti-pattern in BriefStore.editable
(derived from the unverified X-Role header) with server-computed decision
flags, mirroring the existing HerregistratieDecisionsDto pattern:

- Backend: Authz.cs is the single authorization helper — the SAME check
  (Authz.CanActOn) both gates BriefStore.Review's mutations and computes
  the BriefDecisionsDto flags shipped on every brief response, so emit
  and enforce can never drift. New GET /me returns coarse, role-derived
  capabilities (PRD-0002 SS6).
- Every brief endpoint (including send, previously ungated on HttpContext)
  now returns a fresh BriefViewDto so decisions never go stale after a
  mutation.
- FE: brief.store.ts reads canEdit/canApprove/canReject/canSend off the
  loaded decisions instead of computing them from currentRole(); the
  brief.machine carries decisions through every status transition.
- New shared/domain/capability.ts + shared/application/access.store.ts +
  shared/infrastructure/me.adapter.ts: the general capability-spine
  infrastructure (AccessStore.can(), capabilityGuard) for future routes.

Deviates from the original WP-18 draft by NOT renaming auth/domain's
Session to a Principal union — ADR-0002 explicitly defers that refactor
until a second actor exists, and the brief workflow's drafter/approver
identity turned out to be a separate axis from the SSP login session
entirely. See docs/backlog/WP-18-abac-capability-spine.md for the full
as-built record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 20:31:53 +02:00
ehoandClaude Sonnet 5 cbb8ae548c docs(backlog): add WP-18..22 (productie-volwassenheid phase)
CI / storybook-a11y (push) Successful in 4m12s
CI / backend (push) Successful in 1m6s
CI / api-client-drift (push) Successful in 1m37s
CI / frontend (push) Successful in 1m31s
CI / codeql (csharp) (push) Failing after 1m51s
CI / codeql (javascript-typescript) (push) Failing after 1m24s
Gap analysis found the POC's designed-but-unbuilt strategic gaps: ABAC
authorization (ADR-0002/PRD-0002 phase P1), no e2e coverage, unproven
i18n second-locale seam, thin resilience seams (correlation-id,
idempotency, retry), and in-memory-only persistence. Each WP is grounded
in the current code (file paths + line numbers), not just the analysis.

Also corrects PRD-0001's stale 'Proposed' status header — the Mijn
aanvragen vertical is fully built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 20:07:05 +02:00
ehoandClaude Opus 4.8 bf920696ac ci: harden workflow + add security scanning and format gates
CI / storybook-a11y (push) Successful in 4m3s
CI / backend (push) Successful in 1m1s
CI / codeql (csharp) (push) Failing after 39m18s
CI / codeql (javascript-typescript) (push) Failing after 1m22s
CI / frontend (push) Successful in 1m25s
CI / api-client-drift (push) Successful in 1m34s
- permissions: contents:read (least privilege), concurrency cancel,
  scope push to main+tags (was: every branch, double-running with PRs),
  per-job timeout-minutes.
- security: npm audit --omit=dev, CodeQL SAST (TS + C#), Dependabot
  (npm/nuget/actions).
- format: npm run format:check + dotnet format --verify-no-changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:39:31 +02:00
ehoandClaude Opus 4.8 1137f59f7b style: format backend with dotnet format
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:39:31 +02:00
ehoandClaude Opus 4.8 e82309786d style: format frontend, docs and skills with prettier; add .prettierignore
One-time prettier --write so the new format:check CI gate starts green.
.prettierignore excludes generated (api-client.ts, documentation.json),
vendored (public/cibg-huisstijl), and backend (dotnet format owns it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:39:31 +02:00
ehoandClaude Opus 4.8 546097434d fix(ci): point backend test at BigRegister.slnx (solution renamed from .sln)
CI / frontend (push) Successful in 1m12s
CI / storybook-a11y (push) Successful in 4m5s
CI / backend (push) Successful in 43s
CI / api-client-drift (push) Successful in 1m31s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 11:43:44 +02:00
ehoandClaude Opus 4.8 4ba0a020f3 docs: regenerate Compodoc documentation.json
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 11:36:26 +02:00
ehoandClaude Opus 4.8 922f9ec8cf docs: README + Storybook mdx describe the CIBG Huisstijl setup
Replace stale @rijkshuisstijl-community package/theming claims with the
vendored CIBG Huisstijl + token-bridge reality (ADR-0003); system-font
stack instead of Fira Sans; embed the now-existing document-upload story
in atomic-design.mdx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:44:00 +02:00
ehoandClaude Opus 4.8 cf44bda0ce docs(skills): extract house recipes as Claude Code skills for SSP templating
CI / frontend (push) Successful in 2m16s
CI / storybook-a11y (push) Successful in 4m6s
CI / backend (push) Failing after 48s
CI / api-client-drift (push) Successful in 1m32s
8 template-generic skills in .claude/skills/ (new-feature, new-context,
value-object, form-machine, bff-endpoint, mutation-command, ui-component,
new-ssp), condensed from CLAUDE.md/ARCHITECTURE/fp-tea/ADRs and pointing at
this repo's worked examples. CLAUDE.md gains a pointer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:10:38 +02:00
ehoandClaude Opus 4.8 0f360c5939 fix(cibg): datablock row separators + filled Bestand-upload button
- Datablock entries had no separators: CIBG ships the border on dt/dd with a
  :last-of-type reset, but the one-row-per-<div> grouping (for axe) made every
  dt/dd a last-of-type, stripping them all. Carry the separator on the row
  (:host:not(:last-of-type)) in the CIBG line colour (#f1f5f9 = cool-grey-200).
- Upload "Bestand toevoegen" button read as a pale outline: btn-outline-primary
  forced a transparent background over .btn-upload's accent fill. Use
  btn-primary btn-upload (solid #01689b, white text, folder glyph) and drop the
  redundant centred background-image folder.

GREEN: lint, tokens, 183 tests, build, 137 axe stories. Verified via Storybook
screenshots (datablock separators, filled blue upload button).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 08:46:06 +02:00
ehoandClaude Opus 4.8 05314afd98 docs(backlog): WP-11 + WP-12 done; note WP-13 upload correction
The CIBG UI fidelity pass completed WP-11 (aanvragen/application-link) and WP-12
(Datablock), and reworked the upload suite to wrap vendored CIBG classes rather
than mark it as a gap (WP-13's assumption corrected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:56:18 +02:00
ehoandClaude Opus 4.8 ccc0184342 feat(aanvragen): richer rows (purpose + status) linking to a case-detail page
- Aanvragen rows now show what the aanvraag is for (purpose subtitle) and an
  explicit status label (In behandeling / Goedgekeurd / Afgewezen) alongside the
  reference + submit date, via an expanded aanvraag-view (purposeLabel,
  statusLabel, referentie, detailRows) + spec.
- Rows link to a new /aanvraag/:id case-detail page, so the CIBG chevron shows
  and each aanvraag opens as a (stub) case — it lists soort/waarvoor/status/
  referentie/ingediend in a Datablock, with a note that full handling is future.

GREEN: lint, tokens, 183 tests, build, 137 axe stories. Verified visually
(dashboard aanvragen rows, upload drop-zone, datablock) via Storybook screenshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:55:21 +02:00
ehoandClaude Opus 4.8 b5c5d30a65 feat(cibg): adopt the vendored CIBG Bestand-upload component
The upload suite now wraps the vendored CIBG file-upload classes instead of
hand-rolling from tokens:
- file-input becomes a .file-picker-drop-area with drag-and-drop + a real
  .btn-upload button and a visually-hidden, focusable <input>; renders an
  always-visible instruction (allowed types + max size) linked to the input via
  aria-describedby (pattern requirement).
- The file list is a semantic ul.file-list; single-upload is an
  li[app-single-upload] .file-container (native <li> child) with .actions
  (retry/.icon-remove) + progress; document-chip renders the .file block
  (status glyph, .file-name link, .file-meta size/status).
- Category validation moves ABOVE the block as .upload-validation > .feedback.
- Instruction text uses the default (not subtle) foreground for WCAG AA contrast
  on the grey drop-area.

DocumentUpload's public inputs/outputs are unchanged — both wizards keep working.
GREEN: lint, tokens, 181 tests, build, 137 axe stories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:47:25 +02:00
ehoandClaude Opus 4.8 98fd7e4bcd feat(cibg): WP-11 — render "Mijn aanvragen" as the CIBG Aanvragen component
- application-link switches to a `li[app-application-link]` attribute selector
  (native <li> child of the <ul> — axe-clean list) and drops the invented,
  dead `.application` / `.application-title` classes for the real vendored
  `.dashboard-block.applications li a` chain (h3.h3 / .subtitle / .status / .cta).
  Content stacks in a flex column; a non-navigating row mirrors the card surface
  from tokens. Re-enables a11y on the application-link/list stories.
- Dashboard "Mijn aanvragen" now renders through app-application-list +
  <li app-application-link> rows (was a keuzelijst), mapped by a new pure
  submittedRow() view helper (+ spec). Concepts stay the resumable melding.
- aanvraag-block is now concept-only (submitted mapping moved to aanvraag-view).

WP-11 grep gate clean. GREEN: lint, tokens, 181 tests, build, 136 axe stories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:34:21 +02:00
ehoandClaude Opus 4.8 82fc3c493d feat(cibg): WP-12 — CIBG Datablock for application data
Adopt the vendored CIBG Datablock (.data-block / .block-wrapper) as the way to
show application data:
- New app-data-block molecule (grey surface + white panel + projected rows,
  optional heading, stacked variant, aria-label) + stories.
- data-row switches to a `div[app-data-row]` attribute selector so the <dl>'s
  direct child is a native <div> (HTML5.1 dl > div > dt+dd). This makes the
  definition list axe-clean — a bare custom element between <dl> and its dt/dd
  trips axe's definition-list rule regardless of display:contents, a defect the
  dashboard shipped live. Re-enables a11y on the data-row / review-section /
  registration-summary stories (previously disabled pending this rework).
- review-section folds onto app-data-block (drops its hand-carried classes).
- registration-summary + dashboard "Persoonsgegevens (BRP)" drop app-card and
  render as datablocks; both wizards' review rows + the beroep row convert to
  the div selector.

GREEN: lint, check:tokens, 178 tests, build, build-storybook, 136 axe stories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:25:57 +02:00
ehoandClaude Opus 4.8 947d5fa90a docs(backlog): record WP-04 commit hash
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:01:49 +02:00
ehoandClaude Opus 4.8 035e785c95 feat(boundaries): WP-04 — ui ↛ infrastructure + showcase sanction
Move the two wizard lookups behind application-layer facades so ui/ no longer
injects infrastructure adapters directly:
- RegistratieLookupStore (BRP address + DUO diplomas): owns the resources,
  runs the trust-boundary parse, exposes adresStatus/prefillAdres/duoLookup.
- IntakePolicyStore (scholing threshold): owns the policy resource, exposes
  the derived threshold.

Add the lint rule ui/ + layout/ ↛ **/infrastructure/** (@typescript-eslint
variant so it composes with the base direction rules; stories/specs exempted
as test scaffolding). Add the documented showcase sanction (may read every
context). Fix the docs' inventory: 6 contexts / 5 layers, +brief, +contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:01:42 +02:00
ehoandClaude Opus 4.8 3a5c8f157a fix(wizard): don't animate silent ?aanvraag stamp — Firefox double-click on Next
The inschrijven/herregistratie wizards needed two clicks on "Volgende" to advance
in Firefox (and Zen). Root cause: picking a field (e.g. correspondentie) triggers
draft-sync's debounced create, which router.navigate()s to stamp ?aanvraag=<id>
into the URL. withViewTransitions() animated that same-route navigation, and for
the transition's duration Firefox's ::view-transition overlay swallows pointer
events (confirmed: elementFromPoint over the button returns the overlay, not the
button). Chrome sets pointer-events:none on the overlay so clicks pass through —
hence Firefox-only.

Fix: skip the view transition for same-route navigations (compare leaf routeConfig)
so the silent id-stamp doesn't animate; genuine page-to-page transitions still fade.

Verified in Firefox: page navs still animate (skipped:0), the ?aanvraag stamp is
skipped, and Post → single Next advances step 1 → 2. GREEN + a11y.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:51:11 +02:00
ehoandClaude Opus 4.8 6224501e0a docs(backlog): record WP-03 commit hash
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:19:58 +02:00
ehoandClaude Opus 4.8 f9b76e7f6a feat(boundaries): WP-03 — contracts purity + ApiClient confinement
Lint-enforce two architecture rules that were only documented (ADR-0001),
landing the rules with the fixes so the build stays green:

- contracts/ imports nothing: dashboard-view.dto.ts is now pure wire shapes
  (inline string-union enums, no domain imports). The DashboardView FE-view
  type moves to the adapter, which maps wire → domain (compiler-enforced seam).
- ApiClient lives only in infrastructure: change-request-form (UI) no longer
  injects ApiClient — a new ChangeRequestAdapter owns the client and the submit
  becomes a createSubmitChangeRequest() command factory (createDraftSync shape).
  draft-sync's wire-DTO import becomes type-only (allowed via allowTypeImports).
- Role type moves to shared/domain/role.ts; the ?role= reader stays in
  shared/infrastructure/role.ts.
- eslint: contracts import-ban + @typescript-eslint/no-restricted-imports on
  api-client (value-only; type imports permitted; infra + shared/upload exempt).

Also fixes a PRE-EXISTING bug found while verifying the flow: change-request-form
never imported FormsModule, so (ngSubmit) didn't bind and the submit button did a
native form submit (page reload) instead of submitting. Verified end-to-end in the
running app: submit → command → adapter → backend → reference, success alert shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:19:58 +02:00
ehoandClaude Opus 4.8 be3a64f6cf docs(backlog): record WP-02 commit hash
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:32:51 +02:00
ehoandClaude Opus 4.8 88442b0616 feat(gates): WP-02 — harden check:tokens to whole-app colour guard
- Move the guard to scripts/check-tokens.sh; regex now catches hex +
  rgb()/hsl() (was hex-only) across ALL src/app components (was three
  ui/layout dirs). `token-ok` marker suppresses justified false positives;
  px stays out of scope (documented in the script).
- Zero exclusions: debug-state's dark code-editor palette moves to
  --app-devpanel-* tokens in styles.scss (the one exempt file), dropping its
  --exclude hole.
- Tokenize remaining hits: site-footer border via color-mix; three brief
  border widths via --rhc-border-width-* (new --rhc-border-width-lg: 3px).

Verified: planted violation fails the guard; GREEN + test-storybook:ci.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:32:51 +02:00
ehoandClaude Opus 4.8 348422afc9 docs(backlog): record WP-01 commit hash
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:13:26 +02:00
ehoandClaude Opus 4.8 97f7de4590 feat(a11y): WP-01 — axe-on-every-story CI gate
Turn the interactive Storybook a11y addon into a build gate:
- @storybook/test-runner + axe-playwright over the static build
  (.storybook/test-runner.ts reads the a11y tags from story context)
- test-storybook / test-storybook:ci scripts; storybook-a11y CI job
- triage: escape-hatch a11y.disable on stories whose display:contents
  wrapper splits <ul>/<li> or <dl>/<dt>/<dd> (structural, deferred to
  WP-11/WP-12, each with justification + cross-ref)
- fix trivial violations: footer/wizard-shell contrast, text-input label,
  wizard stories missing provideApiClient

Verified: broken story fails the gate; 133 stories pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:13:18 +02:00
eho b4fb0be769 feat(dashboard): render a lopende aanvraag as a CIBG melding
A Concept status now renders as a CIBG "melding" (warning) with its own
verwijderen/openen actions, matching the real CIBG pattern for an in-progress
application, instead of sharing the keuzelijst card shape used by resolved
statuses. Alert atom switches from a hand-rolled surface to the vendored
`.feedback` classes with a visually-hidden icon label per CIBG's a11y
requirement.
2026-07-02 17:08:04 +02:00
eho f769242f76 docs: replace SHOWCASE-ROADMAP.md with docs/backlog/ (17 WPs)
Turns the prior roadmap sketch into ordered, gated work packages (enforcement
gates, FP/DDD consistency, CIBG fidelity, Storybook curriculum, a11y) from the
2026-07-02 showcase-hardening audit.
2026-07-02 17:07:59 +02:00
ehoandClaude Fable 5 a2ed3ae5b8 fix(dashboard): move Annuleren inside the keuzelijst card
"Annuleren" rendered as a separate line below the grey card — visually disconnected
from the choice it belongs to. Fixing this properly means the card can't just be one
big <a> anymore: a <button> can't nest inside an anchor (invalid HTML, broken a11y),
and choice-link's [choiceActions] slot needs Annuleren to sit inside the same box.

choice-link.component.ts now makes the card a <div> always, with the title wrapped in
a vendored Bootstrap `.stretched-link` (its ::after overlay keeps the whole card
clickable, same as before) instead of the whole box being the anchor. The projected
action gets its own `position:relative;z-index:2` (in aanvraag-block.component.ts,
which owns that markup) to stay clickable above the stretched-link overlay. Added
`:focus-within` on the card to restore the focus-accent CIBG's `:focus` rule would
have given the card itself, since focus now lands on the inner title link.

Verified: lint/check:tokens/test/build green; drove it end-to-end — clicking anywhere
on a card body still resumes the wizard, clicking Annuleren cancels without navigating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:15:22 +02:00
ehoandClaude Fable 5 7ac14557dd feat(dashboard): restyle Mijn aanvragen / Wat moet ik regelen as a CIBG keuzelijst
Both sections offer a set of choices the user picks between to proceed, matching
designsystem.cibg.nl/componenten/keuzelijst rather than the "aanvragen" row pattern
("Wat wilt u doen?" keeps that look — it's a static nav list, not a choice list).

- New shared/ui molecules: choice-list (heading + keuzelijst__list, wired via
  aria-labelledby per CIBG's a11y guidance) and choice-link (one keuzelijst__link
  choice; routerLink, imperative-clickable, or a plain non-interactive block).
- choice-link's non-interactive block needed a `--static` modifier: CIBG's
  `.keuzelijst__link:after`/`:hover`/`:focus` key off the bare class (keuzelijst
  assumes every item is a link), unlike `.applications li a::after` which is scoped
  to the anchor — without it, a non-actionable aanvraag row inherited a chevron and
  hover accent it shouldn't have.
- task-list.component.ts now composes choice-list/choice-link internally; public
  API unchanged except a new required `listHeading` input (the heading moves inside
  the list for the aria-labelledby link, so dashboard.page.ts stops rendering it
  separately — same fix applied to "Mijn aanvragen").
- aanvraag-block.component.ts moves from application-link to choice-link, combining
  its separate status/subtitle text into one instructions paragraph (keuzelijst has
  no cta field — the row itself is the action). Only a resumable Concept renders as
  a real choice; InBehandeling/Goedgekeurd/Afgewezen stay non-interactive, unchanged
  from before.

Verified: lint/check:tokens/build green, 178 tests pass, build-storybook succeeds,
and manually driven end-to-end (dashboard renders both sections as keuzelijst cards,
confirmed via screenshot that non-actionable rows have no chevron after the fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:54:02 +02:00
ehoandClaude Fable 5 6257d7ede3 feat(design): adopt CIBG component patterns (header, forms, wizards, dashboard)
Re-skins the app's layout on top of the CIBG Huisstijl theme (previous commit) so it
matches designsystem.cibg.nl, not just its colour tokens — magenta ("robijn") header,
horizontal nav, and the CIBG component markup for forms/wizards/dashboard.

- Header: logo block + robijn titlebar (breadcrumb + user menu) + grey horizontal nav
  (4 links) replacing the dashboard side-nav; breadcrumb restyled for the titlebar
  (no background of its own — CIBG's global `header nav` rule otherwise bleeds a grey
  fill into it, fixed by scoping an override inside BreadcrumbComponent).
- Forms: form-field/radio-group/checkbox rebuilt on CIBG's horizontal `form-group row`
  / `form-check.styled` markup (label col-md-4, control col-md-8); same input() APIs.
- Wizards: stepper rebuilt as the CIBG "stappenindicator" (numbered circles, visited
  steps clickable for back-nav, title merged in); wizard-shell adopts the CIBG
  procesnavigatie button row. Back-navigation wired into all three wizard machines
  (registratie-wizard already had it; added `GaNaarStap` to intake/herregistratie
  machines, pure + spec'd).
- New shared/ui molecules: confirmation (animated bevestiging checkmark, replaces
  plain alerts on submit), review-section (controlestap sections with "Wijzigen"),
  application-list/application-link (CIBG "aanvragen" rows, replace the dashboard's
  card grid and aanvraag-block).
- Cleanup: delete side-nav and now-unused styles.scss utilities (.app-overview,
  .app-form-panel, .app-card-grid); correct design-tokens.mdx (it referenced tokens
  that no longer exist) and document the CIBG-value token bridge.

Verified: build/lint/check:tokens green, 178 tests pass (4 new GaNaarStap cases), and
manually driven end-to-end (dashboard, a full herregistratie submission through to the
confirmation screen, mobile width, keyboard focus).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:33:05 +02:00
ehoandClaude Opus 4.8 7887355ca3 docs(prd): add PRD-0002 — attribute-based access control (ABAC)
Specifies fine-grained, app-owned access control layered on the AD roles: capability
gating, data-scoping, field/PII-level, and step-up/SoD. Backend-authoritative (per
ADR-0001), UI mirrors decisions; extends ADR-0002. Privacy-by-design: data-minimized
decision DTOs, server-side PII redaction, audit, deny-by-default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:33:12 +02:00
ehoandClaude Opus 4.8 b443f1fc04 feat(design): re-theme to CIBG Huisstijl (Bootstrap 5.2), replacing RHC/Utrecht
The portal now adheres to the CIBG design system (designsystem.cibg.nl) — a customized
Bootstrap 5.2 build — replacing the Rijkshuisstijl-Community / Utrecht theme. See ADR-0003.

- Vendor @cibg/huisstijl@3.22.0 under public/cibg-huisstijl/, loaded via a <link> in
  index.html (Storybook serves it via staticDirs). Drop the two @rijkshuisstijl-community deps.
- Token bridge in styles.scss: redefine the app's ~54 --rhc-* tokens onto CIBG/--bs-* values,
  so components keep referencing tokens (no 300+ site rewrite). System-font stack; licensed
  RO/Rijks text fonts intentionally not shipped.
- Re-skin every shared atom to Bootstrap/CIBG classes (btn, form-control, form-check-*,
  table, breadcrumb, …) keeping their input() APIs. alert is hand-rolled (CIBG drops .alert);
  local .card/.badge renamed to avoid Bootstrap collisions.
- Domain pages: drop stray rhc-*/utrecht-* classes; registration-table → table table-striped.

Verified: build green, check:tokens OK, lint clean, 174 tests pass, build-storybook OK,
and the served build loads the vendored CSS (200, .btn-primary present).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:32:50 +02:00
ehoandClaude Opus 4.8 180c08d4ee fix(registratie): silence NG8102 by moving ?? '' guard into a method
`draft().antwoorden[q.id]` types as `string` (Record index, no noUncheckedIndexedAccess),
so the template `?? ''` tripped NG8102's "redundant nullish coalescing" diagnostic — but a
missing key IS `undefined` at runtime, so the guard is real. Moved it into an `antwoord(id)`
component method: same runtime safety, no template-only diagnostic. Build now warning-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:32:13 +02:00
ehoandClaude Opus 4.8 7f55577864 chore(tsconfig): enable strict + strictTemplates
Adds the `strict` umbrella flag (on top of the existing individual flags) and
Angular's `strictTemplates`. Build and full test suite pass with no new errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 08:50:22 +02:00
ehoandClaude Opus 4.8 84c2d1b6a0 feat(brief): locked sections, list formatting, auto/manual placeholder chips
- brief.machine: reducer refuses edits to locked (predefined) sections as
  defense-in-depth; LetterSection gains a `locked` flag
- rich-text: paragraphs gain optional `list` kind; editor gets bullet/numbered
  list buttons, keyboard shortcuts, and backspace-deletes-adjacent-chip
- placeholder chips distinguish auto-resolvable (grey) vs manual (yellow), in
  both the editor and the read-only preview
- fix: preview chip now renders matching {…} braces (was a one-sided ⌗ glyph),
  aligned with the editor's chip styling

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 08:50:22 +02:00
ehoandClaude Opus 4.8 053160c5c9 feat(brief): letter composition + two-person approval (teaching slice)
CI / backend (push) Failing after 22s
CI / frontend (push) Successful in 1m26s
CI / api-client-drift (push) Successful in 1m45s
New `brief` context — a letter-composition feature with a drafter/approver
approval workflow, built as a teaching vertical slice on the repo's existing
FP + Elm + atomic-design patterns (see plan in ~/.claude/plans).

Domain (pure):
- Rich text as a serialisable value tree (placeholders are first-class nodes),
  moved to @shared/kernel/rich-text.ts so the shared editor can use it.
- lintPlaceholders: a pure, total content -> Diagnostic[] linter, derived never stored.
- brief.machine.ts: status sum-type with guarded transitions; frozen-snapshot =
  deep value copy; derived diagnostics/editability. Full specs.

Backend (.NET stub):
- BriefStore + seed, GET/PUT /brief and submit/approve/reject/send endpoints,
  role via X-Role header (mirrors X-Admin), transition + approver!=drafter guards,
  audit logging. Regenerated typed client via gen:api. +6 backend tests.

Seam:
- brief.adapter.ts maps flat wire unions <-> domain discriminated unions at the
  parse boundary (+ spec).

UI (atomic):
- shared atoms: checkbox, placeholder-chip; molecule: rich-text-editor (no-dep
  contenteditable, DOM<->RichTextBlock round-trip tested).
- brief/ui: letter-block, passage-picker, diagnostics-panel, rejection-comments,
  letter-section, letter-composer, letter-preview, brief.page + /brief route.
- Dev-only ?role=drafter|approver toggle + roleInterceptor; dashboard nav link.

Enforcement: @brief/* alias + eslint layer boundary (brief depends only on shared).

Also included (same session):
- Value-object specs (postcode/uren/big-nummer) — closes the "domain must have a spec" gap.
- src/docs/ Storybook MDX foundation pages (atomic design, tokens, FP-in-UI).
- .storybook/tsconfig.json: add @angular/localize to types (Storybook was fully
  broken — $localize unresolved — dev + build).

Verified: 168 FE tests, 68 backend tests, lint/build/check:tokens green,
Storybook boots, end-to-end HTTP smoke (self-approve 403, approver 200, full flow).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:32:22 +02:00
ehoandClaude Opus 4.8 0aada9037e docs: showcase roadmap (Storybook-as-curriculum, FP primitives, enforcement)
CI / frontend (push) Successful in 1m16s
CI / backend (push) Failing after 23s
CI / api-client-drift (push) Successful in 1m22s
Prioritised backlog from a three-part analysis of the atomic-design + FP showcase:
concrete P1-P3 items per track with file paths, teaching value, and effort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:35:37 +02:00
ehoandClaude Opus 4.8 6a61c179cd Registratie: answer-driven required document uploads
Categories stay server-owned (ADR-0001); the FE sends its answers to
/uploads/categories and re-fetches reactively when they change:
- Diplomabewijs required only for a handmatig diploma (DUO is verified digitally;
  nothing required before a diploma is chosen).
- Bewijs Nederlandse taalvaardigheid required only when the applicant answers "ja"
  to the nl-taalvaardigheid (B2) policy question.
CategoriesFor(wizardId, diplomaHerkomst, taalvaardigheid) decides; Find uses the
maximal set so uploads still validate. CategoriesLoaded drops orphaned uploads
when a category disappears. Also: show the foreground-only upload banner only when
there is at least one category.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:35:37 +02:00
ehoandClaude Opus 4.8 9822a45d9a Fix: late draft-resume no longer clobbers in-progress wizard input
draftSync.resume() does async network work and dispatched Seed on completion,
which could land after the user's first action and reset the machine (cursor +
fields) — the "click Volgende twice" symptom. Guard centrally: applyResume()
skips when the user already has progress (snapshot() != null) or there's nothing
to restore. onResume is now only ever called with a real draft on a pristine
machine, so the three wizard callbacks drop the dead `?? initial`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:35:24 +02:00
ehoandClaude Opus 4.8 5027f099cf Mijn aanvragen: list enter/leave animations + 204 null-body fix
- animate.enter/leave on aanvraag cards and upload rows (native Angular, no @angular/animations)
- reduced-motion: skip animation → instant removal
- api-client: null-body statuses (204/205/304) must pass null to Response()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:14:29 +02:00
ehoandClaude Opus 4.8 0f14239f68 Fix Mijn aanvragen: instant cancel + one Concept per type (resume)
Two dashboard bugs from the just-built feature.

1. Cancel didn't reflect until a browser refresh. ApplicationsStore now OWNS the
   list in a writable RemoteData signal instead of projecting a resource() through
   fromResource; cancel removes the row synchronously (guaranteed disappear, no
   dependence on CD timing / HTTP cache / the reloading gap), then confirms the
   DELETE (rollback on failure, no resync). Adapter gains list(); applicationsResource()
   removed. Shared fromResource/remote-data.ts deliberately untouched.

2. Duplicate / inconsistent Concepts per type. createDraftSync.resume() now: a
   ?aanvraag link wins; else it resumes THIS type's existing Concept (loads its
   draft); else fresh. ensureId is gated behind resume so a fast typist can't create
   a duplicate before the lookup lands. restart()/reset() deletes the current Concept
   (submitted → 409, kept) so there's at most one active Concept per type. A non-Concept
   id can't reopen as an editable draft. Backend unchanged.

Gates green: lint, vitest 128, build, check:tokens, backend dotnet 56.
Wiring is not unit-covered — needs live verification (see plan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:34:01 +02:00
ehoandClaude Opus 4.8 168cf9786c Mijn aanvragen (F2): document preview/download on completed uploads
Re-opening a Concept wizard now lets the user preview/download what they already
uploaded (PRD 0001 goal 2).

- document-chip: optional previewUrl input → a "Voorbeeld / Download" link (opens
  the stored bytes; server serves inline for pdf/image, attachment otherwise).
- previewUrlFor callback threaded document-upload → document-category → single-upload
  (which builds the URL from a completed upload's documentId). Keeps URL-building out
  of the presentational atoms.
- registratie + herregistratie wizards supply previewUrlFor via UploadAdapter.contentUrl,
  returning undefined for dev-simulation `demo-*` ids (no stored bytes → no link).
- Story: DocumentChip/WithPreview.

Gates green: vitest 128, lint, ng build, check:tokens; backend dotnet 56.
(build-storybook has a pre-existing Compodoc/$localize issue in untouched files.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:50:40 +02:00
ehoandClaude Opus 4.8 0bef08e5b3 Mijn aanvragen (F1): ApplicationsStore + dashboard blocks
The payoff — A–E become visible. The dashboard now shows a "Mijn aanvragen"
section at the top with a block per application.

- ApplicationsStore (registratie/application, root): the list as a RemoteData
  signal, parsed at the trust boundary; reload() (dashboard revisit reflects
  server-computed auto-approval); optimistic cancel (hide → reload / un-hide on fail).
- aanvraag-block (organism): badge (tag → colour/label) + per-status body
  ("Stap X van Y" / referentie + ingediend-datum / manual note / reden) + actions
  from the pure blockActions. Composes card + status-badge + button. Stories per status.
- dashboard: "Mijn aanvragen" section (hidden when empty), sorted Concept → In
  behandeling → resolved; Verder gaan deep-links the wizard (?aanvraag=<id>),
  Annuleren cancels via the store.

Deferred to F2: document-chip preview/download affordance.
Gates green: vitest 128, lint, build, check:tokens; backend dotnet 56.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:43:36 +02:00
ehoandClaude Opus 4.8 9f217abe19 Mijn aanvragen (E): two-flow submit through the aanvraag + all-wizard persistence
All three wizards now submit through the backend aanvraag lifecycle, so a
submitted Concept actually transitions (dashboard shows it correctly in F).

- blockActions(status) (domain + spec): the pure per-status action decision
  (Concept → resume/cancel; In behandeling → viewDocuments; resolved → none).
- createDraftSync.submit(): ensure the Concept exists, then
  POST /applications/{id}/submit; folded into a Result like the old commands.
- registratie: submit via draftSync (duo → auto, handmatig → manual pending — the
  old 422 path is gone from the wizard).
- intake + herregistratie: adopt createDraftSync (persistence + resume-by-link);
  intake retires sessionStorage `intake-v3`; herregistratie gains persistence.
  Both submit through the aanvraag too. hasProgress added to each machine (+spec).
- Delete now-dead submit-registratie/submit-intake/submit-herregistratie commands.

Deferred: the old /registrations, /intakes, /herregistraties backend endpoints +
RejectRegistratie are now unused by the FE but still present (+ tested) — retiring
them cascades into backend test rewrites, so it's a focused follow-up cleanup.
Gates green: vitest 128, lint, build; backend unchanged (dotnet 56).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:25:02 +02:00
ehoandClaude Opus 4.8 6db7f1e673 Mijn aanvragen (D): backend draft-sync + resume-by-link (registratie slice)
Replaces the registratie wizard's sessionStorage draft with a backend-owned
Concept aanvraag (PRD 0001, phase D — the registratie vertical slice).

- createDraftSync (registratie/application): reusable controller (field-initializer
  idiom, like createUploadController). Creates the Concept lazily on first progress,
  stamps `?aanvraag=<id>` into the URL, debounced-syncs the machine snapshot per
  change, and resumes from `?aanvraag` on load. Inert without a Router or when an
  explicit seed is present (Storybook/tests) — no network there.
- hasProgress (machine, pure + spec): "worth persisting?" — excludes the automatic
  BRP address prefill so a bare page visit creates nothing. Accepted regression:
  a step-0-only address edit isn't persisted until the user advances/chooses.
- Wizard: dropped STORAGE_KEY/restore + the sessionStorage effect; restart() detaches
  the Concept and drops the link.

Deferred (noted): ApplicationsStore -> phase F (dashboard is its only consumer);
intake-v3 + herregistratie persistence -> phase E (copy this pattern).
Gates green: vitest 125, lint, build; backend unchanged (dotnet 56).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:23:30 +02:00
ehoandClaude Opus 4.8 6f250cd987 Mijn aanvragen (C): FE contracts + applications adapter + parse boundary
- Regenerate the NSwag client against the new backend endpoints (application +
  content methods, Aanvraag DTOs) — clears the API-client drift.
- registratie/domain/aanvraag.ts: FE domain view — AanvraagType + AanvraagStatus
  discriminated union (illegal states unrepresentable) + Aanvraag/AanvraagDetail.
  Lives in registratie: the dashboard consumes it, downstream wizards produce it.
- ApplicationsAdapter (infrastructure, the only new network surface): list resource
  + create/syncDraft/cancel/submit commands, with a hand-written parse* boundary
  (parseAanvraagStatus/parseApplicationSummary/parseApplications/parseApplicationDetail)
  mapping untrusted DTO -> domain, per ADR-0001. Spec covers each status tag + rejects.
- UploadAdapter.contentUrl(id): direct href for preview/download (browser opens it).

Gates green: dotnet test 56, vitest 122, lint, build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:41:57 +02:00
ehoandClaude Opus 4.8 7b6aac394b Mijn aanvragen (B): store document bytes + content endpoint
- StoredDocument gains ContentType + byte[] Content; POST /uploads now captures
  the file bytes (in-memory, reset on restart — POC).
- GET /uploads/{documentId}/content streams the bytes: inline for pdf/image
  (browser preview), attachment otherwise (download). 404 for unknown ids
  (covers the demo-* simulation sentinels, which have no bytes).
- Bytes are never serialized into a JSON response; only this endpoint streams them.
- Tests: content served back with type inline for pdf, 404 for unknown. 56/56 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:36:28 +02:00
ehoandClaude Opus 4.8 8c3f4c22ee Mijn aanvragen (A): backend Aanvraag store + lifecycle endpoints
Adds the backend-owned Aanvraag aggregate (PRD 0001, phase A) — the system of
record the dashboard will read. In-memory static store mirroring DocumentStore.

- ApplicationStore: create/get/list/draft-sync/cancel/submit; status COMPUTED ON
  READ (Mappers.ToStatusDto(now)) so auto-approval is pure timestamp arithmetic,
  no timers/jobs (ProcessingWindow = 8s).
- Endpoints: GET /applications, GET/POST/PUT/DELETE /applications/{id},
  POST /applications/{id}/submit.
- Lifecycle: registratie duo -> auto (Goedgekeurd after window), handmatig ->
  manual pending (no 422); herregistratie/intake 0 uren -> Afgewezen else auto.
  Cancel blocks submitted aanvragen (409, no withdrawal in scope).
- Old /registrations endpoint + RejectRegistratie 422 left intact (retire in E).
- ApplicationTests: lifecycle + auto-approve window boundary (pure). 54/54 green.

Also checks in the PRD (docs/prd/0001).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:35:19 +02:00
ehoandClaude Opus 4.8 a2cd7a0ac1 Add ADR 0002: user groups as actors, not bounded contexts
Records how to model Zorgverlener (SSP), Behandelaar (backoffice), and future
actors: personas are actors, not contexts; two capability contexts (Zelfbediening
+ Behandeling) as separate apps over one backend-owned aanvraag aggregate,
integrating via ADR-0001 decision DTOs; identity (typed Principal union in auth)
separated from authorization (backend-authoritative). Boundaries only — no code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:04:40 +02:00
eho a93b64edc0 Merge feat/dotnet-backend: .NET backend + upload feature (+ tab-crash fix) 2026-07-01 10:27:57 +02:00
ehoandClaude Opus 4.8 4a1fd7c581 Fix upload wizard tab crash: dispatch must not track the model signal
CI / backend (push) Failing after 21s
CI / frontend (push) Successful in 1m18s
CI / api-client-drift (push) Successful in 1m27s
createUploadController runs an effect() that calls dispatch. store.ts
dispatch was `model.set(update(model(), msg))` — the reactive model()
read made the effect depend on its own write and re-schedule forever,
livelocking the main thread. Angular's NG0103 guard doesn't cover effect
self-rescheduling, so no error was thrown; Firefox just killed the
unresponsive tab. Only /registreren and /herregistratie (which mount the
upload controller) were affected.

dispatch now uses model.update((m) => update(m, msg)) — the current value
is read untracked, so no effect can loop on its own dispatch. Hardens all
wizard stores. Adds a regression spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:27:50 +02:00
ehoandClaude Opus 4.8 57940234b2 Upload feature (f): demo scenarios (upload-slow/fail) + a11y (file-input label)
- upload-slow/upload-fail scenarios simulated in the adapter (XHR POST bypasses the
  HTTP interceptor); categories/status/delete already honour the global slow/error
- file-input gets a per-category accessible name (aria-label + visible button text)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:42:51 +02:00
ehoandClaude Opus 4.8 bfd957a6d4 Upload feature (e): wire inline upload (registratie beroep) + documenten step (herregistratie)
- Fold UploadState into both wizard machines; route via { tag: 'Upload', msg }
- Gate step validation on requiredCategoriesSatisfied; include deliveryRefs in submit
- Shared createUploadController (effectful glue: categories, transport, focus-poll, File map)
- rejectReason pure format validator + specs; bump registratie storage key to v2

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:38:37 +02:00
ehoandClaude Opus 4.8 9521739ac1 Upload feature (c): atomic UI components (atoms/molecules/organisms) + stories
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 06:25:30 +02:00
ehoandClaude Opus 4.8 c4bfe9d39b Upload feature (b): HTTP adapter (XHR multipart) + shell transport service
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:01:36 +02:00
ehoandClaude Opus 4.8 0e48f44773 Upload feature (d): pure upload domain machine + spec
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:48:24 +02:00
ehoandClaude Opus 4.8 0d37cc097a Upload feature (a): BFF endpoints + category config + tests
- Domain/Documents: server-owned category config per wizard + authoritative
  type/size validation (DocumentRules).
- Data/DocumentStore: in-memory metadata store (no file bytes/PII) + audit log;
  user delete (owner-scoped, 409 once linked), admin delete (role seam via
  X-Admin header), link-on-submit, poll-by-localId status.
- Program.cs: GET /uploads/categories, POST /uploads (multipart, excluded from
  OpenAPI — hand-written on FE), GET /uploads/status, DELETE /uploads/{id},
  DELETE /admin/uploads/{id}. Submit links digital docs + records post-delivery.
- Contracts extended (DocumentRefDto on registratie/herregistratie submit);
  regenerated NSwag client + swagger.json (drift check stays green).
- Tests: 16 new (endpoints + DocumentRules); dotnet test 44/44.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:15:40 +02:00
ehoandClaude Opus 4.8 a079d3259e Add showcase link (Functionele patronen) back to the dashboard sidebar
Restores the /concepts entry the restyle dropped, as a side-nav item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:01:52 +02:00
ehoandClaude Opus 4.8 4ee4f95c92 Fix: restore dashboard entry point to branching intake flow
The Rijkshuisstijl restyle (7a582ae) trimmed the dashboard action-card grid and
dropped the card linking to /intake, orphaning the branching herregistratie intake
questionnaire (reachable only by typing the URL; the breadcrumb still expected it).
Restore the action card (localized). The route, page and branching wizard were
intact — only the entry point was missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 19:51:57 +02:00
ehoandClaude Opus 4.8 9c2a80451f Step 3 (production-readiness): PII storage, validated reads, seams
Implement-now:
- G1: keep PII out of persistent storage — never persist BSN (only `naam`);
  move both wizard drafts (address/email, work data) localStorage → sessionStorage
  so they clear on tab close.
- G2: validate storage reads before trusting the cast — shape/tag guard in every
  restore() (mirrors the parse* HTTP boundary); corrupt/foreign shape → start fresh.
- G3: already satisfied (debug-state redacts via mask.ts).

Show-the-seam (hook + doc, not fully built):
- G4: problemFieldErrors() maps a server validation envelope (ASP.NET
  ValidationProblemDetails `errors`) to the field-keyed map the wizards already
  render; returns {} until the backend sends it. +spec.
- G5: documented the retry/backoff seam at the adapter GET loader; reads may
  retry, mutating submits never do.

Out of scope (named): unsaved-changes warning (persistence prevents data loss),
real auth/tokens, axe-core in CI.

Gate green: lint, check:tokens, build, test 79/79.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:07:10 +02:00
ehoandClaude Opus 4.8 474c040410 Step 2 (i18n): $localize sweep + JA_NEE dedup (M3, M4)
Wrap every user-facing Dutch string in Angular's first-party i18n — `i18n`/
`i18n-<attr>` in templates, `$localize` in TS (value-objects, machines, commands,
label constants, shared-component defaults). Source locale stays nl; a second
locale is now a translation file, not a code change.

- M3: ~145 strings localized with stable @@ ids across registratie,
  herregistratie, auth, shared/ui, shared/layout. Skipped: showcase, debug-state,
  scenario interceptor, generated client, specs/stories, raw status enum tags,
  internal parse* diagnostics.
- M4: single shared JA_NEE (localized labels) in radio-group; both wizard copies
  removed.

Gate green: lint, check:tokens, build, test 77/77.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:00:10 +02:00
ehoandClaude Opus 4.8 1c65025fef Step 2 (code quality): dedup + stop FE recomputing a server rule
- H1: tasksFromProfile takes the server's eligibleForHerregistratie decision
  instead of recomputing isHerregistratieEligible — the FE renders the rule,
  doesn't own it (ADR-0001). Policy reference impl kept for tests.
- M1: one shared runSubmit(fn, fallback) wrapper; the 4 submit-* commands keep
  only their payload mapping. +spec.
- M2: whenTag() kernel helper removes 10 repeated `as Extract<U,{tag}>` casts
  across the wizard/form components.

M4 (shared JA_NEE) folded into the upcoming i18n pass (clean dedup needs
$localize labels to sit in shared without breaking the English-shared-UI rule).
L1 already resolved by the restyle commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 13:48:35 +02:00
ehoandClaude Opus 4.8 94ffcf3d41 Step 1: i18n foundation (@angular/localize) + data inventory
Domain reference data already lives in the backend (per ADR-0001); the only
residual "move" was ~90 inlined Dutch UI strings with no i18n layer.

- Wire @angular/localize ($localize) — Angular first-party, no third-party lib
- Pin the pattern on shared/ui/async: Dutch fallbacks → language-agnostic
  input()s with localizable $localize defaults (English-shared-UI rule)
- CLAUDE.md: drop i18n (now in scope) + stale "real backend"/"OpenAPI codegen"
  (both already shipped); document the $localize convention

Bulk string sweep deferred to Step 2 to avoid touching ~36 files twice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 13:42:12 +02:00
ehoandClaude Opus 4.8 7a582ae2fa Rijkshuisstijl restyle + wizard fixes
Chrome: two-tier Rijksoverheid header (white brand bar + lint-blue
breadcrumb bar, route-driven), dark multi-column footer, white page
surface. Session shown via a shared SESSION_PORT token (keeps shared/
free of the auth context).

Overview ("Mijn overzicht") rebuilt to the NL Design System #392 pattern:
side-nav + "Wat moet ik regelen" task list (derived) + "Mijn registratie"
cards. New shared components: card, task-list, side-nav; pure
tasksFromProfile (+spec).

Wizards: grey form panel, connected numbered stepper, form-field
"(verplicht)" markers + styled description/error, full-width inputs.
Propagated to login, detail, change-request, address-fields.

Bug fixes:
- wizard-shell: add FormsModule so NgForm intercepts submit (wizards now
  advance; no native GET leaking choices into the URL).
- wizard-shell: error-summary links focus the field instead of navigating
  (a fragment href resolved against <base href="/"> reloaded to "/" and
  bounced to login).
- wizard-shell: error-summary focus only on the rising edge, so typing
  while errors are shown no longer scrolls the page up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 13:21:54 +02:00
ehoandClaude Opus 4.8 d08f3877f7 Architect-review remediation: enforce conventions, prod-safe tooling, one form idiom, resilience seams
Acts on the showcase review. Four workstreams; all tests green
(npm run lint, 70 FE tests, ng build, 33 backend tests).

Enforcement + CI:
- eslint.config.mjs bans `any` and enforces layer/context boundaries
  (domain ≠ Angular; herregistratie → registratie → shared, auth → shared);
  `npm run lint` added; ajv 6 scoped to ESLint via nested override.
- .github/workflows/ci.yml: FE lint+check:tokens+test+build, backend dotnet test,
  and an API-client drift check.

One form idiom (the headline finding):
- change-request-form converged onto the wizard pattern — change-request.machine.ts
  (Model/Msg/reduce + value objects) + submit-change-request.ts (Result) + a real
  POST /api/v1/change-requests (server re-validates). Spec + story added; the detail
  page no longer holds an ad-hoc success signal.

Resilience/observability seam:
- api-client.provider.ts: request timeout, X-Correlation-Id, Idempotency-Key for
  writes; comments naming the retry/auth seams.
- Backend logs correlation id + a no-PII submit-audit line; /api/v1 prefix +
  backward-compat note; client regenerated.

Quick wins:
- Dev tooling excluded from prod: scenario.interceptor wired only under isDevMode()
  (?scenario= inert in prod); debug panel @if(isDev) (tree-shaken out).
- src/environments + apiBaseUrl into provideApiClient (angular.json fileReplacements).
- Backend /health + /health/ready.
- Debug view PII-minimised (redactProfile: name/address/DOB redacted, BIG masked).
- IntakePolicyAdapter (removes inline resource in the intake wizard).
- README de-staled; CLAUDE.md gains EN/NL + forms-one-idiom + lint/CI notes.
- Stories: text-input, link, data-row, site-header, site-footer, change-request-form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:25:51 +02:00
ehoandClaude Opus 4.8 cf570a8132 Add ASP.NET Core backend hosting business rules; FE consumes via typed client
Move the authoritative business rules off the frontend into a real backend,
realising the BFF-lite + decision-DTO design (ADR-0001) that until now lived
only in static mock JSON.

Backend (backend/):
- ASP.NET Core (.NET 10) minimal API, contract-first, Swagger UI at /swagger.
- DDD Domain/ rules layer: profession derivation + applicable policy questions
  (DiplomaRules), herregistratie eligibility + reason (HerregistratieRule),
  scholing threshold (IntakePolicy), submit rejections + reference generation
  (SubmissionRules). In-memory seeded data, ProblemDetails (RFC 7807) errors.
- 27 xUnit tests: rule units + endpoint integration incl. BRP no-address and
  DUO not-found fallbacks and 422 submit paths.

Frontend (only infrastructure/ + contracts/ change, as the architecture promised):
- NSwag-generated typed client (api-client.ts), routed through Angular HttpClient
  via a small fetch adapter so the ?scenario= interceptor still applies.
- GET adapters use resource({ loader: client.x }); submit commands call the client
  and map ProblemDetails -> err. The hardcoded uren==0 / manual-diploma rules are
  deleted (now server-side). Domain, stores, UI and format validators unchanged.
- Deleted the now-dead public/mock/*.json.

Tooling/docs:
- npm start proxies /api -> backend; npm run gen:api regenerates the client;
  docker compose up runs both (bind mounts use :z for SELinux/Fedora).
- backend/README.md walkthrough: adding a policy question is a one-file backend
  change, no FE change, no client regen. Updated CLAUDE.md + ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:05:53 +02:00
ehoandClaude Opus 4.8 4e9af05cc1 Add dev-only state debug view (Elm-style "show the Model")
A floating, read-only panel that renders the current root-store state
(SessionStore + BigProfileStore) live via the json pipe. Whole thing is
gated by isDevMode() so it never renders or ships in production.

- Observer only — no new store/library/state pattern (PRD prime directive).
- BigProfileStore resolved lazily on first open; its httpResources fetch
  eagerly on construction, so we avoid a personal-data fetch on pages that
  don't use it.
- bsn masked before render; no persistence/logging/network in the feature.
- maskBsn has a unit spec; UI exercised via a Devtools Storybook story,
  per repo convention (no TestBed component tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:56:12 +02:00
ehoandClaude Opus 4.8 e5a3030dca Add FP + Elm Architecture + atomic design learning guide
A progressive teaching guide (docs/fp-tea-atomic-design.md): FP fundamentals,
The Elm Architecture, and atomic design, taught Elm-then-this-app with the real
store/machine/value-object code, plus four recipes and a glossary. It owns the
teaching arc and cross-references ARCHITECTURE.md/ADR-0001 rather than duplicating
them. Documents reality where the PRD diverged (no state-debug-view feature; per-
wizard stores; reduce vs update naming) and flags the absent debug view as an open
question. Adds pointer links from ARCHITECTURE.md and CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:40:38 +02:00
ehoandClaude Opus 4.8 770d454a32 Extract reusable address-fields organism, adopt in both registratie call-sites
The editable address block (straat/postcode/woonplaats) was hand-built inline in
two places — the registratie wizard and the change-request form. Factor it into one
pure presentational organism (values in, errors in, per-field change out) grouped in
a fieldset/legend, and adopt it in both. Behaviour, validation and state flow are
unchanged: the wizard still dispatches SetField (flipping adresHerkomst on edit) and
the change-request form still parses the postcode on submit. Storybook entry added;
other field clusters left inline by design (single-use or genuinely divergent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:25:39 +02:00
ehoandClaude Opus 4.8 64385999eb Add registratie wizard, BFF dashboard-view, contracts/value-objects, and architecture docs
Checkpoint of in-progress work: the registration wizard (address prefill,
DUO diploma lookup, policy questions), decision-DTO contracts, parse-don't-
validate value objects, infrastructure adapters, plus CLAUDE.md and the
architecture/ADR docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:23:52 +02:00
ehoandClaude Opus 4.8 8a8a2f0f29 Fix radio selection visual and trailing divider in data rows
- radio-group: the Utrecht radio paints its dot only with the
  `utrecht-radio-button--checked` class (not the native :checked); we set
  [checked] but not the class, so a selected radio looked empty. Bind the class
  too. The intake step-1 choice now visibly selects.
- data-row: drop the border-block-end on the last row (:host:last-child) so a
  summary ends cleanly instead of showing a trailing empty row (visible in the
  concepts discriminated-unions card).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:57:56 +02:00
ehoandClaude Opus 4.8 f38f727a60 Regenerate compodoc documentation.json
Reflects the herregistratie jaren field and the branching intake wizard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:38:34 +02:00
ehoandClaude Opus 4.8 7463efdc2d Add branching intake wizard (derived steps + radio-group atom)
A second wizard demonstrating a BRANCHING flow: the visible steps are derived
from the answers by a pure `visibleSteps` function rather than stored, so
answering "buiten Nederland gewerkt? -> ja" or reporting few hours adds steps
and the progress denominator changes live. Same Elm-style store + RemoteData
patterns as the fixed wizard; answers persist to localStorage.

- intake.machine.ts: IntakeState union + Answers + visibleSteps + pure reduce (+spec)
- intake-wizard organism, intake.page, submit-intake command
- new radio-group atom (ControlValueAccessor) in shared/ui
- /intake route + dashboard link + concepts showcase section
- tighten Aantekening.type to a 'Specialisme' | 'Aantekening' union
- README + ARCHITECTURE updated

Verified live end-to-end (branches add steps 4->5->6, review, submit) with no
console errors; build, unit tests, and Storybook all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:38:26 +02:00
ehoandClaude Opus 4.8 164d20a10d Add second question (jaren werkzaam) to herregistratie wizard step 1
Step 1 was a single field, making the wizard feel thin. Add "Aantal jaren
werkzaam" beside "Gewerkte uren" on the same step (no new step): Draft/Valid
gain `jaren`, `next` validates both step-1 fields before advancing, and
`validate` parses it for the submitted payload. Verified live: an empty jaren
blocks advancing with an inline error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:38:11 +02:00
ehoandClaude Opus 4.8 6086729563 Fix login output name collision and herregistratie demo eligibility
Walking through the running app surfaced two issues:

- login-form's `submit` output collided with the native DOM `submit` event
  bubbling to <app-login-form>, so login() also fired with an Event (not the
  BSN string) — "bsn.trim is not a function". Renamed the output to `submitted`
  (matching the other forms).
- The static mock herregistratie deadline (2027-09-01) sat outside the 12-month
  eligibility window, so the wizard was correctly hidden. Moved it to 2027-03-01
  so "verloopt binnenkort" is true and the flow is demoable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 07:38:06 +02:00
ehoandClaude Opus 4.8 8b590a50d9 Regenerate compodoc documentation.json
Reflects the bounded-context restructuring and new state-management modules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 07:20:23 +02:00
ehoandClaude Opus 4.8 8eeffc3d4a Add architecture guide for developers new to FP
Plain-language walkthrough of the bounded-context layering and the state
management (RemoteData, the Elm-style store, combining services, optimistic
updates, value objects), with a glossary — aimed at a junior with no functional
programming background.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 07:20:23 +02:00
ehoandClaude Opus 4.8 2114514ad7 Restructure into DDD bounded contexts + functional state management
Reorganise from atomic-design-only folders into bounded contexts
(auth / registratie / herregistratie) over a shared kernel, each split into
domain / application / infrastructure / ui layers. Dependencies point inward;
the domain layer is framework-free. Path aliases (@shared/@auth/@registratie/
@herregistratie) make import direction explicit.

State management (Elm-style, native TS, no new deps):
- shared/application/store.ts — createStore(init, update): pure reducer + signal
- shared/application/remote-data.ts — add map/map2/map3/andThen combinators so
  several services fold into one RemoteData; <app-async> gains an [rd] input
- registratie/application/big-profile.store.ts — root singleton combining the
  BIG-register and BRP services via map2 into one state; holds the optimistic
  herregistratie flag shared with the dashboard
- herregistratie: machine gains a WizardMsg union + pure reduce; submit is a
  command that calls infra and dispatches the result, with optimistic update +
  rollback against the shared store
- auth: SessionStore + DigiD adapter + functional route guard; login establishes
  the session, protected routes use canActivate

Rich domain: registration.policy.ts (statusColor/label, herregistratie
eligibility, invariants); BigNummer/Postcode/Uren value objects with smart
constructors. status-badge is now domain-free (colour/label inputs).

Specs for the reducer, RemoteData combinators, and eligibility policy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 07:20:13 +02:00
ehoandClaude Opus 4.8 6bd6e854c7 Regenerate compodoc documentation.json
Reflects the new types, components, and pages from the impossible-states work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:56:17 +02:00
ehoandClaude Opus 4.8 727253e5f5 Add /concepts showcase page
A teaching page pairing each pattern's impossible-state-permitting "before"
with the "after" the type system enforces: discriminated unions, the
RemoteData fold, parse-don't-validate (live), and the wizard state machine.
Composition-only — no new atoms. Linked from the dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:56:11 +02:00
ehoandClaude Opus 4.8 80c1b627d0 Drive herregistratie as a state-machine wizard
Model the multi-step form as one tagged union: step/errors exist only while
Editing, and Submitting/Submitted/Failed carry a parsed Valid payload. So
"submitting while a field is invalid" and "success screen with errors set"
are unrepresentable by construction.

Pure transitions (next/back/submit/resolve) with a spec covering the key
invariants; illegal events are no-ops. The page becomes pure composition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:56:04 +02:00
ehoandClaude Opus 4.8 ad50b3fa8f Parse, don't validate: branded types for form input
Add smart constructors parsePostcode/parseUren returning Result<string, Brand>.
The constructor is the only way to mint a Postcode/Uren, so a validated value
is a distinct type from a raw string.

change-request-form now emits a ChangeRequest carrying a parsed Postcode, and
its field errors come straight from the parser's Result — no parallel "is it
valid" flag that can drift out of sync with the value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:55:56 +02:00
ehoandClaude Opus 4.8 57b9f3f804 Model Registration status as a discriminated union
Each status variant now owns its own data: only Geregistreerd carries a
herregistratieDatum; Geschorst/Doorgehaald carry their own dates + reason.
A struck-off registration can no longer hold a future herregistratie date —
that impossible combination is gone from the type.

- status-badge keys color off the tag via a switch + assertNever
- registration-summary renders only the rows a variant's data supports
- registration.json nests the status; stories cover all three variants

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:55:49 +02:00
ehoandClaude Opus 4.8 0920063553 Back AsyncComponent with a RemoteData tagged union
Introduce RemoteData<E,T> (Loading | Empty | Failure | Success) plus
fromResource and an exhaustive foldRemote. The data lives ON the state,
so "loaded without value" or "error with stale value" are unrepresentable.

AsyncComponent now derives a single rd() and pulls value/error out via the
fold instead of a loose State string. Public API (resource/isEmpty inputs,
the four slot directives, the ASYNC array) is unchanged, so the dashboard,
detail page, and async stories need no edits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:55:40 +02:00
ehoandClaude Opus 4.8 43b2f83485 Add native-TS functional toolkit (assertNever, Result, Brand)
The shared foundation for the "make impossible states impossible" work:
- assertNever for compile-time exhaustiveness in union switches
- Result<E,T> + ok/err constructors (plain objects, no classes)
- Brand<T,B> for nominal types

No runtime dependency — this is the whole "library".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:55:30 +02:00