Files
crewli/dev-docs/BACKLOG.md
bert.hausmans d4a450d193 docs(backlog): mark WS-7 Observability as closed (mei 2026)
Acceptance criteria 1-14 voldaan; observability volledig operationeel
op monitoring.hausdesign.nl. Implementation criteria 3, 4, 5, 6, 8,
11, 12, 13, 14 via 4 PRs op feat/ws-7-observability; operationele
criteria 1, 2, 7, 9, 10 via deploy-checklist.

Hernoem 'Observability follow-ups (post WS-7)' sectie-header naar
'(post WS-7 closure)' voor accuratesse na PR-3 + PR-4. Closure-entry
geplaatst onderaan 'Opgeloste items (mei 2026)' om chronologische
volgorde (oldest-first) te respecteren — WS-7 op 2026-05-07 volgt
WS-3 PR-C op 2026-05-06 die volgt op WS-TOOLING-001 op 2026-05-05.

Refs: dev-docs/ARCH-OBSERVABILITY.md, dev-docs/runbooks/observability-{triage,erasure}.md
2026-05-07 22:37:15 +02:00

1893 lines
87 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Crewli — Product Backlog
> Gedocumenteerde wensen en features die bewust zijn uitgesteld.
> Bijgewerkt: April 2026
>
> **Gebruik:** Voeg nieuwe items toe als ze tijdens development ontstaan.
> Geef elk item een prioriteit en fase zodra je het gaat oppakken.
## Architectuur consolidatie sprint (actief)
Zie `dev-docs/ARCH-CONSOLIDATION-2026-04.md` voor volledige scope, principes en
werkstroomvolgorde. Sprint gestart april 2026, 8 werkstromen, 22-32 dagen werk
totaal. Tijdens de sprint worden bestaande backlog-items die door de sprint
worden opgelost daar expliciet gemarkeerd, en krijgen items die na de sprint
worden opgepakt een `[post-consolidatie]` tag.
---
## Fase 3 — Geplande features
### ARCH-01 — Recurrence / Terugkerende events
**Aanleiding:** Schaatsbaan use case — 8 weken, elke za+zo openingsdagen.
**Wat:** Organisator definieert één template sub-event met RRULE.
Platform genereert automatisch alle instanties.
**Details:**
- RRULE formaat (RFC 5545): `FREQ=WEEKLY;BYDAY=SA,SU;UNTIL=20270126`
- `events.recurrence_rule` (string nullable) — al gereserveerd in schema
- `events.recurrence_exceptions` (JSON) — cancelled + modified dates
- UI: "Genereer openingsdagen" wizard
- Aanpassen van één instantie raakt template niet
- "Alleen deze dag" / "Alle volgende dagen" / "Alle dagen" (Google Calendar patroon)
**Schema:** Kolommen al aanwezig in v1.7. Alleen generator-logica ontbreekt.
---
### ARCH-02 — Min/max shifts per vrijwilliger
**Aanleiding:** Zonder limiet claimen enthousiaste vrijwilligers 8+ shifts
(48 uur in één weekend), resulterend in burn-out en no-shows op latere shifts.
**Wat:** Per event/festival instelbaar minimum en maximum aantal shifts
dat een vrijwilliger kan claimen.
**Details:**
- `events.min_shifts_per_volunteer` (int nullable)
- `events.max_shifts_per_volunteer` (int nullable)
- ShiftAssignmentService checkt limiet bij claim/assign
- Portal toont voortgang: "Je hebt 2 van minimaal 4 shifts geclaimd"
- Bij bereiken maximum: verdere claims geblokkeerd met melding
**Prioriteit:** Laag — Nice-to-have. Geen prioriteit op dit moment.
**Afhankelijk van:** Shift claiming flow
---
### ARCH-03 — Sectie templates / kopiëren van vorig event
**Aanleiding:** Organisatoren die elk jaar dezelfde secties en shifts opzetten.
**Wat:** "Kopieer secties van vorig festival" functie in de UI.
Kopieert festival_sections + shifts structuur (zonder toewijzingen).
**Details:**
- UI: dropdown "Kopieer structuur van..." bij aanmaken festival
- Optie: kopieer alleen secties / secties + shifts / alles
- Tijden worden proportioneel aangepast aan nieuwe datums
**Prioriteit:** Hoog — bespaart veel handmatig werk bij terugkerende festivals
---
### ARCH-04 — Cross-festival conflictdetectie
**Aanleiding:** Vrijwilliger die bij twee festivals van dezelfde organisatie
op dezelfde dag ingepland staat.
**Wat:** Waarschuwing (geen blokkade) als iemand al actief is op een
ander festival van dezelfde organisatie op dezelfde datum.
**Details:**
- Soft check — waarschuwing tonen, niet blokkeren
- Relevant bij organisaties met meerdere festivals tegelijk
- Query: `shift_assignments` cross-festival op person_id + datum
---
### ARCH-05 — Shift fairness / prioriteitswachtrij
**Aanleiding:** Populaire shifts worden direct volgeboekt door snelle vrijwilligers.
**Wat:** Optionele wachtrij-modus waarbij het systeem eerlijk verdeelt
op basis van: reliability score, aantal uren al ingepland, aanmeldvolgorde.
**Details:**
- `shifts.assignment_mode` (enum: first_come | fair_queue | manual)
- Fair queue: systeem wijst toe op basis van algoritme
- Organisator keurt resultaat goed voor publicatie
**Prioriteit:** Middel — nice-to-have voor grote festivals
---
### ARCH-06 — Locatie-gebaseerd shift-overzicht
Cross sub-event filter op location_id. Toont alle shifts op een fysieke locatie
ongeacht programmaonderdeel.
**Schema:** `locations` tabel en `shifts.location_id` bestaan al.
**Prioriteit:** Laag
---
### ARCH-07 — Accreditatie-templates per sectie/dag combinatie
MUST-HAVE bij accreditatie build. Templates worden primaire toewijzingsmethode.
Per crowd_type + sectie + dag → automatisch voorgestelde accreditatie-items.
Handmatige per-persoon toewijzing is de uitzondering, niet de norm.
**Schema:** Nieuwe tabel `accreditation_templates` nodig.
**Prioriteit:** Hoog — direct meebouwen bij accreditatie-module
---
## Fase 3 — Communicatie & Notificaties
### COMM-01 — Real-time WebSocket notificaties
**Aanleiding:** Differentiator — geen van de concurrenten heeft dit.
**Wat:** Push notificaties via Laravel Echo + Soketi voor:
- Nieuwe vrijwilliger aanmelding
- Shift geclaimd
- Uitnodiging geaccepteerd
- Shift niet gevuld (waarschuwing)
- No-show alert op show-dag
**Tech:** Laravel Echo + Soketi (zelf-gehoste WebSocket server)
**Frontend:** Notificatie bell in topbar activeren
---
### COMM-02 — Topbar volledig activeren
**Aanleiding:** Vuexy topbar staat er maar is niet aangesloten op Crewli.
**Wat:**
- Zoekbalk (CTRL+K) aansluiten op Crewli-entiteiten
(personen, events, secties zoeken)
- Notificatie bell koppelen aan COMM-01
- App switcher: Organizer / Portal wisselen (admin SPA retired; platform admin in `/platform/*`)
- User avatar: gekoppeld aan ingelogde gebruiker (deels al gedaan)
**Prioriteit:** Middel — werkt zonder maar verbetert UX significant
---
### COMM-03 — Globale zoekfunctie (cmd+K)
**Aanleiding:** Differentiator — cross-entiteit zoeken.
**Wat:** Modal zoekbalk die zoekt over:
personen, events, artiesten, secties, shifts
**Tech:** Meilisearch of database full-text search
**Prioriteit:** Laag — Fase 4
---
### COMM-04 — SMS + WhatsApp campagnes via Zender
**Aanleiding:** WeezCrew heeft dit als sterk punt.
**Wat:** Bulk communicatie via Zender (zelf-gehoste SMS/WhatsApp gateway)
- Normal urgency → email
- Urgent → WhatsApp
- Emergency → SMS + WhatsApp parallel
**Tech:** ZenderService (al gedocumenteerd in dev guide)
**Afhankelijk van:** Communicatie module backend
---
## Fase 3 — Show Day & Operationeel
### OPS-01 — Mission Control
**Aanleiding:** In2Event's sterkste feature.
**Wat:** Real-time operationele hub op show-dag:
- Live check-in overzicht per sectie
- Artiest handling (aankomst, soundcheck, performance status)
- No-show alerts met automatische opvolging
- Inventaris uitgifte (portofoons, hesjes)
**Prioriteit:** Hoog voor show-dag gebruik
---
### OPS-02 — No-show automatisering
**Aanleiding:** 30-minuten alert voor niet-ingecheckte vrijwilligers.
**Wat:** Automatische WhatsApp/SMS via Zender als vrijwilliger
niet is ingecheckt 30 min na shift-starttijd.
**Schema:** `show_day_absence_alerts` al aanwezig ✅
**Afhankelijk van:** COMM-04 (Zender), OPS-01 (Mission Control)
---
### OPS-03 — Allocatiesheet PDF generator
**Aanleiding:** WeezCrew heeft branded PDF per crew.
**Wat:** Gepersonaliseerde PDF per vrijwilliger/crew:
taakbeschrijving, tijden, locatie, QR-code voor check-in.
**Tech:** DomPDF (al geïnstalleerd)
**Prioriteit:** Middel
---
### OPS-04 — Scanner infrastructuur
**Aanleiding:** QR check-in op locatie.
**Wat:** Scanstations configureren, koppelen aan hardware.
`scanners` tabel al aanwezig in schema ✅
**Prioriteit:** Laag — Fase 4
---
## Fase 3 — Vrijwilligers & Portal
### VOL-01 — apps/portal/ vrijwilliger self-service
**Aanleiding:** Vrijwilligers moeten zichzelf kunnen aanmelden en
shifts claimen zonder toegang tot de Organizer app.
**Wat:**
- Publiek registratieformulier (multi-step)
- Login portal voor vrijwilligers
- Beschikbaarheid opgeven (time slots kiezen)
- My Shifts overzicht
- Shift claimen met conflictdetectie
- "Ik kan toch niet komen" workflow
**Afhankelijk van:** Sections + Shifts backend (al klaar ✅)
---
### VOL-02 — Vrijwilliger paspoort + reliability score
**Aanleiding:** Platform-breed profiel dat accumuleert over jaren.
**Wat:**
- Festival-paspoort: visuele tijdlijn van deelgenomen festivals
- Reliability score (0.0-5.0): berekend via scheduled job
- Coordinator-beoordeling per festival (intern, nooit zichtbaar)
- "Would reinvite" indicator bij heruitnodiging
**Schema:** `volunteer_profiles`, `volunteer_festival_history` al aanwezig ✅
---
### VOL-03 — Post-festival evaluatie + retrospectief
**Aanleiding:** Automatische feedback na het festival.
**Wat:**
- 24u na laatste shift: evaluatiemail naar vrijwilligers
- Max 5 vragen (beleving, shift kwaliteit, terugkomen?)
- Gegenereerd retrospectief rapport per festival
- Coordinator-beoordeling parallel (intern)
**Schema:** `post_festival_evaluations`, `festival_retrospectives` al aanwezig ✅
---
### VOL-04 — Shift swap workflow (portal)
**Aanleiding:** Vrijwilliger wil shift ruilen met collega.
**Wat:**
- Open swap: iedereen mag reageren
- Persoonlijke swap: specifieke collega vragen
- Na akkoord beide: coordinator bevestigt (of auto-approve)
- Wachtlijst: bij uitval automatisch aanschrijven
**Schema:** `shift_swap_requests`, `shift_absences`, `shift_waitlist` al aanwezig ✅
---
## Fase 3 — Artiesten & Advancing
### ART-01 — Artist advancing portal (apps/portal/)
**Aanleiding:** Crescat's sterkste feature.
**Wat:**
- Sectie-gebaseerd advance portal via gesignde URL
- Per sectie onafhankelijk submitbaar (Guest List, Contacts, Production)
- Milestone pipeline: Offer In → Advance Received
- Per-artiest zichtbaarheidscontrole van advance secties
- Submission diff tracking (created/updated/untouched/deleted)
**Schema:** `advance_sections`, `advance_submissions` al aanwezig ✅
---
### ART-02 — Timetable (stage + drag-drop)
**Aanleiding:** FullCalendar timeline view voor podia-planning.
**Wat:**
- Timeline view per podium
- Drag-and-drop performances
- B2B detectie (twee artiesten op zelfde podium zelfde tijd)
**Tech:** FullCalendar (al in stack ✅)
---
## Fase 3 — Formulieren & Leveranciers
### FORM-01 — Formulierbouwer
**Aanleiding:** WeezCrew heeft een krachtige drag-sorteerbare builder.
**Wat:**
- Drag-sorteerbaar, conditionele logica
- Live preview
- Iframe embed voor externe websites
- Configureerbare velden per crowd type
**Schema:** `public_forms` al aanwezig ✅
---
### FORM-02 — TAG_PICKER → user_organisation_tags sync rebuild ✅ Done in S2b (2026-04-17)
**Aanleiding:** TagSyncService verwijderd in S2a Form Builder legacy purge. Semantiek (TAG_PICKER-antwoorden syncen naar user_organisation_tags bij registratie-goedkeuring) blijft valide.
**Wat:** Herbouwen als listener op FormSubmissionSubmitted tegen de nieuwe FormValue + TAG_PICKER field_type. Integreren via PersonIdentityService::confirmMatch zonder directe service-injection in PersonController.
**Eerdere call-sites (nu verwijderd):** PersonController::approve(), PersonIdentityService::syncRegistrationTags().
**Landed artefacts:**
- `App\Services\FormBuilder\FormTagSyncService::rebuildForPerson` — idempotent union-of-TAG_PICKER-values rebuild, only mutates `source=self_reported` rows, no-op when `person.user_id IS NULL`.
- `App\Listeners\FormBuilder\SyncTagPickerSelectionsOnSubmit` — ShouldQueue listener on `FormSubmissionSubmitted`, filters to `event_registration` purpose with `subject_type=person` + at least one `TAG_PICKER` value. Logs + swallows errors so sibling listeners (§31.1/§31.3/§31.8) keep running.
- `App\Services\PersonIdentityService::confirmMatch` — calls `FormTagSyncService::rebuildForPerson` after setting `person.user_id` (deferred-sync path for person who submitted before the user account existed).
- Contract frozen in ARCH-FORM-BUILDER.md §31.10 (authoritative block) and covered by `tests/Feature/FormBuilder/Integration/TagPickerSyncListenerTest`.
**Deferred integration tests (move under FORM-03 if needed):** GdprDeleteCascadeTest, EmailNotificationFlowTest, CodeOfConductGatingTest, SupplierIntakeFlowTest, CrowdListAutoAddTest (§31.9). Only §31.10 ships with S2b; other contracts wait until their feature arrives.
---
### FORM-BINDING-SNAPSHOT-MULTI — snapshot shape voor multi-binding per field
**Aanleiding:** WS-5a legt de relationele `form_field_bindings` tabel neer met een UNIQUE op `(owner_type, owner_id, target_entity, target_attribute)`. Dat laat meerdere bindings per field toe zolang ze op verschillende kolom-paren landen. De snapshot-writer (`FormSubmissionService::buildSnapshot` via `FormFieldBindingService::toJsonShape`) embed op dit moment maar één binding per field — de eerste. `schema_snapshot.fields[*].binding` is een object, geen array.
**Wat:** Snapshot-shape besluiten voor multi-binding: ofwel `binding` → array-of-objects, ofwel een nieuwe sleutel `bindings`. Migratiepad voor bestaande snapshots (ARCH §4.6.1). Reader-compat behouden.
**Trigger:** wanneer ARCH §6.1 patroon-scenario's multi-binding op één field rechtvaardigen (bv. Pattern C naar twee target entities tegelijk).
**Prioriteit:** Laag — out-of-scope van WS-5a, geen huidige user impact.
---
### FORM-BUILDER-LIBRARY-AUDIT-LOG — Audit FormFieldLibrary-level changes to bindings, validation rules, configs, and options
**Aanleiding:** Post-WS-5d, four form-builder child-table services (`FormFieldBindingService`, `FormFieldValidationRuleService`, `FormFieldConfigService`, `FormFieldOptionService`) emit activity-log events on FormField subjects only. Changes to FormFieldLibrary entries — which affect organisation-wide reusable field definitions — land silently in the audit log. This is the consistent behaviour inherited from WS-5a and extended through WS-5b/c/d, but it represents an audit-trail gap for library administration.
**Wat:** introduce parallel `library.*` activity-log events (`library.bindings_replaced`, `library.validation_rules_replaced`, `library.configs_replaced`, `library.options_replaced`) emitted by the same four services when the owner is a `FormFieldLibrary`. Document the convention addition in `ARCH-FORM-BUILDER.md` §6.7 and §17.4.2 + §17.5.2 + §17.6.3. Single cross-cutting work package.
**Prioriteit:** Middel — geen blocker; candidate sprint post-WS-5, before any external audit tooling is wired up (consumers shouldn't have to deal with the asymmetry).
**Related:** WS-5a §6.7 activity log events paragraph; WS-5b §17.4.2 / §17.5.2 paragraphs; WS-5d §17.6.3 paragraph.
---
### ~~FORM-BUILDER-MORPH-SCOPE-BASE-CLASS — Extract base class across the four WS-5 morph-scope siblings~~
**Status: closed 2026-04-25**`FormFieldChildTableMorphScope`
abstract base extracted; the four concrete scopes are now marker
subclasses preserving identity. Phase A diff verification confirmed
the four concrete `apply()` + `resolveOrganisationId()` bodies were
byte-equal (zero divergence across three pairwise comparisons). Net
diff: +165 / 377 lines. Tests 1208 → 1208 (3260 assertions, identical).
Larastan baseline clean; Rector dry-run 357 → 355.
See `app/Models/Scopes/FormFieldChildTableMorphScope.php` and
ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md §"Uitvoering — base scope-
class extractie (2026-04-25)".
---
### FORM-04 — `grace_days` configurable on public_token rotation
**Aanleiding:** S2c §10.4 opgeleverd met een hardgecodeerd 7-daagse grace window in `PublicFormTokenResolver`. `rotatePublicToken` endpoint accepteert wel een `grace_days` request param maar schrijft die nergens naartoe; `form_schemas` heeft geen `grace_days` kolom.
**Wat:**
- Kolom `form_schemas.public_token_grace_days` (unsignedSmallInteger nullable, default null).
- `rotatePublicToken` service persisteert de ontvangen `grace_days` value (fallback: config default).
- `PublicFormTokenResolver::GRACE_DAYS` leest uit `form_schemas.public_token_grace_days ?? config('form_builder.public_token.default_grace_days', 7)`.
- Test: rotatie met grace_days=3 levert 410 na 4 dagen.
**Prioriteit:** Laag — operationele tuning, niet frontend-blocking.
---
### DOC-01 — Scramble / OpenAPI generator voor API.md
**Aanleiding:** `dev-docs/API.md` wordt met de hand bijgehouden per sprint — bij snelle iteratie landt hij altijd een slag achter de code. Scramble (of equivalent) genereert OpenAPI uit FormRequest + Resource introspectie zonder annotaties.
**Wat:** Scramble installeren, publieke form endpoints een dedicated `public` tag geven, CI-hook die de generated spec vergelijkt met een checked-in `dev-docs/api.openapi.yaml`, README link naar de live viewer.
**Prioriteit:** Middel — verlaagt docs-drift substantieel; past in een "developer-experience" sprint.
---
### DOC-02 — VitePress docs:build faalt op missing image
**Aanleiding:** `/docs/volunteer/je-aanmelden-via-een-link.md` verwijst
naar `./images/placeholder.png` dat niet bestaat. Dev mode werkt,
build faalt. Blokkeert CI als die `docs:build` gaat draaien.
**Wat:** Placeholder afbeelding toevoegen OF de referentie weghalen /
vervangen door een echte screenshot van het registratieflow.
**Prioriteit:** Laag — cosmetisch, niet blokkerend voor dev.
---
### DOC-03 — Formulieren sidebar story is incompleet
**Aanleiding:** Tijdens S3a PR 2 is
`docs/organizer/forms/concepts/wat-is-een-formulier.md` gewired in de
sidebar, samen met de nieuwe veldtype-pagina's. Maar de bredere
Formulieren-sidebar mist nog: publicatieflow, inzendingen-overzicht,
templates, webhook-configuratie, conditionele logica.
**Wat:** Dedicated docs-sprint voor de Formulieren-module in
VitePress. Schat: 6-8 pagina's in Nederlands, aimed at organisatoren
die formulieren configureren.
**Prioriteit:** Middel — landt best vlak voor/na S3b (organizer
form-builder UI), omdat screenshots pas zin hebben als de UI staat.
---
### DOC-04 — `scripts/install-claude-sync-hooks.sh` opnemen in SETUP/onboarding
**Aanleiding:** WS-4 pre-flight audit vond dat `scripts/sync-claude-docs.sh`
bestaat en door de post-commit hook draait, maar de hook-installer
(`scripts/install-claude-sync-hooks.sh`) is niet terug te vinden in de
developer onboarding-instructies. Nieuwe clones missen de hook.
**Wat:** Voeg een regel aan `dev-docs/SETUP.md` (of een post-install
checklist) toe die nieuwe developers opdraagt `install-claude-sync-hooks.sh`
te runnen. 1 regel, geen scope-impact.
**Prioriteit:** Laag — nice-to-have, niet blokkerend voor dev of CI.
---
### FORM-05 — Smart identity-match on public submission values
**Stub-status (S3a PR 2, 2026-04-23):** Public event_registration
submissions landen al met `identity_match_status='pending'` via de
bestaande `TriggerPersonIdentityMatchOnFormSubmit` listener. De portal
`IdentityMatchBanner` leest dit veld en toont de juiste copy. Contract
ligt vast in `tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest`.
**Resterend werk (de eigenlijke FORM-05):** public form submissions
(subject_type=null) krijgen momenteel *altijd* 'pending' omdat er nog
geen Person bestaat om tegen te matchen. Breid uit met:
- Nieuwe methode op PersonIdentityService:
`detectMatchesByValues(array $values, string $organisationId): MatchResult`
- Een extra tak in `TriggerPersonIdentityMatchOnFormSubmit::resolveStatus`
die voor public submissions de values uit `FormSubmission->values`
extraheert (email / first_name / last_name via de schema binding),
deze methode aanroept, en 'matched' / 'pending' / 'none' schrijft.
Zo krijgt de portal-UX een betekenisvol signaal in plaats van een
constante 'pending'.
Prioriteit: Medium. Kan gebundeld worden met de organizer
`person_identity_matches` UI (ook nog een frontend gap).
---
### SUP-01 — Leveranciersportal + productieverzoeken
**Aanleiding:** Leveranciers moeten productie-informatie kunnen indienen.
**Wat:**
- Token-gebaseerde portal toegang (geen account nodig)
- Productieverzoek indienen (mensen, tech, stroom, voertuigen)
- Crowd list indienen voor hun crew
**Schema:** `production_requests`, `material_requests` al aanwezig ✅
---
## Fase 4 — Differentiators
### DIFF-01 — Cross-event crew pool + reliability score
**Aanleiding:** Vrijwilligers hergebruiken over events van dezelfde organisatie.
**Wat:** Eén klik heruitnodiging op basis van vorig jaar.
Reliability score zichtbaar naast naam in de lijst.
---
### DIFF-02 — Crew PWA (mobiel)
**Aanleiding:** On-site zelfservice voor crew op hun telefoon.
**Wat:** Progressive Web App voor:
shifts bekijken, briefing lezen, clock-in, push notificaties.
---
### DIFF-03 — Publieke REST API + webhooks
**Aanleiding:** Enterprise integraties.
**Wat:** Gedocumenteerde publieke API + webhook systeem
voor third-party integraties (ticketing, HR, etc.)
---
### DIFF-04 — CO2 / Duurzaamheidsrapportage
**Aanleiding:** Toenemende focus op duurzame events.
**Wat:** Emissieberekeningen op basis van transport en energieverbruik.
**Status:** Expliciet out of scope voor v1.x
---
## Apps & Platforms
### ~~APPS-01 — apps/admin/ volledig bouwen~~ RETIRED
**Status:** Retired — admin SPA (`apps/admin/`) is afgeschaft. Super admin functionaliteit is verplaatst naar `apps/app/` onder `/platform/*` routes voor `super_admin` gebruikers.
---
### APPS-02 — OrganisationSwitcher ingeklapte staat fix
**Aanleiding:** Flikkering/hover-bug bij ingeklapte sidebar.
**Wat:** Correcte weergave en animatie in ingeklapte staat.
**Prioriteit:** Low — cosmetisch, werkt functioneel wel
---
## Technische schuld
### TECH-01 — Bestaande tests bijwerken na festival/event refactor
**Aanleiding:** Na toevoegen parent_event_id worden bestaande tests
mogelijk fragiel door gewijzigde factory-setup.
**Wat:** Alle Feature tests reviewen en bijwerken waar nodig.
---
### TECH-05 — ESLint configuratie herstellen in apps/app/
**Aanleiding:** `npm run lint` faalt omdat `.eslintrc.cjs` niet bestaat
en er ook geen flat-config equivalent aanwezig is. Effectief draait
de app zonder lint, wat botst met CLAUDE.md's zero-compromise regels.
**Wat:** Juiste flat-config installeren en afstemmen op het huidige
Vuexy 9.5 template. Moet in één keer groen draaien.
**Prioriteit:** Middel — tooling-gap.
---
### TECH-06 — ESLint config ontbreekt in apps/portal
**Aanleiding:** `npm run lint` faalt in `apps/portal/` omdat
`.eslintrc.cjs` niet bestaat. Geen flat-config equivalent aanwezig.
Portal draait dus effectief zonder lint, wat botst met CLAUDE.md's
zero-compromise regels. Apart van TECH-05 (dat over apps/app gaat).
**Wat:** Flat-config ESLint installeren in `apps/portal/`, afgestemd
op Vue 3 + TypeScript + Vuexy 10.11.1. In één keer groen laten
draaien. Bij voorkeur gedeelde shared-config tussen apps/app en
apps/portal om drift te voorkomen.
**Prioriteit:** Middel — tooling-gap, niet user-facing.
---
### TECH-PORTAL-ESLINT-DEPS — Audit apps/portal/package.json op missing direct ESLint deps
**Aanleiding:** Tijdens de Cursor ESLint-integratie fix in `apps/app/`
(commit `4369806`, 2026-04-30) bleek dat 15 ESLint plugins, parsers en
configs alleen via pnpm-hoisting werden gevonden, niet als directe
dependencies in `package.json`. Cursor's ESLint extension gebruikt
strict module resolution en crashte op elke missing plugin in de
`@antfu/eslint-config-vue` extends-chain. Aannemelijk dat
`apps/portal/package.json` hetzelfde patroon heeft, want zelfde antfu-
config-keten en zelfde pnpm-monorepo-structuur. Zonder fix breekt het
ESLint-formatter pad voor iedereen die de portal opent in Cursor —
zelfde 3-uur-diagnose die we vandaag hebben doorgemaakt.
**Wat:**
- Run de diagnose-keten van vandaag (`ls node_modules/.pnpm | grep
"^eslint-plugin-"` vs `ls node_modules | grep "^eslint-plugin-"`,
plus de scoped variant en `vue-eslint-parser`/`@antfu/eslint-config-*`
audits) op `apps/portal/`.
- Voeg alle ontbrekende plugins als directe deps toe via `pnpm add -D`
met versies die matchen wat in pnpm store zit (zero version shifts).
- Verifieer in Cursor dat de portal ESLint extension activeert zonder
errors in Output Channel, en dat save-on-format ESLint correct firet.
**Prioriteit:** Middel — moet vóór sessie 2 (Pages migration) waar
portal-files actief in Cursor bewerkt worden. Niet kritisch nu, maar
de eerste developer die portal in Cursor opent stuit op hetzelfde
issue als vandaag.
---
### TECH-ESLINT-V9-MIGRATION — Migreer apps/app + apps/portal naar ESLint v9 + flat config
**Aanleiding:** ESLint v8.57.1 is end-of-life sinds eind 2024
(zie pnpm install warnings: `eslint@8.57.1: This version is no longer
supported`). Daarnaast zijn meerdere config-pakketten in onze chain
deprecated en migreren naar flat config: `@antfu/eslint-config-vue`,
`@antfu/eslint-config-basic`, `@antfu/eslint-config-ts`, en
`eslint-plugin-markdown`. De huidige `.eslintrc.cjs` legacy-config
werkt, maar er komen geen security fixes meer voor v8 en de transitieve
deprecated-warnings groeien per `pnpm install`. Migratie naar v9 + flat
config (`eslint.config.js`) + modern `@antfu/eslint-config` lost in één
klap alle deprecated warnings op én moderniseert de toolchain.
**Wat:**
- Eigen workstream — niet meeliften op andere sprints want config-
rewrite raakt 200+ regels.
- ESLint 8.57.1 → 9.x upgrade.
- `.eslintrc.cjs` (legacy) → `eslint.config.js` (flat config).
- `@antfu/eslint-config@0.43.x` (legacy) → `@antfu/eslint-config@latest`
(flat-config variant).
- Alle plugins meeschalen naar versies die met v9 + flat config werken.
- Test op apps/app + apps/portal — de hele lint-baseline moet groen
blijven (0 problems voor app, baseline voor portal).
- Hoort vóór WS-3 sessie 2 (pages migration) als de portal-eslint
baseline ook op 0 staat, anders na alle WS-3 sessies. Schat 1-2
dagen werk inclusief regression-fixing.
**Prioriteit:** Middel-Hoog — security-EOL is een doorslaggevend
argument; uitstel tot na WS-3 acceptabel maar niet onbeperkt. Eigen
sprint waard, geen meelift-pad.
---
### TECH-AXIOS-INTERCEPTOR-TESTS — Coverage voor de vier axios-interceptor scenarios
**Aanleiding:** TECH-AXIOS-STORE-COUPLING (gesloten 2026-05-04, zie
`git log --grep=TECH-AXIOS-STORE-COUPLING`) heeft `lib/axios.ts`
ontkoppeld van de stores via een `registerInterceptors(client, deps)`
seam plus `plugins/3.axios-bindings.ts`. Tijdens de Phase A audit van
die sessie bleek dat de vier acceptatie-scenarios geen van alle een
test hebben — niet vóór en niet ná de refactor. De refactor is
gedragsneutraal (1:1 behoud), dus er is geen regressie geïntroduceerd,
maar het blijft een echte coverage-gap die we niet wilden meeniggen
in de refactor-sessie zelf: refactor-en-test-toevoeging in dezelfde
commit-set vernietigt het vermogen om vast te stellen of de tests pre-
of post-refactor gedrag specificeren.
**Wat:** Vitest-tests die de interceptors echt laden (niet via
`vi.mock('@/lib/axios')`) en assertions doen tegen een gemockte
HTTP-laag (bv. `axios-mock-adapter`). De vier scenarios:
1. **`X-Organisation-Id` header-injection.** Set een actieve
organisatie via `useOrganisationStore`, registreer interceptors
met de bindings-deps, fire een outbound request, assert dat de
header de actieve ULID bevat. Test ook het null-pad: geen
actieve organisatie → header niet gezet.
2. **401 → auth-fail flow.** Mock de response op 401, registreer
interceptors waarbij `onAuthFail` een spy is, assert dat de
spy wordt aangeroepen exact wanneer `useAuthStore.isInitialized`
true is en niet gedurende de eerste `/auth/me`-probe (de
race-conditie die sessie 1b-iii repareerde — die guard moet
blijven werken).
3. **403 + `impersonation_ended` → revocation flow.** Mock de
response op `403` met body `{ impersonation_ended: true }`,
registreer interceptors waarbij `onImpersonationRevoked` een
spy is, assert dat de spy precies één keer wordt aangeroepen
en dat de generieke 403-toast NIET wordt geactiveerd (dat was
een early-return in `axios.ts`, makkelijk per ongeluk te
breken bij een toekomstige refactor).
4. **4xx/5xx error toast.** Mock 403/404/422/503/5xx/network-error
responses, assert dat `notify` de juiste boodschap + level
krijgt voor elk geval. 422 met body-`message` moet het
server-bericht doorgeven; 422 zonder `message` mag geen toast
triggeren (huidige gedrag).
**Hoe niet:** geen unit-tests die het hele `lib/axios.ts`-module
mocken — die testen de seam niet, alleen het mock-framework. De
toegevoegde waarde zit in een test-fixture die de echte
`registerInterceptors` aanroept met een echte axios-instance die
tegen `axios-mock-adapter` praat.
**Niet in scope:** integratietests die echt door Pinia heen lopen.
De seam is bewust callback-injectie zodat tests met spy-callbacks
volstaan. Wie de full-stack flow wil dekken (zie `App.vue`'s
session-init dance) doet dat met E2E in een latere sprint.
**Prioriteit:** Middel — de gap is reëel maar niet blokkerend. De
6 bestaande `vi.mock('@/lib/axios')`-tests vangen "named export
werkt nog" af, en de vier flows zijn manueel via de browser
verifieerbaar. Aanbevolen moment: eerste WS-3 PR die `axios.ts`
of de bindings-plugin opnieuw raakt, of opvolger van TECH-APP-VITEST
als bredere harness-uitbreiding.
---
### TECH-TYPED-ROUTER-DRIFT — apps/app/typed-router.d.ts drifts when pages/ changes are merged without rebuild
**Aanleiding:** Op 2026-05-04 bleek `apps/app/typed-router.d.ts`
achter te lopen op de pages-tree: vier `form-failures` routes
(organisation + platform, list + detail) waren al maanden geleden
gelandt in main, maar de gegenereerde route-types waren nooit
mee-gecommit. Pas een lokale `pnpm build` triggerde
`unplugin-vue-router` om het bestand te regenereren, waarna een
losse commit (`3198698`) de drift dichtmaakte. Zonder die toevallige
build-run had de drift onbeperkt door kunnen lopen — TypeScript
flagde de stale routes niet, en niemand routeert via de typed names
hard genoeg dat het brak.
Het bestand is tracked, niet gitignored. Dat betekent: elke PR die
een file in `apps/app/src/pages/` toevoegt of hernoemt, moet
óók de regeneratie van `typed-router.d.ts` meecommitten. In de
praktijk gebeurt dat nu inconsistent.
**Wat:** Drie reële paden, kies bij implementatie:
- **Approach 1 (preferred): pre-commit hook in lefthook.**
Voeg een lefthook pre-commit hook toe die — wanneer een
`apps/app/src/pages/**` file in de staging-set zit —
`unplugin-vue-router` triggert en `apps/app/typed-router.d.ts`
re-stage't als hij is veranderd. Hook is silent op de happy
path; faalt loud bij regeneratie-error. Voordeel: types altijd
in sync in git, fresh clones werken zonder eerst te builden.
Nadeel: extra commit-tijd voor pages-changes (~2-5s per commit
in de plugin), en `unplugin-vue-router` heeft geen standalone
CLI-mode dus de hook wordt een Node-script dat de plugin laadt
en handmatig aanstuurt — fragiel bij plugin-versie-upgrades.
- **Approach 2 (alternative): gitignore + regenereer in postinstall.**
Voeg `apps/app/typed-router.d.ts` toe aan `.gitignore`. Voeg een
`postinstall` script in `apps/app/package.json` dat `vue-tsc` of
een vergelijkbare prebuild-stap triggert die `unplugin-vue-router`
zijn type-emit laat doen. Voordeel: drift is structureel
onmogelijk — er is niets meer in git om uit sync te raken. Nadeel:
fresh clones zijn een paar seconden trager (`pnpm install` doet
meer werk), en als `postinstall` faalt heb je IDE-rode-squiggles
totdat je het oplost.
- **Approach 3 (status quo + alarm): CI-check.**
Houd het bestand tracked, maar voeg een CI-stap toe die
`unplugin-vue-router` regenereert en faalt als de output
verschilt van wat in git staat. Voordeel: minimale lokale
workflow-impact. Nadeel: CI-only — drift wordt pas gevonden
bij PR-build. Werkt alleen als er CI is (op het moment van
schrijven: er is nog geen GitHub-Actions / Drone / Gitea-Actions
pipeline geconfigureerd in deze repo).
**Prioriteit:** Laag — geen functionele impact, alleen DX en
type-safety-betrouwbaarheid. Geen blocker voor andere
workstreams. Aanbevolen moment: meelift met de eerste
substantiële pages-tree refactor (bijvoorbeeld WS-3 PR-B die
de portal pages naar `apps/app/src/pages/portal/` verhuist —
dán is de drift-kans het grootst en de pijn van onbeschermd
laten ook).
---
### TECH-WS3-BOUNDARIES-SUBZONES — Sub-zone import-boundaries inside components/ and pages/
**Aanleiding:** WS-3 sessie 1c heeft top-level zone-boundaries in
`apps/app/` neergezet via `eslint-plugin-boundaries`. De `/dev-docs/ARCH-CONSOLIDATION-2026-04.md`
§4.2 target layout introduceert sub-zones binnen die top-level zones —
specifiek `components/{organizer,portal,shared}/` en
`pages/{(auth),portal,register,events,persons,organisations,platform}/`.
De architecturale intent is dat `components/portal` niet uit
`components/organizer` mag importeren (en vice versa), met `shared` als
de gemeenschappelijke uitgang. Sessie 1c heeft die sub-zone
enforcement bewust uitgesteld omdat de sub-folders nog niet bestaan;
pre-emptieve rules op niet-bestaande directories worden stille dode
config die drift.
**Wat:**
- **Precondition:** WS-3 PR-B is gemerged en de §4.2 sub-folder
structuur is gelandt (`components/{organizer,portal,shared}/` en
`pages/{(auth),portal,...}/` bestaan fysiek met content).
- Breid `boundaries/elements` in `apps/app/.eslintrc.cjs` uit met:
- `{ type: 'components-organizer', pattern: 'src/components/organizer/**' }`
- `{ type: 'components-portal', pattern: 'src/components/portal/**' }`
- `{ type: 'components-shared', pattern: 'src/components/shared/**' }`
- (ontworpen sub-zones voor `pages/` analoog)
- Voeg per-sub-zone rules toe: `components-portal` en
`components-organizer` mogen beide uit `components-shared` importeren,
maar niet uit elkaar. `pages/portal/` mag niet uit `pages/events/`
(en de andere organizer-pages) importeren, en omgekeerd.
- Resolve violations die bij eerste activatie naar boven komen.
- ETA: 1-2 uur zodra precondities ervoor liggen.
**Prioriteit:** Middel — preventieve architectuur-discipline voor de
multi-tenant context-isolatie tussen organizer en portal UI-paden.
Zonder deze rules is de kans groot dat een ontwikkelaar tijdens een
PR-B follow-up onbewust portal- en organizer-componenten verstrengelt.
---
### TECH-WS3-BOUNDARIES-ROUTER-ZONE — Add `router/` zone to boundaries matrix
**Aanleiding:** WS-3 sessie 1c audit (§3 forward-compatibility) flagde
dat de §4.2 target layout `src/plugins/1.router/` vervangt door een
flat `src/router/`. De huidige boundaries-matrix in
`apps/app/.eslintrc.cjs` mapt router-files naar de `plugins` zone
(omdat ze fysiek in `src/plugins/1.router/` zitten). Zodra de
verhuizing plaatsvindt — geplant in een latere WS-3 PR — moet de
matrix-config dat reflecteren, anders vallen router-files buiten de
`boundaries/elements` mapping en flag-stormt de plugin met "no rule
found".
**Wat:** In dezelfde commit/PR die `src/plugins/1.router/` naar
`src/router/` verhuist:
- Voeg toe aan `boundaries/elements` in `apps/app/.eslintrc.cjs`:
```js
{ type: 'router', pattern: 'src/router/**' },
```
Plaats vóór `plugins` in de array (first-match-wins ordering).
- Voeg toe aan `boundaries/element-types` rules:
```js
{ from: 'router', allow: ['types', 'utils', 'lib', 'plugins', 'stores'] },
```
- Verifieer `pnpm lint` blijft op 0 problemen.
**Trigger:** "src/plugins/1.router/" → "src/router/" verhuizing (latere
WS-3 PR, vermoedelijk PR-B als die de router-tree consolideert).
**Prioriteit:** Laag — geen actie tot de verhuizing plaatsvindt; dan
verplicht 5-minute follow-up.
---
### TECH-08 — Paginated response meta wordt weggegooid in organizer composables
**Aanleiding:** `apps/app/src/composables/api/useSections.ts` en
`apps/app/src/composables/api/useFormSchemas.ts` definiëren beiden een
lokale `PaginatedResponse<T> = { data: T[] }` shape die alleen de
`data` array eruit trekt. De Laravel paginator geeft ook `links` en
`meta` (huidige pagina, totaal, per-page) terug — die informatie gaat
nu verloren. Voor de huidige consumers geen probleem (geen paginatie-
controls), maar zodra een lijstweergave in de organizer UI een
"Volgende"-knop, pagina-selector of totaaltelling wil tonen, loopt de
composable tegen die beperking aan. PR-b2 (/forms lijst-view) is de
eerste concrete trigger.
**Wat:** Upgrade de shared response-shape in beide composables naar
`{ data: T[], links: { first, last, prev, next }, meta: { current_page, from, last_page, path, per_page, to, total } }` (exacte veldnamen
conform Laravel's `ResourceCollection` default). Retourneer het hele
meta-blok mee uit de `useXList` composables zodat de UI kan paginate.
Bij voorkeur één gedeelde TypeScript interface exporteren uit een
nieuwe `apps/app/src/types/api.ts` zodat de derde, vierde, ...
composable die volgt hetzelfde patroon erft. Nieuwe composables voor
lijst-endpoints moeten vanaf dat moment deze interface gebruiken.
**Prioriteit:** Middel — blokkeert geen huidige features, maar elke
composable die zonder paginering-support wordt gebouwd voegt werk toe
aan de latere migratie. Oplossen vlak vóór PR-b2 paginering-UI
introduceert is het natuurlijke moment.
---
### TECH-FRONTEND-URL-CONSOLIDATE — Refactor email controllers to drop per-app URL map
**Aanleiding:** WS-3 PR-B2b consolideerde naar één SPA en één
auth-cookie. Drie controllers bouwen nog een per-app URL map
(`'admin' / 'app' / 'portal' => config('app.frontend_*_url')`) voor
outbound emails. In productie resolven alle `FRONTEND_*` env vars
naar dezelfde host (`https://crewli.app`); de map-structuur is
functioneel redundant maar staat structureel intact.
**Wat:** Refactor de drie controllers om alleen `frontend_app_url`
te gebruiken. Verwijder de `'portal'` key uit de URL maps; collapse
naar een single-URL consumer. Email templates die schakelen op
`app === 'portal'` ook updaten.
**Files:**
- `api/app/Http/Controllers/Api/V1/EmailChangeController.php`
- `api/app/Http/Controllers/Api/V1/PasswordResetController.php`
- `api/app/Http/Controllers/Api/V1/PersonController.php`
- Email templates die de `app` parameter consumeren
**Prioriteit:** Laag — purely code-cleanliness, geen functionele of
security impact (productie env vars zijn al geconsolideerd). Effective
post-WS-3 PR-B2b.
---
### OPS — Retire `portal.crewli.app` DNS record
**Aanleiding:** Post-WS-3 PR-B2b serves crewli.app als single SPA;
WS-3 PR-B2b's deploy-config voegt een 301-redirect server block toe
voor `portal.crewli.app → crewli.app$request_uri`. DNS is nog niet
gerepointed en de zone bestaat nog.
**Wat:** Operationele taak (geen code). Twee stappen:
1. Monitor traffic naar het redirect server block voor 30 dagen.
Bij significant verkeer: identificeer bron (oude bookmarks,
externe links) en notify stakeholders voordat retirement gaat
gebeuren.
2. Bij nul / negligible verkeer: repoint DNS record naar
`crewli.app` (CNAME), of verwijder de zone volledig en laat
het redirect server block in nginx config voor de happstige
transition.
**Prioriteit:** Laag — niet code, geen blocker. Pak op wanneer
analytics monitoring volwassen genoeg is om "is dit nog in gebruik?"
te beantwoorden. Geen deadline.
---
### TECH-PIVOT-ROLES-MULTI — Multi-role per (user, organisation) pivot
**Aanleiding:** WS-3 PR-B2a maakt context-aware routing op
`me.contexts.available` en `me.organisations[].roles`. Het pivot-veld
`organisation_user.role` is vandaag een single string (één rol per user
per org). De resource emit `roles` als 1-element array zodat het
frontend-contract forward-compatible is, maar het schema ondersteunt
nog niet meerdere rollen per relatie.
**Wat:** Architectuur-discussie + design-document, niet een directe
schema-uitbreiding. Te beantwoorden vragen voordat dit gepland wordt:
- Spatie-permission-integratie: blijft `organisation_user.role`
een free-form string of komt het onder `model_has_roles` met
team-id = organisation_id? Spatie's "teams" feature is bedoeld voor
precies dit scenario.
- Multi-role-precedence: als een user `org_admin` ÉN `event_manager` is
binnen dezelfde organisatie, hoe resolven policies? Hoogste
permissie-set? Meest restrictieve? Expliciete merge?
- Migratie-pad: bestaande pivot-rijen (single string) → array of
pivot-tabel naar `model_has_roles`? Backfill-strategie?
- Frontend impact: `organisations[].role` (scalar) blijft voorlopig
staan voor backward-compatibility. Wanneer mag dat veld weg?
**Prioriteit:** Laag — geen blocker voor B2a, B2b of de 4 kern-workflows.
Pas oppakken wanneer een concrete use case multi-role per (user, org)
vereist (denkbaar: festival waarbij organizer ook als crew werkt).
**Belangrijk:** dit is GEEN simpel "voeg een kolom toe" werk. Pak het
niet op als drive-by tijdens een ander ticket; het verdient een eigen
ARCH-discussie en RFC.
---
### TECH-HOOK-001 — `block-dangerous-bash.sh` substring matching botst met commit messages
**Aanleiding:** WS-TOOLING-001 smoke tests onthulden dat `block-dangerous-bash.sh` substring-matchet op de hele bash command-string. Een commit message die een geblokkeerd patroon beschrijft (bv. `git commit -m "blocks git reset --hard"`) triggert de hook op zijn eigen commit. Workaround tijdens implementatie: rephrase de message of `git commit -F /tmp/msg.txt`.
**Wat:**
- Refactor `block-dangerous-bash.sh` naar argv-tokenization in plaats van substring-matching.
- Parse de eerste niet-quoted token als command verb (`git`, `php`, `composer`, etc.).
- Match alleen op verb + flags (eerste paar args), niet op `-m "..."` of `-F file` argument bodies.
- Test fixtures toevoegen: commit messages met blocked patterns, shell-pipes (`&&`, `;`).
- Behoud achterwaartse compatibiliteit voor bestaande blocked patterns (force push, migrate:fresh, db:wipe, dependency updates, rm -rf).
**Prioriteit:** Laag — workaround is werkbaar. Aanpakken bij de eerste keer dat het commit-flow daadwerkelijk blokkeert in een actieve sessie.
**Refs:** `.claude/hooks/block-dangerous-bash.sh`, `dev-docs/CLAUDE_CODE_TOOLING.md`.
---
### TECH-CMD-001 — `/sprint-status` leest geen sprint-anchor docs
**Aanleiding:** WS-TOOLING-001 smoke test 8 liet zien dat `/sprint-status` alleen de eerste 50 regels van `BACKLOG.md` leest. Voor een actieve consolidatie-sprint waarvan de scope in een ARCH-anchor doc staat (`ARCH-CONSOLIDATION-2026-04.md`), mist de output het echte "next item" — het commando antwoordt eerlijk dat dat in een ander document staat, maar dat is niet ideaal voor "waar staan we?".
**Wat:**
- Detecteer aanwezige sprint-anchor docs in `dev-docs/`: `ARCH-CONSOLIDATION-*.md`, `ARCH-*-SPRINT.md`, `RFC-*.md`.
- Voeg de top-30 regels van de meest-recent gewijzigde anchor toe aan de status-output.
- Heuristiek voor "actieve" anchor: meest recent door `git log --name-only` aangeraakt in de laatste 14 dagen.
- Output blijft beknopt (5-10 regels totaal); alleen de relevante anchor-snippet wordt geprepend.
**Prioriteit:** Laag — cosmetisch, het commando geeft nu al een eerlijke "kijk in deze doc" hint.
**Refs:** `.claude/commands/sprint-status.md`.
---
### TECH-STYLE-001 — eenmalige `pint` pass over hele `api/` codebase
**Aanleiding:** WS-TOOLING-001 smoke test 1 onthulde dat `pint --dirty` óók formatteert op niet-aangeraakte regels in een gewijzigd bestand wanneer die regels nog niet eerder door pint heen zijn geweest. Concreet: een edit aan `Event.php` triggerde reformatting van `static::addGlobalScope(new OrganisationScope())` naar `self::addGlobalScope(new OrganisationScope)` in een aparte methode. Functioneel identiek, maar veroorzaakt onverwachte diff-noise in feature-commits en vervuilt `git blame`.
**Wat:**
- Eénmalige `cd api && vendor/bin/pint` over de hele codebase, los van enige feature-werk.
- Eigen commit met message `style: pint pass over codebase`.
- Run vóór de eerstvolgende grote feature-sprint zodat `pint --dirty` in de PostToolUse hook daarna alleen Bert's daadwerkelijke edits formatteert.
- Verifieer dat het hele backend nog groen test na de reformat (`composer test`).
**Prioriteit:** Laag — cosmetisch, niet blokkerend. Aanbevolen moment: vlak vóór RFC-FORM-BUILDER-UI implementatie zodat S3b op een schone baseline landt.
**Refs:** `.claude/hooks/post-edit-pint.sh`, `dev-docs/CLAUDE_CODE_TOOLING.md`.
---
### ~~TECH-02 — scopeForFestival helper op Event model~~ ✅ OPGELOST
---
### ~~TECH-03 — DevSeeder uitbreiden met festival-structuur~~ ✅ OPGELOST
---
### ~~TECH-04 — EventController.store() redundante ternary~~ ✅ OPGELOST
---
### ~~TECH-07 — @form-schema transitive dep op @core/utils/validators~~ ✅ OPGELOST — resolved in PR-a1
---
## Opgeloste items (mei 2026)
- ~~**WS-TOOLING-001**: Claude Code deterministic guard-rail layer (5 hooks, `crewli-reviewer` subagent op Opus 4.7, 3 slash commands `/sprint-status` `/review-multitenancy` `/sync-docs`, `dev-docs/CLAUDE_CODE_TOOLING.md`). 8/8 smoke tests groen, live integratie geverifieerd. Merge `ad36c06` op 2026-05-05. Follow-ups: TECH-HOOK-001, TECH-CMD-001, TECH-STYLE-001.~~ ✅
- ~~**TECH-DOCS-APPS-PORTAL-PURGE**: per-file DELETE/REWRITE/KEEP_AND_PURGE matrix uitgevoerd op alle 9 docs uit de oorspronkelijke entry, plus de `post-edit-eslint.sh` hook (out-of-scope vondst uit Phase A). Vijf obsolete docs verwijderd (`.cursor/instructions.md`, `.cursor/ARCHITECTURE.md`, `dev-docs/MASTER_PROMPT_CC.md`, `dev-docs/MASTER_PROMPT_CURSOR.md`, `dev-docs/dev-guide.md` — totaal ~80 KB). Drie herschreven (`SETUP.md`, `101_vue.mdc`, hook-script). Twee chirurgisch gepurgeerd (`102_multi_tenancy.mdc`, `CLAUDE_CODE_TOOLING.md`). Externe verwijzingen in README.md, CLAUDE_DESKTOP_SETUP.md, ARCH-CONSOLIDATION-2026-04.md en VIBE_CODING_CHECKLIST.md mee bijgewerkt. WS-3 PR-C op 2026-05-06. Single SPA, single cookie, single deploy host. WS-3 compleet.~~ ✅
- ~~**WS-7 Observability — closure (mei 2026)**: 4 PRs gemerged op `feat/ws-7-observability` (infra `5f6fc07`, backend SDK `bdb89a2..0379016`, frontend SDK `bc47783..5c42f27`, docs `754222f..e9da01f`). 1551 backend + 252 frontend tests groen. Acceptance criteria 1-14 voldaan; observability volledig operationeel op `monitoring.hausdesign.nl`. Implementation criteria 3, 4, 5, 6, 8, 11, 12, 13, 14 via PRs; operationele criteria 1, 2, 7, 9, 10 via deploy-checklist (DNS, TLS, superuser+2FA, prod DSNs, email-alerting, retention 90d, cron backup). Architecturale patronen vastgelegd in `dev-docs/ARCH-OBSERVABILITY.md` (730 regels) + 2 runbooks (`observability-triage.md`, `observability-erasure.md`). Twee GlitchTip projecten (`crewli-api` + `crewli-app`), één DSN per project, runtime context-split via `actor_scope` tag. Patronen: explicit > implicit listener registration, default-in-listener / override-in-middleware voor binary tags, tenant resolution chain (route-param → portal-token → super_admin platform → user fallback). Volgsporen: OBS-1, OBS-4, OBS-6, OBS-7, OBS-9 (zie "Observability follow-ups" sectie hieronder).~~ ✅
---
## Opgeloste items (april 2026)
De volgende items zijn geïmplementeerd en afgerond (673+ tests):
- ~~TECH-02: scopeForFestival + scopeWithChildren helper scopes op Event model~~ ✅
- ~~TECH-03: DevSeeder uitgebreid met festival-structuur (secties, tijdsloten, personen)~~ ✅
- ~~TECH-04: EventController.store() redundante ternary~~ ✅
- ~~Auth race condition (CTRL+R fix)~~ ✅
- ~~Section edit dialog bug~~ ✅
- ~~Time slot duplicate button~~ ✅
- ~~Browser autocomplete disabled op dialog form fields~~ ✅
- ~~Category + icon fields op festival_sections~~ ✅
- ~~IconPicker component~~ ✅
- ~~Crowd Types beheer-UI~~ ✅
- ~~Companies CRUD~~ ✅
- ~~Person tags backend (person_tags + user_organisation_tags)~~ ✅
- ~~Event status state machine (dedicated transition endpoint, prerequisites, festival cascade)~~ ✅
- ~~Event status transition buttons (frontend + backend, state machine, cascade)~~ ✅
- ~~Festival tab-navigatie (uniform tabs, Programmaonderdelen tab)~~ ✅
- ~~SectionsShiftsPanel extractie als herbruikbaar component~~ ✅
- ~~Cross-event section auto-redirect~~ ✅
- ~~Shift claiming in portal (5 endpoints, 26 tests, ClaimenTab + RoosterTab)~~ ✅
- ~~Cross-app auth isolation (CookieBearerToken per app, 3 isolatietests)~~ ✅
- ~~Password reset (beide SPAs, custom notification, app-aware links)~~ ✅
- ~~Email change with verification (self-service + admin, 24h token expiry)~~ ✅
- ~~Password change while logged in~~ ✅
- ~~"Lid toevoegen als deelnemer" shortcut (2 endpoints, 11 tests)~~ ✅
- ~~Person Identity Matching (detect→suggest→confirm, fuzzy name, DOB tiebreaker)~~ ✅
- ~~Naam-splitsing first_name + last_name (66 files)~~ ✅
- ~~Date of birth op persons en users~~ ✅
- ~~Smart assign dialog (tags, preferences, availability, cascading filters)~~ ✅
- ~~Soft capacity + approve overbook fix~~ ✅
- ~~Cancellation source tracking + re-assignment~~ ✅
- ~~VitePress user documentation (3 core pages)~~ ✅
- ~~Registration settings (show_in_registration)~~ ✅
- ~~Premium portal wizard (banner, branding, success page)~~ ✅
- ~~Global error handling (useNotificationStore + axios 422 interceptor)~~ ✅
- ~~S3a PR 2: TAG_PICKER / AVAILABILITY_PICKER / SECTION_PRIORITY renderen in het publieke registratieformulier. Seeder uitgebreid met twee showcase-velden + parent-level VOLUNTEER time slot + duplicate section name voor dedup-dekking. SECTION_PRIORITY waarde-shape gevalideerd in FormValueService. `FormSubmissionResource` krijgt admin-facing `identity_match` block. 64 nieuwe assertions over backend + Vitest.~~ ✅
- ~~FORM-09: TriggerPersonIdentityMatchOnFormSubmit sync refactor (eager state transition, async resolution deferred to FORM-05)~~ ✅
---
## Bekende gaps — nog te bouwen
Overzicht van bekende ontbrekende onderdelen die nog niet gebouwd zijn:
| Item | Status | Prioriteit |
|------|--------|-----------|
| Person Tags frontend UI | Backend compleet, geen organiser UI | Hoog |
| Accreditatie Engine (SCHEMA 3.5.6, ARCH-07 templates) | Volgende grote module | Hoog |
| ARCH-03 — Sectie templates / kopiëren van vorig event | Niet gestart | Hoog |
| Briefings & Communicatie basis | Niet gestart | Middel |
| Artist Advancing portal | Niet gestart | Middel |
| UX-01 — Festival setup checklist | Niet gestart | Middel |
| UX-03 — Personen per sub-event | Niet gestart | Middel |
| ARCH-06 — Locatie-gebaseerd shift-overzicht | Niet gestart | Laag |
| ARCH-09 — Artist Eloquent model + migration | Prerequisite for artist_advance purpose | Hoog (blocker voor artist_advance) |
---
## Nieuwe backlog items
### ARCH-06 — Locatie-gebaseerd shift-overzicht
Cross sub-event filter op location_id. Toont alle shifts op een fysieke locatie
ongeacht programmaonderdeel.
**Prioriteit:** Laag
---
### ARCH-07 — Accreditatie-templates per sectie/dag combinatie
MUST-HAVE bij accreditatie build. Templates worden primaire toewijzingsmethode.
Per crowd_type + sectie + dag → automatisch voorgestelde accreditatie-items.
**Prioriteit:** Hoog — direct meebouwen bij accreditatie-module
---
### ARCH-08 — Recurrence voor time slots
Herhalingsfunctie: "genereer 5 time slots in één keer" voor opbouwdagen etc.
**Prioriteit:** Middel
---
### ARCH-09 — Artist Eloquent model + migration
**Aanleiding:** `artist_advance` purpose is geregistreerd in `PurposeRegistry` (v1.0) met `subject_type = 'artist'`, maar het `App\Models\Artist` model en de `artists` tabel bestaan nog niet. `AppServiceProvider::PURPOSE_SUBJECT_FQCN` bevat `'artist' => 'App\\Models\\Artist'` als string-literal (gedocumenteerd in de constant-docblock) om morph-map-registratie te laten slagen — resolution is lazy en knalt pas bij de eerste echte artist-submission.
**Wat:** Artist Eloquent model + migratie + factory, conform het patroon van de overige business-tabellen (ULID PK, `HasUlids`, `OrganisationScope`, soft deletes per SCHEMA §3.5.7). Na het landen van het model: `PURPOSE_SUBJECT_FQCN` omzetten van string-literal naar `Artist::class` import.
**Prioriteit:** Hoog — blokkeert elke feature-sprint rond artist_advance.
**Afhankelijk van:** SCHEMA §3.5.7 finalisatie (artists, performances, stages etc. — momenteel in `/dev-docs/ARCH-PLANNED-MODULES.md` na WS-8).
---
### ART-03 — Artist profile met cross-event rider defaults
Organisatie-niveau artiest-profiel dat rider-defaults, contacten en interne
notities opslaat over events heen. "Importeer van vorig jaar" functie.
**Prioriteit:** Laag
---
### UX-01 — Festival setup checklist / onboarding wizard
Checklist widget op festival dashboard die door de configuratiestappen leidt.
Items worden groen als ze zijn afgerond.
**Prioriteit:** Middel
---
### UX-02 — Aandachtsmatrix op event dashboard
**Aanleiding:** Organisator verliest overzicht bij 200+ vrijwilligers en
30 secties. Kritieke problemen (onderbezette shifts, wachtende goedkeuringen,
onopgeloste identity matches) worden pas ontdekt als het te laat is.
**Wat:** Drie metric cards op het event Overzicht-tab:
- Goedgekeurde personen zonder shift-toewijzing (telling)
- Wachtende shift-claims (telling)
- Onopgeloste identiteitsmatches (telling)
Elke card is klikbaar en navigeert naar de relevante module.
**Prioriteit:** Hoog — eerste frontend-taak op Overzicht-tab.
Data is beschikbaar via bestaande endpoints (aggregate queries).
---
### UX-03 — Personen-tab op sub-event niveau
Gefilterde view: alleen personen met shifts in dit programmaonderdeel.
Met link "Bekijk alle personen op festival-niveau".
**Prioriteit:** Middel
---
### COMM-05 — Resend invitation endpoint
**Aanleiding:** Uitnodigingen kunnen nu alleen ingetrokken worden of
verlopen vanzelf. Organisatoren willen een "opnieuw versturen" actie
voor gevallen waarin de oorspronkelijke mail in de spamfilter belandde,
gemist werd, of het e-mailadres net gecorrigeerd is.
**Wat:**
- Backend: `POST /api/v1/organisations/{org}/invitations/{id}/resend`
(idempotent: regenereert de mail zonder token of verloopdatum te
wijzigen). Zelfde endpoint voor `/admin` scope.
- Frontend: "Opnieuw versturen" actie activeren in de sectie
openstaande uitnodigingen op `/members` (useMembers heeft al een
`useResendInvitation` stub-ready).
**Prioriteit:** Middel — user-requested UX-verbetering.
---
### UX-04 — Leveranciers-deadline waarschuwing
**Aanleiding:** Leveranciers die hun personeelslijst niet tijdig indienen
veroorzaken last-minute chaos. De organisator heeft geen zicht op welke
externe lijsten nog niet compleet zijn.
**Wat:** Op het event dashboard en in de publiekslijsten-tab:
- Badge "Nog niet compleet" op externe lijsten waar persons_count < max_persons
- Optioneel: deadline-datum veld op crowd_lists (nieuw kolom)
- Waarschuwingsbanner X dagen voor de deadline: "3 leveranciers hebben hun
lijst nog niet compleet ingediend"
**Prioriteit:** Middel — meebouwen bij leveranciersportaal (SUP-01)
---
## Larastan reduction sprints
Larastan (PHPStan for Laravel) is geïnstalleerd op level 6 met een
accept-all baseline van 1556 errors over 678 files (41 distinct
identifiers). Zie `/dev-docs/LARASTAN.md` voor werkmodel. Per-categorie
reduction-sprints hieronder — elke sprint mikt op één identifier, laat
de baseline krimpen en regenereert hem aan het einde.
### TECH-LARASTAN-01 — property.notFound
**Priority:** Middel (post-foundation, incremental)
**Scope:** baseline-entries met identifier `property.notFound`.
**Estimate:** 613 errors over 87 files.
**Completion gate:** category count daalt naar 0 in geregenereerde
baseline; volledige test suite groen.
**Approach:**
- Merendeel zit op Eloquent-modellen waar `$user->id` of vergelijkbaar
niet door PHPDoc wordt herkend — los op door `@property` annotaties
op modellen toe te voegen (of via `php artisan ide-helper:models`
als dat acceptabel wordt gevonden).
- Commit per sub-directory als >50 errors.
### TECH-LARASTAN-02 — missingType.generics
**Priority:** Middel
**Scope:** baseline-entries met identifier `missingType.generics`.
**Estimate:** 289 errors over 52 files.
**Completion gate:** category count naar 0; tests groen.
**Approach:**
- Zit vooral op factories (`extends Factory<Model>`) en
`HasFactory`-gebruik zonder template. Voeg type-params toe aan
class-declaraties en docblocks.
- Overlapt deels met TYPE_DECLARATION-sprint van Rector.
### TECH-LARASTAN-03 — argument.templateType
**Priority:** Middel
**Scope:** baseline-entries met identifier `argument.templateType`.
**Estimate:** 154 errors over 31 files.
**Completion gate:** category count naar 0; tests groen.
**Approach:**
- Voornamelijk `collect(...)` calls waar PHPStan de generieke
template TKey/TValue niet kan resolven. Typeer de input expliciet
of gebruik `Collection::make([...])` met generieke annotatie.
### TECH-LARASTAN-04 — missingType.iterableValue
**Priority:** Middel
**Scope:** baseline-entries met identifier `missingType.iterableValue`.
**Estimate:** 98 errors over 61 files.
**Completion gate:** category count naar 0; tests groen.
**Approach:**
- Methode-return-types als `array` zonder value-type. Voeg
`array<string, mixed>` of specifieker toe aan form-requests,
resource `toArray()` methods, factory `definition()` methods.
### TECH-LARASTAN-05 — argument.type
**Priority:** Middel
**Scope:** baseline-entries met identifier `argument.type`.
**Estimate:** 77 errors over 32 files.
**Completion gate:** category count naar 0; tests groen.
**Approach:**
- Reële type-mismatches (bijv. string doorgegeven waar `'strict'|'lax'`
vereist is). Case-by-case reviewen — niet mechanisch.
### TECH-LARASTAN-06 — method.notFound
**Priority:** Middel
**Scope:** baseline-entries met identifier `method.notFound`.
**Estimate:** 50 errors over 26 files.
**Completion gate:** category count naar 0; tests groen.
**Approach:**
- Meestal "Call to an undefined method Illuminate\\…::users()" —
relationship methods die PHPStan niet kent. Los op via
`@method` annotaties of generieke relationship-return types.
### TECH-LARASTAN-07 — method.childReturnType
**Priority:** Laag
**Scope:** baseline-entries met identifier `method.childReturnType`.
**Estimate:** 35 errors over 35 files.
**Completion gate:** category count naar 0; tests groen.
**Approach:**
- Eén-op-één met factory `definition()` methodes. Smeedt samen met
TECH-LARASTAN-02 in één sprint indien praktisch.
### TECH-LARASTAN-08 — method.unresolvableReturnType
**Priority:** Laag
**Scope:** baseline-entries met identifier
`method.unresolvableReturnType`.
**Estimate:** 32 errors over 9 files.
**Completion gate:** category count naar 0; tests groen.
### TECH-LARASTAN-09 — assign.propertyType
**Priority:** Middel (reële type-bug kans hoger dan bij generics)
**Scope:** baseline-entries met identifier `assign.propertyType`.
**Estimate:** 31 errors over 10 files.
**Completion gate:** category count naar 0; tests groen.
**Approach:**
- Meestal Carbon vs string mismatch op Eloquent properties —
modelcasts goed zetten zodat Eloquent de datetime teruggeeft waar
hij beloofd is.
### TECH-LARASTAN-10 — instanceof.alwaysTrue
**Priority:** Laag
**Scope:** baseline-entries met identifier `instanceof.alwaysTrue`.
**Estimate:** 28 errors over 17 files.
**Completion gate:** category count naar 0; tests groen.
**Approach:**
- Dead `instanceof`-checks. Prefab voor Rector's `DEAD_CODE`
sprint — wachten of combineren.
### TECH-LARASTAN-CI — CI integration
**Priority:** Middel
**Scope:** wire `composer analyse` als blokkerende PR-gate in CI.
**Depends on:** CI-infrastructuurkeuze.
### TECH-LARASTAN-L8 — level 8 migration
**Priority:** Laag
**Scope:** niveau 6→8 verhogen nadat level-6 baseline op 0 staat.
**Estimate:** onbekend totdat level 6 leeg is.
---
## Rector application sprints
Rector is geïnstalleerd en geconfigureerd op PHP 8.2 + safe quality
+ Laravel code-quality rule sets. Dry-run rapporteert **487
rule-applications over 357 files** verdeeld over ~35 distinct rules.
Zie `/dev-docs/RECTOR.md` voor werkmodel. Per-ruleset sprints
hieronder — elke sprint beperkt zich tot één scope, past toe,
verifieert tests + Larastan, regenereert waar nodig.
### TECH-RECTOR-01 — DEAD_CODE sprint
**Priority:** Middel
**Scope:** `SetList::DEAD_CODE` over app/, database/, tests/.
**Estimate:** Top 13 unused-variable removals via
`RemoveUnusedVariableAssignRector`, plus verwante dead-code rules.
Exact totaal: ~30-50 changes.
**Completion gate:** `composer rector:apply` clean voor deze set;
test suite groen; Larastan baseline geregenereerd en kleiner (of
gelijk). Commit per sub-directory indien >50 wijzigingen.
**Approach:**
- Tijdelijk in rector.php alleen DEAD_CODE aanzetten, andere sets
uitcommentariseren.
- `composer rector` om diff te reviewen voor zekerheid.
- `composer rector:apply`.
- `composer test` + `composer analyse`.
- Herstel rector.php naar volledige config.
- Commit.
### TECH-RECTOR-02 — TYPE_DECLARATION sprint
**Priority:** Middel (hoogste volume — 174+ changes)
**Scope:** `SetList::TYPE_DECLARATION`.
**Estimate:** ~174+ changes — top drivers:
`AddClosureVoidReturnTypeWhereNoReturnRector` (103),
`AddArrowFunctionReturnTypeRector` (71), plus
`AddArrayFunctionClosureParamTypeRector` en kleinere.
**Completion gate:** zie TECH-RECTOR-01. **Extra**: deze set lost
waarschijnlijk veel `missingType.*` errors in Larastan baseline op —
regenereer en commit gereduceerde phpstan-baseline.neon mee.
### TECH-RECTOR-03 — LARAVEL_CODE_QUALITY + LARAVEL_COLLECTION sprint
**Priority:** Middel
**Scope:** `LaravelSetList::LARAVEL_CODE_QUALITY` +
`LaravelSetList::LARAVEL_COLLECTION`.
**Estimate:** ~80 changes — `AppToResolveRector` (51),
`DispatchToHelperFunctionsRector` (8),
`EloquentOrderByToLatestOrOldestRector` (7),
`CarbonToDateFacadeRector` (4), plus collection-idioms.
**Completion gate:** zie TECH-RECTOR-01.
**Approach:**
- Splits in twee commits (één per set) als het totaal te groot is
om in één review te laten passen.
### TECH-RECTOR-04 — CODE_QUALITY + EARLY_RETURN + PRIVATIZATION
**Priority:** Laag
**Scope:** resterende quality-sets (~80 changes).
**Estimate:** `ConvertStaticToSelfRector` (34),
`ReadOnlyClassRector` (27), `ReturnBinaryOrToEarlyReturnRector` (16),
`ClosureToArrowFunctionRector` (9), etc.
**Completion gate:** zie TECH-RECTOR-01.
### TECH-RECTOR-05 — Laravel modernisation sprint
**Priority:** Laag
**Scope:** review en selectief enable van
`LaravelLevelSetList::UP_TO_LARAVEL_*` in rector.php.
**Estimate:** onbekend tot per-set dry-runs zijn bekeken.
**Completion gate:** per-set applicatie als gescoopte commits.
### TECH-RECTOR-CI — CI integration
**Priority:** Laag
**Scope:** `composer rector` (dry-run) als PR-comment-surface. Apply
blijft handmatig.
---
## Frontend type-safety
### TECH-TS-IMPERSONATION — runtime validation of impersonation state
**Aanleiding:** ts-reset install (april 2026) surfaced dat
`apps/app/src/stores/useImpersonationStore.ts` blindly vertrouwt
op `JSON.parse(sessionStorage.getItem(...))`. De return is nu
`unknown` in plaats van `any`; we fixten dit door twee `as
ImpersonationState` casts toe te voegen. Die maken de bestaande
trust expliciet maar valideren de shape niet.
**Wat:** vervang beide casts (lines 19 + 123) door een
narrowing-helper `parseStoredState(raw: string | null):
ImpersonationState | null` die het JSON parseert, de verwachte
keys controleert (incl. geneste `impersonatedUser: AdminUser`
fields), en `null` teruggeeft als de shape niet klopt. Zelfde
helper gebruiken bij beide call sites. sessionStorage is in theorie
tamperbaar door lokale users, dus dit is ook een kleine security
verbetering.
**Prioriteit:** Laag — defensive hardening, geen user-impact.
### ~~TECH-TS-PORTAL-TSC — `tsc --noEmit` baseline reduction (apps/portal)~~
**Status: closed 2026-04-25** — `pnpm exec vue-tsc --noEmit` exits
clean in `apps/portal/`. Sprint commits: `f7bb864` (tiptap 2.27.2
upgrade) + `a7ccd2b` (4 long-tail fixes).
**Correction to the original framing:** the entry initially read
"22 pre-existing tsc errors in own code + 4 in tiptap node_modules".
The "4" was the **delta** that ts-reset added in foundation-tooling
commit 3 (ts-reset's `JSON.parse → unknown` tightening surfaced 4
new tiptap errors), **not** the absolute pre-existing tiptap baseline.
The actual absolute count was **~707 tiptap node_modules errors**,
silently invisible because the project's CI runs build + vitest only,
not `vue-tsc`.
**Root cause of the 707:** tiptap 2.27.1's `dist/index.d.ts`
re-exported from `'../src/CommandManager.js'` and ~22 similar lines
that referenced `.js` files which did not exist (only `.ts` source).
With `moduleResolution: "Bundler"`, vue-tsc fell through and pulled
tiptap's entire uncompiled source tree into the program. Tiptap
2.27.2 (a patch release) fixed the dist exports to use sibling-
relative paths (`./CommandManager.js`) that resolve correctly to
the existing `dist/*.d.ts` siblings.
**Why `skipLibCheck` did not help:** `skipLibCheck: true` was
already set in `apps/portal/tsconfig.json` (and `apps/app/`)
**but only suppresses checking of `.d.ts` declaration files**.
Tiptap's uncompiled `.ts` source files in `node_modules/.../src/`
were raw `.ts`, not `.d.ts`, so they bypassed `skipLibCheck` once
the import graph reached them. The 2.27.2 packaging fix made the
import graph stop there, no exclusion needed.
**Final error tally:**
- Pre: 729 vue-tsc errors (~22 own-code, of which 18 were TS2339
downstream of tiptap's broken types; ~707 in node_modules/@tiptap)
- Post-tiptap-upgrade: 4 own-code (the tiptap-independent stragglers:
`vite.config.ts` TS7006, `themeConfig.ts` TS2322 via
`LayoutConfig.title` `Lowercase<string>` over-constraint,
`build-icons.ts` TS2307 missing `@iconify/types`,
`casl.ts` TS2345 vue-router meta cast)
- Post-long-tail-fixes: 0 own-code, 0 node_modules
**Aftermath:**
- The S3b form-builder organizer UI now lands on a verified
zero-error baseline.
- The "new code introduces no new errors" discipline still depends
on a CI gate — see TECH-PORTAL-TSC-CI-GATE follow-up.
### TECH-PORTAL-TSC-CI-GATE — vue-tsc as a blocking gate for apps/portal
**Aanleiding:** TECH-TS-PORTAL-TSC reached zero in commits `f7bb864`
+ `a7ccd2b`, but the project has no pre-commit infrastructure
(no husky, lefthook, or simple-git-hooks) and CI does not run
`vue-tsc` either. Without a blocking gate, the baseline can drift
back to non-zero between commits — exactly the discipline gap
that produced the 22 pre-existing errors in the first place.
**Wat:**
- Add `pnpm exec vue-tsc --noEmit` as a CI step in the workflow that
runs portal build/vitest (most natural location).
- OR introduce a pre-commit infrastructure (lefthook is lighter than
husky for monorepos) and run vue-tsc on portal-touching commits.
- Either path: the gate must fail the run on any new vue-tsc error
in `apps/portal/`.
**Out of scope:** extending the same gate to `apps/app/` — that
SPA still has a different mix of errors and would be a separate
sprint (out of scope today, would land alongside TECH-APP-VITEST or
later).
**Prioriteit:** Middel — without the gate, TECH-TS-PORTAL-TSC's
zero state has no enforcement. Should land before S3b organizer UI
work to keep that sprint's "new code introduces no new errors"
discipline mechanically enforceable.
### TECH-APP-VITEST — apps/app Vitest setup
**Priority:** medium → **high before S3b organizer UI lands.**
**Scope:** install Vitest + config + sample test in `apps/app/`.
Mirror `apps/portal/` setup. Add `test` script to package.json.
**Trigger to upgrade priority to "must-fix-now":** any S3b
form-builder organizer UI commit. apps/portal has 113 Vitest tests
as of foundation-tooling commit 0; apps/app has zero. Launching
new organizer UI uncovered while the portal SPA is well-tested is
asymmetric quality, exactly the discipline gap that bites during
post-launch debugging.
**Setup outline (1-2 hours, isolated commit):**
1. `cd apps/app && pnpm add -D vitest @vue/test-utils @testing-library/vue happy-dom`
2. Mirror `apps/portal/vitest.config.ts` adapted for apps/app paths
3. Mirror `apps/portal/tests/setup.ts` if relevant
4. Add `"test": "vitest"` and `"test:run": "vitest run"` to
`apps/app/package.json` scripts
5. Write one sample test against an existing apps/app component
(any simple Vue component with a clear input → output mapping —
confirm the harness works end-to-end). `useImpersonationStore.ts`
is a natural early target (no runtime test today; the
ts-reset-surfaced TECH-TS-IMPERSONATION runtime-validation work
would benefit from that coverage)
6. Foundation-tooling-style return deliverable: confirm
`pnpm test --run` exits 0 with at least one test passing
**Out of scope:** writing comprehensive tests for existing apps/app
code. This sprint sets up the harness; per-feature test coverage
follows organically as features land or are touched.
---
### TECH-FORM-BUILDER-INTEGRATION-TEST-NAME-COVERAGE
ARCH-FORM-BUILDER §31 lists five integration contract tests (IdentityMatchTriggerTest, ShiftAssignmentFromRegistrationTest, CodeOfConductGatingTest, SupplierIntakeFlowTest, CrowdListAutoAddTest) that don't exist under those exact names in api/tests/Feature/FormBuilder/Integration/. Some may exist under different names (e.g., TagPickerSyncListenerTest covers TagSync; FormSubmissionResourceIdentityMatchTest may cover IdentityMatch). When next touching FormBuilder integration tests, audit the §31 list against actual test files and either rename to match or update §31 to reflect actual names. Low priority — coverage may be intact, only the naming index is stale.
Priority: low
---
### WS-6 Deferred
#### ARTIST-ADV-SECTION-APPLY
Section-level binding apply runtime activation.
**Removal trigger:** when artist_advance feature work begins (post-S5).
**Action:** set `FORM_BUILDER_SECTION_APPLY=true`, write section-scoped tests,
activate `ApplyBindingsOnFormSectionSubmitted` listener registration, remove
the feature-flag early-return guard from the listener.
**Refs:** RFC-WS-6.md §3 Q10.
#### FORM-BINDING-COMPOSITE-IDENTITY
Multi-attribute identity-key resolution (e.g. `email OR (first_name+last_name+DOB)`).
**Trigger:** when a purpose requires composite identity matching that single
binding cannot satisfy. Currently `MaxOneIdentityKeyPerTargetEntity` guard
enforces single-key only.
**Refs:** RFC-WS-6.md §3 Q8, §6.
#### FORM-LIBRARY-RESYNC
Admin action to propagate `FormFieldLibrary` binding updates to existing
field instances. Currently library-bindings are copy-on-instantiation
only (see `ARCH-BINDINGS.md §1`).
**Trigger:** when organisations report friction updating shared field
templates.
**Refs:** RFC-WS-6.md §3 Q11.
#### FORM-FAILURE-DAILY-DIGEST
Daily digest mailable for orgs with open `FormSubmissionActionFailure` rows
above a threshold.
**Deferred until** notification framework lands (post-accreditation engine).
Requires NotificationBell + NotificationCenter infrastructure.
**Refs:** RFC-WS-6.md §3 Q5.
#### LOAD-TEST-FOUNDATION
Wall-clock concurrent load testing infrastructure (k6 or equivalent) against
staging-API. Separate workstream from WS-6.
**Trigger:** pre-release hardening phase.
**Refs:** RFC-WS-6.md §4 V4.
#### ARTIST-ADV-BINDING-MODEL
WS-6 v1 omits artist binding-target registry entries entirely. The
artist_advance purpose accepts schemas without bindings (Artist subject
resolved via portal_token, not via field-to-attribute mapping). For v2,
decide:
- Should there be an `Artist` Eloquent model class? Currently the
`artists` table exists but no class — only the morph map alias points
at `App\Models\Artist` (a string).
- Which artist attributes are bindable from form data? The advance form
is OUTPUT-shaped: it gathers info FROM the artist (rider, hospitality,
technical needs); it does not provision Artist attributes the way
event_registration provisions Person attributes.
- Is the binding model even the right abstraction for advance forms,
or do they use a different sync mechanism (e.g. typed AdvanceSection
fields)?
**Trigger:** when v2 design discussions for artist_advance feature
work begin. May result in registry entries, an Artist model, OR a
domain-specific alternative to bindings.
**Refs:** RFC-WS-6.md §3 Q9 v1.2 addendum, ARCH-BINDINGS.md appendix.
#### FORM-BINDING-JSON-PATH
WS-6 v1's binding-target registry handles only top-level model columns.
JSON-path attributes (e.g. `persons.custom_fields.dietary_preferences`)
are not bindable in v1. Adding support requires:
- Registry shape extension (path-string syntax:
`'custom_fields.dietary_preferences'`)
- Applicator's `setAttribute` change → typed json_set / array_set helper
- Conflict resolution: do JSON-path siblings resolve independently?
- Type-validation: dietary_preferences is a list, but custom_fields
itself is JSON — what does identity_key_eligible mean here?
For v1 the recommendation: model dietary_preferences (and similar
`custom_fields` properties) as a `TAG_PICKER` form_field with a
tag_categories config. The TAG_PICKER → user_organisation_tags sync
(per ARCH-FORM-BUILDER §31.10) handles this without requiring
binding-target column mapping.
**Trigger:** when an organisation requests dietary_preferences (or
other `custom_fields` properties) as a form binding target. May
coincide with Crewli's v2 dietary management feature work.
**Refs:** RFC-WS-6.md §3 Q9 v1.2 addendum, ARCH-BINDINGS.md appendix.
## API response validation
### ARCH-API-RESPONSE-VALIDATION — Uniforme typed + runtime-validated contracts op de API-grens
**Aanleiding:** PR-B2a's `contexts.{available, default}` block op `/auth/me` exposeert de bredere
architecturale gap: backend-resources emiteren JSON shapes die frontend-composables consumeren via
hand-geschreven TypeScript interfaces met `as ResponseType` casts. Er is geen runtime-check die
backend ↔ frontend drift vangt. String-literal velden zoals `'portal' | 'organizer'` of
`'ok' | 'failed'` bestaan zonder canonieke enumeratie aan beide kanten van de boundary. Deze
gap is structureel, niet beperkt tot één endpoint; een halve oplossing (alleen frontend Enum,
of alleen één endpoint met Zod) zou een gemixt patroon vastleggen.
**Wat:** Eigen workstream met eigen ARCH-document (`dev-docs/ARCH-API-VALIDATION.md` skeleton
geland). Scope:
- **Backend:** Audit van `App\Http\Resources\**` op enumerated values. PHP Enums onder
`App\Enums\**` voor alle geënumereerde domeinwaarden (auth contexts, statussen, role
categories, purpose names). Resources emiteren `->value` (geen wire-format-breaking change).
- **Frontend:** Adoptie van Zod als runtime-validator op API-response-ingress. Conventie:
elke composable onder `apps/app/src/composables/api/**` parsest zijn response door een
Zod-schema. TypeScript types worden afgeleid via `z.infer<typeof Schema>` — geen
hand-geschreven response-interfaces.
- **Codegen:** Pipeline die backend-resources → Zod-schemas + TS-types genereert. Kandidaten:
Scramble OpenAPI output (per DOC-01) naar `openapi-zod-client` / `orval`, of een
hand-rolled generator op PHP Resource introspectie.
- **Tooling:** Reference-implementatie met round-trip backend Enum → resource → codegen →
Zod-schema → composable → typed return. CI-gate die unvalidated composables flagt.
**Sequentie:** Ingepland **na** WS-3 PR-C (cleanup) en **na** WS-7 (GlitchTip), **vóór**
RFC-FORM-BUILDER-UI implementatie begint. Argumenten:
- PR-C maakt frontend-laag stabiel; geen overlap met layout-refactor
- WS-7 levert de Sentry-compatible exception-capture die Zod parse-failures schoon vangt
voor monitoring na rollout
- RFC-FORM-BUILDER-UI introduceert een grote nieuwe set composables; die landen op het
gevalideerde patroon vanaf de eerste commit i.p.v. retrofit later
**Geschatte inspanning:** 23 dagen voor conventie + reference-implementatie + ~5
hoge-prioriteit endpoints (`/auth/me`, form-builder list-endpoints, identity-match endpoints).
Verdere composable-uitrol gebeurt organisch als features worden geraakt of toegevoegd.
**Out of scope:** form-input validatie (al via VeeValidate + Zod), WebSocket-validatie
(separaat, COMM-01), publieke API-contracten voor third parties (separaat, DIFF-03).
**Open beslissingen:** codegen toolchain (Scramble-pipeline vs hand-rolled), validation
failure-mode (hard fail vs soft fail per env), per-route opt-out, boundary placement
(composable-laag vs axios-interceptor vs dedicated middleware).
**Prioriteit:** Hoog — foundation-investering die voorkomt dat S3b technische schuld stapelt.
Niet blokkerend voor lopend werk.
**Refs:** `dev-docs/ARCH-API-VALIDATION.md` (skeleton), AUTH_ARCHITECTURE.md (eerste
consumer), ARCH-FORM-BUILDER.md (high-priority migratie target), ARCH-BINDINGS.md
(status-fields zijn early-validation kandidaten), DOC-01 (Scramble pipeline kan codegen
voeden).
---
_Laatste update: Mei 2026_
_Voeg nieuwe items toe met prefix: ARCH-, COMM-, OPS-, VOL-, ART-, FORM-, SUP-, DIFF-, APPS-, TECH-, UX-, OBS-_
---
## Observability follow-ups (post WS-7 closure)
> **Status overzicht (mei 2026, na WS-7 closure):**
>
> | Entry | Status |
> |---|---|
> | OBS-1 | Active — wacht op volunteer-rol introductie |
> | OBS-2 | ✅ Resolved (PR-2 architectural-fixes — opgevouwen in AuthScopeContextListener) |
> | OBS-3 | ✅ Resolved (PR-2 architectural-fixes — auto-discovery uit + explicit registratie maakt route-conditional binding overbodig) |
> | OBS-4 | Active — pre-PHPUnit 12 cleanup |
> | OBS-5 | ✅ Resolved (PR-2 smoke-test fix `48f2a00` + `ExceptionReportingTest`) |
> | OBS-6 | Active — wachten op SETUP.md update of CI smoke check |
> | OBS-7 | Active — coverage-test scope-creep, expanded coverage waardevol |
> | OBS-8 | ✅ Resolved (final hardening commits `215405a`, `a939820`, `dee1401`) |
> | OBS-9 | Active — alleen relevant bij staging-introductie |
### OBS-1 — Promote ActorType::VOLUNTEER when volunteer role is introduced
**Aanleiding:** WS-7 PR-2 architectural-fix-commit verwijderde de
`ActorType::VOLUNTEER` enum-case omdat Crewli vandaag geen dedicated
`volunteer` Spatie-rol heeft — vrijwilligers zijn behaviorally bepaald
(users met shift-assignments), niet identitair. De resolver mapt
non-admin authenticated users naar `ORG_MEMBER`.
**Wat:** Wanneer Crewli een `volunteer` rol invoert (bijv. via
volunteer-onboarding workflow), her-introduceer dan de `VOLUNTEER`
case in `app/Enums/Observability/ActorType.php` en update
`ActorType::resolve()` om de rol te checken vóór `ORG_MEMBER`. Update
ook `AuthScopeContextListenerTest` met een `actor_type=volunteer`
testcase.
**Prioriteit:** Laag — wachten op een product-besluit over volunteer-rol
modellering. Geen blocker.
**Refs:** `app/Enums/Observability/ActorType.php`,
RFC-WS-7-OBSERVABILITY.md §3.6.
### OBS-2 — Early-pipeline log context user_id ✅ Resolved
**Aanleiding:** Tijdens PR-2 architectural-fixes ontdekt dat
`Log::withContext` in `BindRequestLogContext` middleware geen
`user_id` had voor log-regels die vóór auth:sanctum draaiden, omdat
de middleware run op api-group-level (vóór per-route auth).
**Status:** Resolved in PR-2 architectural-fixes. `AuthScopeContextListener::bindForUser()`
roept `Log::withContext(['user_id' => $user->id, ...])` aan zodra het
`Authenticated` of `TokenAuthenticated` event vuurt. Vanaf dat punt
in de request-pipeline dragen alle log-regels `user_id`. Pre-auth
log-regels (CSRF / rate-limiter middleware logs) hebben nog steeds
geen `user_id`, maar dat is correct gedrag — er is op dat moment ook
nog geen geauthenticeerde gebruiker.
**Refs:** `app/Listeners/Observability/AuthScopeContextListener.php`,
`app/Http/Middleware/BindRequestLogContext.php`.
### OBS-3 — Sentry-context middleware coverage assertion ✅ Resolved
**Aanleiding:** PR-2 originele design had de Sentry-context binding
als route-level middleware aliased per route group. Risico: een
nieuwe route-group die de alias vergeet had silently geen
auth-scope tags op gecaptured events.
**Status:** Resolved in PR-2 architectural-fixes. De binding is
verplaatst van middleware naar `AuthScopeContextListener` op het
`Authenticated` / `TokenAuthenticated` event. Listener fires
universeel op élke auth-resolution; geen route-conditional binding
meer mogelijk. Daarmee is een coverage-assertion test overbodig — de
listener kan niet "vergeten" worden zoals een route alias dat kon.
Combineer met OBS-8 (auto-discovery uit + explicit registration via
`EventListenerRegistrationTest`): listener-aanwezigheid is
empirisch gevalideerd op test-niveau.
**Refs:** `app/Listeners/Observability/AuthScopeContextListener.php`,
`tests/Feature/Observability/EventListenerRegistrationTest.php`.
### OBS-4 — PHPUnit metadata-in-doc-comment deprecation cleanup
**Aanleiding:** PHPUnit warnt dat metadata in doc-comments (zoals
`@test`, `@dataProvider`) deprecated is en in PHPUnit 12 verwijderd
wordt. Crewli heeft drie tests met deze pattern:
- `Tests\Unit\Support\Json\JsonCanonicalizerTest::test_scalar_passthrough()`
- `Tests\Feature\FormBuilder\Purposes\PurposeSchemaLifecycleTest::test_create_and_publish_succeeds_for_purpose()`
- `Tests\Feature\Schema\UlidPrimaryKeyTest::test_model_uses_has_ulids_and_generates_crockford_ulid()`
**Wat:** Vervang de doc-comment metadata door PHPUnit attributes
(bijv. `#[Test]`, `#[DataProvider]`). Raak alleen aan vóór de PHPUnit 12
upgrade gepland wordt — nu blokkeert het niets.
**Prioriteit:** Laag — kosmetisch totdat PHPUnit 12 upgrade landt.
**Refs:** PHPUnit changelog, de drie genoemde test-files.
### OBS-5 — Crewli render handlers report() invariant ✅ Resolved
**Aanleiding:** PR-2 smoke test toonde dat backend-exceptions geen
events naar GlitchTip stuurden ondanks correct geconfigureerde SDK.
Root cause: sentry-laravel 4.x heeft de `Integration::handles($exceptions)`
registratie-stap die NIET wordt auto-aangeroepen door de package's
ServiceProvider; de host-app moet dit expliciet doen in
`bootstrap/app.php`.
**Status:** Resolved via commit `48f2a00`
`fix: route controller exceptions through sentry-laravel reporter`
+ `tests/Feature/Observability/ExceptionReportingTest.php` als
regression-guard. ExceptionReportingTest installeert een recording
`before_send` hook en verifieert dat `RuntimeException` wel,
`ValidationException`/`AuthorizationException`/`NotFoundHttpException`
niet captured worden — exact de boundary uit RFC §3.10.
OBS-7 (hieronder) breidt deze coverage uit naar Crewli's eigen
render handlers.
**Refs:** `bootstrap/app.php`,
`tests/Feature/Observability/ExceptionReportingTest.php`,
RFC-WS-7-OBSERVABILITY.md §3.10.
### OBS-6 — sentry-laravel installation gap awareness
**Aanleiding:** WS-7 PR-2 smoke test faalde silent omdat sentry-laravel
4.x de `Integration::handles($exceptions)` registratie niet
auto-registreert in zijn ServiceProvider. De host-app moet de regel
expliciet aan `bootstrap/app.php` toevoegen. README documenteert dit,
maar tijdens `composer require sentry/sentry-laravel` +
`php artisan sentry:publish` workflow is het makkelijk te missen.
**Wat:**
- Voeg een waarschuwing toe in `dev-docs/SETUP.md` onder een nieuwe
sectie "Laravel package installation patterns": bij elke nieuwe
package altijd verifiëren dat het package zijn ServiceProvider-
registraties doet voor exception handlers, queue listeners, en log
channels — niet alleen voor routes/views/migrations.
- Overweeg een `tests/Feature/Bootstrap/ExceptionHandlerRegistrationTest.php`
die `app(\Illuminate\Foundation\Exceptions\Handler::class)->getReportableCallbacks()`
introspecteert en assertert dat sentry-laravel's callback
geregistreerd is. Vangt een toekomstige refactor die per ongeluk
`Integration::handles` uit `bootstrap/app.php` verwijdert.
**Prioriteit:** Laag — fix is gedaan en getest, regression mogelijk
maar onwaarschijnlijk gezien de explicit comment in `bootstrap/app.php`.
**Refs:** `bootstrap/app.php`,
`vendor/sentry/sentry-laravel/src/Sentry/Laravel/Integration.php`,
RFC-WS-7-OBSERVABILITY.md §3.10.
### OBS-7 — Custom $exceptions->render() handlers report() invariant
**Aanleiding:** WS-7 PR-2 smoke-test debugging onthulde dat Crewli's
`bootstrap/app.php` 5 custom render handlers heeft. Met
`Integration::handles($exceptions)` geregistreerd werkt
report-before-render correct. Maar een toekomstige render handler die
een Throwable consumeert zonder `report($e)` aan te roepen vóór return
zou Sentry-capture kunnen overslaan voor die exception class.
**Wat:**
- Documenteer in `bootstrap/app.php` (boven het withExceptions block)
een comment: "Render handlers consume exceptions; Laravel's
ExceptionHandler::handle() doet report() vóór render() zodat capture
automatisch is. NIEUWE render handlers MOGEN NIET short-circuiten
voordat report() bereikt is. Verifieer via
tests/Feature/Observability/ExceptionReportingTest.php."
- Uitbreiden van `ExceptionReportingTest.php` met assertions per
bestaande render handler class: throw die exception, assert event
captured.
**Prioriteit:** Medium — bestaande handlers zijn correct, maar het
invariant is subtiel en silent-failure-prone bij toevoegingen.
**Refs:** `bootstrap/app.php`,
`tests/Feature/Observability/ExceptionReportingTest.php`,
RFC-WS-7-OBSERVABILITY.md §3.10.
### OBS-8 — Observability listener double-registration via auto-discovery + explicit Event::listen ✅ Resolved
**Aanleiding:** Bert's live verification na de Sanctum-bearer-token fix
(`adab3be`) toonde via `php artisan event:list` dat de
`AuthScopeContextListener@handleTokenAuthenticated` binding twee keer
geregistreerd stond op `Laravel\Sanctum\Events\TokenAuthenticated` —
één keer via Laravel 12's default listener auto-discovery
(reflection-scan van `app/Listeners/**` op type-hint), één keer via de
expliciete `Event::listen()` call in `AppServiceProvider::boot()`.
Listener firede twee keer per Sanctum-auth. Idempotent vandaag (scope-
tag overwrite-semantiek), maar architecturaal onacceptabel.
Plus aanverwant: de `Authenticated` listener-registratie was via
class-string in plaats van array-callable, waardoor `event:list` de
gebonden methode niet toonde. En `impersonation.active` als binary tag
ontbrak op non-impersonation events (RFC §3.6 vereist always-present
binary signal).
**Status (mei 2026):** Resolved via:
- `215405a` `fix: disable Laravel listener auto-discovery; explicit registrations only`
→ `->withEvents(discover: false)` in `bootstrap/app.php`; alle observability
listeners expliciet via array-callable (`[Class::class, 'method']`) in
`AppServiceProvider::boot()`.
- `a939820` `fix: impersonation.active default tag for non-impersonation authenticated events`
→ baseline `'false'` in `AuthScopeContextListener::bindForUser()`,
override naar `'true'` in `HandleImpersonation` middleware (default-in-
listener, override-in-middleware pattern).
- Commit 3 (deze sessie): `tests/Feature/Observability/EventListenerRegistrationTest.php`
introspecteert `Event::getRawListeners()` en faalt bij count != 1; plus
always-present binary tag invariant test op een live HTTP flow.
Verified via `php artisan event:list`: elke observability listener exact
één keer geregistreerd, met `@method` binding zichtbaar.
**Architecturaal pattern dat dit vastlegt:** explicit > implicit voor
observability-kritische bindings. Toekomstige listeners die op een
event mounten worden expliciet in `AppServiceProvider::boot()`
geregistreerd; auto-discovery is uitgeschakeld zodat silent double-
registration niet meer kan voorkomen.
**Refs:** `bootstrap/app.php`, `app/Providers/AppServiceProvider.php`,
`tests/Feature/Observability/EventListenerRegistrationTest.php`,
RFC-WS-7-OBSERVABILITY.md §3.6.
### OBS-9 — Staging environment GlitchTip CSP whitelist
**Aanleiding:** PR-3 CSP-fix whitelist alleen `localhost:8200` (dev) en
`monitoring.hausdesign.nl` (prod) hard-coded in respectievelijk
`apps/app/index.html` meta-tag en `deploy/nginx/csp-spa.conf`. Wanneer
een staging-omgeving wordt geïntroduceerd (RFC §3.3 noemt
`.env.staging` als voorbeeld), zal de bijbehorende GlitchTip-host
niet ge-whitelist zijn — events worden dan stilletjes geblokkeerd
door browser-CSP zonder waarschuwing aan de test-runner.
**Wat:**
- Bij staging-introductie: voeg staging GlitchTip-host toe aan
`deploy/nginx/csp-spa.conf` óf maak `apps/app/index.html` meta-CSP
environment-aware via Vite build-time injection (vergelijkbaar met
`VITE_SENTRY_DSN_FRONTEND` patroon).
- Update `tests/Feature/Security/CspConnectsToObservabilityTest.php`
met staging-assertion zodat de regression-guard de nieuwe environment
dekt.
**Prioriteit:** Laag — alleen relevant wanneer staging-omgeving wordt
opgezet.
**Refs:** `apps/app/index.html`, `deploy/nginx/csp-spa.conf`,
`tests/Feature/Security/CspConnectsToObservabilityTest.php`,
RFC-WS-7-OBSERVABILITY.md §3.3, ARCH-OBSERVABILITY.md §7 + §10.4.