Skip to content

Assessment Boekhouding: multiuser platform om samen te werken aan assessments#283

Draft
robbertbos wants to merge 33 commits intomainfrom
experimenteel/samenwerken
Draft

Assessment Boekhouding: multiuser platform om samen te werken aan assessments#283
robbertbos wants to merge 33 commits intomainfrom
experimenteel/samenwerken

Conversation

@robbertbos
Copy link
Copy Markdown
Member

@robbertbos robbertbos commented Mar 16, 2026

Summary

  • Monorepo-herstructurering: assessment-core package, boekhouding-frontend, boekhouding-backend en standalone-form apps
  • Multi-user platform met Keycloak-authenticatie, PostgreSQL (Drizzle ORM), projectbeheer en ledenbeheer
  • Optimistic locking met versiegeschiedenis en conflictdetectie voor gelijktijdig bewerken
  • URN-gebaseerde veldidentificatie i.p.v. nanoid instance IDs
  • Versie-consolidatie: opeenvolgende saves van dezelfde gebruiker (< 15 min) worden samengevoegd in één versie
  • Field-level conflict resolution met edits als bron van waarheid
  • Claude Code configuratie, assessments plugin en RVO styling skill

Nog te doen

  • Plugin in marketplace zetten
  • CI workflow testen
  • End-to-end testen met meerdere gebruikers

Migrate from single form-app to a monorepo with three workspaces:
- packages/assessment-core: shared assessment engine extracted from form-app
- apps/boekhouding-frontend: Vue 3 SPA with Keycloak auth and project management
- apps/boekhouding-backend: Fastify REST API with PostgreSQL (Drizzle ORM)
- apps/standalone-form: standalone form (successor to form-app)

Add container setup (Containerfiles, compose.yaml, Keycloak dev config),
database migrations, and updated CI/CD workflows for the new structure.
CLAUDE.md with project conventions, assessments plugin with domain
knowledge skills (assessment-schema, begrippenkader, DPIA) and a
validation agent, plus a standalone RVO styling skill.
- Instance IDs use taskId (non-repeatable) or taskId[index] (repeatable)
  instead of taskId_nanoid, making them deterministic and comparable
- Assessment output uses $schema reference to versioned JSON Schema
- Export contains only answers (no navigation state), flat without
  namespace nesting
- URN in YAML sources split into urn + version; getUrn() assembles them
- Backend diffSnapshots builds URN-based field_id for assessment_edits
- Snapshot migration (v1→v2) converts legacy nanoid keys on load
- Schema files renamed with version: assessment-definition.v1,
  assessment-output.v2, begrippenkader.v1
