The shell (and this switcher within it) is a persistent parent, so the
switcher's computed link only ever ran once at bootstrap against the
initial location.pathname. Navigating client-side afterward left it
frozen on that first route (typically /login), so switching language
from any other page sent you to the stale /en/login instead of the
current route — indistinguishable from being logged out, though the
session was untouched. Recompute on every completed Router navigation,
same toSignal(router.events...) idiom already used by the breadcrumb
in site-header.component.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes phase 6 (Brief v2): a demo script mapping shipped scenarios to
URL+click paths (no Brief v2 PRD ever existed to translate one from —
written directly against the code instead), one e2e spec covering
compose→approve→send and admin republish→drafter-sees-it, and
Storybook state gaps (rejection diff, read-only viewer, org logo,
upload rejection) that prior WPs left uncovered. Flags passage-picker
as dead code, superseded by besluit-panel.
npm run e2e is not verified green in this sandbox — see WP-28's
Deviations section; the pre-existing, unmodified smoke.spec.ts fails
identically here, pointing at a sandbox rendering issue rather than a
regression.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI gate (authoritative): generalize the dangling-reference test in StamdataValidationTests
into a declared, extensible reference list ("every declared reference into a stamdata key
resolves against the currently-valid stamdata"), starting with Diploma.Opleiding →
professions.program. Removing/renaming a referenced program, or expiring it while current
data still references it, fails the PR build (ADR-0004). Editor (fast feedback): confirm
before delete (warns a referenced row fails CI) and, for temporal tables, a "Sluiten per
vandaag" action that closes validity (geldigTot) — steering to expire over hard delete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Catalog declared in code (Domain/Features/FeatureFlags.cs, build-validated), on/off state
persisted in SQLite (FeatureFlagStore + migration). GET /flags (drives FE gating) + admin
PUT /admin/flags/{key} (new flags:manage capability + FlagsAdmin gate). Enforced end-to-end:
the `inschrijving-open` flag hides the Inschrijven nav item + dashboard action (FE) AND makes
POST /applications for a registratie 403 when off (backend). FE FeatureFlagStore mirrors
AccessStore (enabled() deny-by-default); admin toggle page at /beheer/functies in ADMIN_LINKS.
+4 backend tests, /me cap-list updated, client regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The language switch is a full-page navigation to a separate bundle (nl at /, en at
/en/); sessionStorage's per-tab semantics dropped the login across it. localStorage
is unambiguously shared same-origin and survives the hard navigation. Keeps G1 (naam
only, never the BSN). Trade-off: the demo session now survives tab close — a real
portal keeps auth in an httpOnly cookie/token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Serve the source locale (nl) at / instead of /nl/: angular.json sourceLocale is now
{ code: 'nl', subPath: '' } → nl output at browser/ root (base href /), en stays /en/.
Rework localeLinks (nl → bare path, en → /en/…) + spec, and serve-i18n.mjs (nl assets at
root, en under /en/, / serves the nl index). And build the docker demo (+ serve:i18n) with
`--configuration development --localize` so isDevMode() stays true and the dev `⚙ state`
panel + role/scenario switchers render in the localized compose demo (they were correctly
gated off in the previous production build). Verified: nl base href /, en /en/, ngDevMode present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix the hardcoded LOCALE_ID: 'nl' — provide it from $localize.locale (the build-time
locale, 'nl'/'en', undefined→'nl' in dev) and register both nl+en locale data, so the en
bundle formats dates/numbers correctly. Make `docker compose up` serve the LOCALIZED build:
the web service now runs `ng build --localize` then serve-i18n.mjs, which gained a PORT env
+ an /api reverse-proxy (API_PROXY_TARGET → the api container) so both /nl/ and /en/ are
served with the language switcher working end-to-end. Drop the now-unused proxy.conf.docker.json
(serve-i18n proxies /api itself); update ARCHITECTURE. `npm start` stays the nl-only HMR loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a language switcher matching the CIBG Taal-instellen pattern: a <nav> region
(sr-only heading + aria-label) with an endonym link per locale (lang/hreflang, the
active one aria-current + non-link), mounted right after the skip link in the shell.
Compile-time $localize means each locale is its own bundle under /<locale>/, so the
switch is a full navigation to the sibling bundle — active locale read from the baked
base href. Pure localeLinks() (+spec) builds path-preserving targets. Since `ng serve`
serves nl-only at /, add `npm run serve:i18n` (localized build + a tiny static server
with per-locale SPA fallback) so the switch is demoable. +story.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface the besluit-driven assistance that was previously silent: a pure
besluitGuidance() (kern passage count + needs-reason flag) rendered as a hint below
the besluit panel in behandel-scherm (warning to pick a reden, else info on how many
standaardteksten were inserted). Add a read-only notice on letter-composer for a pure
viewer (no edit/approve/reject/send right, e.g. admin) so the read-only letter isn't
mistaken for a broken editor — the "reverted to a view of the letter" confusion was a
role issue (non-drafter sees the composer). +besluitGuidance spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WP-33 panel's <select [value]> was applied before the @for options existed, so
the matching option was never selected (dropdown always showed the first item).
Move selection to [selected] on each <option>, and read role/scenario via getters
(currentRole()/currentScenario()) so the dropdowns reflect the active value whenever
the panel opens.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WP-41 GET /admin/audit trail now has an FE view: a beheer audit page (domain
AuditEntry + adapter/parse + store) rendering the data-minimised trail as a read-only
table, capability-gated on cases:manage. Added to ADMIN_LINKS (header nav + dashboard
Beheer section) and to the role.interceptor ROLE_AWARE list so the admin-gated call
carries X-Role. Closes WP-42's audit half.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persist the security-relevant events (authz denials + BIG-nummer reveal/step-up) into a
data-minimised EF table (AuthzAuditEntry: At/Action/Resource/Decision/Role/CorrelationId —
never a name/BSN/value), extending the DocumentStore AuditEntry pattern (migration AuthzAudit).
AuditAuthz now persists via AuthzAuditStore.Record alongside its log line. GET /admin/audit
(admin-gated by the existing CasesAdmin) returns the trail newest-first. +3 backend tests incl.
a schema-carries-no-PII reflection test. Typed client regenerated (audit() + AuthzAuditDto);
no FE consumer yet (a future audit view must add the ROLE_AWARE prefix). Finishes WP-42's audit half.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "PII — maskeren & parsen" section to /concepts demonstrating the WP-40 pieces
with FP + atomic design, framed for AVG art. 9 / data-minimisation: a live
<app-masked-value> atom (masked BSN that reveals on click; real reveal is step-up +
audited in behandel-scherm) and a live parseBsn elfproef parse. Both show the real
linked source via the WP-39 snippet mechanism (new showcase regions in bsn.ts + pii.ts,
registered in gen-snippets.mjs). Delivers WP-42's showcase demo; the persisted-audit
half stays pending WP-41.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Branded Bsn value object with the elfproef (11-test) checksum in shared/kernel/bsn.ts,
wired into the DigiD login boundary so login does real BSN validation (hint + e2e BSNs
updated to a valid 123456782). Consolidate the pure maskers into shared/kernel/pii.ts
(maskBsn/maskTail/REDACTED); debug-state keeps redactProfile (needs the registratie
BigProfile — boundary). New <app-masked-value> atom (+story) centralises the masked
`.includes('*')` detection + reveal affordance; behandel-scherm refactored onto it.
Session.bsn stays string (persistence boundary drops it for privacy). +specs for bsn/pii.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Anti-drift snippets: `?raw` isn't supported by Angular's esbuild build, so real code
is exposed via // #region showcase:<name> markers in source (registration/remote-data/
postcode/change-request.machine/intake.machine) → scripts/gen-snippets.mjs → committed
snippets.generated.ts, with a CI drift gate so the shown code is the shipped code. The
/concepts page renders the 5 real snippets (union/fold/parse/machine/steps) with a source
caption + a tiny dependency-free highlightTs (+spec); deliberately-wrong illustrations stay
authored. Teaching motion: active state node pops on transition + parse result animates in
(existing reduced-motion-safe toolkit). Generated file prettier-ignored (like api-client).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
currentScenario()/currentRole() read the URL param before sessionStorage, so a
stale ?scenario=/?role= in the address bar overrode the switcher on reload
("stuck on slow"). The switcher now strips both dev params from the URL
(pure stripDevParams + history.replaceState) before reloading, so the stored
value wins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a capability-gated Beheer section to the dashboard listing the admin pages
(Huisstijl, Stamdata, Aanvragen), each shown when the principal holds its
capability — so admin pages are discoverable, not URL-only. Extract the admin
link list to shared/layout/admin-links.ts as one source of truth, reused by the
site header (was ADMIN_NAV_ITEMS) and the new section. Capability-gated, never
role-derived (PRD-0002).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Admin-only overview of all cases across owners + an admin delete, gated by a new
`cases:manage` capability (Authz role→cap + CanManageCases + CasesAdmin gate;
FE capability + guard + nav + role.interceptor prefix — the org-template/stamdata
recipe). Backend adds ApplicationStore.ListAll()/DeleteAny() and GET /admin/cases +
DELETE /admin/cases/{id}; admin delete removes ANY case incl. submitted. Page lives
in registratie/ui (owns the Aanvraag aggregate; reuses aanvraag-view + parse),
routed /beheer/zaken; delete guarded by a native confirm, optimistic with rollback.
Typed client regenerated (documents the new endpoints + owner field).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make "at most one unsubmitted Concept per type" a server invariant instead of a
client-only convenience. ApplicationStore.Create → CreateConcept guards atomically
under the write gate and POST /applications returns 409 when a duplicate would be
created. The FE draft-sync recovers from the 409 by adopting the existing Concept
(ensureId → findConcept) rather than erroring — one-per-type means the second
attempt lands on the existing draft. Typed client regenerated (documents the 409).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restyle the contact-change form with house CIBG surfaces instead of hand-rolled
markup: the read-only BRP address now uses app-data-block/app-data-row (grey
datablock), and the phone field sits in a plain <fieldset> inside the existing
.form-horizontal form (inherits the CIBG grey box). No new CSS surfaces; row keys
reuse the address.* i18n ids. Logic/machine/specs unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reshape the adreswijziging form into a contact-change form: the BRP address is
authoritative and shown read-only (you change it at the gemeente), and the phone
number becomes the editable/submittable field. New Telefoonnummer value object
(parse-don't-validate); backend RejectPhoneChange re-validates as authority.
POST /change-requests now carries { telefoon } (typed client regenerated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface the ?scenario= and ?role= dev stand-ins as dropdowns in the existing
debug-state devtool, so a demo can flip them with a click instead of editing the
URL. scenario.ts/role.ts gain set* setters + exported valid-value lists (reused
by the panel, no duplicated source of truth); scenario becomes tab-sticky like
role so it survives navigation. Applied via location.reload() since both are
read per-request in interceptors. Extends the debug-state eslint exemption to the
ui→infrastructure rule (same devtool precedent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the WP-31 createHistory helper into StamdataStore: per-table undo/redo
over the edited rows, recording only real edits and restoring via the existing
Seed msg. Ctrl/Cmd+Z / +Shift+Z, deferring to native text-undo inside grid
cell inputs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit "apply high-value": extract four shared helpers into shared/application/ and
rewire the editor stores (behaviour unchanged, existing specs are the gate):
- action-state.ts: ActionState/SaveState (were duplicated in both brief stores).
- history.ts: createHistory<T> (extracted from BriefStore's WP-27 undo/redo; WP-32 reuses).
- debounced-save.ts: createDebouncedSave (the 600ms timer/PendingSave dance, was 2×+).
- machine-remote-data.ts: machineRemoteData (the loading/failed/loaded→RemoteData switch, 3×).
Each helper has a co-located spec. Deferred DDD findings (contracts/ inconsistency, a
parse* traverse combinator, the 6× Seed boilerplate) are reported in the WP file, not built.
npm run ci green; 323 tests (+13 helper specs); brief/org-template/stamdata specs unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header now injects AccessStore (→ MeAdapter → ApiClient) for its admin links.
f719676 stubbed that in the site-header story but missed shell.stories.ts, which
renders the header via ShellComponent — with only provideRouter it threw NG0201
(no ApiClient provider), failing the Shell smoke-test and turning storybook-a11y
red on CI (deterministic, not the local worker-contention timeouts). Stub
AccessStore there too (can() → false, no admin links).
Verified: full test-storybook suite single-worker now 60 suites / 170 tests green.
Lesson: when a shared component gains a service dep, every story that renders it —
directly OR via a parent template — needs the provider.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin pages (/beheer/stamdata, /brief/huisstijl) were unreachable in the browser,
for three compounding reasons — all fixed here:
- **Guard raced /me.** capabilityGuard read can() synchronously while /me was still
loading, so it denied even an entitled admin (deny-by-default) and bounced to /login.
It's now async: awaits AccessStore.whenReady() (new — resolves once /me settles), then
allows if entitled; an authenticated-but-unentitled user goes to /dashboard, anonymous
to /login. + auth.guard.spec (the missing test that let this ship).
- **Dev role wasn't sticky.** currentRole() read ?role= from the URL on every request,
but login/nav drop the param, silently reverting admin→drafter mid-session and 403-ing
the admin endpoints. It now persists the role per-tab (sessionStorage), so every
role-aware request keeps it. Dev-only (the interceptor is wired only under isDevMode).
- **No way in.** Added capability-gated Huisstijl + Stamdata links to the header (shown
only when /me grants the cap); injecting AccessStore there also warms /me early. New
en translations for the two labels; site-header story stubs AccessStore (+ AsAdmin
variant) so it needs no HTTP.
Verified live: with ?role=admin the header shows both links, clicking Stamdata loads the
grid (GET /api/v1/stamdata → 200, was 403→redirect); a non-admin sees no link. Full
`npm run ci` green (310 tests); site-header stories pass axe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stamdata admin editor's adapter calls /api/v1/stamdata via the generated
ApiClient → roleInterceptor, but ROLE_AWARE omitted /api/v1/stamdata, so no X-Role
was sent and the backend StamdataAdmin gate resolved Drafter → 403 on every read
(confirmed: 403 without X-Role, 200 with X-Role: admin). Added /api/v1/stamdata to
the allow-list (same class of gap WP-23 fixed for /me) + a roleInterceptor spec so
the next admin endpoint isn't forgotten.
Note: a separate issue still blocks the page in the browser — capabilityGuard
redirects both admin routes to /login because it checks can() before /me resolves;
tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`npm run format:check` (a CI gate) had drifted red across 44 files — pre-existing
files plus recently-added ones committed without formatting. Ran `prettier --write .`;
no logic changes. Also regenerates documentation.json (compodoc reflects the reformatted
component sources).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the last-mile autosave gap: a debounced edit made in the final <600ms
before leaving a page was lost — the wizard draft-sync timer is cleared on
destroy without flushing, and root stores keep an armed timer the teardown
ignores.
New `shared/application/pending-saves.ts`: a root `PendingSaves` registry every
autosave owner joins (BriefStore, OrgTemplateStore, each createDraftSync). Two
seams flush through it — `flushPendingGuard` (CanDeactivate, on the five
autosave routes) awaits the pending write before an in-app route change; a
`beforeunload` handler (provideUnloadFlush) fires it best-effort and raises the
browser's native unsaved-changes prompt. ponytail: the HTTP seam is Angular
HttpClient (no keepalive/sendBeacon), so a hard-close flush can't be guaranteed
— hence the prompt; upgrade path noted in a comment. Each owner now nulls its
timer handle on fire so `hasPendingSave()` is accurate, and exposes
`flushPending()`.
Verified live against the running stack: navigating away 91ms after a keystroke
(well inside the debounce) fires one PUT /brief before the route changes; a
dirty reload raises the prompt, a clean reload does not. FE lint / check:tokens
/ 299 tests (+11) / build / build-storybook green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The green "nieuw" badge used dark text on groen-500 (#39870c) = 3.41:1,
failing WCAG AA 4.5:1 (axe, WithDiff story). Dark text passes on the orange
"changed" badge (4.79:1) but no green passes it. Give the added badge white
text on groen-700 (#176e1b) = 6.4:1; the changed badge is unchanged. The
prior comment's blanket "dark text, not white" was correct for orange but
wrong for green. Full storybook axe gate now green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Realizes ADR-0004's "future low-code editor that commits a PR": an
admin-only stamdata maintenance editor built on the stamdata-as-code
foundation.
Backend: `professions` moves from a hardcoded C# dictionary to an embedded
`professions.json` data-file (typed as `ProfessionMapping`) with valid-time
(geldigVan/geldigTot, half-open). A generic, reflection-driven
StamdataCatalog/StamdataTable/StamdataFile describes every table so one
endpoint pair + one grid editor serve all of them; add a table in one line.
Two read-only, admin-gated endpoints (GET /stamdata, GET /stamdata/{table}
?peildatum=) — no runtime write path. Generic build gate
`Every_catalog_table_is_valid` (keys non-blank, no overlapping validity,
well-formed windows).
Frontend: new `beheer` context (route beheer/stamdata, capabilityGuard
'stamdata:edit'). A schema-driven grid editor edits rows locally; download()
emits {table}.json for the admin to commit as a reviewed PR (no mutation
command — the CI build + StamdataValidationTests stay the authority).
Full gate GREEN both sides; gen:api leaves no drift; new stamdata story
passes axe. See WP-29 + ADR-0004.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move working docs (backlog, prd, roadmap) under docs/project/ and durable
docs (architecture ADRs, guides, audits) under docs/reference/; add a
docs/README.md index. Update every path reference in code comments, CLAUDE.md,
READMEs, and the new-ssp skill. Expand the Storybook Foundations curriculum
(Overview, BDD, i18n; rename Layers→Domain-Driven Design) and reorder the sidebar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One behavior per test across FE machine/store specs and backend endpoint
tests, so a failure names exactly what broke.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compose the herregistratie letter from the besluit instead of a library hunt:
the behandelaar picks positief/negatief (+ reden-checkboxes for a negatief) and
the kern's standaardteksten follow the selection live.
Front-end (this increment):
- Kern is recomposed reactively from the besluit selection (new BesluitSelected
machine msg + composeKern); the "Genereer conceptbrief" button is gone. The
drafter's free text is preserved across a selection change.
- The editor shows only the editable sections; the locked aanhef/slot render in
the preview, not the authoring surface. Slot is a case-type template section
(per templateId), documented as such.
- The panel re-seeds from the letter via inferSelection() — the besluit + redenen
are read back off the kern's passage blocks, so the selection survives reload
with no new wire fields (derive, don't store).
- letter-section drops the now-redundant per-section passage picker (besluit owns
standaardteksten); keeps free-text + block edit/move/remove.
Fix: app-checkbox now falls back to a unique per-instance id when checkboxId is
omitted. The CIBG styled checkbox routes clicks through the label, so the shared
id="undefined" made every reason label toggle the first input — the second
checkbox could never be checked. Verified live (Playwright): each reason toggles
independently.
Backend/seam (brief v3 WIP): besluit/reason passage tags on the wire + seed,
carried through the adapter parse boundary.
Specs updated (besluit, brief.machine) and the affected stories re-pointed at the
new API. FE lint + build + 253 vitest specs green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix: address-fields shipped a scoped `fieldset { padding:0; margin:0 }` that
tied on specificity with CIBG's `.form-horizontal fieldset` and, injected later,
won — flattening the grey-box padding on "Mijn gegevens" and the registratie
wizard address step. Removed the override (kept the legend rule).
Audit cleanup (minimise atoms, converge or document near-duplicates):
- delete dead `app-card` (+ story; zero consumers, superseded by app-data-block)
and the unused `.app-info-box` utility
- review-section raw <h2> -> app-heading; registratie-wizard lone <dl> -> app-data-block
- keep application-link's raw <h3> (vendored `.applications li a h3` chain needs
the bare element) with an inline justification
- document the deliberate "don't merge these" verdicts in a new Convergence
decisions table in atomic-design.mdx; record the card removal in cibg-gaps.mdx
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wizard steps rendered bare .form-group divs, so CIBG's
".form-horizontal fieldset { background:#f1f5f9; margin-bottom:1.25em }"
never matched and inputs showed on white instead of the grey CIBG surface.
Wrap each logical field group per step in a <fieldset> (intake, herregistratie
and registratie wizards); CIBG then gives every group its grey surface with a
1.25em gap between groups. The shell stays group-agnostic (no outer fieldset,
which would hide the white gaps). address-fields already used a <fieldset>.
Adds intake-wizard.component.spec.ts asserting the buitenland step renders its
groups as separate fieldsets (guards against the wrapping being dropped again).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Edit the letter's org identity in place on the same canvas the drafter composes
on (editableRegions='template'): letterhead/signature/footer become inline
controls, content a read-only sample. Margins (bounded), logo upload (reuses the
shared upload transport + single-upload), version history + rollback, proefbrief,
and publish-with-impact-confirmation. House form-machine idiom
(org-template.machine.ts) + root store with debounced save. Capability-gated
(orgtemplate:edit) with a deny-by-default alert; route /brief/huisstijl.
Backend + generated client were already in place (WP-23). Also fixes a
pre-existing red check:tokens (WP-24 canvas hex fallbacks) and threads the
published logo through to the drafter's canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds LetterHtml.Render, a pure composer mirroring the FE letter canvas'
class vocabulary, behind two ExcludeFromDescription()'d endpoints
(GET /brief/preview, GET /admin/org-template/{subOrgId}/preview).
Auto-resolvable placeholders pull from seed/case data; unresolved
manual ones render as "[NOG IN TE VULLEN: label]". A sent brief
archives its composed HTML (BriefEntity.ArchivedHtml) so a later
org-template republish never changes it. FE gets a hand-written fetch
adapter (text/html, not JSON) and a "Voorbeeld" button that opens the
preview in a new tab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One letter surface for every role: LetterCanvasComponent renders the
org template's letterhead/signature/footer around the case-type
sections, with editableRegions content|template|none. public/letter.css
is the FE⇄BE rendering contract (WP-25 inlines it verbatim).
letter-preview deleted — its read-only rendering absorbed into 'none'
mode. brief.machine.ts byte-identical; orgTemplate parses at the
adapter boundary and lives beside the machine in BriefStore.
Also fixes passage-picker multi-select (checkboxes all shared
id="undefined", so labels only toggled the first box) and keeps the
±page-break marks from drawing through canvas content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second template axis (org identity: letterhead, footer, signature,
margins) server-side: OrgTemplateStore with JSON version history,
publish/rollback, sent-brief version pinning, admin role + capability,
5 admin endpoints, org-logo upload category. FE seam widened only
(Role/Capability unions, interceptor); WP-24/26 consume it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm update brought every package to the latest version its existing package.json
range allows (Angular tooling 22.0.2/22.0.4 -> 22.0.5, prettier 3.8.4 -> 3.9.4,
typescript-eslint 8.62.0 -> 8.62.1); package.json itself needed no range changes.
Auditing actual deprecation warnings (not just outdated versions) found nothing
further to fix: @angular/platform-browser-dynamic and @angular-devkit/build-angular
are deprecated by Angular but still required peer dependencies of the latest
published @storybook/angular (10.4.6 — peer range still `>=18.0.0 < 22.0.0`,
already why .npmrc sets legacy-peer-deps); jest-process-manager/expect-playwright
are transitive-only through @storybook/test-runner's latest stable (0.24.4). No
newer version of either Storybook package exists yet that drops them. The
remaining npm audit advisory (@babel/core, low severity) is the same
already-documented, deliberately-left issue in README.md (fixing it downgrades
Angular). Left package.json's overrides untouched.
The prettier bump alone changed formatting opinions on files this session didn't
otherwise touch (a stale markdown italics marker, a few object-literal wrap
points) — reformatted everything so `format:check` (part of CI) doesn't regress.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Correlation id becomes real ASP.NET Core middleware instead of a per-endpoint
read: every request gets one (client-supplied or generated), it's echoed as
an X-Correlation-Id response header, and pushed into the logging scope so
every log line for that request carries it — not just the Submit helper's,
verified against LogBrief which never threads it explicitly.
Idempotency-Key moves from per-HTTP-attempt (defeating its own purpose) to
per-logical-submit: runSubmit mints one key and threads it through a small
bridge (withIdempotencyKey/currentIdempotencyKey) since the NSwag-generated
client has no per-call header hook. Backend gains an IdempotencyStore that
short-circuits a replayed key to the first call's result instead of minting
a second reference — scoped to the Submit-helper endpoints per the WP's own
decision.
GET requests now retry transient failures (rxjs retry({count:2, delay:500}));
writes never auto-retry. Proven with a fake-HttpClient spec
(api-client.provider.spec.ts) rather than a manual network-tab check — the
WP's suggested `?scenario=error` check turned out not to exercise a real
network call at all (the interceptor throws before calling next()), so the
automated test is the actual proof.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a happy-path spec (login → dashboard → registratie wizard, including
a real identity-document upload → real submit) and a degraded-path spec
(?scenario=error → <app-async> error slot → retry), both driving the real
app against the real .NET backend, plus a CI job that boots both.
Writing the retry spec surfaced a real bug: AsyncComponent's retry() only
reloads a [resource]-fed instance, so every real page (all [data]-fed via
a store's RemoteData) had a silently no-op retry button. Added a
retryClicked output and wired it on the dashboard's two async blocks.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds route-change focus management (new page's h1, afterNextRender) plus
scroll-position restoration wired once in app.config.ts; angular-eslint's
templateAccessibility bundle linting every inline template via
processInlineTemplates (verified firing with a planted violation, one real
hit fixed in rich-text-editor); docs/wcag-checklist.md and Foundations/
Accessibility MDX tying the four a11y layers (axe, lint, play tests,
manual checklist) together. The checklist pass already earned its keep —
it found a real 320px overflow in aanvraag-block's warning alert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires text-input's aria-describedby to the form-field description div
(the BSN hint was rendered but never announced), pins desc-before-error
ordering, and switches alert to role=alert for errors vs role=status
for info/ok/warning. Composition contract enforced by story play tests
(form-field+text-input, alert per variant) run in the WP-01 CI gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add the 7 stories CLAUDE.md's testing rule ("UI is exercised via Storybook
stories") was missing: shared/layout/shell (Design System/Templates/Shell)
and all six previously-unstoried brief components (passage-picker,
rejection-comments, diagnostics-panel, letter-block, letter-preview,
letter-section — Domein/Brief/*), each with a default state plus the
meaningful variants (locked/editable, findings/clean, show/entry, etc).
Every *.component.ts in the repo now has a co-located story; *.page.ts
files stay unstoried, matching the existing norm.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Retitle all 49 stories into a sidebar that makes the DDD seam visible:
Foundations (curriculum) -> Design System (Atoms/Molecules/Organisms/
Templates/Devtools, everything in shared/ui + shared/layout) -> Domein
(Registratie/Herregistratie/Auth/Brief, everything in a context's ui/).
Pin the order via storySort. Add layers.mdx explaining the split and
linking the enforcing eslint rules; document the story-title convention
in CLAUDE.md. Fix a stale "status banner" reference in atomic-design.mdx
left over from WP-13's upload-status-banner deletion.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mark every hand-rolled shared/ui surface with a `// CIBG-GAP EXTENSION:`
comment + `cibgGap` story parameter (skeleton, spinner, rich-text-editor,
wizard-shell's error summary, application-link's non-navigating row,
debug-state, status-badge, card, placeholder-chip) so deviations from the
CIBG design system are auditable. Add the register MDX
(Foundations/CIBG Gap Register), cross-linked from ADR-0003. Delete the
near-identity upload-status-banner wrapper; its one consumer now uses
<app-alert> directly (a story added to keep the info-banner state covered).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>