Commit Graph

175 Commits

Author SHA1 Message Date
5771a678ef chore: install ts-reset in both portal and app SPAs
Installs @total-typescript/ts-reset 0.6.1 as a dev-dependency in
apps/portal/ and apps/app/. Patches TypeScript's loosest default
types: Array.filter(Boolean) returns non-nullable, JSON.parse
returns unknown, fetch().json() returns unknown, Map.get() strict,
etc.

Configuration: src/reset.d.ts in each SPA imports the reset. Both
tsconfig.json files already include ./src/**/* so the .d.ts is
picked up automatically — no tsconfig edits needed.

Issues surfaced during install:
  - apps/app — 0 pre-install tsc errors in own code; install
    surfaced 2 errors in src/stores/useImpersonationStore.ts
    (both from JSON.parse on sessionStorage content returning
    unknown instead of any). Fixed inline at lines 19 + 123 via
    `as ImpersonationState` casts that make the existing
    trust-in-sessionStorage explicit. Backlog entry
    TECH-TS-IMPERSONATION tracks proper runtime shape validation.
  - apps/portal — 22 pre-existing tsc errors in own code (mostly
    tiptap editor components — tracked as TECH-TS-PORTAL-TSC,
    unrelated to ts-reset). Zero new errors in portal's own code.
    4 additional errors surfaced in tiptap's uncompiled node_modules
    .ts sources (third-party); left as-is.

Neither SPA achieves `tsc --noEmit` clean today — pre-existing
state unrelated to this work package. Build + vitest are the
actual working gates and both remain green:
  - apps/portal: vitest 113/113 passing; production build succeeds
  - apps/app:    (no vitest setup — tracked as TECH-APP-VITEST);
                 production build succeeds

Documentation: /dev-docs/FRONTEND-TOOLING.md added; CLAUDE.md
quality-gates updated.

Backlog: TECH-TS-IMPERSONATION (runtime validation of stored
impersonation state), TECH-TS-PORTAL-TSC (pre-existing portal tsc
errors), TECH-APP-VITEST (Vitest coverage for apps/app).

No production behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:58:11 +02:00
a043b88bc0 chore: install rector with accept-current-state baseline
Installs rector/rector ^2.0 (v2.4.2) + driftingly/rector-laravel
^2.0 as dev-dependencies. Configures PHP 8.2 language sets + safe
quality rule sets (CODE_QUALITY, DEAD_CODE, EARLY_RETURN,
TYPE_DECLARATION, PRIVATIZATION) + Laravel-specific sets
(LARAVEL_CODE_QUALITY, LARAVEL_COLLECTION).

Dry-run baseline: 487 rule-applications across 357 files. NO
changes applied in this commit — adoption is incremental via per-
set sprints documented in BACKLOG.md.

Top rules by volume:
  103  AddClosureVoidReturnTypeWhereNoReturnRector
   71  AddArrowFunctionReturnTypeRector
   51  AppToResolveRector
   34  ConvertStaticToSelfRector
   27  ReadOnlyClassRector
   18  NullToStrictStringFuncCallArgRector
   16  ReturnBinaryOrToEarlyReturnRector
   16  MakeModelAttributesAndScopesProtectedRector
   13  RemoveUnusedVariableAssignRector
   13  OptionalToNullsafeOperatorRector
   13  FlipTypeControlToUseExclusiveTypeRector

Composer scripts:
  - composer rector              — DRY-RUN (default)
  - composer rector:apply        — apply changes
  - composer rector:clear-cache  — clear Rector cache

Dry-run exits with code 2 when suggestions exist (Rector convention,
not an error state). Apply-mode exits 0 on clean runs.

Documentation: /dev-docs/RECTOR.md added; CLAUDE.md updated.

Backlog: per-set application sprints seeded
(TECH-RECTOR-01..05 + TECH-RECTOR-CI). DEAD_CODE (smallest scope)
and TYPE_DECLARATION (biggest volume, will help reduce Larastan
baseline) are the natural first two.

Disruptive sets deliberately deferred:
  - LaravelLevelSetList::UP_TO_LARAVEL_* — broad bulk upgrades
  - SetList::NAMING — high-churn variable renames
  - SetList::INSTANCEOF — substantial logic changes

Memory limit 2G (dry-run completed within it).

No production behavior change. No code modified — Rector ran
dry-run only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:50:41 +02:00
7542808cab chore: install larastan at level 6 with accept-all baseline
Installs larastan/larastan ^3.0 (v3.9.6) as a dev-dependency. Level
6 is the starting target — catches missing typehints, method-
existence, null-safety, and model-property existence. Level 8
deferred to a follow-up sprint after level-6 baseline reaches zero.

Baseline error count at install: 1556 errors across 678 analysed
files (41 distinct identifiers).

Top 10 identifiers (errors / files):
  613 /  87  property.notFound
  289 /  52  missingType.generics
  154 /  31  argument.templateType
   98 /  61  missingType.iterableValue
   77 /  32  argument.type
   50 /  26  method.notFound
   35 /  35  method.childReturnType
   32 /   9  method.unresolvableReturnType
   31 /  10  assign.propertyType
   28 /  17  instanceof.alwaysTrue

Composer scripts:
  - composer analyse              — run static analysis
  - composer analyse:baseline     — regenerate baseline
  - composer analyse:clear-cache  — clear PHPStan result cache

Config deviation from plan: checkGenericClassInNonGenericObjectType
was removed in PHPStan 2.x (which Larastan 3 bundles) — setting
dropped from phpstan.neon, otherwise config matches the work
package verbatim. Defaults cover the original intent.

