# 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-SCHEMA-DRIFT-DETECTION — Detecteer stale bindings na schema-migrations **Aanleiding:** RFC-WS-6 v1.3 §Q3 addition 4 — schema-drift werd in v1.2 als zijdelingse opmerking afgedaan ("DB modified out from under us"), v1.3 maakt het expliciet als follow-up. **Wat:** Migration-listener die, wanneer een binding-target column wordt renamed/dropped/type-changed, alle `form_field_bindings.target_attribute` scant op matches. Affected `form_schemas` krijgen `needs_revalidation=true`. Schema-detail UI toont banner; affected schemas accepteren geen nieuwe submissions tot organiser re-publisht. **Vereiste:** - Migration-listener (Laravel `events` integration of een custom `MigrationObserver`) - Nieuwe boolean kolom op `form_schemas`: `needs_revalidation` + index - Re-publish flow op schema-detail UI - Tests die simuleren: column rename → schemas met binding op die column gemarkeerd → re-publish reset **Trigger-conditie:** Eerste productie-incident waar een runtime-throw te herleiden is naar een stale binding na een migration. Of: pre-emptive trigger als enterprise customer een grootschalige column-renaming uitvoert. **Estimate:** 2-3 dagen. **Refs:** ARCH-BINDINGS.md §6.5 (binding-change safety, related but distinct), RFC-WS-6.md §Q3 v1.3. --- ### 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; updated post-D2 2026-05-08):** Post-D2 (RFC §Q1 v1.3 add 1) writes the initial `identity_match_status='pending'` in `ApplyBindingsOnFormSubmit::handle` inside the inner transaction. The queued `TriggerPersonIdentityMatchOnFormSubmit` then writes the final `'matched'` / `'pending'` / `'none'` status. FORM-05's `detectMatchesByValues` extension targets the queued listener's path — adds a value-based matcher branch when `$person->user_id === null` AND `$matches->isEmpty()` (currently writes `'none'`), giving public submitters a "we may have found you" path without first creating a Person. 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):** post-D2 (PR #11 `23a5696`) schrijft de queued `TriggerPersonIdentityMatchOnFormSubmit` voor self-registered public submissions (`registration_source='self'`) standaard `'none'` zodra de exact-email-match in `PersonIdentityService::detectMatches` leeg terugkomt — het bestaande fuzzy-name fallback-pad wordt expliciet overgeslagen voor self-registered Persons (zie `PersonIdentityService::detectMatches` regel 117–119, *"skip fuzzy matching for self-registered persons — they provided their own email, so that's the canonical identity"*). Public submitters die een typo in hun email maken of een ander email-account gebruiken dan eerder bij de organisatie geregistreerd, krijgen daardoor geen "we may have found you" signaal en moeten organiser-side handmatig gereconcilieerd worden. Breid uit met: - Nieuwe methode op `PersonIdentityService`: ```php public function detectMatchesByValues( array $values, string $organisationId, ): Collection { // Multi-signal match: email + first_name + last_name + DOB + // optionele custom-field whitelist, gewogen naar // betrouwbaarheid van de bron-velden. Werkt los van // $person->registration_source, dus self-registered submissions // krijgen óók fuzzy-name + DOB candidates terug. } ``` - Een extra match-arm in de status-derivation van `TriggerPersonIdentityMatchOnFormSubmit::handle()`. D2 inlinete de status-derivation in `handle()` zelf — er is geen aparte `resolveStatus` methode meer. De huidige logica: ```php $matches = $this->identityService->detectMatches($person); $status = match (true) { $person->user_id !== null => 'matched', $matches->isNotEmpty() => 'pending', default => 'none', }; ``` wordt uitgebreid met een value-based fallback wanneer de Person geen `user_id` heeft én de exact-email matcher leeg terugkomt: ```php $matches = $this->identityService->detectMatches($person); $valueMatches = $matches->isEmpty() && $person->user_id === null ? $this->identityService->detectMatchesByValues( $this->extractFormValuesForMatching($submission), $submission->organisation_id, ) : collect(); $status = match (true) { $person->user_id !== null => 'matched', $matches->isNotEmpty() || $valueMatches->isNotEmpty() => 'pending', default => 'none', }; ``` De `extractFormValuesForMatching` helper trekt de relevante velden (email / first_name / last_name / date_of_birth + eventuele whitelisted custom-fields) uit `FormSubmission->values` via de schema-binding metadata. Zo krijgt de portal-UX een betekenisvol signaal in plaats van een `'none'` voor elke public submitter die niet exact-email matched. **Prioriteit:** Medium. Bundel met de organizer `person_identity_matches` UI (frontend gap) én met `FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION` (de portal banner update zo dat hij de eindstatus real-time ontvangt). --- ### FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION **Aanleiding:** WS-6 v1.3-delta D2 (PR #11 `23a5696`, 2026-05-08) introduceerde de `FormSubmissionIdentityMatchResolved` broadcast event class en bijbehorende `submission.{id}` private channel. De backend dispatcht het event maar de frontend portal IdentityMatchBanner subscribet nog niet — de banner refresht momenteel via TanStack Query refetch-on-window-focus, wat een interim-pad is maar niet de ontworpen UX. **Wat:** - Pinia store-extension OF inline `Echo.private('submission.${submissionId}')` subscription in de IdentityMatchBanner component - Op event-receipt: invalidate de TanStack Query cache key voor de submission resource zodat de banner copy automatisch update naar de finale staat ('matched' / 'pending' / 'none') - Authorization wordt al afgedwongen door `routes/channels.php` callback (submitter / super_admin / org_admin van submission's organisatie — TECH-CHANNEL-AUTH-ORG-ADMIN closed mei 2026) - E2E test: simuleer een form submission, verify dat de banner copy in real-time van "we're checking matches…" naar de finale status flipt zonder reload **Vereisten:** - Laravel Echo client setup in `apps/app/` (verifieren: bestaat al voor andere channels of moet nog worden toegevoegd?) - Soketi server in development docker-compose en in productie deployment (verifieren: WS-7 noemt Soketi in stack maar is broadcast wiring al getest?) **Trigger-conditie:** Naar voren halen wanneer (a) de UX van TanStack refetch-on-focus klagende klanten oplevert, OF (b) de eerstvolgende form-builder frontend sprint (RFC-FORM-BUILDER-UI / S3b) gepland wordt — neem het mee in diezelfde sprint zodat alle Echo-wiring tegelijk landt. **Estimate:** 0.5–1 dag (afhankelijk van of Echo client setup nog moet). **Refs:** `dev-docs/RFC-WS-6.md` v1.3.1 §Q1 v1.3 add 2, `dev-docs/ARCH-BINDINGS.md` v1.2 §5.3, `apps/app/src/...` (portal IdentityMatchBanner location to be identified during implementation), `routes/channels.php`. --- ### PARTIAL-BINDING-SUCCESS — Granular per-binding success-state in FormBindingApplicator **Aanleiding:** ARCH-BINDINGS v1.0 §19 noemde dit als "future RFC topic" — RFC-WS-6 v1.3 verplaatst het naar expliciete BACKLOG met trigger-conditie. **Wat:** Granular per-binding success/failure tracking voor `FormBindingApplicator`. Huidige v1.0: één binding throw → hele apply rolt terug → submission `apply_status=failed`. Toekomstig: per-binding success-state, failed bindings expliciet zichtbaar in admin UI, gebruiker krijgt "submission gedeeltelijk verwerkt — 11 van 12 velden zijn opgeslagen, één veld vereist organiser-aandacht." **Vereiste designkeuzes:** - SAGA pattern met per-binding compensation, OF - Per-binding transactie met idempotency-keys per binding-application - Trust-precedence resolution moet opnieuw doordacht (huidige model assumeert atomic pass) **Trigger-conditie:** Eerste enterprise customer rapporteert all-or-nothing als UX issue. Concreet signaal: support-ticket of customer-call met "gebruiker vulde 12 velden, één crashte, alles ging verloren." **Estimate:** 4-6 dagen. **Refs:** ARCH-BINDINGS.md §19, RFC-WS-6.md §Q3 v1.3. --- ### 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 > Plan 2.5 shell follow-ups (landed at P8 closure, juni 2026). See > `dev-docs/RFC-WS-PRIMEVUE-PLAN-2-5.md` → Plan 2.5 Closure. ### MOBILE-SHELL-PARITY — Mobile drawer chrome parity with desktop sidebar **Prioriteit:** Middel Mobile drawer chrome doesn't match the desktop sidebar polish: logo placement is incorrect, an extraneous X close button overlaps the drawer, and the WorkspaceSwitcher is hidden on mobile. A dedicated mobile sprint after Plan 2.5; deliberately out of scope per Q3. **Trigger:** before mobile users onboard / mobile traffic becomes non-trivial. --- ### WORKSPACE-DROPDOWN-SUB-CONTENT — Real WorkspaceSwitcher sub-line content (type + metrics) **Prioriteit:** Middel The WorkspaceSwitcher sub line currently shows the placeholder string 'Organisatie' (per AD-2.5-W1 reversal). Real content requires an `organisations.type` enum + a metrics endpoint (e.g., member count, festival/event metadata). Needs a small RFC covering: type enum schema, metrics aggregation, API shape, frontend wiring. **Trigger:** when organisation metadata work begins, OR when the placeholder reads as too empty in production. --- ### DENSITY-AWARE-SPACING — Wire `data-density` to component spacing **Prioriteit:** Laag `` toggles correctly between `comfortable` and `compact`, but no component CSS reads the attribute, so the toggle is visually inert. Plan 4 should pick which spacing surfaces (tables, lists, cards) respond to density and add the corresponding `[data-density="compact"]` overrides. **Trigger:** Plan 4 template-layer work. --- ### TOPBAR-H-VAR-DECLARE — Declare `--topbar-h` in `:root` (or drop it) **Prioriteit:** Laag `--topbar-h` is referenced as `var(--topbar-h, 56px)` in component CSS but never declared in `:root`. Effectively a hardcoded 56px with no override path. Either declare it in `:root` for real or replace usages with `h-14` (Tailwind). **Trigger:** Plan 4 cleanup, or when topbar height changes. --- ### CSP-FONT-SRC-LOCKDOWN — `Content-Security-Policy: font-src 'self'` lockdown **Prioriteit:** Laag The P2-followup webfontloader lesson exposed a structural risk: dynamic font-loading JS can pull from external domains (Google Fonts), bypassing GDPR/privacy posture. A `Content-Security-Policy: font-src 'self' data:` directive provides structural defense — even if a stray font-loader is reintroduced, browsers would block external requests. **Trigger:** pre-launch security hardening. --- ### AUTO-IMPORTS-V2-SCAN — `components-v2/` not in the auto-import scan dirs **Prioriteit:** Laag `vite.config.ts:79-94` `Components({dirs})` doesn't scan `components-v2/`, so v2 components must be explicitly imported. The P5-followup AppBreadcrumb auto-import surprise was traced to this. Either add `components-v2/` to the scan dirs or accept explicit imports (the current trade-off). **Trigger:** when explicit imports become tedious, or a new contributor stumbles. --- ### PNPM-RESOLUTIONS-ROOT — `resolutions` field in app package.json is a no-op **Prioriteit:** Laag `apps/app/package.json` has a `resolutions` field, but pnpm only honors resolutions in the workspace root `package.json`. Currently a no-op. Move to root `package.json` or remove. **Trigger:** next dependency conflict requiring a resolution override. --- ### SHELLUI-STALE-DATA-THEME-CLEANUP — Clean up stale legacy `data-theme` on init **Prioriteit:** Laag `useShellUiStore.applyDomAttributes()` writes `` and `` but doesn't clean up an old `data-theme` attribute if it ever existed (from a prior Vuetify-era implementation). Pre-launch this is a non-issue; for cleanliness, remove the stale attribute on init. **Trigger:** cleanup pass before launch, or if anyone hits attribute-related styling weirdness. --- ### ~~GRADIENT-BRAND-ALIGNMENT — gradient palette off-brand teal anchor~~ ✅ RESOLVED ~~The `utils/v2/gradient.ts` GRADIENT_PALETTE clustered in the Tailwind blue-green family with off-brand teal `#0d9488` for the anchor slot.~~ Resolved in P7-followup-gradient-brand: palette replaced with the crewli-starter diverse 8-color palette, slot 1 anchored to Crewli `#0D9394`/`#075F60`. User-avatar fallback also corrected. Regression-spec locks the brand-teal slot. --- ### TECH-OBSERVER-TEST-CONVERGENCE — Drop `bootstrap_on_org_create` flag once tests converge **Aanleiding:** Session 3 introduceerde `OrganisationObserver` om elke nieuwe organisatie automatisch een `artist_advance` FormSchema te bezorgen (RFC-TIMETABLE v0.2 D15). Vijf bestaande tests (`FormSchemaTest`, `FormSchemaApiTest`, `MultiTenancyTest`, twee `ScopeLeakageTest`-cases) tellen FormSchema-rijen exact en namen aan dat `Organisation::factory()` geen schema's meebezorgt — de auto-bootstrap brak die assumptie. Quick fix: `config/artist_advance.php` met `bootstrap_on_org_create` (default `true`, `phpunit.xml` flipt hem naar `false`); de observer leest de config. **Wat:** Update de vijf FormSchema-counting tests zo dat ze de auto- bootstrapped `artist_advance` schema verwachten (filter op `purpose != 'artist_advance'` of pas counts aan). Verwijder daarna het `bootstrap_on_org_create` flag, de `phpunit.xml` env-override, en de config-check in de observer — productiegedrag = testgedrag, geen branching. **Prioriteit:** Laag — geen blocker, dedicated test-cleanup pass. --- ### ART-ADVANCE-SECTION-FK — Replace name-based AdvanceSection ↔ FormSchemaSection bridge with FK **Aanleiding:** Session 3 wirede de portal-flow door een name-match tussen `advance_sections.name` (engagement-scoped, RFC-TIMETABLE v0.2 §5.3) en `form_schema_sections.name` (org-scoped, FormBuilder). De seeder (`ArtistAdvanceDefault`) creëert vijf `FormSchemaSection`-rijen met deterministische namen; de `EngagementPortalController` filtert `FormField`-rijen door eerst de `FormSchemaSection` met dezelfde naam te vinden als de `AdvanceSection`. Werkt vandaag, maar: 1. **Hernoemen breekt**: een organisatie die "Algemeen" hernoemt naar "Algemene info" via de FormBuilder UI verbreekt de match voor alle bestaande engagements. 2. **Geen referential integrity**: dubbele/ontbrekende naam-matches silenten falen i.p.v. een DB-niveau constraint. 3. **Geen migration-pad voor `Custom`-secties**: organisaties die eigen secties toevoegen aan de FormBuilder schema krijgen geen corresponderende `AdvanceSection`-rij per engagement. **Wat:** Voeg `advance_sections.form_schema_section_id` toe (nullable `foreignUlid`, `nullOnDelete`). Bij `ArtistEngagement::created` (nieuwe observer of uitbreiding van bestaande) provisionineer één `AdvanceSection`-rij per `FormSchemaSection` op de org's `artist_advance` schema, met `form_schema_section_id` gevuld. Migratie voor bestaande data: best-effort name-match per (organisation_id, schema_id) als backfill, gevolgd door log-warning voor unmatched rijen. Update `EngagementPortalController` om via FK te filteren i.p.v. naam. **Prioriteit:** Middel — relevant zodra UI-rename of `Custom`-secties voor het eerst in productie aanlopen tegen het bug. Voor pure default seeded schema's werkt de huidige bridge. --- ### RFC-TIMETABLE-V0.2-DOC-CLEANUP — Strip ARCH-PLANNED-MODULES.md mentions from RFC v0.2 **Aanleiding:** RFC-TIMETABLE v0.2 §1 ("Bron-documenten") en §15 ("Implementatieplan") verwijzen naar `ARCH-PLANNED-MODULES.md §3.5.7` als de locatie waar de oude artist-schema-planning leefde. Dat bestand bestaat niet in de repo — de oude §3.5.7 leefde rechtstreeks in `SCHEMA.md`. Phase A van Session 1 surfaced deze drift; Step 7 reduceerde tot een in-place rewrite van `SCHEMA.md` §3.5.7. De RFC zelf is Approved en frozen — niet ad-hoc patchen. **Wat:** Bij de eerstvolgende RFC v0.2 amendement, vervang of verwijder de `ARCH-PLANNED-MODULES.md` cross-references in §1 en §15. Dit is geen blocker voor implementatie van Sessions 2–6. **Aanvulling Session 2 (2026-05-08):** RFC §9 noemt vier permission-strings (`events.view_program`, `events.manage_program`, `organisations.manage_artists`, `organisations.manage_settings`). De implementatie-keuze (Phase A Option B) bindt deze permissions aan Spatie roles in plaats van aan `Permission`-rijen, omdat de bestaande codebase rolgebaseerd is en migratie naar fine-grained permissions cross-cutting is. De docblocks van `ArtistPolicy`, `ArtistEngagementPolicy`, `StagePolicy`, `PerformancePolicy` en `GenrePolicy` documenteren de exacte mapping. Cross-cutting migratie wordt gevolgd onder `AUTH-PERMISSIONS-MIGRATION` (zie hieronder). **Prioriteit:** Laag — documentatie-hygiëne, niet code. --- ### RFC-TIMETABLE-V0.2-PORTAL-TOKEN-SCHEMA-AMEND — `portal_token` is varchar(64), niet ULID **Aanleiding:** RFC v0.2 §5.3 specificeert `artist_engagements.portal_token` als `ULID unique nullable`. Session 1 implementatie heeft de kolom verbreed naar `varchar(64)` omdat `PortalTokenController` `hash('sha256', $plainToken)` opslaat (64-char hex digest); `char(26)` zou stilzwijgend truncaten onder MySQL strict mode. De implementatie is correct — schema reality is de bron van waarheid — maar de RFC-annotatie is stale. **Wat:** Bij de eerstvolgende RFC amendement-cyclus, hetzij een v0.3 uitbrengen met §5.3 spec gecorrigeerd, hetzij een §5.3 footnote toevoegen aan v0.2. Approved RFCs worden niet ad-hoc gepatched; dit ticket vangt de divergentie totdat een legitieme amendement langskomt. **Reference:** Session 1 commits `eb6d396` (column widening) en `64878f2` (controller wired through `artist_engagements.portal_token`). **Prioriteit:** Laag — pure doc-spec alignment; code is correct. --- ### EVENT-START-END-TIME `events` table currently has `start_date`/`end_date` (date type), which forces day-boundary semantics in `WithinEventBounds` and similar checks. For festivals running past midnight or with intentionally non-24h operating windows, a `start_time`/`end_time` pair (or a unified `start_at`/`end_at` datetime) on the `events` table would let: - the timetable viewport (Session 4 frontend) honour real event hours instead of always showing 00:00 → 23:59 - boundary checks like `WithinEventBounds` reject performances that run past the event's actual close time, even within the same date **Why not now:** - Cross-cutting schema change touching the `events` table — used by 30+ modules across the codebase - Out of scope for the Artist Timetable sprint per Charter §2 (no opportunistic feature-creep) - Sub-events absorb the granularity in 90% of cases via Performance datetimes **When:** - Session 4 frontend timetable viewport reveals concrete UX gaps from date-only event bounds - OR a customer onboards with a non-day-aligned schedule (e.g. a club with a 22:00 → 06:00 nightly window) **Surfaced during:** Session 2 review of `app/Rules/Artist/WithinEventBounds.php`, which uses `startOfDay()`/`endOfDay()` to bridge the date-vs-datetime gap. That bridge is correct given current schema; this ticket is about the schema, not the rule. **Prioriteit:** Middel — works today; upgrade is feature-not-bug. --- ### AUTH-PERMISSIONS-MIGRATION — Migrate alle policies van hasRole() naar hasPermissionTo() **Aanleiding:** Crewli gebruikt vandaag uitsluitend Spatie *roles*; geen `Permission`-rijen worden geseed en geen policy roept `hasPermissionTo()` of `Gate::can()` tegen permission-strings aan. RFC-TIMETABLE v0.2 §9 beschrijft de toegangscontrole in termen van permission-strings (`events.manage_program`, etc.); Phase A van Session 2 (2026-05-08) besloot Option B — de permission-strings worden in policy-docblocks gedocumenteerd en role-based geautoriseerd. Een hybride aanpak (perms seeden maar niet gebruiken) werd afgewezen omdat dat strings creëert zonder source-of-truth-status. **Wat:** Eén dedicated cross-cutting sprint die ALLE policies (niet alleen Artist-domein) overzet van `hasRole()` naar `hasPermissionTo()`. Inclusief: - `PermissionSeeder` voor de complete set permissions die we vandaag via roles uitdrukken (per-domein audit van bestaande policies) - Policy-by-policy refactor met behoud van semantiek - Policy-tests bijwerken (bestaande tests gebruiken role-strings) - Documentatie in `dev-docs/CLAUDE.md` (`Roles and permissions`-blok) **Trigger:** Klant- of charter-vereiste — een specifieke gebruiker moet wel X kunnen maar niet Y, en X+Y delen vandaag dezelfde rol. Niet: interne preferentie of "het is netter". **Reference:** Session 2 Phase A (2026-05-08) Option B beslissing; `feedback_authorization_pattern` user-memory (intent-only — niet in auto-memory geschreven door file-protect hook). **Prioriteit:** Laag — wachten op concrete operationele behoefte. --- ### ART-DEMOTE-NOTIFICATION — Notify project-leader on option-expiry demotion **Aanleiding:** RFC-TIMETABLE v0.2 noemt notificatie van de project-leader wanneer een Option afloopt en automatisch gedemoteerd wordt naar Draft (via de `artist:demote-expired-options` daily command, Session 2 Step 10). Het notificatie-framework landt pas post-Accreditation; daarom schrijft de Session 2 command alleen een `option_expired` activity-log entry, geen e-mail. **Wat:** Wanneer notification-framework live is, hook het in de command in: na elke `transitionStatus()` succes een notificatie naar de project- leader (en optioneel de `program_manager` rol op het evenement). Houd rekening met aggregatie als veel Options tegelijk verlopen — niet één mail per Option. **Reference:** Session 2 commit `feat(timetable): DemoteExpiredOptions scheduled command`; `app/Console/Commands/Artist/DemoteExpiredOptions.php`. **Prioriteit:** Medium — wachten op notification-framework, maar wel een zichtbare gap voor program managers tot dan. --- ### 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 = { 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`. --- ### HARD-DEADLINE-QUERY-TIMEOUT **Aanleiding:** WS-6 v1.3-delta D2 (PR #11 `23a5696`, 2026-05-08) introduceerde `FormBindingApplicator::withDeadline(int $seconds)` — een **soft** deadline via post-call microtime check. Deze beschermt tegen "applicator runs slow over many bindings" (de long tail) maar **kan niet onderbreken** tijdens een hangende MySQL-query of een lock-for-update wait die langer duurt dan de deadline. Voor enterprise SaaS op piekmomenten (vrijwilligersregistratie-windows, festival check-in flows) is een hangende query een reëel risico — PHP-FPM worker blokkeert, queue depth loopt op, downstream impact. **Wat:** Drie potentiële uitvoeringspaden, te kiezen tijdens RFC-fase: 1. **MySQL connection-level timeouts**: zet `read_timeout` en `write_timeout` op de Eloquent connection config voor de inner-transaction context. Eenvoudig, maar globale impact (alle queries via die connection krijgen de timeout). 2. **Per-query `MAX_EXECUTION_TIME` hints**: MySQL 5.7+ ondersteunt `/*+ MAX_EXECUTION_TIME(N) */` query hints voor SELECT statements. Niet ondersteund door MariaDB. Gericht maar database-vendor-specifiek. 3. **`pcntl_alarm` + signal handler**: hard interrupt via SIGALRM in de queue worker context (CLI sapi). Werkt niet in HTTP context (PHP-FPM disablet pcntl in de meeste configuraties). Combineren met optie 1 voor HTTP context. **Trigger-conditie:** Naar voren halen wanneer (a) eerste productie-incident waar `FormBindingApplicatorTimeoutException` getrigerd wordt en de soft deadline blijkt onvoldoende (post-mortem toont een hangende query), OF (b) SLO-vereisten van een enterprise klant strikte response-time-garanties opleggen die de soft deadline niet kan halen. **Estimate:** 2-3 dagen (RFC + implementatie + tests + connection config voor alle environments). **Refs:** `dev-docs/RFC-WS-6.md` v1.3.1 §Q1 v1.3 add 4, `dev-docs/ARCH-BINDINGS.md` v1.2 §5.3, `api/app/FormBuilder/Bindings/FormBindingApplicator.php` (soft deadline implementation). --- ### ~~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 --- ### FRONTEND-ENERGYDOTS-NAN-ROBUSTNESS — EnergyDots/EnergyPicker NaN/Infinity input **Aanleiding:** `EnergyDots.vue` en `EnergyPicker.vue` renderen `data-energy="NaN"` en 0 dots wanneer de input `NaN`/`Infinity` is; `withDefaults` vervangt alleen `undefined`, niet `NaN`. Verbatim port uit crewli-starter in Plan 3 (T7/T8); geen Plan-3 consumer, dus bewust niet in-flight gewijzigd (buiten frozen scope, surfaced ipv silently changed). **Wat:** - Clamp/valideer de numerieke input op de prop-grens (niet-eindige waarden → 0 of geclamped 0–5 bereik). - Voeg `NaN`/`Infinity`-guard tests toe voor beide componenten in één hardening-commit. **Trigger:** Eerste API-gevoede consumer in Plan 4+ (een pagina die ongevalideerde numerieke API-data doorgeeft). **Prioriteit:** Laag — geen huidige consumer; cosmetische degradatie, geen crash. --- ### FRONTEND-DRAGGABLEBLOCK-POINTERCANCEL — DraggableBlock spurious dragend on system-cancel **Aanleiding:** `DraggableBlock.vue` heeft geen `@pointercancel` handler. Een door het systeem geannuleerde drag (iOS home-gesture, Android long-press, stylus out-of-range) vuurt via `lostpointercapture` een spurieuze `dragend` met stale coördinaten terwijl `active` nog `true` is — een parent (TimetableGrid/CueTimelineEditor) herpositioneert dan onterecht een block. Beste vondst van de Plan 3 review; valt buiten het A2-contract (`dd45e899`). **Geblokkeerd door:** A2-contract amendement eerst — het drag-model in `dev-docs/superpowers/plans/2026-05-17-gui-redesign-tier1-primitives-DRAGGABLEBLOCK-CONTRACT.md` moet `pointercancel`-semantiek expliciet specificeren vóór de code-wijziging (frozen-artifact discipline, analoog aan constraint #1). **Wat:** - Amend het A2-contract doc met `pointercancel` semantiek. - Voeg een `@pointercancel` handler toe die de `@pointerup` cleanup spiegelt (clear `active`, reset start-coördinaten; géén `dragend` emit). - Voeg een Vitest cancel-tijdens-drag test toe (geen `dragend`/`click`; navolgende `lostpointercapture` is een no-op). **Trigger:** Tier-4 integratie-sprint (TimetableGrid / CueTimelineEditor). **Prioriteit:** Middel — echte correctheids-gap op touch/stylus zodra een echte consumer DraggableBlock bedraadt. --- ### A11Y-AD3-MENUBAR-EMPTY-MODEL — Empty role="menubar" from AD-3 Menubar-wrap **Aanleiding:** Het RFC AD-3 Menubar-wrap patroon (``, Plan 3 Task 11 Step-4 design) produceert een lege `