Commit Graph
201 Commits
Author SHA1 Message Date
ehoandClaude Opus 4.8 446ea9474b feat(registratie): WP-36 — admin cases page + admin delete
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>
2026-07-23 12:23:34 +02:00
ehoandClaude Opus 4.8 d1abd35b0d feat(registratie): WP-35 — one Concept per case type (server-enforced)
CI / semgrep (push) Has been cancelled
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 5m17s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 3m1s
CI / api-client-drift (push) Successful in 2m4s
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>
2026-07-23 11:07:11 +02:00
ehoandClaude Opus 4.8 57f6f2f8d8 style(registratie): WP-34 — BRP address as datablock, phone in grey fieldset
CI / frontend (push) Successful in 1m53s
CI / storybook-a11y (push) Successful in 4m51s
CI / backend (push) Successful in 1m26s
CI / e2e (push) Successful in 2m51s
CI / semgrep (push) Failing after 28s
CI / api-client-drift (push) Successful in 2m10s
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>
2026-07-23 08:05:40 +02:00
ehoandClaude Opus 4.8 0ea43af7b6 feat(registratie): WP-34 — phone field + BRP address read-only
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>
2026-07-22 20:11:26 +02:00
ehoandClaude Opus 4.8 1ed4850858 feat(dev): WP-33 — in-app dev switchers for scenario + role
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 27m52s
CI / backend (push) Successful in 1m24s
CI / e2e (push) Successful in 27m24s
CI / semgrep (push) Failing after 30s
CI / api-client-drift (push) Successful in 2m7s
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>
2026-07-22 16:53:26 +02:00
ehoandClaude Opus 4.8 610ff7c1cd docs(backlog): mark WP-31/32 done
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:45:28 +02:00
ehoandClaude Opus 4.8 4cf1147fc1 feat(beheer): WP-32 — undo/redo for the stamdata table editor
CI / frontend (push) Successful in 1m50s
CI / backend (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / semgrep (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / storybook-a11y (push) Has been cancelled
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>
2026-07-22 16:10:27 +02:00
ehoandClaude Opus 4.8 ac3e9a9399 refactor(fp): WP-31 — shared store helpers (dedupe brief/org-template/stamdata)
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>
2026-07-22 15:40:02 +02:00
ehoandClaude Opus 4.8 13b3e5e663 docs(backlog): truth-up statuses + add Phase 7 (WP-31..36)
WP-25 (1bb9383) and WP-27 (5e36d68) were done but the table still said todo — fixed.
WP-28/WP-30 remain todo. Added Phase 7 (refinements & hardening): WP-31 shared store
helpers, WP-32 stamdata undo, WP-33 dev switchers, WP-34 adres phone + BRP read-only,
WP-35 one Concept per type, WP-36 admin cases page + delete. Only 31→32 is ordered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 15:26:55 +02:00
ehoandClaude Opus 4.8 9b2a878933 ci(semgrep): install via runner python3/pip, drop container + setup-python
CI / api-client-drift (push) Failing after 30s
CI / frontend (push) Successful in 1m51s
CI / storybook-a11y (push) Successful in 4m50s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 3m0s
CI / semgrep (push) Failing after 29s
The container: approach failed — this act_runner times out pulling its base runner
image (docker.gitea.com/runner-images:ubuntu-latest, IPv6) for container jobs. And
the earlier setup-python step failed downloading Python. Both avoided: run on the
plain ubuntu-latest runner and install semgrep with the preinstalled python3/pip
(`python3 -m pip install --break-system-packages semgrep`; --break-system-packages
survives PEP-668, pip puts semgrep on PATH). Verified in a clean python:3.12
container that pip install lands `semgrep` on PATH and the scan runs. Still
report-only; WP-30 tracks the flip to --error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:10:57 +02:00
ehoandClaude Opus 4.8 b34cc2ba3b ci(semgrep): run in the official Semgrep container (setup-python failed on the runner)
CI / frontend (push) Failing after 31s
CI / storybook-a11y (push) Failing after 31s
CI / backend (push) Failing after 31s
CI / e2e (push) Failing after 30s
CI / semgrep (push) Failing after 25s
CI / api-client-drift (push) Failing after 30s
The setup-python + `pip install semgrep` step errored on the Gitea runner. Switch to
`container: docker.io/semgrep/semgrep` (semgrep preinstalled) — the documented way to
run Semgrep in CI, and the exact execution verified locally (306 rules / 450 files,
27 findings, exit 0). Fully-qualified image name so short-name resolution works under
Docker or podman. Still report-only (no --error); WP-30 tracks flipping to blocking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:46:48 +02:00
ehoandClaude Opus 4.8 c404995980 ci: replace CodeQL with Semgrep (Gitea-compatible SAST)
CI / frontend (push) Failing after 31s
CI / storybook-a11y (push) Failing after 30s
CI / backend (push) Failing after 30s
CI / e2e (push) Failing after 30s
CI / semgrep (push) Failing after 30s
CI / api-client-drift (push) Failing after 30s
CodeQL is GitHub-only — its analyze step uploads SARIF to GitHub's code-scanning
API and assumes a GitHub Security tab; this CI runs on Gitea only, so the job could
never go green (it had been red since it was added). Replace it with Semgrep OSS, a
plain CLI SAST with no account/platform API, which runs fine on Gitea.

- Remove the codeql job (+ its security-events permission) and the schedule trigger
  (it existed only for codeql; semgrep runs on push + PR).
- Add a semgrep job: setup-python + `pip install semgrep` +
  `semgrep scan --config p/default --config p/csharp --metrics=off`. pip-on-runner
  (not container:) mirrors the other jobs' model; anonymous registry, telemetry off.
- Report-only for now (no --error → job stays green): a local dry-run found 27
  findings, mostly CI/config policy (unpinned actions, .npmrc), not app-code vulns.
  WP-30 tracks triaging them + flipping to --error (a blocking gate).

Verified locally: `semgrep scan` runs clean (exit 0 without --error, 306 rules /
450 files). CI behaviour confirmable only on the Gitea runner — watch the run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:34:13 +02:00
ehoandClaude Opus 4.8 f7417ee4e9 fix(storybook): stub AccessStore in the shell story (fixes storybook-a11y red)
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 4m27s
CI / backend (push) Successful in 1m23s
CI / e2e (push) Successful in 2m36s
CI / codeql (csharp) (push) Failing after 2m1s
CI / codeql (javascript-typescript) (push) Failing after 1m29s
CI / api-client-drift (push) Successful in 2m8s
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>
2026-07-21 21:21:01 +02:00
ehoandClaude Opus 4.8 f7196768ea fix(auth): make admin pages reachable — async capability guard + sticky dev role + nav
CI / storybook-a11y (push) Failing after 4m28s
CI / frontend (push) Successful in 1m44s
CI / backend (push) Successful in 1m28s
CI / e2e (push) Successful in 2m49s
CI / codeql (csharp) (push) Failing after 2m8s
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / api-client-drift (push) Successful in 2m6s
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>
2026-07-21 21:03:32 +02:00
ehoandClaude Opus 4.8 c0834cdbce fix(beheer): send X-Role to /api/v1/stamdata (admin reads were 403)
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>
2026-07-21 20:43:01 +02:00
ehoandClaude Opus 4.8 7d3a63a7a5 docs(backlog): WP-30 — CI performance follow-ups
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 5m9s
CI / backend (push) Successful in 1m35s
CI / e2e (push) Successful in 2m54s
CI / codeql (csharp) (push) Failing after 2m12s
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / api-client-drift (push) Successful in 2m8s
Capture the deferred CI options from the speed pass (708d4c2) so they aren't lost:
node_modules cache to skip npm ci, smaller CI runner image (act_runner label→image,
needs admin; or repo-only container: for node-only jobs), path-filtered jobs, a fast
split lint job, and an optional lean prod backend image. Notes the constraints (CI not
observable from the agent env; compose images aren't used by CI). Added the README row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:32:18 +02:00
ehoandClaude Opus 4.8 708d4c2308 ci: speed up pipeline + shrink demo web image
CI / frontend (push) Successful in 1m46s
CI / backend (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / storybook-a11y (push) Has been cancelled
CI (Gitea Actions, .github/workflows/ci.yml):
- CodeQL off the PR critical path: runs on push-to-main + a weekly cron only
  (`if: github.event_name != 'pull_request'`, `schedule: Mondays 03:00 UTC`).
  The 2-language 20-min matrix was the slowest thing on every PR; code is still
  scanned on main + weekly.
- Cache Playwright browsers (~/.cache/ms-playwright) in the storybook-a11y + e2e
  jobs — skips the chromium download on a hit; `install --with-deps` then only does
  the fast apt deps check.
- Cache NuGet (~/.nuget/packages, keyed on **/*.csproj — no packages.lock.json) in
  the backend / e2e / api-client-drift / codeql-csharp jobs.
- `npm ci --prefer-offline --no-audit --no-fund` in the 4 npm jobs.

Demo (docker-compose.yml, local only — NOT used by CI): web image node:24 →
node:24-slim (~1.1GB → 232MB verified). The container only runs `npm ci && ng serve`
and the native deps ship prebuilt glibc binaries, so slim needs no toolchain —
verified: npm ci clean, ng serve boots, app returns 200.

Note: CI timing/behaviour can only be confirmed on the Gitea runner (not observable
locally). Every change here is independently revertable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:28:19 +02:00
ehoandClaude Opus 4.8 2820cd3476 fix(ci): e2e — let Playwright manage both servers (fixes the 2-hour hang)
CI / e2e (push) Successful in 2m50s
CI / frontend (push) Successful in 1m49s
CI / storybook-a11y (push) Successful in 4m55s
CI / backend (push) Successful in 1m18s
CI / codeql (csharp) (push) Failing after 2m3s
CI / codeql (javascript-typescript) (push) Failing after 1m29s
CI / api-client-drift (push) Successful in 1m42s
The e2e job backgrounded `dotnet run &` and `ng serve &` in separate Actions
steps, then `npx wait-on` (no timeout) in a later step. A process started with
`&` in one step is killed when that step's shell exits, so wait-on waited forever
on servers that were already gone — the job hung until the runner's hard limit
(~2h; Gitea's act_runner doesn't reliably enforce timeout-minutes).

Move both servers into Playwright's `webServer` (an array: backend + `npm start`),
so Playwright starts them, waits for readiness, runs the suite, and tears them
down in the one `npm run e2e` process — CI and local alike. The CI e2e job is now
just npm ci / playwright install / npm run e2e. `reuseExistingServer` is on locally
(reuses a running app, incl. the docker stack) and off in CI (fresh start).

Verified locally via `CI=1 npm run e2e` against free ports + a clean db: both
smoke tests pass in ~15s (no hang). The earlier local failure was leftover SQLite
state (WP-22) from a dirty run resuming a Concept — CI checks out fresh, so its db
is always empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:07:35 +02:00
ehoandClaude Opus 4.8 6fa155caa3 fix(ci): green build — en translations, quiet docker npm, local CI check
CI / frontend (push) Successful in 1m51s
CI / storybook-a11y (push) Successful in 5m4s
CI / backend (push) Successful in 1m21s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled
Three fixes so Gitea CI is green and stays that way:

- **ng build --localize**: `messages.en.xlf` (hand-maintained, matched by id) had drifted
  ~93 units behind the source since WP-24 — `beheer.*`, `brief.canvas.*`, `brief.diff.*`,
  `orgTemplate.*`, `brief.besluit.*`, etc. With `i18nMissingTranslation: "error"` that fails
  the frontend job. Added English `<target>`s for every missing unit (interpolation `<x/>`
  tags preserved); both nl + en bundles build clean.
- **docker npm noise**: the web container's `npm ci` now runs `--no-fund --loglevel=error`,
  so `docker compose up` boots without npm 11's deprecation / allow-scripts / funding
  warnings (verified in the running container: 0 warning lines). The underlying Angular
  webpack-builder deprecation is upstream; migrating to @angular/build is out of scope.
- **local CI check**: `scripts/ci-local.sh` + `npm run ci` mirror the CI jobs (lint,
  format:check, check:tokens, test, build --localize, audit, backend format+test,
  api-client drift; `--full` adds storybook-a11y) so a red build is caught before pushing.
  Opt-in pre-push hook at scripts/githooks/pre-push. Documented in CLAUDE.md.

Verified: `npm run ci` passes end-to-end; storybook-a11y 169/169 (the local parallel-worker
timeouts were the docker stack starving chromium, not a regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:56 +02:00
ehoandClaude Opus 4.8 5761b13dd2 style: format the repo with prettier (green format:check)
`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>
2026-07-21 17:20:42 +02:00
ehoandClaude Opus 4.8 7dfbd4501f docs: fix ARCHITECTURE.md mermaid diagrams + document the autosave last-mile guard
CI / frontend (push) Failing after 1m6s
CI / storybook-a11y (push) Successful in 5m3s
CI / backend (push) Successful in 1m20s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled
Rewrite all five mermaid blocks to portable syntax so they render (verified via
a mermaid-native render): split two `classDef`/`class` statements that shared a
line (the "why not just signals" graph); drop the `reduce() — PURE` participant
alias and the `;` inside a Note (§2c sequence); remove `<br/>` and parenthetical
transition labels from the two stateDiagram-v2 blocks (RemoteData, intake),
moving the aside into a note; normalize `<br/>`→`<br>` and drop `<b>` in the
atomic-hierarchy flowchart.

Extend §2g to document the now-closed gap: a CanDeactivate guard flushes the
pending debounce before in-app navigation, and beforeunload warns + best-effort
flushes on hard close (noting the HttpClient/keepalive limitation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:29:09 +02:00
ehoandClaude Opus 4.8 645fad088e feat(fp): flush pending autosave before navigation/unload
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>
2026-07-21 16:29:09 +02:00
ehoandClaude Opus 4.8 e5edae4970 fix(a11y): letter-canvas "added" diff badge contrast (WP-27)
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>
2026-07-21 13:55:24 +02:00
ehoandClaude Opus 4.8 5968ef9030 docs: ADR-0004 stamdata data-file + WP-29 stamdata beheer editor
Amend ADR-0004 to record what shipped in 0e77faf: a table may live as typed
C# or as a typed JSON data-file (the compile-time value check traded for
editor ergonomics, caught by StamdataValidationTests instead); valid-time and
the generic StamdataCatalog/StamdataTable model; the low-code PR-emitting
editor is no longer "future". Add WP-29 backlog file (done) + README row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 13:44:01 +02:00
ehoandClaude Opus 4.8 0e77faf351 feat(stamdata): admin stamdata maintenance editor (beheer)
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>
2026-07-21 13:43:51 +02:00
ehoandClaude Opus 4.8 c459fa0a60 feat(stamdata): extract policy-question text into Stamdata
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / frontend (push) Failing after 1m21s
CI / storybook-a11y (push) Failing after 5m27s
CI / backend (push) Successful in 1m35s
CI / codeql (csharp) (push) Failing after 1m50s
CI / api-client-drift (push) Successful in 2m1s
CI / e2e (push) Failing after 3h14m48s
Move the geldigheidsvragen wording out of DiplomaRules into
Stamdata.PolicyQuestions (business-editable text, config-as-code); DiplomaRules
keeps only the rule of which questions apply. Extend StamdataValidationTests
(no blank id/wording, distinct ids in the manual set) and update ADR-0004.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:46:38 +02:00
ehoandClaude Opus 4.8 a7f737e18c docs: ADR-0004 stamdata-as-code
CI / frontend (push) Failing after 1m20s
CI / storybook-a11y (push) Failing after 5m27s
CI / e2e (push) Has been cancelled
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / backend (push) Has been cancelled
Document the config-as-code strategy for business-tunable reference data:
typed checked-in config validated at compile time (never a production DB),
where UI text (), reference tables (Stamdata/), and letter content
each live, and why org-templates are the deliberate runtime-editable exception.
Index it in docs/README.md and add a CLAUDE.md pointer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:41:21 +02:00
ehoandClaude Opus 4.8 fa7e9c5cfb feat(stamdata): profession↔diploma map as validated config-as-code
Extract the profession↔diploma table out of DiplomaRules into a dedicated
Stamdata.Professions module (business-editable data, separated from the rules
that consume it) and add StamdataValidationTests as the build-time gate: every
seeded diploma program must resolve to a real profession, no blank entries. A
bad edit now fails the build instead of silently rendering "Onbekend". Rules
and behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:41:04 +02:00
ehoandClaude Opus 4.8 14210fa2b0 docs: document form autosave + FE↔BE request lifecycle in ARCHITECTURE
New §2g explains field persistence (keystroke → model → 600ms snapshot
debounce; blur only marks touched, never saves) and §6a refreshes the stale
backend section with the real request lifecycle (NSwag client, httpClientFetch
seam, read/write traces) — both with relative links to the source files.
Add discovery pointers from the learning path (lesson 2.2 and the capstone).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:38:49 +02:00
ehoandClaude Opus 4.8 989a32acb4 docs: add Foundations learning path — paced 3-day onboarding route
CI / e2e (push) Waiting to run
CI / frontend (push) Failing after 1m4s
CI / storybook-a11y (push) Failing after 4m40s
CI / backend (push) Successful in 1m19s
CI / codeql (csharp) (push) Failing after 1m49s
CI / codeql (javascript-typescript) (push) Failing after 1m28s
CI / api-client-drift (push) Failing after 1m22s
New src/docs/learning-path.mdx sequences the existing Foundations docs into
self-contained, bite-size lessons (Day 1 orient / Day 2 functional core /
Day 3 quality + capstone), each with goal, time, exercise, and self-check.
Insert it into the storySort order after Overview, add a pointer from the
Overview hub, and fix a stale cibg cross-link in atomic-design.mdx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:24:46 +02:00
ehoandClaude Opus 4.8 35a9d18374 docs: regenerate Storybook documentation.json
CI / frontend (push) Failing after 1m10s
CI / storybook-a11y (push) Failing after 5m8s
CI / backend (push) Successful in 1m17s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:36:06 +02:00
ehoandClaude Opus 4.8 d4e5a76873 docs: reorganize into project/ + reference/, expand Storybook Foundations
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>
2026-07-20 20:33:32 +02:00
ehoandClaude Opus 4.8 55a0a2d166 test: split multi-assertion specs into single-behavior tests
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>
2026-07-20 20:33:25 +02:00
ehoandClaude Opus 4.8 5cae44f163 feat(security): ABAC P2/P3-lite — BIG-nummer redaction, authz audit, guard; clear dev audit
- fix(deps): pin @babel/core ^7.29.7 via overrides → npm audit 0 (dev+prod),
  no --force / no Angular downgrade; README corrected
- feat(brief): field-level PII reveal (PRD-0002 §5c) — CaseContext BIG-nummer
  ships masked; step-up-stubbed (X-Step-Up), audited POST /brief/reveal-bignummer
  unmasks it; drafter-only capability, deny-by-default. Realized on the BIG-nummer
  (no BSN on the wire)
- feat(authz): no-PII AuditAuthz log for reveal attempts + org-admin denials (§8)
- feat(routes): wire capabilityGuard('orgtemplate:edit') onto brief/huisstijl (§6)
- test: backend +5 (Authz + reveal endpoint), FE +3 (adapter boundary, store swap)
- docs: PRD-0002 §5c/§9, WP-18 follow-up, README

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:59:20 +02:00
ehoandClaude Opus 4.8 0edfbba2a9 docs(storybook): render MDX tables + add i18n & testing-strategy docs/skill
- fix: wire remark-gfm into addon-docs so GFM pipe tables in *.mdx render
  (previously raw text in cibg-gaps/layers/atomic-design docs)
- add src/docs/i18n.mdx (Foundations/Internationalization): the $localize
  locale seam + how to test languages without coupling to copy
- add src/docs/testing.mdx (Foundations/Testing strategy): per-layer spec
  matrix, house style, Storybook a11y gate, GREEN gate
- add .claude/skills/test-strategy skill

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:26:34 +02:00
ehoandClaude Opus 4.8 ba32e3dd9f feat(fp): brief v3 — besluit-driven guided drafting
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>
2026-07-20 18:53:53 +02:00
ehoandClaude Opus 4.8 62ba0b98c4 fix(cibg): address fieldset padding + atomic-design audit cleanup
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>
2026-07-20 15:04:02 +02:00
ehoandClaude Opus 4.8 5e36d68f11 feat(fp): WP-27 — brief UX layer (undo/redo, standaardbrief, passage search, diff badges)
CI / storybook-a11y (push) Failing after 4m43s
CI / frontend (push) Failing after 1m15s
CI / backend (push) Successful in 1m24s
CI / codeql (csharp) (push) Failing after 2m51s
CI / e2e (push) Failing after 3h4m8s
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / api-client-drift (push) Successful in 1m53s
Brief letter-composition UX improvements:
- undo/redo history in the brief store (snapshot stacks, Ctrl/Cmd+Z) + retry-save
- "Standaardbrief invoegen" starter for empty sections; isDefault library passages
  (backend DTO/seed + adapter parse)
- passage-picker client-side search (rich-text textOf helper)
- rejection diff badges on the letter canvas + show/hide changes toggle
  (pure brief-diff domain fns + spec)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:36:32 +02:00
ehoandClaude Opus 4.8 950fb5f0b2 fix(cibg): wizard fields render as grey fieldset groups
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>
2026-07-20 13:39:13 +02:00
ehoandClaude Opus 4.8 f6c837f281 feat(fp): WP-26 — admin org-template editor
CI / codeql (javascript-typescript) (push) Failing after 1m19s
CI / frontend (push) Failing after 57s
CI / storybook-a11y (push) Successful in 4m27s
CI / backend (push) Successful in 1m15s
CI / codeql (csharp) (push) Failing after 1m42s
CI / api-client-drift (push) Successful in 1m40s
CI / e2e (push) Failing after 3h11m33s
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>
2026-07-06 08:22:55 +02:00
ehoandClaude Sonnet 5 1bb9383344 feat(fp): WP-25 — server-rendered letter HTML preview
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>
2026-07-05 12:56:36 +02:00
ehoandClaude Fable 5 c07a33ee3e feat(fp): WP-24 — letter canvas (edit on the letter)
CI / frontend (push) Failing after 59s
CI / storybook-a11y (push) Successful in 4m22s
CI / backend (push) Successful in 1m18s
CI / codeql (csharp) (push) Failing after 1m47s
CI / codeql (javascript-typescript) (push) Failing after 1m20s
CI / api-client-drift (push) Successful in 1m42s
CI / e2e (push) Failing after 3h8m54s
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>
2026-07-05 11:47:51 +02:00
ehoandClaude Fable 5 5a610c10f0 feat(fp): WP-23 — org-template backend + admin role
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>
2026-07-05 11:17:05 +02:00
ehoandClaude Sonnet 5 44eb2d2186 chore(deps): update npm packages within declared ranges; reformat for prettier 3.9.4
CI / frontend (push) Successful in 1m46s
CI / storybook-a11y (push) Successful in 4m23s
CI / backend (push) Successful in 1m14s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled
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>
2026-07-05 10:29:36 +02:00
ehoandClaude Sonnet 5 556f2f47bf feat(fp): WP-22 — durable persistence (SQLite/EF Core)
Applications, documents (+ audit log) and the brief move off static in-memory
Dictionaries onto a real SQLite file via EF Core, so demo data survives a
process restart or `docker compose restart api` for the first time. The three
stores (ApplicationStore/DocumentStore/BriefStore) keep their exact public
signatures and static-class shape — no DI, no async ripple into Program.cs's
minimal-API handlers — each method just opens a short-lived AppDbContext via
Db.Create() under the same lock it already had. Opaque nested shapes (a
wizard's draft snapshot, a brief's sections/placeholders/status) are stored as
JSON text columns rather than redesigned into relational tables, matching the
existing "don't interpret it" posture.

Found two things the WP's own text got wrong, corrected in
docs/backlog/WP-22-durable-persistence.md's Deviations section: SeedData never
seeded these three stores (only the read-only BRP/DUO-mimicking GETs, which
stay in-memory) so there's no seed step; and no new docker-compose volume is
needed since the existing bind mount already covers the SQLite file — verified
against this environment's real podman-backed compose stack, not just by
reading the file.

Also: pinned SQLitePCLRaw.bundle_e_sqlite3 to 3.0.3 (EF Core Sqlite's own
transitive default bundles a pre-3.50.2 SQLite with a known high-severity
memory-corruption advisory); found and fixed a real xUnit test race where
concurrent test-class hosts stomped a shared static connection-string field,
fixed by disabling cross-class test parallelization rather than adding DI the
stores don't otherwise need.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 10:19:23 +02:00
ehoandClaude Sonnet 5 40dbcb2606 feat(fp): WP-21 — resilience seams (correlation-id, idempotency, retry)
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>
2026-07-04 20:03:41 +02:00
ehoandClaude Sonnet 5 e276629107 feat(fp): WP-20 — second locale proof (nl/en build seam)
angular.json gains an i18n block (sourceLocale nl, en translation file) and
an `en` build/serve configuration with i18nMissingTranslation: "error" so a
new $localize string without an English unit fails the build, not silently
falls back. CI now runs `ng build --localize` to build both locales every
run. Verified end-to-end, not just "the build succeeded": the nl bundle
ships "Inloggen met DigiD", the en bundle ships "Log in with DigiD".

Incidental: prettier/compodoc regen noise in docs/wcag-checklist.md,
src/docs/a11y.mdx, documentation.json from the same working session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 18:16:11 +02:00
ehoandClaude Sonnet 5 26c2c5acd0 feat(fp): WP-19 — Playwright e2e smoke against the real FE+backend
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>
2026-07-04 10:13:40 +02:00
ehoandClaude Sonnet 5 e272869f00 feat(fp): WP-17 — app-level a11y: route focus, template lint, WCAG checklist
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>
2026-07-04 08:26:01 +02:00
ehoandClaude Sonnet 5 f3de30b72c feat(fp): WP-16 — component a11y: description wiring + alert role
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>
2026-07-04 08:15:24 +02:00