Documentation: /dev-docs/LARASTAN.md added; CLAUDE.md quality-gates
section introduced (with PHPUnit + Pint + Larastan listed).

Backlog: /dev-docs/BACKLOG.md gets 10 per-identifier reduction
sprints (TECH-LARASTAN-01..10) seeded from the actual baseline top
categories, plus TECH-LARASTAN-CI and TECH-LARASTAN-L8 follow-ups.

Memory limit 2G (baseline generation completed within it).

No production behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:46:27 +02:00
e7c9482474 refactor(form-field): drop form_fields.options + form_field_library.options
Final WS-5d cleanup. The JSON columns that have been unread since
commit 3 are now physically dropped on both source tables. Their
canonical rich-shape lives in form_field_options, accessed
exclusively through the morphMany relation.

Defensive sweep: any lingering translations.{locale}.options key in
either source table's translations bag is stripped. Commit 2's
backfill should already have done so exhaustively; this is
belt-and-braces.

Rollback re-creates the columns as nullable JSON but leaves them
empty. Pair with commit 2's rollback to restore the pre-WS-5d data
shape on every owner row.

The commit-3 getOptionsAttribute accessor-bridge on FormField +
FormFieldLibrary is removed — Eloquent's getAttribute() resolution
now naturally falls through to the morphMany relation since there's
no underlying column to shadow it. New regression test
FormFieldOptionsAccessTest asserts $field->options resolves to an
Eloquent Collection of FormFieldOption instances and lazy-loads in
exactly 2 queries (1 parent + 1 lazy-load options) on a fresh fetch
without with() preload. Same trio for FormFieldLibrary.

Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new drop_form_field_options_json_columns migration on the
rollback stack.

Documentation:
  - SCHEMA.md v2.6: form_field_options table documented; options row
    removed from form_fields and form_field_library; morphMany
    relations updated; cross-references to ARCH-FORM-BUILDER §17.6
    and addendum §Q3 WS-5d Uitvoering added on both source-table
    docblocks.
  - ARCH-FORM-BUILDER.md v1.8: new §17.6 "Field options (relational)"
    mirrors the §17.4 / §17.5 relational-sibling structure with
    sub-sections 17.6.1 rationale, 17.6.2 table + catalogue, 17.6.3
    service / scope / cascade / activity log, 17.6.4 snapshot
    embedding, 17.6.5 external API contract. Existing Webhooks
    section renumbered from §17.6 to §17.7.
  - ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md: "Uitvoering — WS-5d
    (2026-04-27)" section added. Eight paragraphs covering the
    snapshot atomic rewrite, strict-fail backfill dispatch, dual
    activity-log emit, four-sibling base-class extraction warrant,
    commit 0 dead-code precondition, the temporary getOptionsAttribute
    accessor-bridge pattern (with reusability note for future
    JSON→relational refactors), the dev-seeder vergoedingstype RADIO
    normalisation (drift correction explicitly distinguished from the
    parallel apps/app RegistrationFieldTemplate description domain),
    and the WS-5 family completion note.
  - BACKLOG.md: FORM-BUILDER-LIBRARY-AUDIT-LOG entry extended to four
    services (adds library.options_replaced); new
    FORM-BUILDER-MORPH-SCOPE-BASE-CLASS entry added as the WS-5d
    follow-up now that all four concrete morph-scope siblings exist.

Tests: 1193 → 1208 green (+15 across commits 3+4+5; this commit alone:
+2 from the regression test).

This completes the WS-5 family.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:00:20 +02:00
9e181092fc docs(form-builder): WS-5c sign-off — SCHEMA v2.5 + ARCH v1.7 §8 + addendum Q3
SCHEMA v2.5:
- form_fields: conditional_logic row removed; cross-reference note
  added pointing at the two new tables and the addendum Q3 WS-5c
  Uitvoering (no library mirror).
- New sections: form_field_conditional_logic_groups (tree nodes,
  adjacency-list via parent_group_id) and
  form_field_conditional_logic_conditions (leaves; value JSON
  nullable for empty/not_empty). Both tables use the Q2 declarative
  FK-chain resolver via tenantScopeStrategy() — group chain 3 hops,
  condition chain 4 hops (fits the WS-5c-raised cap of 5).

ARCH v1.7 §8 restructured into sub-sections mirroring the §17.4 /
§17.5 pattern:
- 8.1 Tree structure (read-side contract)
- 8.2 Relational tables (column specs, cascade, scope)
- 8.3 Service boundary (logicFor/replaceLogic/toJsonShape/
  assertSpecsValid/assertNoCycles)
- 8.4 Operator catalogues (group + comparison)
- 8.5 Cycle detection (contract preserved, implementation moved)
- 8.6 Activity log (dual-events: field.updated +
  field.conditional_logic_replaced; FormField subject only)
- 8.7 Legacy JSON migration (strict dispatch, rollback reversible)

Addendum Q3 extended with "Uitvoering — WS-5c (2026-04-26)":
- No-library-mirror decision reaffirmed (simple FK, no morph)
- Two-table tree-structure rationale (groups + conditions semantic
  purity over single-table mixed-nullables)
- OrganisationScope cap raise 3 → 5, rationale: legitimate 4-hop
  conditions chain + headroom for future deeper trees without
  denormalising form_field_id onto conditions
- Cycle detection migrated to service, contract unchanged
- Snapshot + resource JSON contract byte-identical via toJsonShape
- Strict validator on save at FormRequest boundary
- Scope-sibling discipline: WS-5c adds two FK-chain models (not
  morph); base-class extraction still parked for WS-5d

