# Crewli — Architectuur Consolidatie Sprint (april 2026) > **Status:** briefing document, vertrekpunt van de consolidatie-sprint. > **Doelgroep:** Bert (product owner + solo dev), Claude (architect/PM rol in chat), Claude Code (executie). > **Positie t.o.v. andere docs:** dit document is tijdelijk. Na afronding van de sprint wordt > de inhoud gedistribueerd over `/CLAUDE.md`, `/dev-docs/SCHEMA.md`, `/dev-docs/ARCH-*.md` en > `/dev-docs/dev-guide.md`. Tijdens de sprint is het de enkele bron van waarheid voor wát > we aan het doen zijn en waaróm. --- ## 0. Context in één alinea Crewli is een multi-tenant SaaS-platform voor festival- en event-personeelsbeheer (vrijwilligers, crew, artiesten, leveranciers, pers, gasten). Pre-launch. Geen gebruikers, geen live data, geen tijdsdruk. Ambitie: een enterprise-grade product dat binnen Stichting Echt Feesten gebruikt wordt en daarna bredere markt op kan. Het fundament is vandaag op ~80% — functioneel grotendeels aanwezig, architecturaal op sommige plekken een compromis dat onder productie-druk had moeten standhouden. Dit document legt vast hoe we die laatste 20% invullen voordat we aan de 4 kern-workflows (EVENT_REGISTRATION, ARTIST_ADVANCE, SUPPLIER_INTAKE en één generiek type) beginnen. --- ## 1. Leidende principes voor de sprint Deze principes zijn toetssteen voor elke beslissing tijdens de consolidatie. Bij twijfel tijdens executie: teruglezen, toetsen, beslissen. **P1 — Enterprise betekent discipline in scope, niet afwezigheid van scope.** We bouwen een fundament dat 23 denkbare purposes aan kan, maar we implementeren er nu 4. De rest is vocabulaire, geen functionaliteit. Elke feature die niet onmisbaar is voor het fundament of voor de 4 kern-workflows wordt op de backlog gezet met duidelijke trigger-conditie ("als X, dan bouwen we Y"). **P2 — Queryable data krijgt eigen kolommen of tabellen. JSON is uitsluitend voor echt opaque config.** Herinterpretatie van het bestaande principe: strenger toepassen. Validatieregels, conditional logic, field options, bindings — allemaal eigen tabellen. `settings` JSON krimpt fors. **P3 — ULID overal. Zonder uitzonderingen.** Ook pivot-tabellen. Ook logging-tabellen. Consistent = onderhoudbaar = veilig. **P4 — Polymorfe relaties zijn een bewust architectuur-patroon, geen kludge.** `form_schemas.owner_type/owner_id` en `form_submissions.subject_type/subject_id` blijven polymorf. Nieuwe polymorfe relaties voegen we toe waar het domein dit rechtvaardigt (niet speculatief). **P5 — Eén SPA, twee hostnames.** Geen twee SPAs. Geen drie. Herconsolidatie van `apps/portal/` in `apps/app/` met domein-gebaseerde routing. Zie §4. **P6 — End-to-end-werkend hoofdpad voor fancy features.** FormBindingApplicator + Person-creation + entity-updates zijn eerste-klas fundament, niet een addendum. Geen enkele fancy feature (conditional logic, webhooks, delegation) mag afgerond heten zolang de basis-pipeline niet volledig gebouwd en getest is. **P7 — Observability is fundament, geen nice-to-have.** Een enterprise-product zonder gestructureerde logging, error tracking en performance monitoring is geen enterprise-product. Deze laag wordt in de sprint neergezet. **P8 — Documentatie loopt synchroon met code.** Elke sprint-PR wijzigt of creëert dev-docs. Geen "docs volgen later". Dit is al praktijk bij Crewli en blijft zo. --- ## 2. De drie valstrikken, en hoe we ze tegenhouden De grote risico's van een open-ended consolidatie-sprint zijn bekend. Expliciet benoemen zodat we ze kunnen herkennen als ze zich voordoen. **Valstrik 1 — Scope-creep tijdens refactor.** "We zijn toch bezig in deze code, laten we ook X meenemen." Weerstand: X komt op `/dev-docs/BACKLOG.md` met trigger-conditie, wordt niet mee-genomen. Enige uitzondering: X is een directe blocker voor het huidige consolidatie-werkpakket. **Valstrik 2 — Gold-plating.** Iets verder perfectioneren dan nodig. Elke refactor-actie moet een concrete trigger hebben: "dit veroorzaakt drift in Y" of "dit blokkeert feature Z" of "dit is een security-risico". "Dit kan eleganter" is geen geldige trigger. **Valstrik 3 — Feature-sprawl verkapt als fundering.** "Als we toch bezig zijn met bindings, laten we ook een binding-designer-UI bouwen." Nee. Fundering = datamodel + services + tests. Features komen daarna, in de 4-workflow-build. **Bij elke nieuwe ingeving tijdens de sprint stelt Claude de drie vragen:** 1. Is dit een scope-uitbreiding? → backlog 2. Is dit elegantie-verbetering zonder concrete trigger? → niet doen 3. Is dit een feature verkleed als fundering? → naar na de consolidatie --- ## 3. Vastgelegde besluiten (niet ter discussie in deze sprint) Deze beslissingen zijn genomen en worden *niet* opnieuw geopend. Voor volgende chats: behandel deze als gegeven. 1. **Binding als eerste-klas burger.** Aparte `form_field_bindings` tabel met typed kolommen: `field_id`, `target_entity`, `target_attribute`, `merge_strategy`, `trust_level`. Eén veld kan meerdere bindings hebben (multi-binding scenarios). Details in §6.1. 2. **FormBindingApplicator als hoofdpad.** Nieuwe service die na `FormSubmissionSubmitted` bindings doorloopt en entities bijwerkt. Inclusief automatische Person-creation bij public EVENT_REGISTRATION submissions zonder subject. Details in §6.2. 3. **Denormalized columns op `form_submissions`.** `event_id`, `organisation_id` en `submitted_by_user_id` zijn al aanwezig of worden toegevoegd als directe foreign keys, niet alleen via schema-joins. Details in §6.3. 4. **Purpose registry in config, geen `custom` escape.** Afgeslankte set van 7 purposes voor v1.0: EVENT_REGISTRATION, ARTIST_ADVANCE, SUPPLIER_INTAKE, POST_EVENT_EVALUATION, INCIDENT_REPORT, SIGNATURE_CONTRACT, USER_PROFILE. Registry leeft in `config/form_builder/purposes.php` + typed value-objects. Nieuwe purposes vereisen een migratie + listener — dat is correct, niet lastig. Details in §6.4. 5. **ULID consistent overal, ook pivots.** Bestaande integer-PK-pivots worden gemigreerd. Opsporings-pas in week 1 van de sprint. Details in §6.5. 6. **Eigen tabellen voor `validation_rules`, `conditional_logic`, `form_fields.options`.** Typed, queryable. `form_schemas.settings` krimpt naar echt opaque config (rendering hints). Details in §6.6. 7. **Eén SPA op één domein.** `apps/portal/` wordt een route-tree binnen `apps/app/`. Alles op `crewli.app`. Route-guards bepalen welke layout, welke context, welke permissies. Multi-role users krijgen een context- switcher in de nav-balk. Details in §4. 8. **Observability foundation.** Sentry voor error tracking, structured logging via Laravel's context, performance monitoring via Laravel Telescope (dev) + Sentry Performance (prod). Details in §6.7. 9. **Enterprise scope betekent 4 kern-workflows voor v1.0.** Niet 23. De overige purposes in de registry zijn vocabulaire zonder implementatie; zij krijgen implementatie wanneer een concrete klant-case dit rechtvaardigt. --- ## 4. Het één-SPA besluit: uitvoerbaar overzicht Gekozen architectuur: **één codebase, één build, één domein.** ### 4.1 Hostnames - `crewli.app` — alle gebruikers, alle rollen, één SPA - `api.crewli.app` — Laravel backend (ongewijzigd) - `docs.crewli.app` — VitePress docs (ongewijzigd) Rationale: vrijwilligers en organizers zijn verschillende rollen binnen hetzelfde product, niet verschillende producten op hetzelfde platform. Dit volgt het patroon van Slack/Linear/Notion/GitHub, waar alle gebruikers onder één domein werken en de UI zich aanpast aan rol + context. Subdomeinen per rol (zoals Shopify/Stripe) worden pas zinvol wanneer er fundamenteel verschillende producten zijn — niet ons geval. ### 4.2 Directory-structuur na consolidatie ``` apps/ app/ ← enige Vue SPA src/ pages/ (auth)/ ← login, forgot-password, MFA portal/ ← wat nu in apps/portal/src/pages/ zit my-shifts/ availability/ profile/ ... register/ ← publieke formulier-flow (niet ingelogd) [public_token].vue events/ ← organizer: events + sub-pages persons/ ← organizer: persons overview organisations/ ← organizer: settings platform/ ← super-admin (role:super_admin) index.vue ← post-login landing of context-switcher components/ organizer/ ← organizer-only componenten portal/ ← portal-only componenten shared/ ← gedeeld tussen beide composables/ forms/ ← voormalige packages/form-schema/ komt hier ... layouts/ OrganizerLayout.vue ← voor organizer routes, met organizer-nav PortalLayout.vue ← voor portal routes, met portal-nav PublicLayout.vue ← voor /register/* routes, minimaal router/ index.ts ← route-guards bepalen layout + toegang routes.ts ← één route-tree, layout per section plugins/ theme.ts ← context-based Vuetify theme injectie portal/ ← verdwijnt na sprint api/ ← ongewijzigd packages/ form-schema/ ← verdwijnt, inhoud verhuist naar apps/app/src/composables/forms/ ``` ### 4.3 Routing en context-switching Er is geen hostname-detectie. Routes bepalen alles: ```ts // Conceptueel — definitieve implementatie in sprint const routes = [ // Publiek (geen auth) { path: '/login', component: LoginPage, meta: { layout: 'PublicLayout' } }, { path: '/register/:publicToken', component: PublicFormPage, meta: { layout: 'PublicLayout' } }, { path: '/forgot-password', component: ForgotPasswordPage, meta: { layout: 'PublicLayout' } }, // Portal routes (auth required, any role) { path: '/portal', meta: { layout: 'PortalLayout', requiresAuth: true, context: 'portal' }, children: [ { path: 'my-shifts', component: MyShiftsPage }, { path: 'availability', component: AvailabilityPage }, { path: 'profile', component: ProfilePage }, ], }, // Organizer routes (auth + organizer role) { path: '/', meta: { layout: 'OrganizerLayout', requiresAuth: true, context: 'organizer', requiresRole: 'organizer' }, children: [ { path: '', component: OrganizerDashboard }, { path: 'events', component: EventsList }, // ... etc ], }, // Platform admin (auth + super_admin role) { path: '/platform', meta: { layout: 'OrganizerLayout', requiresAuth: true, requiresRole: 'super_admin' }, children: [...] }, ] ``` **Post-login landing voor multi-role gebruikers:** - Enkel portal-rol → redirect naar `/portal/my-shifts` - Enkel organizer-rol → redirect naar `/` (dashboard) - Beide rollen → landing-page op `/` met twee kaarten (Portal / Organizer) óf direct naar laatst-gebruikte context (lokaal opgeslagen als preference) **Context-switcher in nav-balk** voor multi-role users: - Permanent zichtbaar in OrganizerLayout en PortalLayout - Eén klik switch naar andere context - Vergelijkbaar met Slack's workspace-switcher ### 4.4 Wat blijft werken ongewijzigd - Sanctum session auth (één cookie, één domein) — simpeler dan eerst gedacht - Portal-token-auth voor publieke routes (`/register/{public_token}`, `/public/forms/...` API-endpoints) — blijft als aparte middleware - Impersonation — blijft binnen organizer-tree - API-calls naar `api.crewli.app` — ongewijzigd **Wat wegvalt door één-domein-keuze:** cross-domain cookie isolation is niet meer nodig. De `CookieBearerToken`-middleware per app (uit AUTH_ARCHITECTURE.md) mag vereenvoudigd worden in de sprint — dit wordt meegenomen onder WS-3. ### 4.5 Wat dit kost Reële inschatting: 2-4 dagen werk in de consolidatie-sprint. - 1 dag: directory-structuur omzetten, imports repareren - 1 dag: router + layouts + route-guards + context-switcher - 1 dag: theming per context, Vuetify-config, auth-cleanup - 1 dag: testen, deploy-config verifiëren Iets simpeler dan bij het hostname-alternatief omdat we geen hostname-detector hoeven te bouwen en omdat cross-domain cookie complexiteit vervalt. ### 4.6 Toekomst-deur openhouden Als later één van deze drie scenario's optreedt, kunnen we alsnog splitsen zonder dat we vandaag te vroeg gesplitst hadden: - Portal wil SSR voor SEO → portal splitsen naar Nuxt op aparte subdomain - Een third product (marketplace, kassa-app) verschijnt → apart project op eigen subdomain - Whitelabel per organisatie met eigen subdomain → routing-laag erboven Geen van deze scenario's is vandaag aan de orde. Splitsen vanuit één SPA is goed te doen wanneer nodig (één tot twee weken werk). --- ## 5. Prioriteitsvolgorde werkstromen De consolidatie-sprint kent **acht werkstromen** (afgekort WS). Ze worden in deze volgorde uitgevoerd, met waar mogelijk parallelle PRs binnen een werkstroom. **WS-1 — Opsporings-pas (1 dag)** Volledige scan van de codebase op: niet-ULID primary keys, JSON-kolommen met queryable data die ik nog niet geïdentificeerd heb, polymorfe relaties die inconsistent zijn. Output: een definitieve lijst van te migreren items. Gaat voor alle andere werkstromen, omdat latere WS-en hun scope ervan afhangen. **WS-2 — Purpose registry + legacy purpose-cleanup (2-3 dagen)** Van 23 naar 7 purposes. Registry in config. Typed value-objects. Migratie die bestaande `form_schemas` opschoont (alle schemas met "weg te halen" purposes → archief of delete, afhankelijk van of er seed-data hangt). **WS-3 — Eén SPA consolidatie (3-5 dagen)** Zoals in §4. Kan deels parallel aan WS-4 en WS-5 want raakt vooral frontend. Apart maken van frontend-consolidatie en backend-consolidatie vermijdt rebase-hel. **WS-4 — ULID-consistentie + denormalized submission columns (2-3 dagen)** Pivot-tabellen naar ULID. `event_id`, `organisation_id` op submissions (en elders waar de opsporings-pas ze vindt). **WS-5 — JSON-kolom-opsplitsing (4-6 dagen)** Vier sub-werkstromen die in serie moeten (elk heeft data-migratie): - WS-5a — `form_field_bindings` tabel - WS-5b — `form_field_validation_rules` tabel - WS-5c — `form_field_conditional_logic` tabel - WS-5d — `form_field_options` tabel **WS-6 — Binding-pipeline / FormBindingApplicator (4-5 dagen)** Nieuwe service. Listener. Person-creation-flow. Guardrails in FormSchemaService. Volledige testdekking voor alle 7 purposes (ook als ze nog geen functionaliteit hebben — tests verifiëren dat de binding- infrastructuur ze ondersteunt). **WS-7 — Observability foundation (2-3 dagen)** Sentry SDK (backend + frontend). Structured logging conventies. Telescope voor dev. Performance monitoring. Runbook voor incident response. **WS-8 — Documentatie-consolidatie (2-3 dagen)** `/CLAUDE.md` bijwerken met nieuwe conventies. `/dev-docs/SCHEMA.md` volledige rewrite naar v2.0 met nieuwe tabellen. `/dev-docs/ARCH-FORM-BUILDER.md` bijwerken. Nieuwe `/dev-docs/ARCH-BINDINGS.md`. Nieuwe `/dev-docs/ARCH-OBSERVABILITY.md`. `/dev-docs/BACKLOG.md` opschonen: verwijderen wat door de sprint is opgelost, expliciet markeren wat na de sprint nog wordt opgepakt. **Totale inschatting sprint:** 22-32 dagen werk. **Doorlooptijd bij actieve inzet:** 4-6 weken. **Aantal PRs:** 25-35, veel zijn klein en focused. --- ## 6. Per werkstroom: scope, uitkomst, klaar-criteria ### 6.1 WS-5a — form_field_bindings **Scope:** Nieuwe tabel `form_field_bindings`. Vrije `binding` JSON-kolom op `form_fields` wordt vervangen door een queryable relatie. Eén veld kan meerdere bindings hebben. **Tabel-ontwerp (concept, finetunen in sprint):** ``` form_field_bindings: id (ulid, primary) form_field_id (ulid, foreign, cascade) target_entity (string, 50) -- 'person', 'artist', 'company', 'user' target_attribute (string, 100) -- 'email', 'first_name', 'stage_name', etc. merge_strategy (enum) -- 'overwrite', 'append', 'replace', 'first_write_wins' trust_level (int, default 50) -- 0-100, hoger = dominanter bij conflict is_identity_key (bool, default false)-- wordt gebruikt voor person-matching? created_at, updated_at unique (form_field_id, target_entity, target_attribute) ``` **Uitkomst:** - Bestaande `binding` JSON-data gemigreerd naar rijen in deze tabel - Builder-UI (later) kan een dropdown tonen gevuld uit binding-registry - Backend kan queryen "welke velden binden aan `person.email`" - Conflict-strategie per binding expliciet **Klaar-criteria:** - Migratie loopt vooruit én terug zonder data-verlies - Feature tests: schema create → binding toevoegen → submission submit → entity updated - `form_fields.binding` JSON-kolom verdwenen - ARCH-BINDINGS.md geschreven met complete uitleg ### 6.2 WS-6 — FormBindingApplicator **Scope:** Nieuwe service `App\Services\FormBuilder\FormBindingApplicator`. Nieuwe listener `App\Listeners\FormBuilder\ApplyBindingsOnFormSubmit`. Person-creation- helper. Update van `FormSchemaService` met pre-publish-validation. **Uitkomst:** - Bij elke `FormSubmissionSubmitted` worden alle bindings doorlopen - Voor elke binding: lookup value in submission, haal merge-strategie op, update target entity - Bij public EVENT_REGISTRATION zonder subject: creëer Person, zet als subject op submission, voer dan bindings uit - Identity-match draait na binding-applicatie op nieuwe Person - Alle actions gelogged in activity log met bron-submission **Klaar-criteria:** - Test: public registratie → Person aangemaakt met alle binding-attributen - Test: authenticated update-submission → bestaande Person bijgewerkt - Test: conflict (twee submissions, zelfde email-binding, verschillende waarde) → last-write-wins met activity log entry - Test: tag-picker additief (niet overschrijvend) - Test: schema met AVAILABILITY_PICKER zonder event-koppeling kan niet publiceren - Test: EVENT_REGISTRATION schema zonder email-binding kan niet publiceren **Status:** Complete (verified 2026-05-04). All three sessions landed in main; backend test count 1486, exceeds RFC §8 target of 1445-1465. See dev-docs/WS-6-COMPLETE-VERIFICATION-2026-05-04.md for verification details. ### 6.3 WS-4 — Denormalized submission columns **Scope:** Kolommen `event_id` (nullable), `organisation_id` (niet-nullable), `submitted_by_user_id` (bestaat al) op `form_submissions`. Observer vult ze bij create. Backfill-script voor bestaande submissies (indien aanwezig in seed-data). **Uitkomst:** - Queries zoals "alle submissions voor event X" worden directe queries, geen joins - Analytics / reporting kan efficiënt draaien - Multi-event reuse scenario's (ARTIST_ADVANCE) werken: één schema, submissies naar meerdere events **Klaar-criteria:** - Feature tests: submission krijgt correcte event_id en organisation_id - Scope-middleware gebruikt event_id direct (waar relevant) - SCHEMA.md bijgewerkt ### 6.4 WS-2 — Purpose registry **Scope:** `app/FormBuilder/Purposes/PurposeDefinition.php` (value object). `config/form_builder/purposes.php` (registry). `PurposeRegistry` service. `form_schemas.purpose` blijft een string-kolom, maar `custom_purpose_slug` kolom verdwijnt. **Purposes v1.0:** 1. `event_registration` — vrijwilligers/crew aanmelden, subject=Person 2. `artist_advance` — artiesten advance invullen, subject=Artist 3. `supplier_intake` — leveranciers onboarding, subject=Company 4. `post_event_evaluation` — feedback na afloop, subject=Person 5. `incident_report` — incidenten melden, subject=Person 6. `signature_contract` — contracten ondertekenen, subject=User 7. `user_profile` — profiel-updates, subject=User **Uitkomst:** - Purposes zijn data-driven: je kunt in config een nieuwe toevoegen zonder enum-migratie, maar je moet wel bewust zijn dat een nieuwe purpose meestal een listener vereist - Elke PurposeDefinition heeft: label, subject_type, default_submission_mode, allows_public_access, required_bindings (lijst van bindings die het schema moet hebben om publiceerbaar te zijn) **Klaar-criteria:** - Tests voor elke purpose: kan schema gecreëerd worden, kan gepubliceerd worden (met juiste bindings), submit-flow werkt end-to-end - ARCH-FORM-BUILDER.md bijgewerkt ### 6.5 WS-4 — ULID-consistentie **Scope:** Alle pivot-tabellen met integer PK → ULID. Opsporings-pas (WS-1) definieert de exacte lijst. Data-migratie per tabel. **Uitkomst:** Consistent ID-gebruik. Geen ID-enumeration-risico meer. Eenvormig test-patroon. **Klaar-criteria:** - Alle primary keys in business-schema zijn ULID - Alle foreign keys consistent - Tests lopen door ### 6.6 WS-5b/c/d — Remaining JSON-splits Dezelfde aanpak als WS-5a maar voor: - `form_field_validation_rules` (targeting `validation_rules` JSON) - `form_field_conditional_logic` (targeting `conditional_logic` JSON, met AND/OR tree structuur) - `form_field_options` (targeting `options` JSON, met position + label + metadata per optie) **Conditional logic verdient speciale aandacht.** AND/OR trees in een relationele tabel zijn niet triviaal. Overweging in de sprint: platte lijst met `logical_group_id` vs recursieve tabel met `parent_id`. Beslissing tijdens WS-5c, niet vooruit. ### 6.7 WS-7 — Observability **Scope:** - Sentry SDK installeren in `apps/api/` (Laravel) en `apps/app/` (Vue) - Structured logging convention: alle logs via `Log::channel('structured')` met context (organisation_id, user_id, request_id) - Laravel Telescope in dev only - Sentry Performance voor transaction tracking in prod - Runbook in `dev-docs/RUNBOOK.md` met: hoe te reageren op alerts, waar logs te vinden, hoe een error te tracen **Klaar-criteria:** - Test: geforceerde exception in een controller verschijnt in Sentry (staging) - Test: elke request heeft een unique request_id in logs - Telescope werkt in dev, niet in prod ### 6.8 WS-3 — Eén SPA consolidatie Zie §4 voor scope en stappen. **Voortgang:** - **Sessie 1a (2026-04-29)** — _foundation gap closure, klaar._ Lefthook geïnstalleerd op repo-root; `.githooks/post-commit` en `pre-push` 1:1 gemigreerd naar `lefthook.yml` (scripts blijven source of truth, lefthook dispatcht). Drie layout-skeletons toegevoegd in `apps/app/src/layouts/`: `OrganizerLayout.vue`, `PortalLayout.vue`, `PublicLayout.vue` — nog niet aan router gekoppeld. Vitest-smoketests dekken alle drie (8 nieuwe tests, 41 → 49 in apps/app). ESLint-baseline en router-wiring volgen in 1b/1c. - **Sessie 1b-i (2026-04-29)** — _lint baseline reductie + audit, klaar._ Risk-tiered `eslint --fix` pass: Tier 1 (Vue templates) + Tier 2 (TypeScript plumbing) + trailing-whitespace cleanup. De baseline ging van **1451 → 231** problemen. Tier 3 (vite.config.ts, themeConfig.ts, vitest.config.ts) is bewust uitgesteld naar 1b-ii vanwege runtime-risico van quote/semi/sort-imports autofixes op config bestanden. Audit rapport met 6-bucket categorisatie van de resterende 231 items in `dev-docs/WS-3-LINT-BASELINE-AUDIT-2026-04-29.md`. Vijf open vragen voor Bert + Claude Chat staan klaar voor 1b-ii. - **Sessie 1b-ii (2026-04-29)** — _lint baseline cleanup execution, klaar._ Alle 16 actiepunten uit het 1b-i audit-rapport uitgevoerd volgens de Q1-Q5 dispositions. Baseline ging van **231 → 32** problemen (30 errors, 2 warnings). `pnpm lint` is ontkoppeld van `--fix` (`pnpm lint:fix` is nu de expliciete autofix-variant). Tier 3 config files met succes hand-gereviewed en gefixt (vite/themeConfig/vitest; build smoke groen). `src/@core/**` en `src/@layouts/**` toegevoegd aan ignorePatterns (vendored Vuexy). Twee bekende restpunten voor follow-up: 24 indent items in `useTimeSlotDropdown.ts` (rule-config conflict tussen `indent` rule's default `SwitchCase: 0` en de codebase's `SwitchCase: 1` style), en 2 promise/no-promise-in-callback warnings in `lib/axios.ts:61,73` (Q4's `void` prefix recipe blijkt empirisch de rule niet te kalmeren — vraag voor 1b-iii). - **Sessie 1b-iii (2026-04-29)** — _lint baseline mop-up, klaar._ Drie restpunten uit 1b-ii afgesloten via twee `.eslintrc.cjs` tweaks en één axios refactor: (1) `indent` rule krijgt `{ SwitchCase: 1 }` om de codebase-style te matchen (resolveert 24 items in useTimeSlotDropdown.ts in één config-regel, geen code-rewrite nodig); (2) per-`*.vue` override op `lines-around-comment` met `beforeBlockComment: false` + `beforeLineComment: false` zodat SFC `