132 KiB
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 schemaevents.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_assignmentscross-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_historyal 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_retrospectivesal 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_waitlistal 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_submissionsal 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_formsal 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 mutatessource=self_reportedrows, no-op whenperson.user_id IS NULL.App\Listeners\FormBuilder\SyncTagPickerSelectionsOnSubmit— ShouldQueue listener onFormSubmissionSubmitted, filters toevent_registrationpurpose withsubject_type=person+ at least oneTAG_PICKERvalue. Logs + swallows errors so sibling listeners (§31.1/§31.3/§31.8) keep running.App\Services\PersonIdentityService::confirmMatch— callsFormTagSyncService::rebuildForPersonafter settingperson.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
eventsintegration of een customMigrationObserver) - 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). rotatePublicTokenservice persisteert de ontvangengrace_daysvalue (fallback: config default).PublicFormTokenResolver::GRACE_DAYSleest uitform_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: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 inhandle()zelf — er is geen aparteresolveStatusmethode meer. De huidige logica:$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_idheeft én de exact-email matcher leeg terugkomt:$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
extractFormValuesForMatchinghelper trekt de relevante velden (email / first_name / last_name / date_of_birth + eventuele whitelisted custom-fields) uitFormSubmission->valuesvia 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.phpcallback (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_requestsal 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-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:
- Hernoemen breekt: een organisatie die "Algemeen" hernoemt naar "Algemene info" via de FormBuilder UI verbreekt de match voor alle bestaande engagements.
- Geen referential integrity: dubbele/ontbrekende naam-matches silenten falen i.p.v. een DB-niveau constraint.
- Geen migration-pad voor
Custom-secties: organisaties die eigen secties toevoegen aan de FormBuilder schema krijgen geen corresponderendeAdvanceSection-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
WithinEventBoundsreject performances that run past the event's actual close time, even within the same date
Why not now:
- Cross-cutting schema change touching the
eventstable — 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:
PermissionSeedervoor 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-"vsls node_modules | grep "^eslint-plugin-", plus de scoped variant envue-eslint-parser/@antfu/eslint-config-*audits) opapps/portal/. - Voeg alle ontbrekende plugins als directe deps toe via
pnpm add -Dmet 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:
X-Organisation-Idheader-injection. Set een actieve organisatie viauseOrganisationStore, 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.- 401 → auth-fail flow. Mock de response op 401, registreer
interceptors waarbij
onAuthFaileen spy is, assert dat de spy wordt aangeroepen exact wanneeruseAuthStore.isInitializedtrue is en niet gedurende de eerste/auth/me-probe (de race-conditie die sessie 1b-iii repareerde — die guard moet blijven werken). - 403 +
impersonation_ended→ revocation flow. Mock de response op403met body{ impersonation_ended: true }, registreer interceptors waarbijonImpersonationRevokedeen 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 inaxios.ts, makkelijk per ongeluk te breken bij een toekomstige refactor). - 4xx/5xx error toast. Mock 403/404/422/503/5xx/network-error
responses, assert dat
notifyde juiste boodschap + level krijgt voor elk geval. 422 met body-messagemoet het server-bericht doorgeven; 422 zondermessagemag 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-routertriggert enapps/app/typed-router.d.tsre-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), enunplugin-vue-routerheeft 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.tstoe aan.gitignore. Voeg eenpostinstallscript inapps/app/package.jsondatvue-tscof een vergelijkbare prebuild-stap triggert dieunplugin-vue-routerzijn 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 installdoet meer werk), en alspostinstallfaalt 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-routerregenereert 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}/enpages/{(auth),portal,...}/bestaan fysiek met content). - Breid
boundaries/elementsinapps/app/.eslintrc.cjsuit 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-portalencomponents-organizermogen beide uitcomponents-sharedimporteren, maar niet uit elkaar.pages/portal/mag niet uitpages/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/elementsinapps/app/.eslintrc.cjs:
{ type: 'router', pattern: 'src/router/**' },
Plaats vóór plugins in de array (first-match-wins ordering).
- Voeg toe aan
boundaries/element-typesrules:
{ from: 'router', allow: ['types', 'utils', 'lib', 'plugins', 'stores'] },
- Verifieer
pnpm lintblijft 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.phpapi/app/Http/Controllers/Api/V1/PasswordResetController.phpapi/app/Http/Controllers/Api/V1/PersonController.php- Email templates die de
appparameter 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:
- 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.
- 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.roleeen free-form string of komt het ondermodel_has_rolesmet team-id = organisation_id? Spatie's "teams" feature is bedoeld voor precies dit scenario. - Multi-role-precedence: als een user
org_adminÉNevent_manageris 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.shnaar 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 fileargument 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-onlyaangeraakt 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/pintover 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 --dirtyin 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:
-
MySQL connection-level timeouts: zet
read_timeoutenwrite_timeoutop de Eloquent connection config voor de inner-transaction context. Eenvoudig, maar globale impact (alle queries via die connection krijgen de timeout). -
Per-query
MAX_EXECUTION_TIMEhints: MySQL 5.7+ ondersteunt/*+ MAX_EXECUTION_TIME(N) */query hints voor SELECT statements. Niet ondersteund door MariaDB. Gericht maar database-vendor-specifiek. -
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
pointercancelsemantiek. - Voeg een
@pointercancelhandler toe die de@pointerupcleanup spiegelt (clearactive, reset start-coördinaten; gééndragendemit). - Voeg een Vitest cancel-tijdens-drag test toe (geen
dragend/click; navolgendelostpointercaptureis 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
(<Menubar :model="[]">, Plan 3 Task 11 Step-4 design) produceert een
lege <ul role="menubar"> in de DOM. Raakt elke consumer van het
chrome-shell patroon, te beginnen met AppTopbar (Task 11).
Plan-frozen Step-4 ontwerp; pattern-level beslissing, dus bewust niet
in-flight gewijzigd.
Wat (pattern-level beslissing):
- Óf onderdruk de lege
ulvia een PT-slot override op het Menubar-wrap patroon, - óf vervang de Menubar-wrap door een dunnere chrome-primitive zonder menubar-semantiek.
- Documenteer de gekozen lijn in
PRIMEVUE_COMPONENTS.md(v2 primitives registry — Menubar-wrap sectie).
Trigger: F5 a11y-audit batch (per RFC-WS-FRONTEND-PRIMEVUE §A.7 item 17).
Prioriteit: Laag — niet user-blocking; a11y-hygiëne, op te lossen in de geplande a11y-batch vóór brede herbruik van het patroon.
STORYBOOK-DARKMODE-DECORATOR — Storybook decorator voor dark-mode story rendering
Aanleiding: Plan 2.5 P3 verplaatste de dark class van <body>
naar <html> (AD-2.5-D1, commit d0dd45c0) zodat Tailwind v4's
@variant dark (@custom-variant dark (&:where(.dark, .dark *)))
correct cascadeert naar elk element binnen het document. Storybook
stories renderen componenten echter in een geïsoleerde preview-iframe
waar <html> niet door de SPA-runtime (useThemeStore / vue-use
usePreferredDark) wordt geannoteerd — dark-mode varianten verschijnen
daardoor niet in Storybook ook al werken ze correct in de app.
Wat:
- Voeg een Storybook decorator toe die de
darkclass op de iframedocument.documentElementtoggle't op basis van een Storybook global (toolbar toggle, evt. per-story parameter override). - Wire de decorator globaal in
.storybook/preview.tszodat elke story automatisch in light + dark gereviewd kan worden zonder per-story setup. - Documenteer het patroon in
PRIMEVUE_COMPONENTS.md(theming sectie) naast de bestaande dark-mode conventies.
Trigger: Eerste story die dark-mode parity moet aantonen (visual-regression baseline, design-review van een PrimeVue surface, of F5 a11y-batch contrast-check).
Prioriteit: Laag — dark-mode werkt correct in de SPA; alleen Storybook visuele review van dark-varianten is geblokkeerd. Geen runtime impact op gebruikers.
Opgeloste items (mei 2026)
WS-TOOLING-001: Claude Code deterministic guard-rail layer (5 hooks,✅crewli-reviewersubagent 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. Mergead36c06op 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.shhook (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— totaal80 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(infra5f6fc07, backend SDKbdb89a2..0379016, frontend SDKbc47783..5c42f27, docs754222f..e9da01f). 1551 backend + 252 frontend tests groen. Acceptance criteria 1-14 voldaan; observability volledig operationeel opmonitoring.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 indev-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 viaactor_scopetag. 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).WS-6 v1.3-delta — closure (mei 2026): Architecturele review-sessie 2026-05-07 identificeerde vijf verfijningen op RFC-WS-6 v1.2 (Q1 listener queueing, Q2 invariant cleanup, Q3 failure-UX additions, plus §19 BACKLOG-pointer). v1.3 amendement gecommit (✅845b6e6, 2026-05-07); v1.3.1 drift closure (b255879, 2026-05-08) sloot code-vs-docs gaten pre-implementation. Implementatie geland als D1 + D2: D1 (PR #10c6f4d1b) leverde de data-laag —failure_response_codekolom opform_submissions, abstractFormBindingApplicatorExceptionhiërarchie + 4 reason-coded subclasses (FormBindingSchemaConfigException,FormBindingInfraException,FormBindingApplicatorTimeoutException,FormBindingDataIntegrityException),IdentityMatchInvariantViolationsibling,FormBindingExceptionClassifierhelper,FormSubmissionIdentityMatchResolvedbroadcast event class,FormFieldBindingMergeStrategy::validForTargetTypematrix method, cast + factory state. D2 (PR #1123a5696) wired alle building blocks in de listener-chain —ApplyBindingsinitialpendingwrite + deadline wrapper + classifier in catch;TriggerPersonIdentityMatchqueued + gating-invariant + invariant throw + broadcast dispatch;routes/channels.php+ bootstrap routing (NIEUWE broadcast wiring, submitter-only auth); gating-invariant opSyncTagPicker;AppServiceProvider::bootv1.3 layout;FormFailureRetryService::recordFailureclassifier + apply_completed_at symmetrie-fix;apply_deadline_secondsconfig key (default 5). Tests: pre-WS-6 baseline 1208 → pre-D1 1551 → post-D2 1621. 0 Larastan errors. Phase F (ConditionalRequirement(public_token)wrapper drop) was no-op — change had silently landed pre-D2. Open follow-ups:TECH-CHANNEL-AUTH-ORG-ADMIN(extendsubmission.{id}channel auth to org admins na Spatie Permission helper-audit); GlitchTip alert rule opapply_status=failed AND form_schema.has_public_token=true(operationele taak in GlitchTip web-UI opmonitoring.hausdesign.nl; runbook procedure indev-docs/runbooks/observability-triage.md§7); frontend Echo subscription voorFormSubmissionIdentityMatchResolved(separate frontend follow-up, out of WS-6 scope, backend-infra ready).PARTIAL-BINDING-SUCCESSenFORM-SCHEMA-DRIFT-DETECTIONblijven open conform v1.3 amendement (trigger-condities nog niet gevuurd). Closure docs-PR: RFC-WS-6.md v1.3.1 implementation-status marker + §10 closure entry, ARCH-BINDINGS.md v1.2 onveranderd, runbook §7 toegevoegd.ARCH-09 — Artist Eloquent model + migration — closure (mei 2026): Foundation for the Artist & Timetable module landed as RFC-TIMETABLE v0.2 Session 1 op✅feat/timetable-session-1. Delivered: 10 migrations (genres, artists, companies.handles_buma column, artist_contacts, stages, stage_days, artist_engagements, performances, advance_sections, advance_submissions); 7 PHP enums underApp\Enums\Artist\(ArtistEngagementStatusD9 with Dutch labels,BumaHandledByD26,FeeType,PaymentStatus,AdvanceSectionType,AdvanceSectionSubmissionStatus,AdvanceSubmissionStatus); 9 Eloquent models withOrganisationScope(direct on Artist/Genre/ArtistEngagement, FK-chain viatenantScopeStrategy()on the rest) andLogsActivitybaseline; 2 observers (ArtistEngagementObserverfororganisation_iddenorm + cross-tenant guard viaCrossTenantEngagementException+ cascade soft-delete to performances + hard-delete to advance_sections;PerformanceObserverfor D14 optimistic-lockversionbump on UPDATE); 8 factories +ArtistTimetableDevSeederreproducing the prototype fixture (4 stages, 12 stage_days, 6 artists, 12 engagements, 13 performances incl. 1 parked);PURPOSE_SUBJECT_FQCNswitched from string-literal toArtist::class(MorphMapAlignmentTest green); SCHEMA.md §3.5.7 rewritten in place (ARCH-PLANNED-MODULES.md was assumed by the RFC pre-amble but did not exist — seeRFC-TIMETABLE-V0.2-DOC-CLEANUP); ARCH-FORM-BUILDER.md §3.2.5 updated for engagement-scoped sections and §17.3 footnote onArtistResolver::fromPortalTokenengagement context resolution. PR #XX, 2026-05-08.ART-OBSERVER-ADVANCE-AGGREGATE — closure (mei 2026): AdvanceSectionObserver implemented in RFC-TIMETABLE v0.2 Session 3 on✅feat/timetable-session-3. Recomputesartist_engagements.advancing_completed_count+advancing_total_countatomically on every section lifecycle event (created / updated-status-only / deleted). Concurrency safety viaDB::transaction+lockForUpdateon both the parent engagement and sibling section rows; counter writes usedisableLogging()so housekeeping doesn't pollute the activity log. Section's ownupdatedevent continues to log viaLogsActivityonAdvanceSection.TECH-CHANNEL-AUTH-ORG-ADMIN — closure (mei 2026):✅submission.{id}private channel auth uitgebreid van submitter-only naar drie-paths: submitter (submitted_by_user_id === user.id) → super_admin Spatie HasRoles app-wide bypass → org_admin van submission's organisatie via pivot-table check opuser_organisation(->wherePivot('role', 'org_admin')). Pattern: directe port vanFormSubmissionActionFailurePolicy::canAccess, codebase canonical (gebruikt in 17+ policy sites). Spatie teams is disabled inconfig/permission.php, dus org-scoping leeft in de pivot, niet in Spatie. super_admin bypass is een audit-surfaced bonus (origineel BACKLOG entry vroeg alleen om org-admin extension; tijdens Phase A audit bleek dat elke analoge policy super_admin bypass heeft, dus toegevoegd voor consistency — zonder die bypass zouden super_admins op de admin-panel banner mysterieus geen live updates krijgen). Tests: 4 nieuw (test_super_admin_can_subscribe,test_organisation_admin_of_submission_org_can_subscribe,test_organisation_admin_of_different_org_cannot_subscribe(kritische cross-tenant guard),test_regular_organisation_member_cannot_subscribe); 1 verwijderd (de "should flip" denied-by-default test uit PR #11). Test count: 1621 → 1624 (+3 net). 0 Larastan errors. Inline TODO uitroutes/channels.phpverwijderd. SiblingFRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTIONblijft open (frontend portal IdentityMatchBanner subscription is de pair met deze backend-auth uitbreiding).
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.✅FormSubmissionResourcekrijgt admin-facingidentity_matchblock. 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 |
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
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/adminscope. - Frontend: "Opnieuw versturen" actie activeren in de sectie
openstaande uitnodigingen op
/members(useMembers heeft al eenuseResendInvitationstub-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->idof vergelijkbaar niet door PHPDoc wordt herkend — los op door@propertyannotaties op modellen toe te voegen (of viaphp artisan ide-helper:modelsals 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>) enHasFactory-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 gebruikCollection::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
arrayzonder value-type. Voegarray<string, mixed>of specifieker toe aan form-requests, resourcetoArray()methods, factorydefinition()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
@methodannotaties 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'sDEAD_CODEsprint — 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.mdvoor 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 rectorom 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)
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.tsTS7006,themeConfig.tsTS2322 viaLayoutConfig.titleLowercase<string>over-constraint,build-icons.tsTS2307 missing@iconify/types,casl.tsTS2345 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 runvue-tsceither. 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 --noEmitas 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 toapps/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):
cd apps/app && pnpm add -D vitest @vue/test-utils @testing-library/vue happy-dom- Mirror
apps/portal/vitest.config.tsadapted for apps/app paths - Mirror
apps/portal/tests/setup.tsif relevant - Add
"test": "vitest"and"test:run": "vitest run"toapps/app/package.jsonscripts - 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.tsis a natural early target (no runtime test today; the ts-reset-surfaced TECH-TS-IMPERSONATION runtime-validation work would benefit from that coverage) - Foundation-tooling-style return deliverable: confirm
pnpm test --runexits 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
ArtistEloquent model class? Currently theartiststable exists but no class — only the morph map alias points atApp\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
setAttributechange → 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 onderApp\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 viaz.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: 2–3 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 (via @core/utils/validators + Zod
payload schemas — see CLAUDE.md "Forms"), 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.phpals regression-guard. ExceptionReportingTest installeert een recordingbefore_sendhook en verifieert datRuntimeExceptionwel,ValidationException/AuthorizationException/NotFoundHttpExceptionniet 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.mdonder 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.phpdieapp(\Illuminate\Foundation\Exceptions\Handler::class)->getReportableCallbacks()introspecteert en assertert dat sentry-laravel's callback geregistreerd is. Vangt een toekomstige refactor die per ongelukIntegration::handlesuitbootstrap/app.phpverwijdert.
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.phpmet 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:
215405afix: disable Laravel listener auto-discovery; explicit registrations only→->withEvents(discover: false)inbootstrap/app.php; alle observability listeners expliciet via array-callable ([Class::class, 'method']) inAppServiceProvider::boot().a939820fix: impersonation.active default tag for non-impersonation authenticated events→ baseline'false'inAuthScopeContextListener::bindForUser(), override naar'true'inHandleImpersonationmiddleware (default-in- listener, override-in-middleware pattern).- Commit 3 (deze sessie):
tests/Feature/Observability/EventListenerRegistrationTest.phpintrospecteertEvent::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 maakapps/app/index.htmlmeta-CSP environment-aware via Vite build-time injection (vergelijkbaar metVITE_SENTRY_DSN_FRONTENDpatroon). - Update
tests/Feature/Security/CspConnectsToObservabilityTest.phpmet 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.
VEE-001 — VeeValidate removed from stack ✅ Resolved
Status: Closed in feat/timetable-session-4 follow-up.
vee-validate and @vee-validate/zod shipped in apps/app/package.json
since Vuexy onboarding but were never imported anywhere in the SPA. A
strict regex sweep (from 'vee-validate', <Field>, <Form>,
<ErrorMessage>, defineRule(, useForm(<args>)) returned zero
hits across apps/app/src/. Earlier fuzzy matches were false
positives from useForm colliding with Crewli's own useFormDraft /
useFormSteps / useFormSchemas / useFormFailures composables.
Removed both packages from apps/app/package.json, regenerated
pnpm-lock.yaml. Canonical form pattern formalised in CLAUDE.md
"Forms" + dev-docs/VUEXY_COMPONENTS.md "Form validation" row.
Refs: Session 4 follow-up Step 1; apps/app/src/components/timetable/AddPerformanceDialog.vue and apps/app/src/components/sections/CreateShiftDialog.vue as canonical references.
TEST-INFRA-001 — Migrate timetable component+a11y tests to Playwright Component Testing ✅ Resolved
Status: Closed in chore/test-infra-001 (commits b8d18e6,
82af117, f6509d9, 2dfb1e8). Sprint executed per
RFC-WS-FRONTEND-PRIMEVUE Amendment A-1.
Resolution: Playwright + axe-core installed; CT and e2e runners
configured; Git LFS enabled for screenshots; mountWithProviders
helper established; full provider stack (Vuetify [TEMPORARY: replaced
in F3], Pinia, TanStack Query, Memory-history Router) wired in
apps/app/playwright/index.ts's beforeMount hook. Existing
402 Vitest+jsdom tests left unchanged per amendment §A.3 goal 5
(natural replacement during F4 component migration).
Deviations from original:
- CI integration deferred to TEST-INFRA-002 — no CI exists in repo today. Sprint scope cut to "passes locally" per amendment A-1 acceptance terms.
- Provider plugins wired in
playwright/index.tsrather than at mount call time (Playwright CT API divergence from@vue/test-utils). Documented inmountWithProviders.tsJSDoc anddev-docs/ARCH-TESTING.md§6.
Aanleiding: Session 4 follow-up landed component-mount, integration, keyboard a11y, and axe-core tests on Vitest + jsdom as a deliberate intermediate step. JSDOM does not faithfully reproduce browser layout, PointerEvents-with-capture, drag-threshold semantics, or computed CSS visual properties (color contrast resolution). For a module whose core surface is drag/resize/lane-stacking/pixel-coordinates, jsdom-based assertions are necessary but not sufficient.
Wat:
- Open branch
feat/playwright-ct-foundationafter the timetable PR merges. - Install
@playwright/experimental-ct-vue(and Playwright runners). - Build the equivalent of
apps/app/tests/utils/mountWithVuexy.tsfor Playwright CT (Vuetify, Pinia testing, QueryClient, router, token CSS injection). - Migrate to
apps/app/tests-pw/component/:PerformanceBlock.test.tsStageRow.test.tsWachtrij.test.tsAddPerformanceDialog.test.tsuseTimetableMutations.test.ts(drag-threshold assertions especially benefit)keyboard.test.tsaxe.test.ts
- Migrate
tests/integration/timetable-flow.test.tstoapps/app/tests-pw/integration/. - Keep Vitest + jsdom for
tests/unit/only (pure-logic + Zod + simple composables). - CI strategy: Playwright CT runs on every PR (slow lane); Vitest unit on pre-commit (fast lane).
Trigger: Eerstvolgende sprint na merge van
fix/timetable-stabilization. ART-S4-UX-PARITY en alle Sessie 5+ werk
gates op merge van TEST-INFRA-001. Reden: drie incidenten in dit
sprint-blok hebben aangetoond dat jsdom-tests structureel niet
beschermen tegen schema drift, filter drift, of UX divergence. Verder
bouwen zonder Playwright + visual regression infrastructure herhaalt het
patroon.
Refs: Session 4 follow-up commits 5f135ec..985a5ab,
RFC-TIMETABLE D14/D20/D21, the new apps/app/tests/utils/mountWithVuexy.ts
helper (designed to translate cleanly into Playwright CT's mount() API).
TEST-CONTRACT-001 — End-to-end 409 conflict contract test against running Laravel ✅ Resolved
Status: Closed in chore/test-infra-001 commit 2dfb1e8 (B4 of
TEST-INFRA-001 sprint).
Resolution: apps/app/tests/playwright-e2e/timetable/409-conflict. spec.ts runs against a real Laravel test server (php artisan serve --port=8001) seeded by api/database/seeders/E2EBaselineSeeder.php
via Playwright's globalSetup. Test asserts first-move 200 and
second-move 409 with errors.conflict: 'version_mismatch'. The
schema-drift bug class that motivated the entry (timetable-
stabilization B5) is now caught end-to-end.
Deviations from original:
- Single-context replay instead of two-browser-context concurrent edit. The 409 is server-determined by stored version, not by session identity, so single-context replay is functionally equivalent for contract validation. Multi-context concurrent-edit test is documented in ARCH-TESTING.md §9 as deferred to F4.
- UI rollback assertion (popover toast, block snap-back) deferred to F4 UI-driven e2e — out of scope for B4 contract test.
- CI integration deferred to TEST-INFRA-002.
Aanleiding: The 409 rollback path in useTimetableMutations.move()
is currently asserted against a mocked axios response shape (Session 4
follow-up Step 9 + Step 12). A frontend Zod schema drift vs. the backend's
actual StaleVersionException serialization would not be caught — only
the unit test's mocked shape is validated. For a contract that protects
against multi-user collisions (RFC D14), the integration must be verified
against the actual backend.
Wat:
- After TEST-INFRA-001 lands the Playwright foundation, add
apps/app/tests-pw/e2e/timetable-409-conflict.spec.ts. - Spin up Laravel via
php artisan servein the test setup (or use the existing test-DB seeded againstcrewli_test). - Seed two browser contexts authenticated as the same organizer.
- Both load the same timetable; both attempt to
POST /timetable/moveon the same performance with the sameversionvalue. - Assert: first request succeeds (200, returns new version + cascaded[]); second request fails (409 with the actual backend conflict shape); frontend correctly rolls back and shows the conflict toast.
- Validate the response shape parses against
MoveTimetableConflictResponseZod — this is the contract proof.
Trigger: First e2e flow added after TEST-INFRA-001 lands. Highest contract-protection value per line of test code.
Refs: RFC-TIMETABLE D14, Session 4 follow-up Step 4 (zodParseFailure regression) + Step 9 (mocked 409).
TEST-VISUAL-001 — Visual regression baselines for PerformanceBlock states ✅ Resolved
Status: Closed in chore/test-infra-001 commit f6509d9 (B3 of
TEST-INFRA-001 sprint).
Resolution: 5 composite baselines captured from the canonical
prototype at resources/Crewli - Artist Timetable Management/ crewli-timetable.html (note: actual filename, not "Crewli Timetable.
html" as referenced in the older Aanleiding section below). Tests
live in apps/app/tests/playwright-ct/visual/prototype.spec.ts,
PNGs at apps/app/tests/playwright-ct/__screenshots__/visual/ prototype.spec.ts/. Tracked via Git LFS.
| Baseline | Captures |
|---|---|
canvas-friday.png |
Status colors, B2B indicators, multi-lane stacking |
canvas-saturday.png |
Conflict ring, capacity warning |
stage-row-multilane.png |
First row in isolation |
wachtrij-populated.png |
Sidebar list, status badges, counts |
popover.png |
Block-click popover layout |
Deviations from original 8-state-minimum scope:
- Composite-over-isolated strategy. Prototype DOM exposes status only
via inline
style.background, nodata-*attributes. Isolated- block locators by artist name would lock tests to specific seed data. Composite captures yield the same visual vocabulary in fewer more stable images. Documented indev-docs/ARCH-TESTING.md§4. - 9 surfaces from RFC §A.3's enumerated list documented as
test.skip()with gap reasons (cancelled status absent from prototype data, drag-mode flaky under simulated pointer events, empty-state surfaces unreachable from canonical seed). All deferred to F4 isolated component-level baselines using stabledata-test-idattributes. - Pixel tolerance
maxDiffPixelRatio: 0.001(0.1%) per RFC §A.6. - Linux+Chromium only per RFC §A.5; no Mac/Windows baselines.
- CI integration deferred to TEST-INFRA-002.
Aanleiding: Status badge colors, capacity icon presence, B2B dots, conflict ring, and cascade-pulse animation are UX contracts encoded in CSS tokens (RFC D21, D22, D25, D26). The Vitest+jsdom component tests assert that the right token resolves (Step 6's getComputedStyle roundtrips) but a developer changing token values, border widths, padding, or animation timing would not trigger a test failure even though the visual contract is broken.
Wat:
- Visual regression baselines for PerformanceBlock states, generated from
./resources/Crewli - Artist Timetable Management/Crewli Timetable.htmlrendered states (the canonical prototype is the baseline source — not hand-curated screenshots). - Compare implementation Playwright renders against prototype renders pixel-for-pixel (with reasonable tolerance for font rendering).
- Failures block PR merge unless a baseline update is intentional and reviewed.
- Apply to:
- PerformanceBlock — 8 states minimum: option / requested / confirmed / contracted / cancelled × with-warning / without-warning; plus B2B dots and cascade-pulse at the millisecond keyframe peak.
- PerformancePopover — open state with full detail (avatar + time/duration + multi-select status + advancing detail breakdown).
- AddPerformanceDialog — drag-mode (pre-fill from drop target) and button-mode (name/genre/status/duration → land in Wachtrij).
- Wachtrij — filtered / unfiltered / grouped / ungrouped.
- Pin font hinting and OS rendering: run visual tests only on Linux CI runners (consistent rendering across Mac/Windows is expensive — Linux baseline is sufficient).
- Commit baseline PNGs to
apps/app/tests-pw/visual/__screenshots__/. - CI fails on diff > 0.1% pixel delta.
Trigger: Onderdeel van TEST-INFRA-001 sprint. Tweede toevoeging na TEST-CONTRACT-001.
Refs: RFC-TIMETABLE D21, D22, D25, D26;
./resources/Crewli - Artist Timetable Management/ prototype.
TEST-INFRA-002 — CI integration for Playwright + visual + e2e
Aanleiding: TEST-INFRA-001 sprint scope was cut to "passes locally" because no CI exists in this repo today. The test infrastructure is operational on developer machines (Vitest 402, Playwright CT smoke + sanity + 5 visual baselines, e2e 409 contract test) but no automated gate prevents drift over time.
Wat:
- Decision: Gitea Actions vs. GitHub Actions vs. self-hosted runner. Determines runner image availability, secrets management, and pipeline DSL. No deadline; surface when first review cycle feels drift without automated tests.
- Runner image with PHP 8.2+ (composer, ext-bcmath, ext-zip),
MySQL 8 (or service container), Node v22, pnpm 10, Chromium for
Playwright. Probably layered on top of a stock
ubuntu-22.04image with explicit installs to keep image size predictable. - Caching strategy:
pnpm-storekeyed onpnpm-lock.yamlhashvendor/keyed oncomposer.lockhash~/.cache/ms-playwrightkeyed on Playwright version__screenshots__/cached locally for diff base; fetched via Git LFS on baseline tests
- Pipeline jobs (suggested):
lint+typecheck— pnpm lint, pnpm typecheck (fast lane)vitest— pnpm test (fast lane, runs in parallel with #1)playwright-component— pnpm test:component (medium lane, after #1+#2 pass)playwright-visual— pnpm test:visual (medium lane, parallel with #3). Diff PNGs uploaded as artifacts on failure; PR comment with diff summary if runner supports it.playwright-e2e— pnpm test:e2e (slow lane). Likely label- gated or nightly only, not on every PR. Requires MySQL service + Laravel server; ~10× more expensive than CT.phpunit— composer test (medium lane). Independent of #3-#5.
- Screenshot-diff artifact upload — failing visual tests upload expected.png + actual.png + diff.png as job artifacts. PR comment links to artifact download.
- Branch protection — pre-merge required: lint, typecheck, vitest, playwright-component, playwright-visual, phpunit. e2e optional gate.
- E2E DB strategy in CI — fresh
crewli_testper workflow run viamake test-db-createthenmigrate:fresh + seedfrom globalSetup. No state shared across runs (unlike local development). - Multi-browser-context e2e patterns for optimistic locking flows with UI rollback validation in the second context (cut #4 from TEST-INFRA-001 sprint)
Trigger: No explicit deadline. Surfaces when first review cycle feels drift without automated tests, OR when first regression slips through the local-only gates, OR when team scales beyond solo maintainer.
Refs: RFC-WS-FRONTEND-PRIMEVUE Amendment A-1 §A.7 DoD-17/19
deferral; chore/test-infra-001 sprint commits b8d18e6-2dfb1e8
(local infrastructure operational); dev-docs/ARCH-TESTING.md §5
(CI strategy stub).
ART-S4-UX-PARITY — Timetable UX parity with prototype
Aanleiding: Manual browser testing after fix/timetable-stabilization
merge surfaced substantial UX divergence between implementation and the
canonical prototype at ./resources/Crewli - Artist Timetable Management/Crewli Timetable.html.
Mechanical layer (backend, schema, layout, scroll, sticky panes) is
correct; visual and interaction layer diverge significantly.
Categories of divergence (seed list — full gap analysis is sprint Phase A):
-
A. Component-shape drift
- PerformanceBlock missing: genre tag, advancing progress bar, capacity-warning visibility
- PerformancePopover missing: avatar, time+duration display, multi-select status, advancing detail breakdown
- Wachtrij missing: multi-select status filter pills, status grouping with counts, grouping toggle, status/genre per item
-
B. Interaction drift
- Drag within timetable: not working
- Drag between Wachtrij and timetable: not working
- Click handling: inconsistent across blocks (some open popover, some don't)
- Resize handles: missing entirely
-
C. Logic drift / rendering
- Conflict detection: not visually surfacing (red border + warning icon)
- Lane stacking on overlap: behavior unverified against prototype
- Capacity warnings: rendering unverified
- Block rendered outside timetable bounds (Bert's screenshot 3)
-
D. AddPerformanceDialog two-mode behavior
- "Add via timetable click-drag" mode: should pre-fill stage/start/end/duration from drop target — currently always shows full form
- "Add via +Optreden button" mode: should only ask name/genre/status/duration and land in Wachtrij — currently asks stage/time/lane
For the complete 20-item itemization with severity ratings, see the Phase
A report in feat/timetable-stabilization finalization commit.
Wat:
- Sprint Phase A: line-by-line gap analysis of prototype HTML vs. current implementation, produces an exhaustive table (this seed list above is incomplete on purpose — full audit belongs in the sprint, not in this BACKLOG entry)
- Sprint Phase B: implement parity, prioritised by category and severity from Phase A
- Sprint Phase C: visual regression baselines locked in via TEST-VISUAL-001
- Sprint Phase D: manual prototype-side-by-side walkthrough by Bert before merge
Trigger: Direct na merge van TEST-INFRA-001 sprint. Sessie 5 (Engagement Detail) en alle volgende Artist-domain frontend sprints gates op merge van ART-S4-UX-PARITY.
Refs: Bert's screenshot report (chat thread on stabilization merge
readiness), dev-docs/audits/PROTOTYPE-AUDIT-ARTIST-TIMETABLE.md,
RFC-TIMETABLE D8/D17/D18/D19/D20/D21/D22/D25/D26.
ART-S4-TESTS — Session 4 test coverage closure ✅ Resolved
Status: Closed by the Session 4 follow-up branch
(commits 5c53dcd through 985a5ab).
VeeValidate removed (VEE-001). CSS tokens moved to .css for jsdom-time
loadability. mountWithVuexy helper + axe-core dev dep + segmented
vitest configs landed. Zod runtime parsing wired into all timetable
queries + mutations with regression tests. ?day query is now the
source of truth via useActiveDay composable with corrective fallback
for missing / invalid / cross-org IDs. Component tests cover
PerformanceBlock visuals + interactions, StageRow lane stacking,
Wachtrij rendering + drag, AddPerformanceDialog validation + submit.
useTimetableMutations 409 + idempotency-key semantics tested.
Keyboard a11y model fully covered (RFC D20). axe-core scans clean on
the user-facing surfaces (two real bugs surfaced + fixed inline:
VProgressLinear missing aria-label, dialog close button missing
aria-label). Full add → drag → resize → park → delete integration flow
verified through the mutation composable.
Test count delta: 252 → 385 (+133 across the two PRs).
Follow-up sprints: TEST-INFRA-001 (Playwright CT migration), TEST-CONTRACT-001 (real-backend 409), TEST-VISUAL-001 (visual regression).