Sign-off table: WS-5c afronding 2026-04-26 added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:09:12 +02:00
4fcff2367a docs(backlog): open FORM-BUILDER-LIBRARY-AUDIT-LOG — library-level activity-log gap surfaced during WS-5b review 2026-04-24 22:51:09 +02:00
d494478c08 feat(form-builder): form_field_configs relational table + non-validation key split + drop validation_rules JSON columns 2026-04-24 22:42:35 +02:00
9d2758a42c docs(form-builder): WS-5b partial sign-off — SCHEMA v2.3 + ARCH v1.5 §17.4 + addendum Q3 2026-04-24 22:30:17 +02:00
67bede2e49 docs(form-builder): WS-5a follow-up — §6.2 registry, §6.7 dual activity-log note, Q3 commit-3 caveat 2026-04-24 20:51:28 +02:00
60c3abbe26 docs(form-builder): WS-5a — SCHEMA v2.2 §3.5.12, ARCH v1.4 §6.7, addendum Q3 sign-off 2026-04-24 20:13:51 +02:00
fe2202d835 test(persons): verify SoftDeletes behaviour per WS-1 finding D-05
Pre-flight audit verified the implementation was already in place,
so this commit is test-only (as the task prompt allowed):
- api/database/migrations/2026_04_08_220000_create_persons_table.php:27
  already carries $table->softDeletes().
- api/app/Models/Person.php lines 18 + 24 already use the SoftDeletes trait.

No production code change required.

tests/Feature/Person/PersonSoftDeleteTest.php pins the documented
behaviour so WS-6 (FormBindingApplicator + ARTIST_ADVANCE /
EVENT_REGISTRATION workflows) can rely on it without a second
verification pass:
- delete() sets deleted_at and excludes the row from default queries
- Person::withTrashed()->find($id) returns the soft-deleted row
- Person::onlyTrashed()->count() reports accurately
- restore() clears deleted_at
- forceDelete() removes the row permanently

PersonPolicy was spot-checked alongside the audit; existing view/update/
delete methods do not invoke withTrashed() on controller resolution.
That's acceptable today — no caller relies on editing a trashed Person —
so AUTH_ARCHITECTURE.md is left unchanged. If a restore-workflow lands
in a future sprint, the controller binding will need to switch to
->withTrashed() and the behaviour deserves a dedicated doc paragraph
then.

Minor path note: the task prompt specified
tests/Feature/Persons/PersonSoftDeleteTest.php (plural), but the
existing convention in this repo is tests/Feature/Person/ (singular).
Matched the existing directory rather than introducing a singular/plural
split.

Also carries the BACKLOG DOC-04 entry Bert asked for during WS-4 review:
scripts/install-claude-sync-hooks.sh belongs in SETUP.md / onboarding
so new clones don't miss the post-commit sync hook.

