refactor: strip WP-/RB- ticket refs from apps and libs (RD-18)

204 WP-NN/RB-NN comments named a closed ticket instead of the code they
sit next to. git blame already records history and stays correct when
code moves; the comment does not. This sweep removes the reference and
keeps the sentence, across 95 files in apps/ and libs/ plus the
behaviour-spec generator's header text.

Eleven references stay: five story files justify an a11y disable per
the README's rule, and one line in a11y.mdx documents that convention.
Two sentences needed a rewrite, not a deletion, so the reference's
meaning survives its removal. behaviour-spec.mdx is regenerated, not
hand-edited.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-04 21:23:07 +02:00
co-authored by Claude Sonnet 5
parent 3895588b9a
commit dd11eafe50
102 changed files with 361 additions and 197 deletions
+3 -3
View File
@@ -20,7 +20,7 @@ export const routes: Routes = [
},
{
path: 'aanvraag/:id',
// Same capability the werkvoorraad list itself is gated by (WP-64/65) — the
// Same capability the werkvoorraad list itself is gated by — the
// detail page is reachable only from a row already filtered to that capability.
canActivate: [capabilityGuard('aanvraag:beoordelen')],
loadComponent: () =>
@@ -36,14 +36,14 @@ export const routes: Routes = [
},
{
path: 'beheer/audit',
// Admin-only authz/PII-reveal audit trail (WP-41/42). capabilityGuard denies-by-default
// Admin-only authz/PII-reveal audit trail. capabilityGuard denies-by-default
// unless GET /me resolved `cases:manage` (reused for audit read). Backend re-enforces.
canActivate: [capabilityGuard('cases:manage')],
loadComponent: () => import('@beheer/ui/audit.page').then((m) => m.AuditPage),
},
{
path: 'beheer/functies',
// Admin-only feature-flag toggles (WP-47), gated by `flags:manage`.
// Admin-only feature-flag toggles, gated by `flags:manage`.
canActivate: [capabilityGuard('flags:manage')],
loadComponent: () =>
import('@beheer/ui/feature-flags.page').then((m) => m.FeatureFlagsPage),
@@ -4,7 +4,7 @@ import { MEDEWERKER_ID, currentRollen } from './medewerker';
/**
* Infrastructure: resolves the current medewerker identity into a `Principal`
* (ADR-C-004/RB-13). Stands in for a real employee-SSO redirect flow (ADR-0002 §3,
* (ADR-C-004). Stands in for a real employee-SSO redirect flow (ADR-0002 §3,
* "out of scope here") — there is no credential to enter and, unlike `DigidAdapter`'s
* BSN check, no format to reject, so `authenticate()` takes no input and returns the
* `Principal` directly rather than a `Result` with an error variant that can never
@@ -2,7 +2,7 @@ import { Component, output } from '@angular/core';
import { ButtonComponent } from '@shared/ui/button/button.component';
/**
* Organism: employee-SSO-style mock login (ADR-C-004/RB-13). No real auth — and,
* Organism: employee-SSO-style mock login (ADR-C-004). No real auth — and,
* unlike the SSP's DigiD form, no credential to enter at all: a Behandelaar has no
* BSN, and this app has no password of its own to check either way. There is
* nothing to compose beyond one button, which is itself evidence for the ADR — the
@@ -8,7 +8,7 @@ import {
type Err = Error | undefined;
/** One aanvraag's beoordeling detail (WP-65) — a root singleton like `WerkvoorraadStore`.
/** One aanvraag's beoordeling detail — a root singleton like `WerkvoorraadStore`.
Keyed by id: navigating to a different case resets to Loading. */
@Injectable({ providedIn: 'root' })
export class BeoordelingStore {
@@ -8,7 +8,7 @@ import {
type Err = Error | undefined;
/** The behandelaar's queue (WP-64) — a root singleton like `AdminCasesStore`'s ssp
/** The behandelaar's queue — a root singleton like `AdminCasesStore`'s ssp
counterpart. Fetch + parse at the trust boundary, publish as RemoteData. */
@Injectable({ providedIn: 'root' })
export class WerkvoorraadStore {
@@ -2,8 +2,8 @@ import { formatDatumNl } from '@shared/kernel/datum';
import { AanvraagType } from './werkvoorraad-item';
import { BeoordelingStatus, BeoordelingView } from './beoordeling';
/** View-model mapping shared by the werkvoorraad list (WP-64) and the beoordeling
detail screen (WP-65): type/status → labels. Pure, no Angular. Lives here (not in
/** View-model mapping shared by the werkvoorraad list and the beoordeling
detail screen: type/status → labels. Pure, no Angular. Lives here (not in
`werkvoorraad-item-view.ts`) because `BeoordelingStatus` is the wider of the two
status unions — `werkvoorraad-item-view.ts` re-exports these for its own use. */
@@ -1,8 +1,8 @@
import { AanvraagType } from './werkvoorraad-item';
/**
* A case's full status lifecycle as the beoordeling detail screen sees it (WP-65)
* wider than `WerkvoorraadStatus` (WP-64), which only ever sees the two "still open"
* A case's full status lifecycle as the beoordeling detail screen sees it —
* wider than `WerkvoorraadStatus`, which only ever sees the two "still open"
* tags. This is the same five-tag union ssp's `AanvraagStatus` models (minus `Concept`
* — the detail endpoint 404s a Concept, it isn't a case a behandelaar can treat yet).
*/
@@ -1,6 +1,6 @@
import { Result, assertNever } from '@shared/kernel/fp';
/** The three actions the beoordeling screen offers a behandelaar (WP-65b) — mirrors the
/** The three actions the beoordeling screen offers a behandelaar — mirrors the
backend's `Besluit` enum member names 1:1 (the wire convention: a string, not a raw
enum — see `RecordBesluitRequest`). */
const BESLUIT_TAGS = ['Goedkeuren', 'Afwijzen', 'MeerInfoOpvragen'] as const;
@@ -1,5 +1,5 @@
/**
* A queue entry as the behandelportal sees it (WP-64) — the parsed, domain-side view
* A queue entry as the behandelportal sees it — the parsed, domain-side view
* of the backend's cross-owner `GET /werkvoorraad`. Pure types, no Angular.
*
* The status union is narrower than the SSP's full `AanvraagStatus` (ssp's
@@ -13,7 +13,7 @@ import {
import { AanvraagType } from '@behandeling/domain/werkvoorraad-item';
/**
* Infrastructure adapter for the beoordeling detail read (WP-65) — the only place its
* Infrastructure adapter for the beoordeling detail read — the only place its
* HTTP lives (ADR-0001 anti-corruption boundary). The untrusted response is validated +
* mapped to domain by the parse* boundary below.
*/
@@ -3,7 +3,7 @@ import { ApiClient } from '@shared/infrastructure/api-client';
import { Valid } from '@behandeling/domain/besluit.machine';
/**
* Infrastructure adapter for recording a behandelaar's decision (WP-65b) — the single
* Infrastructure adapter for recording a behandelaar's decision — the single
* place its HTTP lives. No return value: a successful call means the server accepted
* the transition; the caller reloads `BeoordelingStore` to see the new status (the
* server, not this adapter, re-validates and is the authority).
@@ -8,7 +8,7 @@ import {
} from '@behandeling/domain/werkvoorraad-item';
/**
* Infrastructure adapter for the behandelportal's queue read (WP-64) — the only
* Infrastructure adapter for the behandelportal's queue read — the only
* place its HTTP lives (ADR-0001 anti-corruption boundary). The untrusted response
* is validated + mapped to the (narrower) queue domain shape by the parse* boundary
* below; a case whose status isn't `Ingediend`/`InBehandeling` is a parse error, not
@@ -1,7 +1,7 @@
import { Component, input } from '@angular/core';
import { BeoordelingDocument } from '@behandeling/domain/beoordeling';
/** Organism: the documents linked to an aanvraag (WP-65) — plain links to the existing
/** Organism: the documents linked to an aanvraag — plain links to the existing
(pre-existing, unauthenticated — same as ssp's own document previews) content
endpoint. No new shared atom: a context-local list, not a reusable building block. */
@Component({
@@ -14,8 +14,8 @@ import { BeoordelingDocumentenComponent } from '@behandeling/ui/beoordeling-docu
import { BesluitFormComponent } from '@behandeling/ui/besluit-form/besluit-form.component';
/**
* Page: one aanvraag's beoordeling detail (WP-65). The werkvoorraad list (WP-64) links
* here. `canBesluiten` (server-computed, ADR-0001) gates the decision form (WP-65b)
* Page: one aanvraag's beoordeling detail. The werkvoorraad list links
* here. `canBesluiten` (server-computed, ADR-0001) gates the decision form —
* the page never recomputes the lifecycle itself. On a recorded decision the form emits
* `decided`, and the page just reloads (the server is the authority on the new status).
*/
@@ -14,7 +14,7 @@ import { BesluitState, BesluitMsg, initial, reduce } from '@behandeling/domain/b
import { createSubmitBesluit } from '@behandeling/application/submit-besluit';
/**
* Organism: the decision form (WP-65b) — goedkeuren/afwijzen/meer-info-opvragen. Same
* Organism: the decision form — goedkeuren/afwijzen/meer-info-opvragen. Same
* idiom as every other form in this house (`change-request-form`): all state in one
* signal driven by the pure `reduce` (besluit.machine.ts), submitted via a `submit-*`
* command returning `Result`. The server re-validates the transition and is the
@@ -4,9 +4,9 @@ import { ApplicationLinkComponent } from '@shared/ui/application-link/applicatio
import { WerkvoorraadItem } from '@behandeling/domain/werkvoorraad-item';
import { werkvoorraadRow } from '@behandeling/domain/werkvoorraad-item-view';
/** Organism: the behandelaar's queue as CIBG "aanvragen" rows (WP-64) — composition
/** Organism: the behandelaar's queue as CIBG "aanvragen" rows — composition
of the two existing shared/ui molecules, no new atom. Each row links to the
beoordeling detail page (WP-65). */
beoordeling detail page. */
@Component({
selector: 'app-werkvoorraad-list',
imports: [ApplicationListComponent, ApplicationLinkComponent],
@@ -10,10 +10,10 @@ import { WerkvoorraadStore } from '@behandeling/application/werkvoorraad.store';
import { WerkvoorraadListComponent } from '@behandeling/ui/werkvoorraad-list/werkvoorraad-list.component';
/**
* Page: the behandelaar's werkvoorraad (WP-64) — the behandelportal's landing page.
* Page: the behandelaar's werkvoorraad — the behandelportal's landing page.
* Deny-by-default capability gate (`aanvraag:beoordelen`), same idiom as ssp's
* AdminCasesPage: a denial alert for a non-behandelaar, the queue for one. Opening
* a case's detail is out of scope here (WP-65).
* a case's detail is out of scope here.
*/
@Component({
selector: 'app-werkvoorraad-page',
@@ -69,7 +69,7 @@ export class WerkvoorraadPage {
private loadRequested = false;
constructor() {
// Load once the capability resolves to allowed (a 403 GET would be wasted otherwise) —
// same guard-against-the-loop idiom as AdminCasesPage (WP-26 lesson).
// same guard-against-the-loop idiom as AdminCasesPage.
effect(() => {
if (this.canBeoordelen() && !this.loadRequested) {
this.loadRequested = true;
@@ -8,7 +8,7 @@ export const NAV_ITEMS: readonly HeaderNavItem[] = [
/** This app's admin pages — provided to the shared site header via HEADER_ADMIN_LINKS.
No huisstijl (that's the SSP's brief context) or zaken entry — inherited as-is from
WP-61's bootstrap trim, not revisited by this migration. */
the bootstrap trim, not revisited by this migration. */
export const ADMIN_LINKS: readonly AdminLink[] = [
{
label: $localize`:@@header.nav.stamdata:Stamdata`,
+4 -4
View File
@@ -61,7 +61,7 @@ export const routes: Routes = [
},
{
path: 'brief/huisstijl',
// Admin-only org-template editor (WP-26): capabilityGuard denies-by-default
// Admin-only org-template editor: capabilityGuard denies-by-default
// unless GET /me resolved `orgtemplate:edit` (Admin role). Backend re-enforces
// via the OrgAdmin gate — the guard just avoids loading a page that would 403.
canActivate: [capabilityGuard('orgtemplate:edit')],
@@ -78,7 +78,7 @@ export const routes: Routes = [
},
{
path: 'beheer/zaken',
// Admin-only cases overview + delete (WP-36): capabilityGuard denies-by-default
// Admin-only cases overview + delete: capabilityGuard denies-by-default
// unless GET /me resolved `cases:manage` (Admin role). Backend re-enforces via the
// CasesAdmin gate — the guard just avoids loading a page that would 403. The page
// lives in registratie/ui (which owns the Aanvraag aggregate); routed under /beheer.
@@ -88,14 +88,14 @@ export const routes: Routes = [
},
{
path: 'beheer/audit',
// Admin-only authz/PII-reveal audit trail (WP-41/42). capabilityGuard denies-by-default
// Admin-only authz/PII-reveal audit trail. capabilityGuard denies-by-default
// unless GET /me resolved `cases:manage` (reused for audit read). Backend re-enforces.
canActivate: [capabilityGuard('cases:manage')],
loadComponent: () => import('@beheer/ui/audit.page').then((m) => m.AuditPage),
},
{
path: 'beheer/functies',
// Admin-only feature-flag toggles (WP-47), gated by `flags:manage`.
// Admin-only feature-flag toggles, gated by `flags:manage`.
canActivate: [capabilityGuard('flags:manage')],
loadComponent: () =>
import('@beheer/ui/feature-flags.page').then((m) => m.FeatureFlagsPage),
@@ -7,7 +7,7 @@ import { Principal } from '../domain/principal';
@Injectable({ providedIn: 'root' })
export class DigidAdapter {
// ponytail: fake DigiD — any elfproef-valid BSN authenticates to a fixed identity.
// Real BSN validation (parseBsn, WP-40) is the trust boundary; swap the fixed identity
// Real BSN validation (parseBsn) is the trust boundary; swap the fixed identity
// for a real OIDC redirect flow when there's an IdP.
async authenticate(bsn: string): Promise<Result<string, Principal>> {
const r = parseBsn(bsn);
@@ -54,7 +54,7 @@ const caseContext: CaseContext = {
const view: BriefView = { brief, availablePassages: [], decisions, orgTemplate, caseContext };
/** A recording fake of BLOB_PRESENTER (RB-28/TE-006) — records every call instead of
/** A recording fake of BLOB_PRESENTER (TE-006) — records every call instead of
touching the DOM, so a spec can assert a command's success path directly. */
function fakeBlobPresenter() {
const opened: Blob[] = [];
@@ -158,7 +158,7 @@ describe('BriefStore action state (Idle | Busy | Failed)', () => {
});
});
// --- WP-27: undo/redo history + rejection diff ---
// --- Undo/redo history + rejection diff ---
function block(id: string, text: string): LetterBlock {
return {
@@ -308,7 +308,7 @@ describe('BriefStore rejection diff', () => {
describe('BriefStore.previewLetter', () => {
afterEach(() => vi.restoreAllMocks());
it('opens the composed letter via BLOB_PRESENTER on success (RB-28)', async () => {
it('opens the composed letter via BLOB_PRESENTER on success', async () => {
const { presenter, opened } = fakeBlobPresenter();
const store = setup(
{
@@ -412,11 +412,11 @@ describe('BriefStore.flushPending (CanDeactivate guard / beforeunload)', () => {
});
});
// --- RB-22 (CQ-007 expand half): a 404 from GET /brief tolerates by calling the
// existing reset() command, exactly once. Today's backend never 404s (RB-23 adds
// that); this fake adapter is what exercises the branch until then. ---
// --- CQ-007's expand half: a 404 from GET /brief tolerates by calling the
// existing reset() command, exactly once. Today's backend never 404s yet;
// this fake adapter is what exercises the branch until then. ---
describe('BriefStore.load — 404 tolerance (RB-22)', () => {
describe('BriefStore.load — 404 tolerance', () => {
const notFound: Result<BriefLoadFailure, BriefView> = { ok: false, error: { tag: 'notFound' } };
const resetOk: Result<string, BriefView> = { ok: true, value: view };
@@ -55,8 +55,8 @@ export class BriefStore implements PendingSave {
/** Surfaced autosave state for the indicator + aria-live region. */
readonly saveState = signal<SaveState>({ tag: 'Idle' });
/** Undo/redo is SHELL state, not machine state (WP-27): a `createHistory` stack of
`Brief` snapshots (WP-31 extracted the mechanics). Only CONTENT edits are recorded
/** Undo/redo is SHELL state, not machine state: a `createHistory` stack of
`Brief` snapshots (the mechanics live in a shared helper). Only CONTENT edits are recorded
(they flow through `edit()`); status transitions never enter history, or undo would
replay workflow state. Restore re-dispatches the existing `Seed` Msg — zero machine
changes. */
@@ -64,7 +64,7 @@ export class BriefStore implements PendingSave {
readonly canUndo = this.history.canUndo;
readonly canRedo = this.history.canRedo;
/** The letter as it stood when it was REJECTED, captured shell-side (WP-27). The
/** The letter as it stood when it was REJECTED, captured shell-side. The
approver diffs it against the resubmitted letter. POC limit: in-memory only, so a
full page reload loses it — a real system would persist the rejected revision. */
private rejectionSnapshot = signal<Brief | null>(null);
@@ -81,7 +81,7 @@ export class BriefStore implements PendingSave {
);
readonly hasRejectionDiff = computed(() => this.blockDiffs().size > 0);
/** The org template the letter renders with (WP-24). Server-owned appearance data,
/** The org template the letter renders with. Server-owned appearance data,
not letter state — held beside the machine, never inside it (`brief.machine.ts`
stays untouched by design). Set from every server view that carries it. */
readonly orgTemplate = signal<OrgTemplate | null>(null);
@@ -125,7 +125,7 @@ export class BriefStore implements PendingSave {
return !!b && canSubmit(b) && !hasBlockingErrors(this.diagnostics());
});
/** True once a 404-triggered recovery has been attempted (RB-22, CQ-007's expand
/** True once a 404-triggered recovery has been attempted (CQ-007's expand
half — see `recoverFromMissingBrief`). This is the structural once-only bound:
a repeated 404 falls straight to the `error` branch below and can never reach
`adapter.reset()` a second time, regardless of how many times `load()` runs. */
@@ -200,7 +200,7 @@ export class BriefStore implements PendingSave {
}
// 600ms debounced autosave (the server is the store of record). Timer mechanics live in
// the shared helper; `flushSave` below is the store-specific write + save-state (WP-31).
// the shared helper; `flushSave` below is the store-specific write + save-state.
private debouncedSave = createDebouncedSave({
canSave: () => this.canEdit(),
flush: () => this.flushSave(),
@@ -223,7 +223,7 @@ export class BriefStore implements PendingSave {
}
}
/** Retry a failed autosave — reuses the existing flush path, no new state (WP-27). */
/** Retry a failed autosave — reuses the existing flush path, no new state. */
retrySave() {
void this.flushSave();
}
@@ -311,7 +311,7 @@ export class BriefStore implements PendingSave {
this.store.dispatch({ tag: 'Approved', by: s.approvedBy, at: s.approvedAt, decisions });
break;
case 'rejected':
// Capture the letter as-rejected for the resubmission diff (WP-27). This is the
// Capture the letter as-rejected for the resubmission diff. This is the
// "before" snapshot the approver later compares against.
this.rejectionSnapshot.set(brief);
this.store.dispatch({
@@ -32,7 +32,7 @@ const subOrgs: SubOrgSummary[] = [
{ subOrgId: 'cibg-registers', orgName: 'CIBG', publishedVersion: 1 },
];
/** A recording fake of BLOB_PRESENTER (RB-28/TE-006) — records every call instead of
/** A recording fake of BLOB_PRESENTER (TE-006) — records every call instead of
touching the DOM, so a spec can assert a command's success path directly. */
function fakeBlobPresenter() {
const opened: Blob[] = [];
@@ -70,10 +70,10 @@ function setup(
return TestBed.inject(OrgTemplateStore);
}
// --- RB-28 (TE-006): proefbrief() ends in BLOB_PRESENTER.open, not a raw
// --- TE-006: proefbrief() ends in BLOB_PRESENTER.open, not a raw
// window.open(URL.createObjectURL(...)) call, so both outcomes are assertable. ---
describe('OrgTemplateStore.proefbrief (RB-28)', () => {
describe('OrgTemplateStore.proefbrief', () => {
it('opens the rendered proefbrief via BLOB_PRESENTER on success', async () => {
// Given a loaded sub-org template.
const { presenter, opened } = fakeBlobPresenter();
@@ -28,7 +28,7 @@ const LOGO_CATEGORY = 'org-logo';
const NO_SUBORGS = $localize`:@@orgTemplate.noSubOrgs:Er zijn geen organisatiesjablonen om te beheren.`;
/**
* Root singleton for the admin org-template editor (WP-26). The Elm machine owns the
* Root singleton for the admin org-template editor. The Elm machine owns the
* editable draft; commands here do the debounced save, publish (impact-confirm),
* rollback and proefbrief, then dispatch the outcome — the reducer stays pure. The
* logo upload reuses the shared upload transport; its completion mutates the draft
@@ -151,7 +151,7 @@ export class OrgTemplateStore implements PendingSave {
this.debouncedSave.schedule();
}
// 600ms debounced autosave (same idiom as BriefStore, WP-31). Timer mechanics live in the
// 600ms debounced autosave (same idiom as BriefStore). Timer mechanics live in the
// shared helper; `flushSave` below is the store-specific write + save-state.
private debouncedSave = createDebouncedSave({
canSave: () => this.loaded() !== null,
+1 -1
View File
@@ -2,7 +2,7 @@ import { Brief, LetterBlock, allBlocks } from './brief';
/**
* The rejection diff as a PURE function over two immutable `Brief` values — the whole
* teaching payload of WP-27: because state is one value, "what changed since the letter
* teaching payload here: because state is one value, "what changed since the letter
* was rejected" is just a fold over two snapshots, no change-tracking bookkeeping.
*
* Blocks are matched by `blockId` (stable `local-N`/seed ids):
@@ -3,7 +3,7 @@ import { Margins, OrgTemplate, OrgTemplateAdminView, OrgTemplateVersion } from '
import { UploadMsg, UploadState, initialUpload, reduceUpload } from '@shared/domain/upload.machine';
/**
* The admin org-template editor as one Elm-style machine (WP-26, PRD Brief v2 §5) —
* The admin org-template editor as one Elm-style machine (PRD Brief v2 §5) —
* the same idiom as the wizards. The DRAFT org template is form state (edited in
* place on the canvas); publish/rollback are effects that come back as `DraftLoaded`.
* `dirty` tracks unsaved edits (the store debounce-saves them). The logo upload is
@@ -1,9 +1,9 @@
/**
* The organization template (Brief v2 PRD §3, WP-23/24): the SECOND template axis —
* The organization template (Brief v2 PRD §3): the SECOND template axis —
* appearance/identity per sub-organization (letterhead, footer, signature, margins).
* Orthogonal to the case-type template (sections + placeholders); the two only meet
* at render time, on the letter canvas. Server-owned: the FE renders it verbatim,
* never edits it here (the admin editor is WP-26).
* never edits it here (the admin editor does).
*/
export interface Margins {
@@ -30,7 +30,7 @@ export interface OrgTemplate {
readonly version: number;
}
// --- admin editor (WP-26) ---
// --- admin editor ---
/** A published snapshot in the version history: who is faked, `publishedAt` is real. */
export interface OrgTemplateVersion {
@@ -38,9 +38,9 @@ import { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/ric
* (ProblemDetails → error string, plus the Idempotency-Key mint), then parses the
* returned brief. `load` (the only read) does its own try/catch instead of the
* shared `runResult` fold, because it needs one extra bit `runResult` throws away:
* whether the failure was an HTTP 404 (see `BriefLoadFailure` — RB-22, CQ-007's
* expand half). Today's backend never 404s `GET /brief` (RB-23 adds that), so the
* `notFound` branch is unreached until RB-23 ships; this adapter is ready in advance.
* whether the failure was an HTTP 404 (see `BriefLoadFailure` — CQ-007's
* expand half). Today's backend never 404s `GET /brief`, so the
* `notFound` branch is unreached until it does; this adapter is ready in advance.
*/
export interface BriefView {
@@ -66,7 +66,7 @@ export const BRIEF_ACTION_FAILED = $localize`:@@brief.action.failed:De actie is
/** True when the thrown value carries an HTTP 404 status — matches both the
generic `SwaggerException` (today's shape, since `GET /brief` declares no 404
response yet) and a parsed `ProblemDetails` (RFC 7807 `status`, the shape once
RB-23 gives the endpoint a documented 404 response). */
the endpoint gets a documented 404 response). */
function isHttpNotFound(e: unknown): boolean {
return !!e && typeof e === 'object' && (e as { status?: unknown }).status === 404;
}
@@ -14,19 +14,19 @@ export const PREVIEW_FAILED = $localize`:@@brief.preview.failed:De voorvertoning
* to keep the NSwag-generated client JSON-only (same seam as uploads) — so this is a
* hand-written fetch, not the `ApiClient`. That also means it bypasses `HttpClient`'s
* `roleInterceptor` AND `subjectInterceptor`, so both `X-Role` and `X-Subject` are set
* here explicitly (WP-74 — without `X-Subject` this always previewed
* here explicitly (without `X-Subject` this always previewed
* `DocumentStore.DemoOwner`'s letter regardless of who was actually logged in). Both are
* dev-only identity stand-ins (`role.ts`/`subject.ts`) and are sent only under
* `isDevMode()`, mirroring how the interceptors themselves are only registered in dev
* (`app.config.ts`) — a production build sends neither header from this call (BIO-012).
*
* `cache: 'no-store'` (WP-74): the endpoint has no `Cache-Control`, only a CORS-driven
* `cache: 'no-store'`: the endpoint has no `Cache-Control`, only a CORS-driven
* `Vary: Origin`, and its content changes at the SAME URL as the letter moves
* draft → sent. Explicitly bypassing the HTTP cache is the correct default for any
* mutable resource served under one unversioned URL — independent of WP-74's
* identity work, and not a complete fix by itself: see the KNOWN GAP note below.
* mutable resource served under one unversioned URL — independent of the
* identity work above, and not a complete fix by itself: see the KNOWN GAP note below.
*
* KNOWN GAP (WP-74, not fixed here): under a non-`DocumentStore.DemoOwner` `X-Subject`,
* KNOWN GAP (not fixed here): under a non-`DocumentStore.DemoOwner` `X-Subject`,
* this repo's own e2e run against a real backend observed this endpoint's SENT
* response still carrying the draft watermark, even though (a) the outgoing request
* carried the correct `X-Subject`, and (b) `curl` against the same backend at the
@@ -34,7 +34,7 @@ export const PREVIEW_FAILED = $localize`:@@brief.preview.failed:De voorvertoning
* did not change the outcome, so it is very unlikely a client-side caching artifact —
* it looks like a genuine backend-side staleness/race in `BriefStore`'s SQLite-backed
* read path, reproducible for MULTIPLE distinct owners and NOT reproducible for
* `DemoOwner`, which needs backend-side investigation (out of WP-74's file scope —
* `DemoOwner`, which needs backend-side investigation (out of this file's scope —
* see `e2e/brief-v2.spec.ts`'s header comment, which keeps that spec on the shared
* `zorgverlener` identity until this is root-caused).
*/
+2 -2
View File
@@ -167,7 +167,7 @@ export class BriefPage {
void this.store.resetDemo();
}
/** Typed narrowing for the `<app-async>` loaded slot — see WP-06: a structural
/** Typed narrowing for the `<app-async>` loaded slot: a structural
directive's context can't inherit a generic from a sibling host input, so the
Success value is unwrapped here instead of through `let-`. */
protected readonly loaded = computed(() => {
@@ -179,7 +179,7 @@ export class BriefPage {
void this.store.load();
}
/** Ctrl/Cmd+Z = undo, Ctrl/Cmd+Shift+Z = redo (WP-27). Ignored while focus is in the
/** Ctrl/Cmd+Z = undo, Ctrl/Cmd+Shift+Z = redo. Ignored while focus is in the
rich-text editor or a form control, so the browser's own text undo keeps working
there — our shell-level undo is for structural edits (add/remove/reorder blocks). */
protected onKey(e: KeyboardEvent) {
@@ -53,9 +53,9 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
footer around the case-type template's sections. `editableRegions` picks who edits
what: `'content'` hosts the editable letter-sections in place (drafter), `'none'`
renders everything read-only (approver/locked, absorbs the old letter-preview),
`'template'` reserves the org-identity regions for the admin editor (WP-26).
`'template'` reserves the org-identity regions for the admin editor.
Letter typography/geometry come from the shared `public/letter.css` contract —
the same file the backend preview renderer inlines (WP-25). */
the same file the backend preview renderer inlines. */
@Component({
selector: 'app-letter-canvas',
imports: [NgTemplateOutlet, ButtonComponent, PlaceholderChipComponent],
@@ -82,7 +82,7 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
color: var(--rhc-color-foreground-subtle);
font-variant-numeric: tabular-nums;
}
/* Rejection-diff badge (WP-27): a small pill above a changed/added block. */
/* Rejection-diff badge: a small pill above a changed/added block. */
.diff-block.diff-changed {
border-inline-start: 3px solid var(--rhc-color-oranje-500);
padding-inline-start: var(--rhc-space-max-sm);
@@ -98,7 +98,7 @@ const A4_HEIGHT_PX = (297 * 96) / 25.4;
background: var(--rhc-color-oranje-500);
}
.diff-badge.added {
/* added = white on groen-700 (6.4:1); dark text on any green fails 4.5:1 (WP-29 axe). */
/* added = white on groen-700 (6.4:1); dark text on any green fails 4.5:1 (axe). */
color: var(--rhc-color-wit);
background: var(--rhc-color-groen-700);
}
@@ -345,12 +345,12 @@ export class LetterCanvasComponent {
brief = input.required<Brief>();
orgTemplate = input.required<OrgTemplate>();
/** Who edits what on the surface: read-only ('none', the drafter preview + approver
view) or admin editor ('template', WP-26). Authoring moved to letter-editor. */
view) or admin editor ('template'). Authoring moved to letter-editor. */
editableRegions = input<'template' | 'none'>('none');
diagnostics = input<readonly Diagnostic[]>([]);
/** Initial zoom; the in-canvas controls take over from here (WP-27). */
/** Initial zoom; the in-canvas controls take over from here. */
zoom = input(1);
/** Blocks changed/added/removed since the letter was rejected (WP-27); badged when
/** Blocks changed/added/removed since the letter was rejected; badged when
`showDiff` is on. Removed blocks aren't in the map's rendered set — they no longer
exist in the letter — the composer surfaces them as a count. */
blockDiffs = input<ReadonlyMap<string, BlockDiffKind>>(new Map());
@@ -445,7 +445,7 @@ export class LetterCanvasComponent {
constructor() {
// ponytail: whole-surface height / A4-interval — ignores that a break never truly
// falls mid-line; the caption says "±" and WP-25's server preview is authoritative.
// falls mid-line; the caption says "±" and the server preview is authoritative.
const observer = new ResizeObserver(([entry]) => {
// ~1cm tolerance so a letter ending on a page boundary gets no edge-hugging mark.
const pages = Math.ceil((entry.target.scrollHeight - 40) / A4_HEIGHT_PX);
@@ -127,12 +127,12 @@ export const ReadOnlyZonderBevindingen: Story = {
args: { editableRegions: 'none', diagnostics: [] },
};
/** Admin editor focus (consumer arrives in WP-26): body read-only, no "not yours" tint. */
/** Admin editor focus: body read-only, no "not yours" tint. */
export const TemplateMode: Story = { args: { editableRegions: 'template' } };
export const Zoomed: Story = { args: { editableRegions: 'none', zoom: 0.6 } };
/** Approver's "Toon wijzigingen": blocks changed/added since rejection are badged (WP-27). */
/** Approver's "Toon wijzigingen": blocks changed/added since rejection are badged. */
export const WithDiff: Story = {
args: {
editableRegions: 'none',
@@ -150,14 +150,14 @@ export const PageBreak: Story = {
args: { editableRegions: 'none', brief: longBrief, diagnostics: [] },
};
// Inline SVG so the story needs no backend/upload round-trip (WP-26 logo upload).
// Inline SVG so the story needs no backend/upload round-trip (the logo upload).
const sampleLogo =
'data:image/svg+xml;utf8,' +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><rect width="120" height="40" fill="#003366"/><text x="60" y="25" font-size="14" fill="white" text-anchor="middle">CIBG</text></svg>',
);
/** Published org logo (WP-26 AC2): the letterhead shows it above the org name. */
/** Published org logo: the letterhead shows it above the org name. */
export const MetLogo: Story = {
args: { editableRegions: 'none', diagnostics: [], logoUrl: sampleLogo },
};
@@ -137,7 +137,7 @@ export class LetterComposerComponent {
canReject = input(false);
canSend = input(false);
busy = input(false);
/** Rejection diff (WP-27): the changed/added/removed blocks and their count. The
/** Rejection diff: the changed/added/removed blocks and their count. The
"Toon wijzigingen" toggle only appears when there's something to show. */
blockDiffs = input<ReadonlyMap<string, BlockDiffKind>>(new Map());
removedCount = input(0);
@@ -181,7 +181,7 @@ export const Sent: Story = {
}),
};
/** Approver's "Toon wijzigingen" (WP-27): a resubmitted letter with blocks changed,
/** Approver's "Toon wijzigingen": a resubmitted letter with blocks changed,
added and removed since the last rejection. */
export const RejectionDiff: Story = {
render: () =>
@@ -70,7 +70,7 @@ export const SAMPLE_LETTER_BRIEF: Brief = {
};
/**
* Organism (WP-26): the admin org-template editor. The mirror of the drafter's
* Organism: the admin org-template editor. The mirror of the drafter's
* composer — the letter canvas runs in `editableRegions='template'` so the
* letterhead/signature/footer are edited in place, while the content is a read-only
* sample. Margins, logo upload, version history and the publish bar sit around it.
@@ -87,12 +87,12 @@ const sampleLogo =
'<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><rect width="120" height="40" fill="#003366"/><text x="60" y="25" font-size="14" fill="white" text-anchor="middle">CIBG</text></svg>',
);
/** Published logo (WP-26 AC2): the letterhead canvas shows it above the org name. */
/** Published logo: the letterhead canvas shows it above the org name. */
export const MetLogo: Story = {
args: { logoUrl: sampleLogo },
};
/** Client-side upload rejection (existing `rejectReason`, WP-26 AC5) — type/size caught
/** Client-side upload rejection (existing `rejectReason`) — type/size caught
before the file ever reaches the backend. */
export const LogoUploadFout: Story = {
args: {
@@ -7,7 +7,7 @@ import { AccessStore } from '@shared/application/access.store';
import { OrgTemplateStore } from '@brief/application/org-template.store';
import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-template-editor.component';
/** Page: thin container for the admin org-template editor (WP-26). Deny-by-default
/** Page: thin container for the admin org-template editor. Deny-by-default
capability gate (`orgtemplate:edit`) — a denial alert for non-admins, the editor
for admins. Loads once the capability resolves; wires store commands to the organism. */
@Component({
@@ -10,8 +10,8 @@ import { LibraryPassage } from '@brief/domain/brief';
inserts ALL checked passages at once (a single message upstream) — there is no
single-insert path. Presentational: emits the chosen passages in list order.
Superseded by `besluit-panel` (WP-27's guided drafting): no consumer left in
`src/app` outside its own story (WP-28 audit). Kept for now rather than deleted
Superseded by `besluit-panel`'s guided drafting: no consumer left in
`src/app` outside its own story. Kept for now rather than deleted
in-flight of an unrelated WP; a future cleanup can remove it. */
@Component({
selector: 'app-passage-picker',
@@ -87,7 +87,7 @@ export class PassagePickerComponent {
protected checked = signal<Record<string, boolean>>({});
protected query = signal('');
/** Client-side filter on label + rendered content text — the library is small, so no
server search (WP-27). Placeholder keys are searchable too (see `textOf`). */
server search. Placeholder keys are searchable too (see `textOf`). */
protected filtered = computed(() => {
const q = this.query().trim().toLowerCase();
if (!q) return this.passages();
@@ -147,7 +147,7 @@ describe('intake acceptance journeys', () => {
});
});
it('raising uren above the threshold after answering scholing drops both fields (WP-69 §6)', () => {
it('raising uren above the threshold after answering scholing drops both fields', () => {
// Given a journey that answered the scholing question while uren was low.
const atReview = givenIntake(
{ tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' },
@@ -170,7 +170,7 @@ describe('intake acceptance journeys', () => {
);
// Then the submission succeeds, and BOTH the stale answer and its punten are gone —
// exactly the crafted-POST-shaped payload WP-69's server rule rejects.
// exactly the crafted-POST-shaped payload the server rule rejects.
expect(done.tag).toBe('Submitted');
expect(done.tag === 'Submitted' && done.data.aanvullendeScholing).toBeUndefined();
expect(done.tag === 'Submitted' && done.data.punten).toBeUndefined();
@@ -171,7 +171,7 @@ describe('submit', () => {
expect(withScholing.data.punten).toBe(200);
});
it('does not require punten for a hidden question (WP-69 §6)', () => {
it('does not require punten for a hidden question', () => {
// scholingGevolgd is a stale 'ja' from when uren was low, but uren is now above
// threshold — the template hides the question, so punten must not be required either.
const staleScholingNoPunten = givenIntake(
@@ -183,7 +183,7 @@ describe('submit', () => {
expect(good.data.aanvullendeScholing).toBeUndefined();
});
it('drops punten when raising uren hides the question (WP-69 §6)', () => {
it('drops punten when raising uren hides the question', () => {
// Same stale answer, but this time punten was also filled in while uren was low.
const staleScholingWithPunten = givenIntake(
{ tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' },
@@ -116,7 +116,7 @@ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Resu
// visible (lageUren) AND scholing was followed — matching the template's
// `@if (scholingZichtbaar())`. Without the `lageUren` guard, answering 'ja' and then
// raising uren above the threshold left an error on a field the template no longer
// renders (WP-69 §6).
// renders.
if (lageUren(a, scholingThreshold) && a.scholingGevolgd === 'ja') {
const p = parseUren(a.punten ?? '');
if (!p.ok) errors.punten = p.error;
@@ -149,8 +149,8 @@ function validateAll(a: Answers, scholingThreshold: number): Result<Errors, Vali
const aanvullendeScholing = lageUren(a, scholingThreshold)
? a.scholingGevolgd === 'ja'
: undefined;
// Punten are derived from aanvullendeScholing, NOT the raw scholingGevolgd answer (WP-69
// §6) — a stale 'ja' left over from when uren was low, after uren was raised above the
// Punten are derived from aanvullendeScholing, NOT the raw scholingGevolgd answer
// a stale 'ja' left over from when uren was low, after uren was raised above the
// threshold, must not leak a punten value into the parsed, submitted ValidIntake.
const punten = aanvullendeScholing === true ? parseUren(a.punten ?? '') : undefined;
return ok({
@@ -273,7 +273,7 @@ export class IntakeWizardComponent {
private store = createStore<IntakeState, IntakeMsg>(initial, reduce, {
Submitting: async (s, store) => {
this.profile.beginHerregistratie();
// WP-69: the scholing answer rides along so the server can re-validate it as the
// The scholing answer rides along so the server can re-validate it as the
// authority (IntakePolicy.RejectIncompleteScholing) — undefined members are dropped by
// JSON.stringify, so a wizard above the threshold sends neither field.
const r = await this.draftSync.submit({
@@ -46,8 +46,8 @@ describe('AanvragenStore', () => {
expect(store.lastError()).toBeNull();
});
// RB-20: a failed cancel must not be silent — the row rolls back AND the store
// surfaces the error the page renders. Before RB-20 this only rolled back
// A failed cancel must not be silent — the row rolls back AND the store
// surfaces the error the page renders. Before this fix it only rolled back
// (bare `catch { this.state.set(before) }`), so `lastError()` stayed null forever.
it('rolls back the removal and surfaces the error when the cancel fails', async () => {
const cancel = vi.fn().mockRejectedValue(new Error('boom'));
@@ -14,7 +14,7 @@ type Err = Error | undefined;
* change-detection timing, HTTP caching, or a resource `reload()`. `reload()` re-fetches
* so a page revisit reflects auto-approval (Concept In behandeling Goedgekeurd is
* computed server-side on read). Cancel goes through `runSubmit` and rolls back plus
* surfaces `lastError` on failure (RB-20).
* surfaces `lastError` on failure.
*/
@Injectable({ providedIn: 'root' })
export class AanvragenStore {
@@ -23,7 +23,7 @@ export class AanvragenStore {
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
readonly aanvragen = this.state.asReadonly();
/** Set on a failed cancel (RB-20): the optimistic removal already rolled back by
/** Set on a failed cancel: the optimistic removal already rolled back by
then, this is only the message for the alert the page renders above the list. */
private error = signal<string | null>(null);
readonly lastError = this.error.asReadonly();
@@ -55,7 +55,7 @@ export class AanvragenStore {
/** Cancel a Concept: drop it now (synchronous, guaranteed), then confirm the DELETE.
No resync the delete succeeded, so the optimistic removal is authoritative. On
failure, roll back AND surface the error (RB-20) a silent reappearance leaves the
failure, roll back AND surface the error a silent reappearance leaves the
user guessing why the block came back. */
async cancel(id: string) {
const before = this.state();
@@ -44,8 +44,8 @@ describe('AdminCasesStore', () => {
expect(s.tag === 'Success' && s.value.map((c) => c.id)).toEqual(['b']);
});
// RB-20: a failed delete must not be silent — the row rolls back AND the store
// surfaces the error the page renders. Before RB-20 this only rolled back
// A failed delete must not be silent — the row rolls back AND the store
// surfaces the error the page renders. Before this fix it only rolled back
// (bare `catch { this.state.set(before) }`), so `lastError()` stayed null forever.
it('rolls back the removal and surfaces the error when the delete fails', async () => {
const deleteAny = vi.fn().mockRejectedValue(new Error('boom'));
@@ -7,11 +7,11 @@ import { AanvragenAdapter, parseAanvragen } from '@registratie/infrastructure/aa
type Err = Error | undefined;
/**
* Admin view of ALL cases across owners (WP-36; `cases:manage`) the back-office
* Admin view of ALL cases across owners (`cases:manage`) the back-office
* counterpart of the user-facing `AanvragenStore`. Same shape: one root singleton
* owns the list as a writable RemoteData signal, delete removes the row synchronously
* (optimistic), goes through `runSubmit`, and rolls back plus surfaces `lastError` on
* failure (RB-20). Admin delete removes any case (any owner, submitted or not the
* failure. Admin delete removes any case (any owner, submitted or not the
* server enforces the capability).
*/
@Injectable({ providedIn: 'root' })
@@ -21,7 +21,7 @@ export class AdminCasesStore {
private state = signal<RemoteData<Err, Aanvraag[]>>({ tag: 'Loading' });
readonly cases = this.state.asReadonly();
/** Set on a failed delete (RB-20): the optimistic removal already rolled back by
/** Set on a failed delete: the optimistic removal already rolled back by
then, this is only the message for the alert the page renders above the list. */
private error = signal<string | null>(null);
readonly lastError = this.error.asReadonly();
@@ -47,7 +47,7 @@ export class AdminCasesStore {
}
/** Delete a case: drop it now (synchronous), then confirm the DELETE; roll back on error
AND surface it (RB-20) a silent reappearance leaves the admin guessing why. */
AND surface it a silent reappearance leaves the admin guessing why. */
async delete(id: string) {
const before = this.state();
if (before.tag === 'Success') {
@@ -96,7 +96,7 @@ describe('createDraftSync', () => {
expect(r.ok).toBe(false);
});
it('recovers from a create conflict by adopting the existing Concept (WP-35)', async () => {
it('recovers from a create conflict by adopting the existing Concept', async () => {
// Server enforces one Concept per type: a stale/cross-tab create is rejected (409),
// and ensureId adopts the existing Concept from the list instead of erroring.
const create = vi.fn().mockRejectedValue({ status: 409 });
@@ -63,7 +63,7 @@ export function createDraftSync(deps: DraftSyncDeps) {
if (id) return id;
ensuring ??= adapter
.create(deps.type)
// WP-35: one Concept per type is server-enforced. Within a tab the resumeGate
// One Concept per type is server-enforced. Within a tab the resumeGate
// already prevents a second create, but a cross-tab/stale race can still hit the
// server's guard (409) — recover by adopting the existing Concept instead of
// erroring. Only recover when one actually exists; otherwise surface the failure.
@@ -3,7 +3,7 @@ import { AanvragenAdapter, parseAanvragen } from '@registratie/infrastructure/aa
/**
* Read half of the Concept lookup that `createDraftSync` (`draft-sync.ts`) needs
* before it can start writing (RB-21 / CQ-001). Free functions that take the adapter
* before it can start writing (CQ-001). Free functions that take the adapter
* as a parameter, not `inject()`, so they get a direct spec without Angular TestBed.
* `createDraftSync` keeps the closure state (`id`, `resumeGate`) and the write path;
* these two functions only read.
@@ -10,8 +10,8 @@
*/
export type AanvraagType = 'registratie' | 'herregistratie' | 'intake';
// Ingediend/MeerInfoGevraagd (ADR-0002/WP-63) are widened into the union so the parse
// boundary + renderers are ready, but no backend path emits them yet — that's WP-65's
// Ingediend/MeerInfoGevraagd (ADR-0002) are widened into the union so the parse
// boundary + renderers are ready, but no backend path emits them yet — that's the
// behandelaar-facing transition endpoint.
export type AanvraagStatus =
| { tag: 'Concept'; stepIndex: number; stepCount: number }
@@ -29,7 +29,7 @@ export interface Aanvraag {
createdAt: string;
updatedAt: string;
submittedAt?: string;
/** The case owner (a BSN). Only populated by the admin cross-owner list (WP-36);
/** The case owner (a BSN). Only populated by the admin cross-owner list;
the user's own list leaves it undefined. */
owner?: string;
}
@@ -5,7 +5,7 @@ import {
} from '@registratie/domain/value-objects/telefoonnummer';
/** What the user is typing (raw, possibly invalid). The BRP address is NOT part of
the form it is authoritative and shown read-only (WP-34); only the phone number
the form it is authoritative and shown read-only; only the phone number
is editable here. */
export interface Draft {
telefoon: string;
@@ -32,12 +32,12 @@ export class AanvragenAdapter {
return this.client.aanvragenAll();
}
/** Admin: every case across all owners (WP-36; `cases:manage`). Parsed at the boundary. */
/** Admin: every case across all owners (`cases:manage`). Parsed at the boundary. */
listAll(): Promise<AanvraagSummaryDto[]> {
return this.client.casesAll();
}
/** Admin: delete ANY case (any owner, submitted or not — WP-36). */
/** Admin: delete ANY case (any owner, submitted or not). */
deleteAny(id: string): Promise<void> {
return this.client.cases(id);
}
@@ -117,7 +117,7 @@ function parseCommon(dto: AanvraagSummaryDto): Result<string, Aanvraag> {
createdAt: dto.createdAt,
updatedAt: dto.updatedAt,
submittedAt: dto.submittedAt,
owner: dto.owner, // only present on the admin cross-owner list (WP-36)
owner: dto.owner, // only present on the admin cross-owner list
});
}
@@ -6,7 +6,7 @@ import { Valid } from '@registratie/domain/change-request.machine';
* Infrastructure adapter for the telefoonwijziging POST (`/api/v1/change-requests`)
* the single place the network client lives for contact changes, so the command
* and the UI never touch `ApiClient`. The BRP address is authoritative and not
* submitted (WP-34); only the phone number is. Returns the server reference; the
* submitted; only the phone number is. Returns the server reference; the
* server re-validates and is the authority.
*/
@Injectable({ providedIn: 'root' })
@@ -13,7 +13,7 @@ import { TYPE_LABELS, statusLabel, referentie } from '@registratie/domain/aanvra
import { AdminCasesStore } from '@registratie/application/admin-cases.store';
/**
* Admin page: every case across all owners, with an admin delete (WP-36). Lives in
* Admin page: every case across all owners, with an admin delete. Lives in
* `registratie` (which owns the Aanvraag aggregate) the back-office counterpart of the
* user's dashboard, reusing the same view labels + trust-boundary parse. Deny-by-default
* capability gate (`cases:manage`): a denial alert for non-admins, the list for admins.
@@ -109,7 +109,7 @@ export class AdminCasesPage {
private loadRequested = false;
constructor() {
// Load once the capability resolves to allowed (a 403 GET would be wasted otherwise).
// Depends only on canManage() + a plain flag — never the store model (WP-26 loop lesson).
// Depends only on canManage() + a plain flag — never the store model (the loop lesson).
effect(() => {
if (this.canManage() && !this.loadRequested) {
this.loadRequested = true;
@@ -20,7 +20,7 @@ import { createSubmitChangeRequest } from '@registratie/application/submit-chang
/**
* Organism: contact-change (telefoonwijziging) form. The BRP address is authoritative
* and shown READ-ONLY (WP-34) you change your address at the gemeente, not here so
* and shown READ-ONLY you change your address at the gemeente, not here so
* only the phone number is editable. Uses the SAME idiom as the wizards: all state in
* one signal driven by the pure `reduce` (change-request.machine.ts), submitted via a
* `submit-*` command returning `Result`. The server re-validates.
@@ -136,7 +136,7 @@ export class DebugStateComponent {
pendingHerregistratie: this.profileStore?.pendingHerregistratie(),
}));
// Dev switchers (WP-33): flip role/scenario without hand-editing the URL. Both are
// Dev switchers flip role/scenario without hand-editing the URL. Both are
// read per-request in interceptors, so a reload re-runs them and re-fetches decisions.
protected readonly roles = ROLES;
protected readonly scenarios = SCENARIOS;
@@ -158,7 +158,7 @@ export class DebugStateComponent {
this.applyAndReload();
}
// Strip the dev params from the URL before reloading (WP-37) so a stale ?scenario=/?role=
// Strip the dev params from the URL before reloading so a stale ?scenario=/?role=
// in the address bar can't override the value the switcher just stored (currentScenario/
// currentRole read the URL first) — otherwise a switch to "default"/"drafter" gets stuck.
private applyAndReload(): void {
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Tiny, dependency-free TS highlighter for the teaching showcase (WP-39). Escapes HTML,
* Tiny, dependency-free TS highlighter for the teaching showcase. Escapes HTML,
* then wraps line-comments, strings, and a fixed keyword set in `.c`/`.s`/`.k` spans (the
* classes `concepts.page` styles). Deliberately naive good enough for the short, curated
* snippets shown here; not a real tokenizer. Input is always our OWN source (extracted by
+2 -2
View File
@@ -10,8 +10,8 @@
url(../fonts|icons|images) refs resolve against the vendored folder at runtime.
Licensed Rijksoverheid fonts are not used — styles.scss overrides the stack to system-ui. -->
<link rel="stylesheet" href="cibg-huisstijl/css/huisstijl.min.css" />
<!-- The letter-rendering contract (WP-24): shared verbatim with the backend's
HTML preview renderer (WP-25 inlines this same file) — keep it self-contained. -->
<!-- The letter-rendering contract: shared verbatim with the backend's
HTML preview renderer (the backend inlines this same file) — keep it self-contained. -->
<link rel="stylesheet" href="letter.css" />
</head>
<!-- brand--cibg activates CIBG's official palette: robijn layout chrome + lintblauw accents