769 Commits

Author SHA1 Message Date
ad82110a69 feat(login): migrate login form to FormField + Zod (F3 sample, validates FormField API)
Replaces the Vuetify VForm + AppTextField + VBtn stack with the F3
form pattern: @primevue/forms' <Form> with a Zod resolver, the
project-owned <FormField> wrapper from B5, and PrimeVue InputText /
Password / Checkbox / Button at the input layer. Surrounding chrome
(VRow / VCol illustration column, VCard, VAlert reset-success banner,
auth-logo link, MfaChallengeCard) stays Vuetify until F4b migrates
the auth surface in full.

Zod schema:
- email: required, valid email format
- password: required

Both messages are Dutch (per F3 sprint plan convention).

422 error handling routes through useFormError() from B5. The Laravel
response shape (errors.<field>: string[]) feeds applyApiErrors directly.
rate_limited and other reason-only failures are synthesized into the
email field's error map so they surface visually under the email input,
preserving the existing UX.

The remember-me checkbox is rendered with PrimeVue Checkbox (no schema
coverage — it's UI state, not validated input). The password visibility
toggle is delegated to PrimeVue's Password component's built-in
toggle-mask prop (replaces the previous manual isPasswordVisible ref
and append-inner-icon plumbing).

Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass unchanged.
- pnpm build — succeeds; login chunk grew from ~21 KB to ~84 KB raw
  due to @primevue/forms + Password/Checkbox component code (gzip 22 KB).
  Will normalize during F4 as more pages share these modules.
- Manual browser test deferred to Phase C brand-review screenshot
  capture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:14:15 +02:00
4391550140 feat(layouts): rewrite layout shells with PrimeVue Drawer + Menubar + Avatar
Layout-shell rewrite per RFC AD-3, B7-option-B. R-10 isolation invariant
honored — this single commit is revertible to roll back the layout
change without losing B1–B6 progress.

New component (PrimeVue-only, no Vuetify imports per F3 hard constraint):
- apps/app/src/layouts/components/AppShell.vue (~210 lines)
  - Desktop sidebar (Tailwind grid, lg+ breakpoint) renders nav items
    as PrimeVue Buttons + Icons. Mobile (<lg) hides sidebar; PrimeVue
    Drawer slides in on hamburger toggle.
  - Top bar (Tailwind) has hamburger + title (mobile) and an Avatar +
    Menu (PrimeVue) for the user dropdown with "Mijn Profiel" and
    "Uitloggen" actions.
  - Nav items accept the existing { title, to: { name }, icon: { icon } }
    shape from src/navigation/vertical so call-sites stay terse.

Five top-level layouts delegate to AppShell (filename preserved per
AD-3 so vite-plugin-vue-meta-layouts continues to resolve routes
unchanged):
- default.vue       — org + (super-admin) platform nav
- OrganizerLayout   — same nav as default; matches authenticated org UX
- PortalLayout      — portal-specific 2-item nav ("Mijn evenementen",
                       "Mijn Profiel")
- blank.vue         — minimal chrome-less wrapper for login etc.
- PublicLayout      — minimal wrapper for public form-fill routes;
                       uses <main> for semantic structure

F3 functional regressions (intentional — F4 sub-packages reintroduce
each item through PrimeVue):
- NavSearchBar (Vuetify-heavy combobox/overlay) — absent from top bar
- ContextSwitcher (Vuetify VBtn + VMenu) — absent
- NavbarThemeSwitcher (Vuetify IconBtn) — absent; dark mode driven by
  PrimeVue's darkModeSelector: '.dark' continues to work via the
  existing @core skin classes until F6 cleanup
- NavbarShortcuts (Vuetify-heavy) — absent
- NavBarNotifications (Vuetify-heavy) — absent
- UserProfile from @/layouts/components/ (Vuetify-heavy menu) — replaced
  with the minimal Avatar + Menu dropdown described above; rich profile
  panel returns in F4
- ImpersonationBanner — absent; super-admin impersonation UX is F4 work
- PortalLayout event-mode vs platform-mode topbar (route.meta.navMode
  driven) — absent; F4 reintroduces via AppShell prop or slot
- Suspense + AppLoadingIndicator wrapping pages — dropped; pages handle
  their own loading via PrimeVue ProgressSpinner

VApp at App.vue level still wraps everything, so Vuetify components
inside still-Vuetify pages continue to render correctly during the
parallel-mode window.

Test updates (no Vuetify in layout structure to assert against anymore):
- OrganizerLayout.spec.ts — mocks AppShell instead of the deleted
  DefaultLayoutWithVerticalNav reference; provides Pinia.
- PortalLayout.spec.ts — same mock pattern; new structural assertions
  go through AppShell stub; the new third test verifies
  PortalLayout forwards portal nav items + title to AppShell.
- PublicLayout.vue — uses <main> for semantics; PublicLayout.spec.ts
  still passes unchanged.

Auto-generated component/auto-import dts files refreshed for the new
AppShell component (committed for stable dev workflow).

Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass (test count unchanged after spec rewrites).
- pnpm build — succeeds in 14.05s; AppShell chunk is ~57 KB raw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:12:06 +02:00
f5a9e491ce feat(primevue): add Icon component and mount Toast + ConfirmDialog services
Two additions complete the F3 runtime scaffolding:

apps/app/src/components/Icon.vue — generic Iconify renderer wrapping
@iconify/vue's <Icon> component. F4 migration substitutes
<VIcon icon="tabler-X" /> with <Icon name="tabler-X" /> at call-sites,
producing real SVG output and using the existing Crewli "tabler-*"
naming convention. Props: name (required, e.g. "tabler-eye"), optional
size. The component avoids @iconify/vue's auto-import for clarity at
call-sites.

apps/app/src/App.vue — mounts <Toast /> and <ConfirmDialog /> at the
template root inside VLocaleProvider. Both render alongside the
existing VSnackbar and VDialog confirm patterns during the F3–F4
parallel-mode window. F4 sub-packages migrate call-sites to PrimeVue's
useToast() / useConfirm() composables.

UnoCSS-style i-tabler-* utility-class rendering (RFC AD-5 v1.0 wording)
is not adopted — UnoCSS is not installed in the Crewli stack. The
RFC will be aligned in B9.

Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:06:11 +02:00
c1190ab045 feat(forms): add FormField wrapper + useFormError composable per RFC Appendix A
Two new artifacts that together provide the F4 form-migration target:

apps/app/src/components/forms/FormField.vue — project-owned wrapper
around @primevue/forms' built-in FormField. The default slot accepts
the actual input (e.g. <InputText name="email" />); the wrapper renders
label (with optional required asterisk), error Message, and hint chrome
around it. Reads field state from the parent <Form> via the built-in
FormField's scoped slot, so call-sites do not need to thread $form
manually.

apps/app/src/composables/useFormError.ts — the API 422 bridge. Parent
component calls useFormError() once; the composable provides an
apiErrors ref through Vue inject. Each FormField in the component
reads its own field name from that map. applyApiErrors() reads the
Crewli backend's { errors: { field: string[] } } shape and surfaces
the first message per field; clearApiErrors() resets between submits.

Error precedence per RFC Appendix A: explicit apiError prop > inject
apiErrors map > Zod resolver error from $field.

Signature note: RFC's useFormError(formRef) is implemented as
useFormError() — the formRef parameter is unused in the provide/inject
implementation, and Crewli convention avoids unused parameters. RFC
will be aligned in B9 if it remains a meaningful spec gap during F4.

Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass unchanged.
- B8 will exercise the components end-to-end on the login page; F4d
  validates against the public-registration multi-step form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:04:58 +02:00
7660d12a8c feat(primevue): register PrimeVue plugin in main.ts alongside Vuetify
installPrimeVue(app) runs AFTER registerPlugins(app) (which registers
Vuetify + router + Pinia via the Vuexy @core machine). Placing the
PrimeVue install outside @core/utils/plugins is deliberate — it keeps
PrimeVue free of the Vuexy plugin loader so F6 can remove @core/
without disturbing PrimeVue registration.

Both frameworks are now active at runtime. Existing Vuetify pages
continue to render unchanged; PrimeVue components become available
for the layout-shell rewrite (B7) and the FormField wrapper (B5).

Verification:
- pnpm typecheck — clean.
- pnpm build — succeeds in 14.26s, no PrimeVue or theme-related errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:02:59 +02:00
90d5c1678c feat(tailwind): install Tailwind v4 alongside Vuetify (parallel mode)
Three additions wire Tailwind v4 into the SPA without disturbing the
existing Vuetify pipeline:

- apps/app/src/assets/styles/tailwind.css — Tailwind v4 CSS-first entry.
  Uses @import "tailwindcss"; @plugin "tailwindcss-primeui"; and
  @source pointing at apps/app/src/ to scan template content.

- apps/app/vite.config.ts — adds the @tailwindcss/vite plugin between
  vue() and vuetify(). After vue() so it sees compiled template
  content; before vuetify() so Vuetify's SCSS pipeline runs unimpeded.

- apps/app/src/main.ts — imports tailwind.css before the Vuetify/Vuexy
  SCSS so utility classes are available alongside Vuetify's cascade.

optimizeDeps.exclude remains ['vuetify'] (no PrimeVue addition) — HMR
behaves correctly in dev with the current config; revisit if needed.

Verification:
- pnpm typecheck — clean.
- pnpm build — succeeds in 13.97s; CSS emitted per-route as expected.
- pnpm test — 402 tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:02:05 +02:00
0272961a95 feat(primevue): add PrimeVue plugin with Aura preset and Crewli teal tokens
Scaffolds apps/app/src/plugins/primevue/ as three files mirroring the
Vuetify plugin structure at apps/app/src/plugins/vuetify/:

- theme.ts — CrewliPreset extends Aura via definePreset(). Primary
  palette (50–950) is the exact token plan from RFC Appendix B,
  centered on Crewli teal #0D9394 (light primary, primary.500) and
  #0B7F80 (dark primary, primary.600). Surface tokens use Aura
  defaults. colorScheme.light/dark map primary.color, hover, active,
  and contrastColor per Appendix B.

- defaults.ts — empty pt (PassThrough) defaults object. F3 ships this
  scaffold; F4 sub-packages populate component-level defaults as each
  Vuetify surface migrates.

- index.ts — installPrimeVue(app) registers PrimeVue with the preset,
  Dutch locale (primelocale/nl.json → nl.nl), darkModeSelector: '.dark'
  (matches Vuexy convention per AD-2), and the three services Toast,
  Confirmation, Dialog.

Theme imports use @primeuix/themes (the maintained successor PrimeVue's
official docs prescribe), not RFC v1.0's @primevue/themes. See B1 commit
for substitution rationale. RFC will be aligned in B9.

The plugin is not yet registered in main.ts — that lands in B4 after
Tailwind v4 wiring (B3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:00:01 +02:00
c8dcecbb49 chore(deps): install PrimeVue 4.5 + Tailwind v4 + form ecosystem for F3 foundation
Packages installed:
- primevue@4.5.5
- @primeuix/themes@2.0.3 (substitutes @primevue/themes per ecosystem
  state — see rationale below)
- @primevue/forms@4.5.5
- primelocale@1.6.0 (pinned to ^1 per RFC)
- tailwindcss@4.3.0
- @tailwindcss/vite@4.3.0
- tailwindcss-primeui@0.6.1

Package substitution: @primevue/themes → @primeuix/themes

RFC v1.0 §6 F3 specifies @primevue/themes@^4.5, but during install pnpm
reported this package as deprecated by its maintainers (PrimeFaces) with
explicit guidance to migrate to @primeuix/themes. Web verification confirms
that the official PrimeVue 4 install documentation at primevue.org/vite/
now specifies `@primeuix/themes` directly, not the deprecated path:

  pnpm add primevue @primeuix/themes
  import Aura from '@primeuix/themes/aura';

@primeuix/themes is maintained by the same maintainers (mert.sincan,
cagatay.civici), has the same API surface (Aura preset, definePreset,
semantic tokens), and is the path PrimeVue 4's documentation now
prescribes. The substitution is not a deviation from PrimeVue v4
conventions — it IS the current PrimeVue v4 convention.

The RFC will be amended in B9 to align AD-2 and Appendix B with this
ecosystem state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:58:36 +02:00
8d6a001c2d docs(playwright): correct F3→F5 comments in CT provider stack
Both comments in apps/app/playwright/index.ts (header block lines 38-45
and inline at line 66) state that the Vuetify provider gets replaced by
PrimeVue in F3. This predates the RFC clarification that test-runtime
flip is F5, not F3 (per ARCH-TESTING.md §7).

F3 builds the PrimeVue runtime in main.ts but keeps the test runtime
on Vuetify. Component tests continue to mount with the Vuetify provider
until F5 deliberately swaps it. This commit aligns the comments with
that decision so no future contributor wonders whether the F3 sprint
should have touched this file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:52:56 +02:00
d99f9567c3 Merge pull request 'fix(lefthook): remove duplicate git-lfs pre-push command — resolves pre-push deadlock' (#23) from fix/lefthook-lfs-deadlock into main
Reviewed-on: #23
2026-05-11 00:33:43 +02:00
37af961b3e fix(lefthook): remove duplicate git-lfs pre-push command
Lefthook v2 runs `git lfs pre-push` internally for pre-push hooks (per
docs/usage/features/git-lfs.md; confirmed in internal/run/controller/
lfs.go where the internal handler invokes `git lfs pre-push <remote>
<url>` with a buffered `cachedStdin`). Our manual `git-lfs:` command
in lefthook.yml was a second invocation against the same remote; the
duplicate is directly visible in `LEFTHOOK_VERBOSE=1` output as
`[git-lfs] executing hook` (internal) followed by `[lefthook] run:
git lfs pre-push` (manual).

The previous fix attempt (piped: true, commit 1b06804) was based on a
wrong understanding of `piped`'s semantics — `piped` controls
fail-fast behavior, not stdin routing or sequencing. Default lefthook
behavior is already sequential per docs/configuration/parallel.md.
That "fix" was placebo; incident 2 (F2 push, zero LFS objects, commit
99eedb6) proved it.

Phase A investigation: documentary + source confirmation that lefthook
owns the LFS pre-push call. Phase B sandbox test against a filesystem
remote confirmed the duplicate execution in logs but did NOT reproduce
the production hang — likely because the duplicate manual call against
a local remote has no LFS server to interact with. A network-y remote
(Gitea over SSH/HTTPS) appears to be part of the trigger. Two
mechanisms remain plausible (H1: PTY-stdin without EOF in
`while read` loop per docs/configuration/use_stdin.md; H4: server-side
LFS interaction on the duplicate call). Both are eliminated by the
same fix: remove the manual command. LFS uploads continue to work via
lefthook's internal handler (verified in sandbox post-fix).

Regression coverage: scripts/test-lefthook-pre-push.sh asserts exactly
one internal LFS invocation, zero manual ones, and `Uploading LFS
objects: 100%` present, against a disposable sandbox.

See dev-docs/ADR-LEFTHOOK-LFS-INTEGRATION.md for full context, both
misconceptions to prevent regression, and the alternative-scenarios
playbook if Phase E ever regresses.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-11 00:18:56 +02:00
834611103e Merge pull request 'chore(docs): F2 — PrimeVue documentation foundation' (#22) from chore/f2-primevue-docs into main
Reviewed-on: #22
2026-05-10 23:23:52 +02:00
99eedb6004 chore(sync): add PRIMEVUE_COMPONENTS.md to .claude-sync.conf
Closes B5 of F2 (RFC-WS-FRONTEND-PRIMEVUE). PRIMEVUE_COMPONENTS.md
joins the synced doc set so Claude Project Knowledge picks it up on
next upload of .claude-sync/.

Sync output:
- .claude-sync.conf: 34 → 35 entries (+1: PRIMEVUE_COMPONENTS.md)
- .claude-sync/*.md: 34 → 35 files (sync script output;
  SYNC_MANIFEST.md auto-regenerated, not counted as net-new)

VUEXY_COMPONENTS.md kept in conf (deprecation stub still useful as a
forwarding marker during F4); removed in F6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:51:54 +02:00
1701e32fdf docs(cursor): update .cursorrules for PrimeVue migration phase
Mirrors CLAUDE.md changes in B3:

- Stack line notes PrimeVue + Tailwind v4 as target, Vuetify as legacy
- New "UI framework strategy (migration-aware)" section forwards to
  PRIMEVUE_COMPONENTS.md with surface-level guidance
- Vuexy reference-path section retained but scoped to legacy surfaces
- Vue 3 section split: Tailwind + pt on migrated, Vuetify-first on
  legacy
- Top blockquote signals this file evolves as F4 progresses

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:40 +02:00
b5765221bb docs(claude): point UI-framework conventions to PRIMEVUE_COMPONENTS.md; document migration-phase guidance
CLAUDE.md updated for the Vuetify→PrimeVue migration phase per
RFC-WS-FRONTEND-PRIMEVUE F2:

- Stack line: notes PrimeVue + Tailwind v4 as target, Vuetify still
  present on un-migrated surfaces
- Replaced "Vuexy reference source" + "Vuexy-first strategy" sections
  with a single "UI framework strategy (migration-aware)" section that
  splits guidance into migrated / un-migrated / new surfaces and
  forwards to PRIMEVUE_COMPONENTS.md
- Forms section now documents both target (@primevue/forms + Zod
  resolver via FormField) and legacy (ref + VForm + :rules) patterns,
  with the surface-level-consistency rule
- UI section reframed: PrimeVue + Tailwind on migrated surfaces,
  Vuetify utilities on legacy surfaces, three-state pattern preserved
  on both
- Order of work: framework note added for new pages during F4

Framework-agnostic sections (database, multi-tenancy, ULID,
controllers, models, security, testing) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:10 +02:00
4f07a673a1 docs(vuetify): replace VUEXY_COMPONENTS.md with deprecation stub (F6 deletion target)
Vuexy/Vuetify component reference is superseded by PRIMEVUE_COMPONENTS.md
per RFC-WS-FRONTEND-PRIMEVUE. Stub forwards readers to the new doc and
provides the explicit pre-F2 SHA (1c449ff620)
for retrieving the original 777-line content during F4a–F4c on
un-migrated surfaces.

File deleted entirely in F6 cleanup. Stub-not-delete decision per
2026-05-10 project chat (Bert): explicit forwarding marker beats
git-history archaeology while parallel-mode is in force.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:48:08 +02:00
9e137cffb9 docs(primevue): add PRIMEVUE_COMPONENTS.md — component mapping, forms pattern, Aura theming, Tailwind integration
Foundation document for F2 of RFC-WS-FRONTEND-PRIMEVUE. Encodes
Crewli-specific conventions for the Vuetify→PrimeVue migration:

- Component mapping by category (form / layout / data display /
  feedback / navigation / overlays), each with a paragraph on
  migration spirit; cross-references PrimeVue docs rather than
  duplicating reference material
- Aura theme + Crewli teal primary token plan (full token list in
  RFC Appendix B; F3 implements)
- Canonical forms pattern: @primevue/forms + Zod resolver +
  <FormField> wrapper (full API spec lives in RFC Appendix A —
  cross-referenced, not duplicated)
- DataTable conventions: lazy / virtual / column-template, with a
  slot translation cheat sheet from VDataTable
- pt API + Tailwind v4 + Aura tokens decision matrix
- Migration phase guidance (surface-level consistency rule, no
  back-porting, F6 cliff)
- VIcon stays Iconify-Tabler per RFC AD-5; PrimeIcons not installed

Length: 385 lines. F4 sub-packages will extend §3 as surfaces migrate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:47:43 +02:00
1c449ff620 Merge pull request 'chore(test-infra): TEST-INFRA-001 — Playwright + visual regression + real-backend e2e foundation' (#21) from chore/test-infra-001 into main
Reviewed-on: #21
2026-05-10 22:09:21 +02:00
1b06804e8c fix(lefthook): serialize pre-push commands to avoid stdin deadlock
Default parallel execution of sync-check and git-lfs commands within
the pre-push hook deadlocks: both read from stdin (git pipes the push
refspec to pre-push hooks), and two parallel readers never reach EOF.

Add piped: true to force sequential execution. sync-check runs first
(only inspects push_files via lefthook templating, doesn't actually
consume stdin), then git-lfs runs second with clean stdin access.

Observed during chore/test-infra-001 sprint: LFS upload completed
100% but pre-push hook hung indefinitely. Workaround was --no-verify;
this commit removes the need for that.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-10 22:01:00 +02:00
d5ff391acb Merge pull request 'docs(rfc): WS-FRONTEND-PRIMEVUE planning — F1 audit, RFC v1.0, Amendment A-1, sync conf expansion' (#20) from audit/primevue-migration into main
Reviewed-on: #20
2026-05-10 21:20:47 +02:00
9a63d5dcd2 docs(testing): dedupe Section 9 multi-context line; minor decision-tree clarity
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:15:12 +02:00
e15fc4f400 docs(backlog): track multi-context e2e gap from TEST-INFRA-001 cut #4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:13:43 +02:00
a2fce268fa docs(backlog): close TEST-INFRA-001 / TEST-CONTRACT-001 / TEST-VISUAL-001; open TEST-INFRA-002
Marks all three sprint backlog entries Resolved with sprint commit
references and documented deviations:

- TEST-INFRA-001 (b8d18e6, 82af117, f6509d9, 2dfb1e8) — Playwright
  foundation operational locally. CI deferred.
- TEST-CONTRACT-001 (2dfb1e8) — 409 conflict shape verified against
  real Laravel. Single-context replay instead of two-browser
  concurrent edit; UI rollback assertion deferred to F4.
- TEST-VISUAL-001 (f6509d9) — 5 composite baselines from canonical
  prototype. Composite-over-isolated rationale: prototype DOM lacks
  data-* attributes; isolated artist-name locators would rot. F4
  adds isolated baselines using stable data-test-id.

Opens TEST-INFRA-002 for the deferred CI work: Gitea/GitHub Actions
decision, runner image, caching, screenshot-diff artifacts, label-
gated nightly e2e. No deadline; surfaces when first review cycle
feels drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:29:33 +02:00
7e21c6a633 docs(testing): add ARCH-TESTING.md — test pyramid, scope per tier, anti-patterns
B5 of TEST-INFRA-001 (RFC-WS-FRONTEND-PRIMEVUE Amendment A-1).

- Add dev-docs/ARCH-TESTING.md (~13 KB):
  §1 Five-tier pyramid (Unit / Component / Integration / Visual /
     E2E) with environment, cost, and purpose per tier
  §2 Decision tree — pick by what is being verified, not by speed
  §3 Mock-vs-real-backend rules + the self-confirming-bias anti-
     pattern that motivated TEST-CONTRACT-001
  §4 Visual baseline workflow including the composite-over-isolated
     strategy used in B3
  §5 CI strategy stub — deferred to TEST-INFRA-002
  §6 Conventions + 5 anti-patterns
  §7 Vuetify-during-PrimeVue-migration: explicit doc that the
     Vuetify plugin in playwright/index.ts is INTENTIONAL TEMPORARY
     STATE replaced in F3 by PrimeVue. Forbids the "abstract the UI
     framework provider" deferred-cost trap.
  §8 Host setup — Node, pnpm, Chromium, Git LFS, MySQL 8, PHP, .env;
     known risks (unpkg.com flakiness, shared crewli_test DB)
  §9 Deferred work cross-references to BACKLOG entries
- Update CLAUDE.md ### Testing section to reference ARCH-TESTING.md
- Add ARCH-TESTING.md to .claude-sync.conf so the dev-docs sync
  pipeline picks it up; sync script run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:29:18 +02:00
2dfb1e8bae test(e2e): real-backend 409 conflict contract test (TEST-CONTRACT-001)
B4 of TEST-INFRA-001 (RFC-WS-FRONTEND-PRIMEVUE Amendment A-1).

- Add api/database/seeders/E2EBaselineSeeder.php — deterministic seed
  for Playwright e2e: e2e@test.local user (org_admin) on a fresh org +
  event + stage + StageDay + artist + engagement + performance
  (version=0). Writes seeded IDs to api/storage/app/e2e-fixtures.json
  so the Playwright fixture can construct API URLs without API
  discovery calls.
- Add apps/app/tests/playwright-e2e/global-setup.ts — runs
  `php artisan migrate:fresh --force --seed` against crewli_test (the
  existing PHPUnit MySQL test DB) before the test suite starts.
  Uses --env=testing to satisfy the dangerous-bash hook's migrate:fresh
  guard.
- Add apps/app/tests/playwright-e2e/utils/fixtures.ts — typed reader
  for e2e-fixtures.json. Cached after first read.
- Add apps/app/tests/playwright-e2e/utils/auth.ts — login helper that
  POSTs /api/v1/auth/login and returns user/org IDs. Uses Bearer-via-
  cookie flow (per api/.../SetAuthCookie.php), not stateful Sanctum.
- Add apps/app/tests/playwright-e2e/timetable/409-conflict.spec.ts —
  the contract test: first move with version=0 returns 200, second
  move with same stale version returns 409 with shape
  `errors.conflict: 'version_mismatch'`. Catches the schema-drift
  bug class that timetable-stabilization B5 surfaced.
- Update apps/app/playwright.config.ts — wire globalSetup, webServer
  for `php artisan serve --port=8001`, baseURL `http://localhost:8001`
  (NOT 127.0.0.1 — auth cookie's domain=localhost requires hostname
  match).
- Update .gitignore — runtime e2e-fixtures.json never committed.

DoD-19 met locally: `pnpm test:e2e` passes against a real Laravel
test server. CI integration deferred to TEST-INFRA-002 (per A-1
amendment).

Constraint: e2e tests share the crewli_test DB with PHPUnit. Running
both concurrently would collide. Documented in ARCH-TESTING.md (B5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:24:33 +02:00
f6509d938b test(visual): prototype static-server fixture + 5 composite baselines (TEST-VISUAL-001)
B3 of TEST-INFRA-001 (RFC-WS-FRONTEND-PRIMEVUE Amendment A-1).

- Add tests/playwright-ct/visual/static-server.mjs: 60-line Node http
  server that serves the canonical prototype directory. No new
  dependency added (vs. http-server / serve packages).
- Wire static server into playwright-ct.config.ts via webServer; tests
  navigate to http://127.0.0.1:5179/crewli-timetable.html.
- Add tests/playwright-ct/visual/prototype-smoke.spec.ts to verify the
  prototype loads in CT runner.
- Add tests/playwright-ct/visual/prototype.spec.ts with 5 @visual
  composite baselines:
    canvas-friday.png       — all status colors, b2b indicators,
                              multi-lane stacking
    canvas-saturday.png     — conflict ring + capacity warnings
    stage-row-multilane.png — first row in isolation
    wachtrij-populated.png  — sidebar list with parked + pending
    popover.png             — block-click popover layout
  9 additional surfaces from RFC §A.3's enumerated list are documented
  as test.skip() with reasons (cancelled status absent from prototype
  data, isolated-block locators would lock to artist names, drag-mode
  flaky under simulated pointer events, empty Wachtrij/empty day not
  reachable from canonical seed). All deferred to F4 component-level
  Vue baselines that will use stable data-test-id attributes.
- Baselines stored at tests/playwright-ct/__screenshots__/visual/
  prototype.spec.ts/*.png; tracked via Git LFS (.gitattributes).

Composite-over-isolated rationale: the prototype's DOM exposes status
only via inline style.background, no data-* attributes. Isolated-block
baselines would require artist-name locators that silently rot if
prototype data changes. Composite captures yield the same visual
vocabulary in fewer, more stable images. dev-docs/ARCH-TESTING.md (B5)
documents this strategy and the F4 transition plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:04:51 +02:00
82af11754a test(infra): mountWithProviders helper + Vuetify CT sanity test
B2 of TEST-INFRA-001 (RFC-WS-FRONTEND-PRIMEVUE Amendment A-1).

- Add tests/playwright-ct/utils/mountWithProviders.ts: ergonomic
  wrapper around Playwright CT's mount() exposing buildMountArgs()
  and readNotificationState(). Documents the Vue Test Utils ↔
  Playwright CT API divergence (provider plugins must be wired in
  beforeMount, not at call time) and the Vuetify-temp lifecycle
  (replaced by PrimeVue in F3).
- Add tests/playwright-ct/components/SanityButtonHarness.vue: a
  v-btn harness with a click counter; lives in a .vue file so Vite
  bundles its CSS-side-effect imports for the browser context
  (Playwright CT runs the test orchestrator in Node and components
  in a Vite-bundled browser, unlike Vitest's single jsdom graph).
- Add tests/playwright-ct/components/sanity-vuetify.spec.ts: two
  tests proving (a) v-btn renders and propagates clicks, (b) the
  --v-theme-primary CSS variable resolves to a parseable RGB triplet.
- Update playwright/index.ts: import 'vuetify/styles' so the v-btn
  renders with its actual visual appearance (not unstyled). Required
  for B3's visual baselines.

3 component tests pass. 402 Vitest tests still pass unchanged.
Lint + typecheck clean on new files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:56:48 +02:00
b8d18e63af chore(test-infra): install Playwright + axe-core; configure CT and e2e runners; enable Git LFS for screenshots
B1 of TEST-INFRA-001 (RFC-WS-FRONTEND-PRIMEVUE Amendment A-1).

- Add @playwright/test, @playwright/experimental-ct-vue,
  @axe-core/playwright as dev deps in apps/app
- Add @vue/compiler-dom (transitively required by ct-vue's Vite build
  pipeline; not auto-resolved on Vite 7)
- Install Chromium via `playwright install chromium` (host cache only,
  not committed)
- Configure Git LFS clean/smudge filters globally; track
  apps/app/tests/playwright-{ct,e2e}/__screenshots__/**/*.png
- Integrate `git lfs pre-push` into lefthook.yml since LFS's per-repo
  hook would conflict with the existing sync-staleness hook
- Add playwright/index.html + playwright/index.ts hook file with the
  full provider stack (Vuetify [TEMPORARY: replaced in F3 by PrimeVue],
  Pinia, TanStack Vue Query, memory-history Router with no auth
  guards)
- Add playwright.config.ts (e2e, Chromium-only, baseURL :5173, auto-
  starts `pnpm dev` via webServer)
- Add playwright-ct.config.ts (component testing, Linux-Chromium-only
  baselines, maxDiffPixelRatio 0.001, snapshot path template,
  ssr.noExternal: ['vuetify'] mirroring vitest.config.ts)
- Add scripts: test:component, test:e2e, test:visual,
  test:visual:update
- Add smoke test proving Chromium boots in the CT runner
- Update .gitignore for Playwright runtime artifacts (test-results/,
  playwright-report/, blob-report/, playwright/.cache/)

Vitest's existing 402 tests still pass unchanged.
DoD-17 / DoD-19 CI integration deferred to TEST-INFRA-002 per Amendment
A-1 scope cut (no CI exists in this repo today).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:53:57 +02:00
253f8a32e6 chore(sync): expand .claude-sync.conf to cover 11 missing dev-docs
Following an audit triggered by amendment commit 0d4afcd not auto-
regenerating .claude-sync (because RFC-WS-FRONTEND-PRIMEVUE.md was
absent from .claude-sync.conf), this commit closes the gap.

Added (11):
- 2 PrimeVue migration docs (RFC + audit)
- 5 tooling docs referenced from CLAUDE.md (CLAUDE_CODE_TOOLING,
  FRONTEND-TOOLING, LARASTAN, RECTOR, TELESCOPE)
- 1 architectural policy (FORM_BUILDER_SCOPE_POLICY)
- 1 sprint audit referenced from CLAUDE.md (WS-3-SESSION-1C-AUDIT)
- 2 reference docs (COPY_CATALOGUE, TEST_SCENARIO)

Skipped (12) with rationale documented in chat thread: one-time setup
docs, sprint-bound audits, end-user docs, superseded precursors, and
duplications of CLAUDE.md content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-10 14:07:22 +02:00
0d4afcd072 docs(rfc): amendment A-1 — TEST-INFRA-001 inserted before F2
Trigger: timetable-stabilization sprint (PR #18, #19) surfaced three
diagnostic incidents that the RFC v1.0 sequencing did not anticipate.

Adds TEST-INFRA-001 as prerequisite sprint before F2 (Playwright +
visual regression infrastructure, baselines against prototype HTML).
Extends F5 with dual-tier visual regression scope. Adds R-11 to risk
register, DoD-16 through DoD-20 to Definition of Done.

No changes to F2-F6 internal architecture, Aura preset, FormField API,
Tailwind v4, or bundle size targets.

Effort impact: +5-7 working days. Total now 15-19 days.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-10 13:53:52 +02:00
e2d9797de3 docs(rfc): WS-FRONTEND-PRIMEVUE migration plan v1.0 2026-05-10 02:14:15 +02:00
5d9399b03d docs(audit): WS-FRONTEND-PRIMEVUE F1 codebase inventory
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:57:12 +02:00
62afbdedf8 Updated the demo/mockup for the Artist Management / Timetable module 2026-05-10 01:28:45 +02:00
0d701bfed9 Merge pull request 'fix(timetable): mechanical-layer stabilization — seeder Model A, Zod decimal drift, freeze-panes layout, ?day URL flicker' (#19) from fix/timetable-stabilization into main
Reviewed-on: #19
2026-05-10 01:16:18 +02:00
4acf42429e docs(backlog): sharpen test-infra triggers; add ART-S4-UX-PARITY with seed-list scope
Three trigger upgrades + one new entry, in priority order:

TEST-INFRA-001 — trigger upgraded from "before opening Sessie 5" to
"eerstvolgende sprint na merge van fix/timetable-stabilization", with
explicit dependency: ART-S4-UX-PARITY and all Sessie 5+ work gate on
TEST-INFRA-001 merge. Reden quote captures the three sprint-blok
incidents that proved jsdom-tests do not protect against schema /
filter / UX drift.

TEST-VISUAL-001 — scope expanded to use the prototype HTML at
`./resources/Crewli - Artist  Timetable Management/` as the visual
baseline source (not hand-curated screenshots). Added explicit state
matrix per surface: PerformanceBlock 8 states + B2B + cascade-pulse;
PerformancePopover full detail; AddPerformanceDialog drag-mode +
button-mode; Wachtrij filtered/grouped axes. Trigger remains "tweede
toevoeging na TEST-CONTRACT-001" inside the TEST-INFRA-001 sprint.

TEST-CONTRACT-001 — unchanged. Trigger ("eerste e2e na TEST-INFRA-001
lands") was already correct.

ART-S4-UX-PARITY (NEW) — captures Bert's screenshot-report findings as
a seed list grouped A/B/C/D (component-shape / interaction / logic /
AddPerformanceDialog two-mode). Explicit pointer at the bottom to the
Phase A finalization report for the full 20-item itemisation with
severity ratings. Trigger gates Sessie 5 + all subsequent Artist-domain
frontend work behind ART-S4-UX-PARITY merge.

Spelling consistency: VEE-001 entry "formalized" → "formalised" to
match British-English already used elsewhere in the doc and now
mandated by the new CLAUDE.md "Diagnostic discipline" section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:16 +02:00
0f28af9f43 docs(claude): codify audit-before-assume principle as diagnostic discipline
New section in CLAUDE.md after "Order of work for each new module".

Three consecutive incidents in the timetable sprint led to formalising
this principle:

  - B1 (controller assumed buggy, seeder was wrong) — Phase A's
    schema-verify gate against SCHEMA.md:1285 + RFC §10.2 inverted the
    fix direction.
  - B5 (enum-shape assumed drifted, decimals were wrong) — Phase A's
    field-by-field response audit caught the actual decimal-as-string
    drift before any "fix" against the wrong hypothesis was written.
  - Timetable UX (test-passing layer diverged from prototype) — the
    mechanical-vs-UX split surfaced via browser test, not via the
    389-test suite which all agreed with the buggy state.

Pattern across all three: the initial hypothesis was wrong. The fix
prompts ALL gated Phase A as STOP-and-report; the schema/contract/
prototype audit was reviewed before any code was written. Codifying
this as an explicit project principle so future fix prompts inherit
the gate by default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:14 +02:00
8b678c0626 fix(timetable): eliminate ?day URL flicker by deriving isFlatEvent from event_type instead of subEvents length (B7)
Phase A finding A5 traced this race in the browser logs:

  GET .../performances?day={festival_id}  → 200, 0 results   ← wrong day
  GET .../children                        → 200, 3 sub_events
  GET .../performances?day={subevent_id}  → 200, 13 results  ← correct

The pre-fix `isFlatEvent` was:
  computed(() => !subEvents.value || subEvents.value.length === 0)

While `subEvents` was still loading (undefined), `!undefined` is `true`,
so isFlatEvent erroneously returned `true` for festivals during the
loading window. dayOptions then took the flat-event branch and seeded
validSubEventIds with the FESTIVAL id. useActiveDay's corrective watcher
rewrote the URL to `?day={festival_id}` and fired a wasted query that
returned zero results (correct semantics — performances live at sub-event
level — but waste + visible URL flicker).

Fix:
  computed(() => eventDetail.value?.event_type === 'event')

EventResource always serialises event_type (verified at
api/app/Http/Resources/Api/V1/EventResource.php:26). EventTabsNav
already consumes event_type / is_festival from the same shape
(apps/app/src/components/events/EventTabsNav.vue:175,266) so this is
the canonical signal, not a one-off addition.

New behavior trace:
  - Both queries pending  → eventDetail=undefined → isFlatEvent=false
                          → festival branch returns (subEvents ?? []).map(...)
                          → validSubEventIds=[] → activeDayId=null
                          → usePerformances.enabled=false → NO fetch
  - subEvents resolves first → festival branch populates dayOptions
                          → fetch fires with correct sub-event id
  - eventDetail resolves first to flat event → flat branch fires
                          → fetch with eventDetail.id (correct)
  - eventDetail resolves first to festival → still false until subEvents
                          → no false-positive flat-event fetch

402 tests still pass; typecheck + lint + production build all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:14 +02:00
bce3081cb2 test(timetable): add real-API contract fixtures as schema regression test (3 shape variants) (B6)
apps/app/tests/unit/schemas/timetableContractShape.test.ts (NEW, 5 tests):
  - base shape: one performance with stage assigned + full engagement
    (Bert's browser-tested sample, field-for-field). Asserts decimal-as-
    string contract on fee_amount/buma_percentage/vat_percentage AND
    enum-label wrapper on booking_status AND nested computed object.
  - parked shape: stage_id=null, stage=null (Wachtrij case)
  - multi-perf shape: two performances sharing engagement_id
    (RFC §D17 "Friday + Saturday under one combined deal")
  - sanity: individual performanceSchema parses each fixture element
  - regression guard: a payload with NUMBER fee_amount throws (locks
    out the pre-B5 bug class)

Every fixture spells out explicit `null` for the schema's nullable-but-
required fields (timestamps, notes, deal_breakdown) so the
nullable() vs optional() distinction is exercised, not glossed over.

Schema surface change to support the test:
  apps/app/src/schemas/timetable.ts now EXPORTS performanceArraySchema
  (previously a private const inside useTimetable.ts).
  apps/app/src/composables/api/useTimetable.ts imports the shared one
  instead of redeclaring it locally — single source of truth for the
  array shape consumers and tests share.

Test count: 397 → 402 (+5). Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:13 +02:00
1eee1f9415 fix(timetable): align Zod decimal fields with backend wire format (decimal-as-string per Laravel cast) (B5)
Phase A diagnosed the "Kon timetable niet laden" browser symptom as Zod
schema drift. The prompt's hypothesis (enum {value, label} mismatch) was
incorrect — the schema already uses the enumLabel() wrapper for every
enum field. The actual drift is decimal-cast columns: Laravel serialises
`decimal(N,M)` columns as strings to preserve precision, but the schema
expected numbers, so the very first response triggered a ZodError.

Affected fields, all on `artist_engagements`:
  fee_amount         decimal(10,2)  → wire `"11503.58"`, schema was z.number()
  buma_percentage    decimal(5,2)   → wire `"7.00"`,     schema was z.number()
  vat_percentage     decimal(5,2)   → wire `"21.00"`,    schema was z.number()
  deposit_percentage decimal(5,2)   → wire `"…"`,        schema was z.number()

Backend has no explicit `decimal:N` cast on these columns
(api/app/Models/ArtistEngagement.php:64-85 — the `casts()` method covers
the enums + booleans + dates + integers, but skips decimals).

Per the strategic decision (frontend adapts, backend stays):
  - schemas/timetable.ts: four fields → z.string().nullable()
  - types/timetable.ts: matching ArtistEngagement interface fields →
    `string | null`
  - PerformancePopover.vue:129: only consumer doing arithmetic on a
    decimal field; coerce at the use site via Number(...).toFixed(2).
    Single line.
  - tests/component/PerformanceBlock.test.ts + tests/a11y/axe.test.ts:
    spot-checked mocks; the two with hand-built engagement payloads
    flipped fee_amount/buma_percentage/vat_percentage from numbers to
    strings to match the new schema. No other mocks needed updating.

The {value, label} enum wrapper claim in the prompt was specifically
debunked in Phase A — every consumer (Wachtrij, PerformanceBlock,
WachtrijCard, PerformancePopover, AddPerformanceDialog, page entry)
already uses .value/.label access against an enumLabel-wrapped schema.

B6 will lock the wire-format contract with a real-API fixture
regression test.

All 397 tests still pass; typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:13 +02:00
3d4bd3fc38 test(timetable): row-height helper + StageHeaderCell prop seam (B4)
B4 — jsdom-runnable assertions for the structural pieces of B2/B3.

apps/app/tests/unit/lib/timetable/row-height.test.ts (4 tests):
  - laneCount=0 → 52px (Math.max(1, 0) fallback path)
  - laneCount=1 → 52px (single-lane stage row)
  - laneCount=3 → 148px
  - laneCount=10 → 484px (10 × 48 + 4)

apps/app/tests/component/StageHeaderCell.test.ts (4 tests):
  - row-height-px prop applies as inline blockSize on the root
  - prop omitted → no inline blockSize set (legacy `block-size: 100%`
    CSS path takes over for any caller still relying on parent-driven sizing)
  - 484px for laneCount=10 round-trips through the prop without truncation
  - conflict badge renders only when conflictCount > 0 (existing behavior;
    locked in as part of touching this surface)

Visual scroll/alignment proof (sticky-left freeze pane, sticky-top axis,
horizontal scroll cohesion across 14 stages, diagonal trackpad scroll,
pixel-perfect header↔row alignment) is deferred to TEST-VISUAL-001
explicitly: jsdom does not compute position:sticky offsets, scrollbar
visibility, layout overflow chains, or scroll containment ancestry. This
is a known limitation of jsdom-based component testing — not a test gap
in this branch. The sticky behavior, z-index ladder, and DOM structure
are all in place per E1-E4; their validation requires a real browser,
which is exactly what the Playwright CT migration on TEST-INFRA-001 +
TEST-VISUAL-001 unlocks.

No existing tests asserted the old broken layout (no references to the
deprecated `tt-page__rows`, `tt-page__stages`, or `<GridBg>` in tests/).
The unused GridBg component file remains on disk; deleting it is a
stylistic cleanup outside this stabilization scope.

Test count: 389 → 397.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:12 +02:00
82ca920b81 fix(timetable): canvas layout — sticky-left + sticky-top freeze panes, single canvas scroll (B3)
Restructures the canvas so the spreadsheet-feel works correctly with
the seeder's 14 stages: horizontal scroll moves the rows AND the
TimeAxis together; vertical scroll moves the rows but keeps TimeAxis
pinned; both panes intersect at a fixed corner cell. Diagonal trackpad
scroll behaves naturally because there's only one scroll container.

DOM restructure (E2 — sticky resolves to its nearest scroll ancestor;
fixed by giving sticky elements the right scroll-container parent
instead of patching with absolute positioning):

  .tt-page__canvas           position: relative; overflow: auto
   └ .tt-page__layout        display: grid; grid-template-columns: 200px auto;
                             inline-size: max-content
      ├ .tt-page__corner     sticky top:0 left:0  z=3
      ├ .tt-page__axis       sticky top:0         z=2  (full 1872px wide, no clip)
      └ for each stage:
        ├ .tt-page__header-cell  sticky      left:0  z=2
        │  └ <StageHeaderCell :row-height-px="row.rowHeightPx">
        └ .tt-page__row-cell     normal              z=1  (height = same value)
           └ <StageRow>

Z-index ladder (E1) is documented in the page CSS:
  corner=3, axis row=2, header rail=2, row content=1, blocks=auto.
Popover + AddPerformanceDialog stay above via Teleport-to-body.

Drops the broken pre-stabilization layout:
  - `grid-template: "corner axis" 28px "stages rows" 1fr / 200px 1fr`
    that put ALL stage headers in ONE grid cell (cause of "lanes too tall"
    via headers stretching to 100% of the 570px cell)
  - nested `overflow: auto` on `.tt-page__rows` (cause of horizontal-scroll
    desync — only the rows pane scrolled, axis stayed put)
  - `overflow: hidden` on `.tt-page__axis` (E4 — clipped axis ticks beyond
    the 1fr cell width)
  - `<GridBg :total-height="0" />` which was a no-op anyway; gridlines now
    render directly on each `.tt-page__row-cell` background

`inline-size: max-content` on the layout grid forces it wider than the
canvas viewport, so `overflow: auto` on the canvas actually fires a
horizontal scrollbar. Without this, the `auto` second column shrinks to
viewport and nothing overflows.

The page now passes `:row-height-px` to StageHeaderCell (B2 seam, now
load-bearing). Both header and row cell get the same explicit blockSize
inline so the freeze panes align pixel-for-pixel under whatever
laneCount each stage resolves to.

Visual scroll/alignment proof is deferred to TEST-VISUAL-001 — jsdom
cannot verify position:sticky behavior, scrollbar visibility, or pixel
alignment of the freeze panes. This is a known limitation, not a test
gap. B4 covers the structural assertions jsdom CAN verify.

All 389 existing tests still pass; production build smoke clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:12 +02:00
4d2282f546 refactor(timetable): extract computeStageRowHeight helper; StageHeaderCell takes :row-height-px prop (B2)
Pure structural seam — no layout changes yet (B3 wires the page through).

apps/app/src/lib/timetable/row-height.ts (NEW):
  computeStageRowHeight(laneCount, laneHeightPx, lanePadPx) — one-line pure
  function with the existing math: max(1, laneCount) * (laneHeight + lanePad) + lanePad.
  Math.max(1, laneCount) keeps an empty stage row visible at single-lane
  height instead of collapsing.

apps/app/src/components/timetable/StageRow.vue:
  Switches its inline rowHeightPx computation to call the helper. Behavior
  identical (the math was the helper's body).

apps/app/src/components/timetable/StageHeaderCell.vue:
  New optional `rowHeightPx?: number` prop. When provided (B3 will pass it
  from the page via the same helper), the header root applies blockSize
  inline so the sticky-left column aligns pixel-for-pixel with the row.
  When omitted, the legacy `block-size: 100%` CSS still applies — every
  existing call-site keeps working.

apps/app/src/lib/timetable/index.ts: re-export the new helper.

Tests still green (389 across 54 files); typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:12 +02:00
006755ac1b fix(seeder): align ArtistTimetableDevSeeder to canonical engagement-vs-performance model (B1)
Phase A diagnosed an empty SPA timetable as a controller filter bug. B1.1's
schema-verify gate proved the opposite: the seeder violates Model A, the
controllers are correct.

Canonical model (Model A) per:
  - dev-docs/SCHEMA.md:1285  artist_engagements.event_id → festival OR flat event
  - dev-docs/SCHEMA.md:1329  performances.event_id      → sub-event OR flat event ("show host")
  - dev-docs/RFC-TIMETABLE-Artist-Timetable-Module.md:1247-1257 (§10.2 contract)
    "performance.event_id must be flat event OR a sub-event of the
     engagement.event_id festival"
  - dev-docs/RFC-TIMETABLE-Artist-Timetable-Module.md:455-477 (§D17)
    "Friday + Saturday under one combined deal = 1 engagement, 2 performances"
    — only works if engagement is at festival level

Controller audit (B1.2): all five filters in
api/app/Http/Controllers/Api/V1/Artist/{PerformanceController,
ArtistEngagementController, StageController}.php already match Model A.
No controller changes needed.

Seeder change (B1.3) — single consistent fix:

ArtistTimetableDevSeeder::seedForFestival now creates one engagement per
(artist, festival) instead of per (artist, sub-event). When the same artist
recurs across iterations on different sub-events, the existing engagement
is reused and another performance is added (the D17 multi-perf path).
Performances continue to carry event_id = sub-event.

Same model fix in seedForSeries (engagement at parent series, performance
at week sub-event).

seedForFlatEvent already conformed (engagement.event_id = performance.event_id
= the flat event itself).

Existence-check semantics shift from `where event_id = $subEvent->id` to
`where event_id = $festival->id` (or $parent->id for series). Numerically
the test counts hold because the bucket-cycling makes scheduled artists
distinct within the festival window.

Tests (B1.4) — new TimetableSeederControllerIntegrationTest with 7 assertions:
  - engagement.event_id is at festival level (DB invariant)
  - performance.event_id is at sub-event level (DB invariant)
  - GET /performances?day={subEvent} returns non-empty + correct event_ids
  - GET /performances unfiltered returns all sub-event performances
  - GET /performances?stage_id=null returns the seeded parked perf
  - GET /engagements returns engagements with event_id = festival
  - GET /stages returns 5 stages with event_id = festival

This locks the visible-symptom regression from Session 4: an empty SPA
timetable on a freshly-seeded festival cannot land again silently.

Existing ArtistTimetableDevSeederTest (4 tests) and the broader Artist
suite (121 tests) all stay green. composer analyse + Pint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:11 +02:00
74b802a803 Merge pull request 'RFC-TIMETABLE v0.2 Session 4 — Frontend Timetable + Test Coverage Closure' (#18) from feat/timetable-session-4 into main
Reviewed-on: #18
2026-05-10 00:32:34 +02:00
89d137e714 Add addtional test data using seeders for Artist Management module 2026-05-09 20:06:52 +02:00
3b255a36de feat(events): add Programma tab to EventTabsNav for timetable access
The timetable canvas page at /events/{event}/timetable was added in
RFC-TIMETABLE Session 4 but had no UI entry point. EventTabsNav now
exposes it as the "Programma" tab between Artiesten and Briefings on
flat events, and between Artiesten and Briefings on festivals (in the
re-ordered tab list, post-Artiesten / pre-Briefings).

Changes:
- baseTabs gains the Programma entry at position 6 (after Artiesten).
- The festival re-order computed switches from positional indexing
  (baseTabs[5], [6], [7]) to name-based lookup via a findTab helper —
  insertions to baseTabs no longer break the festival branch.
- Icon: tabler-calendar-time. Conservative Dutch label "Programma" —
  doesn't collide with "Programmaonderdelen" (the festival sub-events
  page) since festivals see both tabs side-by-side.

vitest.config.ts: extend the component-project AutoImport to include
'vue-router' so tests of components that auto-import useRoute/useRouter
mount cleanly. (EventTabsNav was the first such test.)

tests/component/EventTabsNav.test.ts (NEW, 4 assertions):
- Programma tab is rendered with the correct label
- it carries the tabler-calendar-time icon
- the route binding resolves to the events-id-timetable name with the
  /events/.../timetable URL pattern
- the tab is also visible on a festival (re-ordered tab list path)

Mocks the useEvents composables so the component skips its skeleton/
error branches and renders tabs immediately.

Test count: 385 → 389.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-09 08:58:22 +02:00
fb5ba5052e chore(timetable): drop unnecessary void on router.replace inside useActiveDay glue
Lint cleanup spotted during Phase C — `router.replace` returns Promise<void>
which the no-void rule rejects. The dropped void had no behavioural effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:58:37 +02:00
a156fe2a53 docs(backlog): add TEST-INFRA-001, TEST-CONTRACT-001, TEST-VISUAL-001 with sharp triggers; close ART-S4-TESTS
Three new entries that codify the test-architecture roadmap surfaced
during the Session 4 follow-up:

TEST-INFRA-001 — Migrate timetable component+a11y tests to Playwright
Component Testing. **Trigger: before opening the Sessie 5 prompt.**
Sessie 5 builds Engagement Detail (6 tabs) + Portal pages (drag-to-
reorder, file uploads); adding more jsdom-based tests for those
surfaces compounds the migration cost.

TEST-CONTRACT-001 — End-to-end 409 contract test against running Laravel.
Trigger: first e2e flow added after TEST-INFRA-001 lands. Highest
contract-protection value per line of test code.

TEST-VISUAL-001 — Visual regression baselines for PerformanceBlock
states (RFC D21/D22/D25/D26). Trigger: second addition to the
TEST-INFRA-001 sprint.

ART-S4-TESTS marked  Resolved with the audit trail of all 9 commits
that landed the test coverage closure (252 → 385 tests across both PRs).

.claude-sync/ regenerated by the post-commit hook (gitignored;
re-uploaded to Project Knowledge separately).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:57:02 +02:00
985a5ab987 test(timetable): full add → drag → resize → park → delete integration flow (Step 12)
Two integration tests that drive the entire RFC D17 lifecycle through
the mutation composable + TanStack cache:

  1. happy-path lifecycle (5 stages):
     - ADD       → POST /performances + Idempotency-Key
     - DRAG      → POST /timetable/move (target_lane=1, version bumps),
                   server returns cascaded[] sibling — both surface in
                   the resolved Promise
     - RESIZE    → POST /timetable/move with new end_at + new version
     - PARK      → POST /timetable/move with target_stage_id=null
     - DELETE    → DELETE /performances/p1
     final wire: 4 POSTs + 1 DELETE
  2. drag rollback on 409:
     - server returns version_mismatch
     - mutation rejects with VersionMismatchError shape
     - notification.show() invoked with the Dutch toast + 'error'

Why not the full page mount: events/[id]/timetable/index.vue requires
EventTabsNav, useEventDetail, useEventChildren, multiple VTabs/VBtn/
VDialog teleports — too brittle for jsdom CI. The end-to-end + visual
flavour of this flow lives on TEST-INFRA-001's Playwright migration
backlog (and TEST-CONTRACT-001 covers the 409 path against a real
backend).

Test count: 383 → 385.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:55:22 +02:00
66a6f7ddc3 test(timetable): axe-core zero-violation a11y enforcement (Step 11)
Three jsdom axe scans covering the user-facing surface of the canvas.
The scans surfaced two real a11y bugs which are fixed in this same
commit:

  1. PerformancePopover — VProgressLinear (advancing aggregate) had no
     accessible name. Added aria-label that announces "X van Y secties
     afgerond (N%)".
  2. AddPerformanceDialog — the icon-only close button (×) was missing
     aria-label. Added 'Sluiten'.

Test scenarios:
  - PerformanceBlock with focus
  - PerformancePopover open
  - AddPerformanceDialog open

Page-level axe rules (region, page-has-heading-one, landmark-one-main,
color-contrast) are disabled for fragment scans — they only make sense
on a full page, and color-contrast resolution is jsdom-blind. Both are
covered by Playwright CT in TEST-INFRA-001 / TEST-VISUAL-001.

Test count: 380 → 383.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:53:16 +02:00