5 tests / 10 new assertions. Full suite: 1005 passed (2716 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:10:37 +02:00
b688ec26f0 feat(scope): declarative FK-chain strategy for OrganisationScope, register on 14 models per addendum Q2 + D-03/D-04
Refactors OrganisationScope to support a declarative, recursive FK-chain
resolver and registers the scope on 14 models that previously relied on
caller-discipline for tenant isolation.

Scope resolver (app/Models/Scopes/OrganisationScope.php):
Models now declare their strategy via:

    public static function tenantScopeStrategy(): array
    {
        return ['column' => 'organisation_id'];           // terminal
        // OR
        return ['via' => FormSchema::class, 'fk' => 'form_schema_id'];
    }

The apply() path walks the chain recursively, building whereIn subqueries
against parent models until it hits a column-based strategy. Max 3 hops;
deeper chains raise App\Exceptions\TenantScopeResolutionException. The
walker accepts BOTH the new tenantScopeStrategy() and the legacy
$organisationScopeColumn property at every hop — so PersonIdentityMatch
can chain via Person, which still uses the legacy event_id bridge, without
requiring Person/Event/Shift/FestivalSection/TimeSlot to migrate to the
new convention in this work package. That migration is a separate
backlog ticket — explicitly scope-controlled per the addendum.

Fourteen newly-scoped models:

  Form-builder child models (D-03):
    FormSchemaSection             via FormSchema                    (1 hop)
    FormField                     via FormSchema                    (1 hop)
    FormSubmission                column organisation_id (Commit 2)
    FormValue                     via FormSubmission                (1 hop)
    FormValueOption               via FormValue -> FormSubmission   (2 hops)
    FormSubmissionSectionStatus   via FormSubmission                (1 hop)
    FormSubmissionDelegation      via FormSubmission                (1 hop)
    FormSchemaWebhook             via FormSchema                    (1 hop)
    FormWebhookDelivery           via FormSubmission                (1 hop)

  Event-data models (D-04 event-data subset):
    ShiftAssignment               via Shift (legacy festival_section_id)
    ShiftWaitlist                 via Shift
    VolunteerAvailability         via TimeSlot (legacy event_id)
    PersonSectionPreference       via FestivalSection (legacy event_id)
    PersonIdentityMatch           via Person (legacy event_id)

Note — task directive specified VolunteerAvailability "via: Event, fk: event_id",
but the table has no event_id column (only person_id + time_slot_id).
Rerouted via TimeSlot, which carries the legacy event_id bridge; same
end result, correct FK.

Security-relevant callers made explicit:
  PublicFormSchemaResource::toArray() now eagerly loads fields + sections
  with withoutGlobalScope(OrganisationScope::class). Prior to this commit
  the public form endpoint silently relied on those relations being
  unscoped. The PublicFormCrossOrgScopeTest pre-existing assertions still
  pass — behaviour unchanged, intent now explicit.

Test fix: FormSchemaApiTest::test_publish_sets_is_published_true was
flaky (factory randomly picked EVENT_REGISTRATION which requires
bindings). Pinned to USER_PROFILE for determinism; PurposeSchemaLifecycleTest
covers the binding-enforcement path.

Test flip: MultiTenancyTest::test_form_schema_webhook_is_not_globally_scoped
renamed to is_scoped_via_fk_chain and asserts the new behaviour: scope
filters by route org, withoutGlobalScope() still exposes cross-org rows.
The test's original purpose ("pin current behaviour so a future refactor
is intentional") is now satisfied by Commit 3 being that intentional
refactor.

Docs:
  SCHEMA.md §3.5.11 Rule 5 — tenantScopeStrategy() convention documented;
    the 14 newly-scoped models enumerated; link to addendum Q2.
  ARCH-FORM-BUILDER.md §4.14 — new section "Multi-tenancy scope chain"
    with the hop-count table for all 14 chains and the withoutGlobalScope
    pattern for cross-org callers.

Tests: tests/Feature/MultiTenancy/ScopeLeakageTest.php — two orgs with
fully-populated record chains down to each of the 14 leaf models; asserts
scoped queries never cross, withoutGlobalScope still does. Plus: three-
hop chain (FormValueOption) explicitly exercised, legacy-column bridge
verified, over-deep chain raises TenantScopeResolutionException. 16 tests /
31 new assertions. Full suite: 1000 passed (2706 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:08:33 +02:00
ae8e2fdb4e feat(form-builder): denormalize organisation_id and event_id on form_submissions per addendum Q2
Adds direct tenant + event columns to form_submissions so rapportage-hot
aggregate queries (dashboards, CSV-exports, counts over thousands of rows
per org or per event) skip the form_schemas join. This is the single
denormalization exception per addendum Q2; every other form-builder child
table continues to resolve tenancy via FK-chain through its parent
(implemented in Commit 3).

Schema:
- form_submissions.organisation_id  ULID FK → organisations, cascade delete, NOT NULL
- form_submissions.event_id          ULID FK → events, null on delete, nullable
- Indexes: (organisation_id, status), (event_id, status)

Observer: App\Observers\FormBuilder\FormSubmissionObserver::creating
resolves both columns when the caller has not set them.
  - organisation_id <- form_schema.organisation_id (always present —
    form_schemas carries OrganisationScope's column directly)
  - event_id <- schema.owner_id when owner_type === 'event'; else the
    active route's {event} parameter; else null (user_profile /
    signature_contract purposes)
The observer docblock spells out both resolution paths and is covered
by the observer test below.

Model: FormSubmission gains organisation_id + event_id in $fillable, a
belongsTo organisation() and belongsTo event() relation.

Factory: FormSubmissionFactory gains forOrganisation($org) and
forEvent($event) states for tests that need to override the observer's
automatic resolution (e.g. cross-org leakage scenarios in Commit 3).
Normal factory usage does not need the states — the observer populates
both fields on save.

Docs:
- SCHEMA.md §3.5.12 form_submissions table — organisation_id and event_id
  inserted between form_schema_id and subject_type; indexes added;
  addendum Q2 rationale paragraph at the bottom explaining why this is
  the only denormalized form-builder child.
- ARCH-FORM-BUILDER.md §4.3 — mirror changes + rationale inline on the
  columns and in the indexes list.

Tests: tests/Feature/FormBuilder/FormSubmissionObserverTest.php — 7 tests
covering organisation resolution from schema, event resolution from
event-owned schema, null event_id for non-event-owned schemas without
route context, route-based event resolution, organisation_id populated
on every create path (factory / new() / Model::create), index presence,
and belongsTo relations. 13 new assertions. Full suite: 984 passed
(2675 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:56:53 +02:00
a92ddc48ec refactor(schema): migrate eleven pivot/EAV tables to ULID per addendum Q1
Retires the "integer AI PK for join performance" exception documented
in earlier migrations and SCHEMA.md §3.5.11 Rule 1. Every business and
pivot table now uses ULID primary keys, per
/dev-docs/ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md Q1.

Tables migrated (WS-1 A-01 through A-11):
- Pure pivots: organisation_user, event_user_roles, crowd_list_persons,
  event_person_activations
- Model-backed: user_organisation_tags, person_section_preferences,
  mfa_backup_codes, mfa_email_codes, form_submission_section_statuses,
  form_values, form_value_options

Migration pattern: one new migration per table (plus one combined for
the form_values / form_value_options FK pair), timestamped today,
dropping + recreating with the new ULID PK. Pre-launch — no backfill
required. Original migrations remain in place; the new migrations
apply in timestamp order for a clean schema history.

Pivot model correction (addendum drift):
The addendum's "no model required for pure pivots" reading did not
account for Laravel's BelongsToMany::attach() — it cannot auto-generate
a pivot ULID without a Pivot subclass. Minimal Pivot classes under
app/Models/Pivots/ (OrganisationUser, EventUserRole, CrowdListPerson,
EventPersonActivation) carry HasUlids so attach() works. The six
belongsToMany relations (User.organisations / .events, Organisation.users,
Event.users, CrowdList.persons, Person.crowdLists) now ->using(...) the
appropriate Pivot class. DB::table()->insert() on event_person_activations
in DevSeeder populates the ULID inline via Str::ulid(). FormValueObserver
uses bulk FormValueOption::insert() which bypasses model events — ULIDs
are now generated inline there too.

Docs:
- SCHEMA.md §3.5.11 Rule 1 rewritten to mandate ULID on pivots too, with
  legacy note citing the addendum.
- All eleven table entries updated from "int AI PK" to "ULID PK" with
  addendum Q1 references.
- form_values and form_submission_section_statuses prose blocks updated
  to drop the retired ARCH §4.4 / "high-volume pivot" rationale.
- form_value_options.form_value_id column type corrected from
  "int FK" to "ULID FK".

Tests: tests/Feature/Schema/UlidPrimaryKeyTest.php covers HasUlids trait
presence, ULID shape + 26-char Crockford pattern, Route::bind resolution,
distinct + sortable pivot ULIDs, attach() auto-generation on pure pivots,
and the A-10/A-11 FK chain. 10 tests / 28 new assertions. Full suite:
977 passed (2662 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:38:08 +02:00
5a82497da4 docs(backlog): track Artist model prerequisite for artist_advance purpose
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:46:20 +02:00
598593b0db docs(schema,arch-form-builder): reflect purpose registry v1.0
- SCHEMA.md §3.5.12 header rewritten for the 7-purpose vocabulary and
  `PurposeRegistry`. The `custom_purpose_slug` column is dropped from
  the `form_schemas` table and removed from the index list. The
  `form_submissions.subject_type` note cites
  `PurposeRegistry::allSubjectTypes()` instead of the deleted
  `config/form_subjects.php`.
- ARCH-FORM-BUILDER.md TL;DR updated: goal bullet cites 7 purposes
  (v1.0); §3.2 bullet notes the legacy 22-variant vocabulary is
  retired. §17.3 replaced: the "Custom purposes per organisation"
  section is gone; the new "Purpose registry" section documents the
  seven-slug table, PurposeDefinition shape, PurposeRegistry API,
  MorphMapAlignmentTest guard, the pre-publish binding check, and a
  step-by-step "adding a new purpose" checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:36:18 +02:00
ad941cc944 docs: add architect decisions addendum after WS-1 2026-04-24 13:49:05 +02:00
4f47e054b9 docs: add WS-1 architecture discovery report 2026-04-24 13:08:00 +02:00
83821b1bd5 docs(architecture): land consolidation sprint briefing document
Adds ARCH-CONSOLIDATION-2026-04.md as the authoritative reference for the
upcoming 8-workstream architecture consolidation sprint: purpose registry
cleanup, ULID consistency, JSON column split, binding infrastructure,
FormBindingApplicator, single-SPA consolidation to crewli.app, observability
foundation, docs consolidation.

Sprint scope, leading principles, workstream ordering, and chat-transition
protocol are captured in the document. Follow-up chats will start from this
document as primary context.

Also updates BACKLOG.md with an active-sprint marker pointing to the briefing.
2026-04-24 11:51:28 +02:00
a777c8335f docs(backlog): log TECH-08 for paginated response meta in organizer composables
Documents the technical debt introduced when the organizer API composables
(useSections, useFormSchemas) adopted a minimal PaginatedResponse shape
that discards Laravel's links and meta blocks. PR-b2 will surface the
first UI consumer that needs pagination controls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:32:40 +02:00
2b16d4fd8f docs(policy): add Form Builder scope discipline policy
Deferred-work guidance for Form Builder expansions. Prevents
speculative building for hypothetical use cases while preserving
the one justified forward-looking investment (polymorphic subject
picker in S3b). Based on April 2026 coverage analysis of ~60
practical form use cases.
2026-04-24 10:18:44 +02:00
214a2debee refactor(form-schema): inline validators to remove @core transitive dep
Resolves TECH-07. Copies the four validators actually used
(requiredValidator, emailValidator, urlValidator, regexValidator) from
@core/utils/validators into packages/form-schema/src/utils/validators.ts
as pure boolean functions. Vuexy template copies in apps/*/src/@core/
remain for non-form UI use. Package is now genuinely standalone —
grep -rn "@core/" packages/form-schema/ returns zero matches.

Also corrects two documentation inconsistencies from commit 42dd626e:
dev-guide heading translated to Dutch for style consistency, and the
BACKLOG entry renumbered from TECH-DEBT-01 to TECH-07 to match the
flat numbering in the Technische schuld section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 00:20:15 +02:00
42dd626e37 docs(form-schema): document shared package boundary and tech debt
Documents the "share schema, not UI" principle in dev-guide.md so the
boundary stays intact in future work. Logs TECH-DEBT-01 for the
@core/utils/validators transitive dependency discovered during PR-a.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 00:04:41 +02:00
6f032a0311 docs(backlog): move FORM-09 to resolved — listener refactored in previous commit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:19:00 +02:00
1875e79ce1 docs(backlog): add gaps discovered during S3a PR 2
- TECH-06 — ESLint config ontbreekt in apps/portal (pendant van TECH-05).
- DOC-02 — VitePress docs:build faalt op missing image in
  /docs/volunteer/je-aanmelden-via-een-link.md.
- DOC-03 — Formulieren sidebar story is incompleet; nog geen
  publicatieflow, inzendingen-overzicht, templates, webhooks,
  conditionele logica.
- FORM-09 — TriggerPersonIdentityMatchOnFormSubmit ShouldQueue
  herzien: async queue-dispatch levert null bij submit-response;
  eager state + lazy resolution patroon invoeren nu de refactor
  nog klein is (voor FORM-05 landt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 20:44:46 +02:00
a97922d6a4 docs(form-builder): document S3a PR 2 complex field types and update FORM-05 stub note
- Add VitePress pages for AVAILABILITY_PICKER and SECTION_PRIORITY
  and a TAG_PICKER configuration note. Wire them into the organisator
  sidebar under a new Formulieren section alongside the existing
  "Wat is een formulier" page.
- BACKLOG.md: nuance FORM-05 — the stub-shaped behaviour for public
  event_registration submissions is already shipping via the existing
  TriggerPersonIdentityMatchOnFormSubmit listener (writes 'pending').
  The real work (PersonIdentityService::detectMatchesByValues + an
  extra branch in resolveStatus) is what remains. Added a done entry
  for S3a PR 2 to the Opgeloste items list.
- API.md: add VALIDATION_FAILED to the public-form error code table
  and document the SECTION_PRIORITY shape error messages (Dutch copy
  served under errors."values.{slug}").
- COPY_CATALOGUE.md: new S3a PR 2 section capturing the seeder
  help_text, the IdentityMatchBanner copy (clearly marking the
  backend message as authoritative), all empty/error state copy for
  the three new components, and the SECTION_PRIORITY shape error
  strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 20:34:34 +02:00
d274284fd4 docs: add CLAUDE_DESKTOP_SETUP.md describing Gitea MCP context strategy 2026-04-23 17:27:08 +02:00
d67502eaec docs(api): refresh Form Builder public API surface notes
Updates API.md and S3a discovery doc to reflect submitter-details
handling and the draft/submit split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:21:15 +02:00
102b6006fa docs(backlog): add FORM-05 smart identity-match on public submission values 2026-04-17 23:18:45 +02:00
68d2c830a0 docs(form-builder): API.md Form Builder (Public), SCHEMA v2.1, ARCH §10.4, BACKLOG
S2c Phase 8.

- API.md: new **Form Builder (Public)** section documenting all 6
  public endpoints (GET schema + time-slots + sections; POST draft,
  PUT save, POST submit) with request/response examples, error codes,
  and the identity_match / schema_drift contracts. No PII-echo noted
  explicitly.
- SCHEMA.md bumped to v2.1:
  - changelog entry for S2c.
  - form_submissions table gains schema_version_at_open +
    identity_match_status columns; UNIQUE (form_schema_id,
    idempotency_key) replaces the composite index; a new composite
    index (form_schema_id, identity_match_status) landed for the
    organiser "pending-match" dashboard.
- ARCH-FORM-BUILDER.md bumped to v1.3 with new §10.4 "Public
  submission lifecycle — draft/save/submit split" documenting the
  three-endpoint contract, idempotency, schema-drift detection,
  access rules, the standardised error envelope, and the dependency
  data sub-endpoints.
- BACKLOG.md adds:
  - FORM-04 (grace_days configurable — current implementation still
    uses the hard-coded 7-day window)
  - DOC-01 (Scramble / OpenAPI generator for API.md to reduce the
    docs-drift effort going forward).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:07:26 +02:00
8dd874916f docs(discovery): S3a public Form Builder API surface report
Carried over from the prior discovery session. Lists the 12 gaps (4 hard
blockers, 8 soft) that S2c closes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:55:16 +02:00
a51f3d3a47 docs(schema): bump v1.8 → v2.0 with Form Builder tables, drop legacy registration EAV
Reflects the post-S1+S2a+S2b database state. Nothing but SCHEMA.md changed.

- Header: Version → 2.0, added v2.0 changelog entry covering the 13 new
  tables, the 3 dropped legacy tables, the preserved
  person_section_preferences, organisations.default_locale, and the
  events.registration_show_* drops.
- Table of Contents: updated §3.5.5b name to "Section Preferences",
  added entries for §3.5.10 Email Infrastructure, §3.5.11 Rules,
  §3.5.12 Form Builder (which were already in the file but missing
  from the TOC).
- §3.5.1 organisations: added default_locale column (FormLocaleResolver
  fallback chain, ARCH §16.2).
- §3.5.1 events: removed registration_show_section_preferences +
  registration_show_availability columns with a pointer at
  form_fields.is_portal_visible / conditional_logic.
- §3.5.4: removed the never-created volunteer_profiles table block;
  the other three tables in that section (volunteer_festival_history,
  post_festival_evaluations, festival_retrospectives) are unchanged.
- §3.5.5b: renamed to "Section Preferences"; design note pointing at
  events.registration_show_section_preferences replaced with a pointer
  at form_fields.is_portal_visible / conditional_logic.
- §3.5.9: renamed to "Check-In & Operational"; removed the never-created
  public_forms stub and the colliding legacy form_submissions block
  (both documented planned-but-never-created tables) with a short note
  pointing at the Form Builder as the home for form concepts. Flagged
  separately below because it's technically beyond the task's explicit
  scope but unavoidable (SCHEMA.md would otherwise describe two
  different tables under the same name).
- §3.5.12 Form Builder: summary replaced with full per-table
  documentation for all 13 tables in the ARCH §4 order — user_profiles,
  form_schemas (polymorphic owner, public_token rotation with
  public_token_previous + public_token_rotated_at, edit_lock_*),
  form_schema_sections, form_field_library, form_fields, form_submissions,
  form_submission_section_statuses, form_submission_delegations,
  form_values (observer-driven typed columns value_indexed/number/date/bool
  and form_value_options multi-value rebuild per ARCH §7.2),
  form_value_options, form_templates, form_schema_webhooks,
  form_webhook_deliveries. Added short notes on activity log strategy
  and the §31.10 FORM-02 tag-sync listener.

Migrations-vs-ARCH discrepancies (migrations win, per CLAUDE.md):

- form_values carries created_at / updated_at timestamps, though ARCH §4.4
  does not list them. Documented as present.
- form_webhook_deliveries has no timestamps columns; last_attempt_at is
  the effective timestamp. Documented as such.
- form_schema_webhooks stores url / secret as encrypted TEXT columns
  (Eloquent-cast encryption); ARCH says "encrypted" without specifying.
  Documented the column type.
- public_forms + legacy form_submissions documented in §3.5.9 never
  existed in the DB (confirmed via Schema::hasTable). Removed those
  doc stubs; the naming collision with the new Form Builder
  form_submissions made leaving them in place a correctness hazard.
2026-04-17 21:48:57 +02:00
2d6d2b2991 docs(form-builder): API.md, ARCH §31.10, BACKLOG
Phase 7 of S2b.

- API.md: "Form Builder" section rewritten with every new route
  (schemas / fields / submissions / values / delegations / templates /
  field library / webhooks / filter registry / public token flow).
  Calls out §22.8 typed-confirmation deletes, §6.5 binding-change guard,
  §9 signature hash on submit, §7.4–§7.5 FilterQueryBuilder contract,
  and that FormSubmissionSubmitted is the trigger for the §31.10
  TAG_PICKER sync listener.
- BACKLOG.md: FORM-02 marked done with the shipped artefacts and the
  deferred §31.9 contract tests spelled out.
- ARCH-FORM-BUILDER.md §31.10 already rewrote authoritatively in Phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:28:54 +02:00
4495ab017e feat(form-builder): FORM-02 TAG_PICKER sync listener (ARCH §31.10)
Rebuilds the tag-sync flow purged in S2a, now listener-driven against the
universal FormBuilder (ARCH §31.10).

- SyncTagPickerSelectionsOnSubmit listener: ShouldQueue on connection=redis
  queue=default. Filters to event_registration + person subjects with at
  least one TAG_PICKER form_value. Logs on failure, never rethrows so
  sibling listeners keep running.
- AppServiceProvider registers the listener via Event::listen alongside
  the existing S1 observers.
- PersonIdentityService::confirmMatch now calls
  FormTagSyncService::rebuildForPerson after setting person.user_id — the
  deferred-sync path for persons who filled in TAG_PICKER fields before
  their account was linked.
- ARCH-FORM-BUILDER.md §31.10 rewritten with the authoritative contract
  block from this session. Header bumped to v1.2.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:00:17 +02:00
a3ca596362 S2a: purge legacy Form Builder PHP code and routes 2026-04-17 18:43:00 +02:00
cfc7610497 docs(forms): SCHEMA crosswalk, foundation concept page, getting-started + migration playbook, copy catalogue init
SCHEMA.md
- New §3.5.12 "Form Builder" with the legacy-tables-retained note
  placed prominently directly under the section header (per S1 wrap-up
  Path 3 decision: Phase 8 deferred to S2).
- Crosswalk: every legacy volunteer_profiles column → its new home
  (user_profiles columns vs form_fields vs person_tags).
- Summary table for the 13 new tables with one-line purpose + ARCH §
  pointer each.
- Activity log strategy and multi-tenancy discipline noted.
- §3.5.4 marked SUPERSEDED with a pointer to the new section.

/dev-docs/form-builder-migration-playbook.md (new)
- Operator runbook for forms:migrate-legacy-data on real legacy data.
- Pre-flight audit, dry-run, migrate, verify, spot-check, rollback
  paths spelled out. Same legacy-tables-retained note prominently.

/dev-docs/form-builder-getting-started.md (new)
- Developer onboarding. Mental model, code samples for creating a
  schema/field/submission/value, adding a new subject type, registering
  a custom field type, suppressing activity log via
  App\Support\ActivityLog::suppressed.

/dev-docs/COPY_CATALOGUE.md (new)
- Seeded verbatim from ARCH §30 (naming conventions, tooltip catalogue,
  warning catalogue) with a header explaining purpose, growth strategy,
  and the per-PR update workflow.

/docs/organizer/forms/concepts/wat-is-een-formulier.md (new VitePress)
- Dutch, informal je/jij. Follows /docs/.templates/concept-page.md.
- Three example use-cases: vrijwilligersregistratie, artist advance,
  incidentrapportage. Light foundation; depth arrives in S2-S5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:06:53 +02:00
032ad9d953 docs(architecture): upgrade form builder architecture to v1.2 2026-04-17 10:39:36 +02:00
2d86fcbf7e chore(backlog): add TECH-05 and COMM-05 items 2026-04-16 22:46:11 +02:00
e552eebb85 docs(architecture): add festival hierarchy UX specification
Captures the approved UX specification for the festival hierarchy:
four dropdown scenarios (standard sub-event, cross_event section,
flat event, location-based), context-preservation on navigation,
and info-tooltip placement rules. This document has been referenced
in implementation work since April but was never committed.
2026-04-16 22:21:22 +02:00
9718e27029 feat: registration form field display_width and option descriptions
Add configurable column widths (full/half) and optional descriptions
for radio/select/checkbox options on registration form fields.

- Migration adds display_width column to both tables
- FieldDisplayWidth enum with smart defaults per field type
- normalized_options accessor for backwards-compatible option format
- Portal form renderer uses display_width for VRow/VCol grid layout
- Radio/select/checkbox options render with descriptions
- Admin field editor supports display_width toggle and description input
- System templates updated with appropriate widths and descriptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:46:36 +02:00
4df668b5b8 feat: replace token-based impersonation with enterprise-grade header-based system
Replaces the insecure token-in-localStorage approach with a header-based
impersonation system backed by cache sessions and MFA verification.

Key changes:
- New impersonation_sessions audit table (immutable, ULID PK)
- MFA verification required to start impersonation (TOTP/email/backup)
- X-Impersonate-User header + HandleImpersonation middleware
- Per-request auth context swap (admin session never modified)
- IP pinning, sensitive route blocking, no nesting, sliding 60-min TTL
- Activity log auto-tagged with impersonated_by during sessions
- Frontend: sessionStorage, BroadcastChannel sync, countdown timer
- ImpersonateDialog with reason + MFA verification flow
- 26 comprehensive tests covering core, middleware, audit, lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 02:42:53 +02:00
948687f27e feat: enterprise MFA with TOTP, email codes, backup codes, and trusted devices
Three verification methods (TOTP authenticator, email code, backup codes),
trusted device management with 30-day expiry, role-based enforcement for
super_admin and org_admin, admin reset capability, and full test coverage
(46 tests). Modifies login flow to support MFA challenge/response with
temporary session tokens stored in cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:45:55 +02:00
65978104d8 feat: complete email infrastructure with queue, templates, logging, and API
Adds the full transactional email system:
- Redis queue (QUEUE_CONNECTION=redis), SES config in .env.example
- 3 migrations: organisation_email_settings, organisation_email_templates, email_logs
- EmailTemplateType and EmailLogStatus enums with Dutch defaults
- EmailService as central entry point for all email sending
- SendTransactionalEmail queued job with retries and idempotency
- TransactionalMail mailable with responsive HTML + plain text templates
- Organisation-level branding (colors, logo, footer, reply-to)
- Per-type template overrides with {variable} substitution
- Email log with filtering by status, type, date range, recipient
- Preview and send-test endpoints for template management
- API endpoints: email-settings, email-templates (CRUD), email-logs (read-only)
- Integrated into existing flows: invitations, password reset, email
  verification, registration approval/rejection
- 37 new tests across 4 test files, all existing tests updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:12:21 +02:00
28727f246b chore: remove admin SPA and update to two-app production setup
Remove apps/admin/ entirely — platform admin functionality now lives
in apps/app/ under /platform/* routes for super_admin users.

Production URL scheme changed:
- Organizer app: crewli.app (was app.crewli.app)
- Portal: portal.crewli.app (unchanged)
- API: api.crewli.app (unchanged)
- admin.crewli.app and app.crewli.app retired

Backend:
- Removed FRONTEND_ADMIN_URL config and admin cookie (crewli_admin_token)
  from SetAuthCookie, CookieBearerToken, cors.php, app.php
- Updated .env and .env.example (two origins, no port 5173)
- Updated cookie test: admin origin test → unknown origin fallback test

Infrastructure:
- Makefile: removed admin target
- deploy/nginx: updated CSP comment, removed admin vhost
- Updated README.md, CLAUDE.md, and all dev-docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:44:10 +02:00
945e22f322 docs: remove admin SPA references and update production URLs
The admin SPA (apps/admin/) has been retired. Its functionality now
lives in apps/app/ under /platform/* routes for super_admin users.
Updated all documentation to reflect: 2 SPAs instead of 3, removed
FRONTEND_ADMIN_URL/port 5173 references, changed production URL from
app.crewli.app to crewli.app. Retired admin-specific security audit
findings (A13-2, A13-4, A13-5, A13-7) and APPS-01 backlog item.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:21:44 +02:00
ddf26dad33 feat: platform admin backend — controllers, services, routes, tests
Add cross-organisation admin API endpoints behind role:super_admin middleware:
- AdminOrganisationController: CRUD with search, filter, billing_status management
- AdminUserController: user management with role assignment across orgs
- AdminStatsController: platform-wide aggregate statistics
- AdminActivityLogController: filterable activity log viewer
- AdminImpersonationController + ImpersonationService: user impersonation with
  token-based session management and activity logging
- BillingStatus enum, form requests, API resources, 23 feature tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:33:16 +02:00
7bc0f1a0c7 feat: fix time slot hierarchy — seeder, API include_children, frontend dropdown, navigation
Restructure the festival hierarchy end-to-end:

Seeder: Remove duplicate festival-level VOLUNTEER time slots, keep only CREW
operational slots. Rename sub-events to "Dag 1/2/3 — ..." pattern. Change
Nachtsecurity to Security (cross_event). EHBO/Security shifts now use sub-event
time slots via cross_event exception. Add flat event "Braderie Dorpstown 2026".

API: Add ?include_children=true to TimeSlotController for festivals, returning
all sub-event time slots with source and event_name fields. Update
StoreShiftRequest and UpdateShiftRequest to accept child time slots for
cross_event sections.

Frontend: Create useTimeSlotDropdown composable with 4-scenario dropdown logic.
Replace AppSelect with VAutocomplete in CreateShiftDialog with grouped items,
dimmed festival slots, and info tooltips. Add InfoTooltip reusable component.
Show festival context labels on cross_event sections in sub-event section lists.
Add read-only festival time slots on sub-event time-slots page. Add cross_event
context banner with "Bekijk alle diensten" link.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:07:37 +02:00
ea159a34fe docs: add development prompts and vibe coding checklist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:04:19 +02:00
3e93048461 docs: update BACKLOG.md with current project status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:50:55 +02:00
a29fa32ac6 feat: add "Lid toevoegen als deelnemer" shortcut for org members
Adds two new API endpoints to quickly add organisation members as event
persons with user_id pre-linked and status approved:
- GET /organisations/{org}/members/available-for-event/{event}
- POST /organisations/{org}/events/{event}/persons/from-member

Includes frontend dialog with member search, crowd type selection, and
click-to-add behavior in the Personen tab.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:38:53 +02:00