Files
crewli/dev-docs/ARCH-CONSOLIDATION-2026-04.md
bert.hausmans 5512e22f2b docs: WS-3 session 1b-iii complete — baseline 32 → 1
ARCH-CONSOLIDATION-2026-04.md §6.8: session 1b-iii recorded. Three
restpunten from 1b-ii resolved:
- indent SwitchCase: 1 (24 items in useTimeSlotDropdown.ts)
- lines-around-comment per-*.vue override (3 items in
  PortalLayout/PublicLayout/AppKpiCard)
- axios.ts async/await rewrite (2 promise/no-promise-in-callback
  warnings on lines 61, 73)

Lint baseline: 32 → 1. The remaining 1 item is a pre-existing
sonarjs/no-collapsible-if at useImpersonationStore.ts:103 — was
already in the 32 baseline (not specifically called out in 1b-iii's
three planned tasks per scope rules).

WS-3 lint cleanup workstream complete; session 1c
(eslint-plugin-boundaries) can proceed on a clean baseline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 18:49:46 +02:00

28 KiB

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:

// 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

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 <script>-tag-aangrenzende comments het rule niet meer triggeren (resolveert 3 items in PortalLayout/PublicLayout/AppKpiCard, lost het 1b-ii-empirisch geconstateerde conflict met vue/block-tag-newline op); (3) axios response interceptor herschreven van error => { void import(...).then(...) } naar async error => { ... await import(...) } — semantisch identiek (de 2 sites navigeren beide via window.location.href weg) maar voldoet wel aan promise/no-promise-in-callback. Build smoke groen (12.13s). Baseline ging van 32 → 1. Het laatste item is een pre-existing sonarjs/no-collapsible-if op useImpersonationStore.ts:103 — niet in scope van 1b-iii's drie geplande wijzigingen, doorschuiven naar follow-up. WS-3 lint cleanup workstream effectief afgerond; sessie 1c (eslint-plugin-boundaries) kan starten op een schone baseline.

Klaar-criteria:

  • apps/portal/ is verwijderd
  • Beide hostnames serveren correcte route-tree
  • Auth werkt op beide hostnames met isolatie
  • Alle bestaande tests in apps/app/ draaien groen
  • Nieuwe tests voor hostname-detectie
  • Deploy-config (DirectAdmin) verifieert beide hostnames

7. Starten van de volgende chat

Deze chat wordt afgesloten. De volgende chat start met een dichte briefing die exact dit document en de volgende meta-instructie als context geeft.

Opening-prompt voor de volgende chat (kopieer letterlijk):

Nieuwe chat in project Crewli. We beginnen de architectuur-consolidatie-sprint
zoals vastgelegd in /dev-docs/ARCH-CONSOLIDATION-2026-04.md (lees dat document
eerst uit Gitea, main branch).

Context:
- Pre-launch. Geen gebruikers, geen tijdsdruk. Doel: enterprise-grade fundament.
- 8 werkstromen (WS-1 t/m WS-8). We starten met WS-1: opsporings-pas.
- Ik wil dat je ook /CLAUDE.md en /dev-docs/SCHEMA.md leest voor basiscontext,
  en kort aangeeft wat je hebt begrepen.

Verzoek voor deze eerste chat:
1. Bevestig dat je het consolidatie-document en CLAUDE.md hebt gelezen.
2. Lever de prompt voor WS-1 (opsporings-pas). Deze moet een Claude Code opdracht
   zijn die het hele codebase doorloopt op:
   - Primary keys die geen ULID zijn
   - JSON-kolommen met queryable data die niet in §6 zijn benoemd
   - Polymorfe relaties die inconsistent gemodelleerd zijn
   - Overige architectuur-smells die we in de consolidatie moeten meenemen
3. Output van WS-1 is een rapport (Markdown), geen code. Op basis daarvan
   beslissen we of de scope van andere werkstromen aanpassing behoeft.

Regels die tijdens de sprint gelden:
- Elke nieuwe ingeving: toets aan de drie valstrikken in §2 van het
  consolidatie-document. Bij twijfel: backlog.
- Elke architectuur-keuze tijdens executie: toets aan de principes in §1.
- Geen scope-uitbreiding zonder expliciete go van mij.

Bevestig dat je klaar bent voor WS-1.

8. Verwijzingen naar bestaand materiaal

Voor de volgende chat is het nuttig dat deze documenten als achtergrond beschikbaar zijn (niet allemaal lezen, maar weten dat ze bestaan):

  • /CLAUDE.md — projectafspraken, zero-compromise regels
  • /dev-docs/SCHEMA.md v2.1 — huidig database-schema
  • /dev-docs/ARCH-FORM-BUILDER.md — bestaande form-builder architectuur
  • /dev-docs/AUTH_ARCHITECTURE.md — dual-mode auth, cross-app isolation
  • /dev-docs/VUEXY_COMPONENTS.md — frontend component-bibliotheek
  • /dev-docs/BACKLOG.md — bestaande backlog, wordt opgeschoond in WS-8

9. Openstaand (niet in scope van consolidatie-sprint)

Om expliciet te zijn over wat niet in deze sprint gebeurt:

  • Implementatie van de 4 kern-workflows (komt erna als aparte sprint S3c...S3f)
  • Nieuwe UI-modules (komt erna)
  • Marketplace / subscription / billing (toekomst)
  • Mobile app (toekomst)
  • Locale-uitbreiding voorbij NL/EN (komt wanneer marktbehoefte concreet is)
  • Performance-optimalisatie voorbij observability foundation (wanneer metrics dit rechtvaardigen)

10. Sign-off ruimte

Voor Bert om te bevestigen voordat we de volgende chat starten:

  • Leidende principes §1 akkoord
  • Vastgelegde besluiten §3 akkoord
  • SPA-consolidatie §4 akkoord
  • Werkstroom-volgorde §5 akkoord
  • Purposes v1.0 lijst §6.4 akkoord
  • Openings-prompt §7 klaar om te gebruiken

Na jouw sign-off commit ik dit document naar de repo en sluiten we deze chat.