- Answer fields renamed: timestamp→lastEditedAt, added lastEditedBy
- Metadata: savedAt→createdAt, optional createdBy for audit exports
- Fix: login redirect to /projecten, anchor button color inheritance
Update Claude plugin marketplace source from "local" to "directory"
and rename standalone fallback URL from /standalone/ to /invulhulpen/.
Rename @par-assessment/* to @overheid-assessment/* across all packages,
imports, configs and documentation. Clean up Swagger UI by hiding the
Fastify topbar and servers dropdown, adding rate limit documentation
to the OpenAPI spec, and increasing the rate limit to 300/min.
@robbertbos robbertbos self-assigned this Mar 16, 2026
Edits worden de enige bron van waarheid voor assessment-state. De
state-kolom is verwijderd uit assessmentVersions — state wordt
opgebouwd via rebuildState() door edits te replayen. cachedState op
de instance blijft de snelle read-path.

Backend:
- initial_state edit bij POST voor bootstrapping van rebuild
- rebuildState() utility met fallback naar cachedState voor legacy data
- GET /versions/:v/edits endpoint voor versie-diffs
- GET /versions/:v?includeState=true rebuildt state on demand
- expectedVersion check verplaatst vóór alle save-paden (inclusief
  no-change path) — voorkomt stale saves
- expectedVersion is nu verplicht bij state-saves (400 bij ontbreken)
- Consolidation verwijderd — elke save maakt een nieuwe versie voor
  betrouwbare optimistic locking

Frontend:
- VersionHistory gebruikt edits-API i.p.v. twee volledige state-diffs
- Edits worden gecollapsed tot netto wijzigingen per veld
- task_instance_add/remove gefilterd uit diff-weergave
- HTML definition spans gestript in diff en conflict dialog
- ConflictResolutionDialog.vue voor field-level conflict resolution
- ApiPersistence: pendingChanges tracking, auto-merge bij geen
  veld-overlap, conflict dialog bij overlap
- Auto-merge en resolve slaan direct op (niet via debounce)
- flushSave annuleert alleen timer, geen API call bij unmount
- lastSavedState altijd gezet (ook voor lege assessments)
- structuredClone vervangen door JSON.parse/stringify (Vue proxies)
- true/false weergegeven als Ja/Nee in conflict dialog
- Lege currentRootTaskId overgeslagen bij applyAppState (restore fix)
- Ophalen van taken... tekst uitgelijnd en spatie voor ellipsis verwijderd
Same-user saves within 15 minutes are consolidated into the existing
version instead of creating a new one, reducing version noise during
active editing. Schema renamed savedBy/savedAt to createdBy/createdAt
with new updatedAt column. Debounce lowered from 2s to 500ms since
consolidation now prevents version proliferation. Seed script cleaned
up unused state field on assessment_versions inserts.
- Switch build and release workflows from npm to pnpm via corepack
- Bump actions/checkout, setup-node, setup-python from v4 to v5
- Rename YAML sources to lowercase (case-sensitive Linux CI)
- Remove PR preview workflow (not needed on this branch)
- Fix standalone-form build: add env.d.ts to assessment-core,
  fix allowImportingTsExtensions conflict, fix clearSavedState type
- Apply ruff-format to generate_licenses.py
- Fix trailing newlines in Drizzle files and SKILL.md
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch from f67eb1a to 6697937 Compare March 16, 2026 17:04
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch 4 times, most recently from 4dcd720 to f429263 Compare March 16, 2026 21:20
- Move all container files to containers/ directory (Containerfiles,
  compose.dev.yaml, nginx config, keycloak realm)
- Add build-containers.yaml workflow for GHCR (dev/frontend, dev/backend)
- Rename build.yaml to build-standalone.yaml
- Add nginx security headers (NCSC/BIO2) with nginx-unprivileged on port 8080
- Add runtime config.json via envsubst (no more VITE_* build-args needed)
- Align env var names with ZAD platform (OIDC_URL, OIDC_REALM,
  OIDC_CLIENT_ID, DATABASE_SERVER_FULL, PUBLIC_HOST for CORS)
- Add OIDC_INTERNAL_URL for split-horizon Keycloak in Docker networks
- Add deployment.md with architecture, env vars, CI/CD, ZAD config
- Remove hardcoded python-version from all workflows
- Include standalone form dist (with favicon) in frontend container
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch from f429263 to d7e284a Compare March 16, 2026 21:24
- Inline favicon als data-URI in standalone HTML via custom Vite plugin
- Kopieer favicon naar boekhouding-frontend public/ voor /favicon.ico
- Voeg security headers toe aan standalone invulhulpen nginx location
- Hernoem workflow naar "Build Containers"
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch from 053c8cd to c9e066c Compare March 17, 2026 16:42
@MinBZK MinBZK deleted a comment from github-actions Bot Mar 17, 2026
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch from 277aae6 to d82c693 Compare March 17, 2026 22:09
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch 2 times, most recently from d3d5de7 to e2e1aaa Compare March 17, 2026 22:23
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch from e2e1aaa to e676139 Compare March 18, 2026 06:15
- Import detecteert automatisch DPIA of pre-scan via URN, namespace of
  answer keys — gebruiker hoeft niet meer zelf het type te kiezen
- Ondersteunt zowel AssessmentOutput (flat) als AssessmentState
  (namespaced) formaat, inclusief v1→v2 migratie
- Navigatieknoppen (vorige/volgende/voltooid) wrappen correct op kleine
  schermen via flexbox met responsive breakpoint
- applyStateToStores en parseAndValidateImport als gedeelde utils
  (vervangt gedupliceerde logica in Api/LocalPersistence)
- completedTasks in export-schema zodat sectiestatus bewaard blijft
- snapshotBaseline in Form.vue voorkomt phantom diffs na initialisatie
- Versie 1 wordt niet meer geconsolideerd (importstatus behouden)
- WCAG: aria-labels op file inputs, role="group" op navigatie
- ExportProvider/EXPORT_KEY verwijderd, directe exportToPdf aanroep
- Migreer van @v2 (sync) naar @V3 (async V2 API met task polling)
- Combineer frontend + backend deploy in één API call via components input
- Schakel wait-for-ready en PR comment met preview URLs in
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 20, 2026

🚀 Preview Deployment

Your changes have been deployed to a preview environment:

api: https://assessments.rijksapp.nl

frontend: https://assessments.rijksapp.nl

This deployment will be automatically cleaned up when the PR is closed.

- Voeg must-revalidate toe aan hoofd-SPA en standalone invulhulp
- Browser checkt altijd bij server, krijgt 304 als ongewijzigd
- Hashed assets (js/css) blijven 1 jaar gecached via immutable
- Update Autoriteit Persoonsgegevens URL (verplaatste pagina)
- Exclude Docker-interne hostnames (keycloak, backend, postgres)
- Exclude npmjs.com (blokkeert bots) en httpproblems.com (template literal)
Herhaalbare groepen (zoals persoonsgegevens) konden niet meerdere
antwoorden opslaan. Oorzaak: het state-formaat had geen structuur
voor meerdere instanties. Herschreven naar arrays met _index per
element, namespace-wrapping verwijderd, completedTasks naar metadata,
UI-state naar localStorage.

Pre-scan import in DPIA: antwoorden gaan naar apart _prescanAnswers
veld zodat usePreScanReferences ze vindt zonder DPIA-secties onterecht
als voltooid te markeren. completedTasks worden niet meer afgeleid uit
answer keys voor moderne exports (met $schema/urn).

Backend diffStates en rebuildState herschreven voor nieuw formaat met
backward-compatibiliteit voor opgeslagen legacy data.
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch from 9f1e1ef to 14c9122 Compare March 22, 2026 10:17
Nieuw `image` veldtype voor het uploaden van afbeeldingen in assessments.
Afbeeldingen worden als base64 data-URI opgeslagen in de JSON-state,
waardoor exports (JSON, PDF, Markdown) volledig zelfstandig zijn.

- ImageValue datamodel met optionele titel, omschrijving en bron
- Drag & drop upload met visuele overlay bij vervangen
- Client-side resize met formaatbehoud (PNG→PNG, JPEG→JPEG)
- SVG-bestanden worden veilig gerasterd naar PNG op 2x resolutie
- EXIF-metadata (GPS, auteur, camera) wordt automatisch verwijderd via Canvas API
- Slimme foutmeldingen voor onondersteunde bestandstypen
- PDF export: afbeeldingen als losse blokken met metadata, paginabreed
- Markdown export: afbeeldingen buiten tabellen met metadata
- Versiegeschiedenis: afbeelding-thumbnails in plaats van raw base64
- Legacy migratie: oude URL-referenties worden graceful afgehandeld
- Assessment definition schema v2 met image type en item_name
- Output schema uitgebreid met imageValue definitie
- API body-limiet verhoogd naar 25 MB
- YAML-bronnen verwijzen naar definitie-schema
- item_name voor enkelvoudsvorm in knoppen van herhaalbare velden
- Documentatie over image handling en privacy
Taak 1.2 "Afbeeldingen" is omgezet van een tekstveld (URL-referentie) naar
een volledig image-veldtype met drag & drop upload, metadata en embedded
opslag als WebP data-URI.

- ImageValue datamodel: data (WebP base64), titel, omschrijving, bron
- Drag & drop upload met visuele overlay bij vervangen
- EXIF-metadata automatisch verwijderd via Canvas API
- SVG veilig gerasterd naar WebP (XSS-preventie)
- Lossless WebP voor diagrammen, lossy voor foto's
- PDF export: on-the-fly WebP→PNG conversie (pdfmake ondersteunt geen WebP)
- Markdown export: referentie-stijl links (data onderaan document)
- Versiegeschiedenis: afbeelding-thumbnails i.p.v. raw base64
- Legacy migratie: oude URL-referenties graceful afgehandeld
- Assessment definition schema v2 met image type en item_name
- Toegankelijkheid: ARIA labels, keyboard support, live regions
- Documentatie: docs/image-handling.md
Gedeelde autoGrowTextarea util toegevoegd en toegepast op alle
textarea-velden. Bestaande duplicatie in ProjectDetail en
VersionHistory vervangen door dezelfde util.
Open tekstvelden hebben nu een Lezen/Bewerken toggle die markdown
rendert als opgemaakte tekst. Beveiligingsmaatregelen: allowlist-renderer
(geen DOMPurify nodig), protocol-filtering op links (javascript:/data:
gestript), raw HTML en afbeeldingen gestript, task list checkboxes als
unicode. PDF-export verwerkt markdown-opmaak in antwoorden.
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch 3 times, most recently from 5a0483c to 454898d Compare March 25, 2026 23:08
Voegt een opmerkingensysteem toe waarmee projectleden opmerkingen kunnen
plaatsen bij individuele formuliervelden, reacties kunnen geven op elkaars
opmerkingen, en threads kunnen oplossen.

Backend:
- Comments tabel met threading (max 1 niveau diep)
- REST API met CRUD, resolve/reopen, en polling via ?since= parameter
- Rolgebaseerde toegang: commenter+ mag plaatsen, editor+ mag oplossen
- currentUserId in response voor frontend eigenaarschap-check

Frontend:
- CommentPanel met positionering naast formuliervelden
- CommentBadge met teller in de header
- Inline "Opmerking" buttons per veld via useFieldCommentIndicators
- Pinia store met 10s polling en incrementele merge
- Bewerken door op eigen tekst te klikken, auto-resize textareas
- Outline action-buttons met icons en kleur-coded hover states
- Responsief: op mobiel stackt het panel met klikbare veldnaam-links
- Panel schuift naar content via CSS transform op brede schermen
- Versie-mismatch banner wanneer een collega wijzigingen aanbrengt

Toegankelijkheid (WCAG 2.1 AA):
- Keyboard-toegankelijke bewerkbare opmerkingen (role, tabindex, keydown)
- aria-hidden op decoratieve icons, aria-label op textareas
- focus-visible outlines op alle interactieve elementen
- Kleurcontrast timestamps verbeterd (#999 → #666)
- Opgeloste threads via border/achtergrond i.p.v. opacity
…ming

Verlopen tokens werden niet gedetecteerd: saves faalden stilletjes zonder
melding aan de gebruiker, waardoor werk verloren ging bij pagina-refresh.

- Background token refresh elke 4 minuten via updateToken()
- SessionExpiredError bij 401-responses in api.ts
- Modale dialoog "Je bent uitgelogd" met re-login knop
- Onopgeslagen wijzigingen bewaard in sessionStorage over re-login heen
- Comment polling stopt bij sessie-verloop
- User-switch detectie na re-login voorkomt data-vermenging
- 23 unit tests voor auth, API-interceptie en sessionStorage-roundtrip
@robbertbos robbertbos force-pushed the experimenteel/samenwerken branch from 454898d to 40cea1d Compare March 26, 2026 06:29
robbertbos and others added 6 commits March 26, 2026 08:35
lastModifiedAt retourneerde 1970-01-01 (Unix epoch) bij assessments
zonder comments, waardoor de frontend steeds met ?since=1970 pollde.
Nu retourneert de backend null en pollt de frontend zonder since-parameter.
Standaard markdown voegt regels samen tenzij er twee spaties of een
lege regel tussen staan. Met breaks: true (GFM-gedrag) worden enkele
newlines nu <br> in zowel de HTML-preview als de PDF-export.
Co-authored-by: mybee-bot <mybee@local>
Co-committed-by: mybee-bot <mybee@local>
`syncInstances` koppelde target-instances aan bronnen via de in-memory property `mappedFromInstanceId`. Die wordt nergens gepersisteerd, dus na elke herload was ``ie leeg — iedere bron werd als nieuw gezien en kreeg een duplicaat-target via `max_index + 1`. Op productie was taak 7.1 zo opgelopen tot **569 instances** bij 3 bron-instances.

Reconciliatie gebeurt nu op gedeelde `_index`, die wél persisteert. Single-pass over de index-union: match → pointer bijwerken, alleen bron → target aanmaken, alleen target → target verwijderen.

## Meegenomen fixes

- **Parent-link bij nieuwe targets**: `addRepeatableTaskInstance` krijgt `task.parentId` mee, zodat TaskGroup ze via `getInstanceIdsForTask(taskId, parentInstanceId)` daadwerkelijk rendert.
- **Geneste repeatables**: `rebuildRepeatableInstances` slaat repeatables over waarvan de parent-taak zelf al repeatable is. Die child-instances zijn al correct gelinkt via `createTaskInstance`'s child-propagatie; opnieuw aanmaken overschreef de parent-link met een task-id i.p.v. een instance-id, waardoor TaskGroup de outer instance als lege groep renderde.
- **Sectie-brede warning bij lege bron**: `missingSourceDependencies` in TaskSection liep alleen over leaf-tasks. Dependencies op intermediate repeatables (zoals 7.1) werden zo overgeslagen — de sectie verborg zichzelf zonder melding. Recursie telt elk niveau nu mee.
- **Inline waarschuwing per instance**: als een gemapte instance gekoppeld is aan een bron met lege waarde (bijv. half-ingevulde Partij namen bij sectie 6), toont 7.1 voor die instance *Vul eerst "Partij naam" in bij sectie "6. Betrokken partijen"* in plaats van lege invulvelden. Zodra de bron ingevuld wordt, verschijnt de normale inhoud.

Co-authored-by: mybee-bot <mybee@local>
Co-committed-by: mybee-bot <mybee@local>
Voorkomt stille dataverdwijning op twee plekken en voegt een gedeelde
utility toe die uitrekent welke antwoorden onbereikbaar worden door een
gebruikersactie.

Drie consumers:

- Delete-dialog in TaskGroup: klik op Verwijder van een repeatable
  instance met ingevulde afhankelijke antwoorden toont eerst een
  native <dialog> met opsplitsing per sectie. Instances zonder
  afhankelijke data worden zonder tussenklik direct weggehaald.
  Was: cascade-verwijdering ging stil en zonder waarschuwing.
- Conditional undo-window in Form.vue: wijzigt de gebruiker een
  parent zodat dependents onzichtbaar worden, dan worden die
  antwoorden uit de store gehaald (save schrijft ze echt weg) en
  parallel 60 seconden in een in-memory cache gezet. Flipt de
  gebruiker binnen die window terug, dan komen ze weer in de store
  — natuurlijke undo zonder toast of knop. Was: verborgen
  antwoorden bleven stil in cachedState staan tot expliciet wissen.
- JSON-export-filter: buildOutputData past shouldShowTask toe voor
  groepering, gelijk aan de pdf- en markdown-export. Was: JSON
  lekte antwoorden die in PDF/Markdown al gefilterd werden.

Meegenomen fixes:

- parseAndValidateImport behoudt groepering in de output zodat de
  backend-opslag ná import consistent blijft met wat latere saves
  opleveren. Zonder deze wijziging vertaalt diffStates de eerste
  save als honderden structuur-edits omdat v1 flat was opgeslagen
  en v2 groept.
- Vue-watcher in useConditionalHideReconcile staat via een armed-
  flag inert tot seedFromStore draait na init. Voorkomt dat
  applyStateToStores + rebuildRepeatableInstances + syncInstances
  tijdens load worden aangezien voor gebruikerswijzigingen.

Co-authored-by: mybee-bot <mybee@local>
Co-committed-by: mybee-bot <mybee@local>
- Vier files (drizzle migration + meta + test fixture) missten een trailing
  newline, wat de end-of-file-fixer pre-commit hook liet falen.
- Het voorbeeld 'http://myserver:5174' in vite.config.ts werd door de
  link-checker als echte URL behandeld en faalde op DNS. Scheme verwijderd
  zodat het een illustratief host:port is.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant