Commit Graph

14 Commits

Author SHA1 Message Date
641ca5131d fix(theme): remove webfontloader Public Sans path (AD-2.5-T1 completion)
P2 reverted CSS font-family to Inter but missed the JS font-loading
path: src/plugins/webfontloader.ts loaded Public Sans from Google
Fonts via WebFont.load(). The wf-publicsans-n4-active class on <html>
(found during P5 manual smoke) proved Public Sans was still loaded at
runtime, plus an external Google Fonts CDN request — both contrary to
AD-2.5-T1 (local @fontsource/inter, no CDN).

Audit context: the plugin was auto-registered via the Vuexy
registerPlugins() glob (src/@core/utils/plugins.ts walks
plugins/*.{ts,js}). No explicit import / call site to delete — file
removal is enough. The plugin only ever loaded Public Sans (no other
families), so full deletion is correct.

Changes:
- Removed src/plugins/webfontloader.ts (auto-registration falls away
  with the file itself; no manual unregister needed).
- Removed webfontloader (1.6.28) + @types/webfontloader (1.6.38) from
  package.json / pnpm-lock.yaml.
- Strengthened tests/unit/styles/typography.spec.ts with a new
  describe block that scans every src/plugins/*.ts for: any
  webfontloader reference, any WebFont.load call, any "Public Sans"
  spelling, any fonts.googleapis.com URL. Plus a regression-lock
  spec asserting webfontloader.ts itself stays deleted.

Suite delta: 552 → 554 (+2 new JS-path specs). vue-tsc clean.
Scoped ESLint clean (0 errors).

Manual smoke pending (Bert): hard-reload /v2/dashboard, confirm
- wf-publicsans-* and wf-active classes are gone from <html>
- computed font-family on body text starts with "Inter"
- Network tab has no fonts.googleapis.com request

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:25:28 +02:00
41af180168 feat(theme): Plan 2.5 P2 — Inter typography (AD-2.5-T1)
Per RFC-WS-PRIMEVUE-PLAN-2-5 §4 AD-2.5-T1. Establishes Inter as the
canonical Crewli body font via @fontsource/inter (local package, no
Google Fonts CDN).

Audit findings (pre-change):
- No @fontsource/public-sans package was installed.
- No <link> tag in index.html loaded Public Sans.
- Only one Public Sans reference existed in source: the vendored
  Vuexy SCSS variable $font-family-custom at
  src/@core/scss/template/libs/vuetify/_variables.scss, which drives
  Vuetify's $body-font-family on legacy surfaces during F4.
- No src/main.css exists; the Tailwind v4 entry lives at
  src/assets/styles/tailwind.css with no @theme block yet.

Changes:
- @fontsource/inter@^5.2.8 added to dependencies; weights
  400/500/600/700 imported at main.ts ahead of tailwind.css.
- src/assets/styles/tailwind.css: new @theme block declaring
  --font-sans Inter-first, plus :root --crewli-font-family and
  html/body font-family applying that variable cascade-wide.
- src/@core/scss/template/libs/vuetify/_variables.scss:
  $font-family-custom switched from the historical body font to
  Inter (vendored edit, narrowly scoped, F6 removes @core/ entirely).
- tests/unit/styles/typography.spec.ts: 3-spec regression lock
  (Tailwind direct stacks, Vuexy SCSS variable, zero historical
  references in either file). File-content inspection — jsdom does
  not cascade from imported stylesheets, so getComputedStyle would
  always pass.

Suite delta: 570 → 573 (+3; the prompt's template was +2 but the
audit revealed two distinct font-config files, so each gets its own
assertion per the prompt's "cover all sites" rule). vue-tsc clean.
Scoped ESLint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 08:36:11 +02:00
1a66ac6e64 refactor(gui-v2): delete X.vue stub, repoint 2 boundary refs to StatusTag, add shared/* regression locks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 13:48:21 +02:00
20af2ebd32 feat(gui-v2): statusSeverity SoT map + bidirectional §8.X consistency test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:23:44 +02:00
a341a60412 feat(boundaries): add layouts-v2 zone so v2 shell layout can use components-v2
AD-G2 ("OrganizerLayoutV2 wraps AppShellV2") was in tension with AD-G5:
src/layouts/OrganizerLayoutV2.vue classified as the v1 `layouts` zone,
which is deliberately barred from components-v2. New `layouts-v2` zone
(src/layouts/*V2*.vue, mode:file) gets pages-v2-equivalent v2 capability;
the v1 `layouts` zone is unchanged so v2 isolation is preserved. RFC
AD-G5 amended; locked by 3 boundaries-v2.spec.ts regression tests (7/7).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 02:03:00 +02:00
4e9eeb99c4 fix(lint): mode:'file' for the components-foundation Icon.vue bridge
Plan-1 Task-4 added { type:'components-foundation', pattern:
'src/components/Icon.vue' } without mode:'file'. eslint-plugin-boundaries
defaults to folder mode, so the single-file pattern never matched and
Icon.vue fell through to the generic `components` catch-all — breaking
the sanctioned components-v2 -> Icon bridge (RFC AD-G5) for every v2
shell component. Plan-1's boundary test only exercised the forms/**
folder-glob edge so the gap was latent. Adds mode:'file' + a regression
test locking the components-v2 -> Icon.vue edge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 19:23:00 +02:00
2465290614 docs(test): note boundaries/element-types deprecated-alias coupling
Documents that the test filter and the .eslintrc rule key both use the
v5-era 'boundaries/element-types' alias; a future eslint-plugin-boundaries
bump that drops the alias must update both together or the filter silently
matches nothing. Addresses the Task 4 code-review Minor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:55:02 +02:00
b1d3b9f53b feat(lint): add components-v2/pages-v2 boundary zones (no back-port)
Adds three new eslint-plugin-boundaries element zones and their matrix
rows so the GUI-redesign v2 surface is structurally isolated: v1 code
cannot import from v2 (back-porting forbidden), v2 can reach the
narrow FormField/Icon bridge via the components-foundation zone, and
pages-v2 can import from components-v2. Backed by a Vitest spec
running via the ESLint Node API (node environment; happy-dom's
document object breaks the case-police resolver). Adds a placeholder
src/components-v2/shared/X.vue so the resolver can classify the
import target during the test (unresolvable imports are not boundary-
checked by the plugin).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:46:31 +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
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
fbfe72d090 test(timetable): useTimetableMutations 409 rollback + idempotency-key semantics (Step 9)
Minimal seam in src/composables/api/useTimetableMutations.ts: the move()
mutation's onError now calls useNotificationStore().show(...) on a 409
status. Generic axios errors stay quiet here — the global response
handler in lib/axios/factory.ts already toasts those. RFC D14 wanted
the version-mismatch toast specifically.

apps/app/tests/component/useTimetableMutations.test.ts (NEW, 5 tests):
  - on success: returns server payload with bumped version + sends the
    Idempotency-Key supplied by the caller
  - 409: rejects with VersionMismatchError + notification.show()
    invoked once with the Dutch translation + 'error' level
  - cascade: success with cascaded[] populated puts those peers into
    the result.cascaded array
  - Idempotency-Key uniqueness: two distinct logical move() calls send
    distinct keys
  - Idempotency-Key reuse: caller-controlled retry within the same
    logical action sends the SAME key on the wire (so the backend's
    60s idempotency middleware dedupes)

The two existing unit-project tests now register a Pinia instance
(createPinia + setActivePinia) so useNotificationStore() resolves.
Existing assertions unchanged.

Test count: 364 → 369.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:48:39 +02:00
e99acbde95 fix(timetable): make ?day query the source of truth with validation and fallback
Per Phase A finding A6 — the previous three-watcher Pinia-store design had
no validation. Landing on /events/{e}/timetable?day=DOES_NOT_EXIST quietly
set store.activeDayId to that bogus value and showed an empty page.
Cross-org sub-event IDs were silently accepted (backend OrganisationScope
returned an empty perf list, so the UI looked broken without telling the
user).

New design (Session 4 follow-up Step 5):

- src/composables/timetable/useActiveDay.ts (NEW)
  - The URL `?day` is the source of truth; Pinia does NOT hold this value.
  - `activeDayId` is a computed: queryDay if it appears in `validIds`,
    else the first valid id, else null when the list is empty.
  - One corrective watcher (immediate:true, flush:'post') quietly rewrites
    the URL when `?day` is missing or invalid; runs after Vue settles and
    after validIds has been recomputed from a fresh fetch.
  - `setActiveDay(id)` is the user-driven entry point — calls replace().
  - Cross-org IDs are blocked transparently: OrganisationScope keeps them
    out of validIds, so they fail the .includes() check and fall back.

- src/stores/useTimetableStore.ts
  - Removed `activeDayId` state and `setActiveDay()` action; the store
    docstring now documents that day-state lives at the URL.

- src/pages/events/[id]/timetable/index.vue
  - Replaced the three watchers + onMounted bootstrap with one
    `useActiveDay({ queryDay, validIds, replace })` call. The day-change
    side-effect watcher (clear drag, deselect performance) stays.
  - VTabs binds dayIdRef + setActiveDay directly.

- tests/unit/pages/timetableDaySync.test.ts (NEW, 9 tests)
  - Valid ?day=X → activeDayId=X, no URL rewrite.
  - Missing / invalid / cross-org ?day → fallback + URL replaced once.
  - Empty validIds → activeDayId=null, URL untouched.
  - setActiveDay(id) → calls replace.
  - setActiveDay(null) → no-op.
  - External URL change (browser back) → activeDayId follows.
  - validIds populated AFTER mount → fallback fires correctly.

- tests/unit/stores/useTimetableStore.test.ts: assert that activeDayId
  and setActiveDay are GONE from the store surface.

Test count: 324 → 333.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:37:31 +02:00
8d1cb39172 feat(timetable): validate API responses against Zod schemas at runtime
Per Phase A finding A5 — Zod schemas in @/schemas/timetable.ts were
types-only; nothing parsed actual server responses. Backend → frontend
contract drift would only surface as TypeError deep in components.

useTimetable.ts queries now parse:
  - useStages       → stageArraySchema.parse()
  - usePerformances → performanceArraySchema.parse()
  - useWachtrij     → performanceArraySchema.parse()
  - useEngagement   → artistEngagementSchema.parse()

useTimetableMutations.ts mutations now parse:
  - move success    → moveTimetableSuccessSchema.parse()
  - move 409 errors → moveTimetableConflictSchema.parse() (the .errors
                      sub-object — see backend canon at TimetableMoveController:64)
  - create / updateNotes → performanceSchema.parse()
  - createStage / updateStage → stageSchema.parse()

The move() success parse runs OUTSIDE the try/catch so a Zod failure on
a 200 response surfaces as a true error rather than being misclassified
as a 409. Per Phase A finding A8 the conflict shape already matches
backend field-for-field; no schema correction needed, but the parse()
locks future drift in.

Regression test (tests/unit/composables/api/zodParseFailure.test.ts):
  - move() success with missing fields → rejects with ZodError
  - move() 409 with malformed errors payload → rejects with ZodError
  - createStage() with missing fields → rejects with ZodError

Existing test fixture for createStage was missing created_at/updated_at;
fixed in same commit (real backend responses always include them).

Test count: 321 → 324.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:32:21 +02:00
39fdc0fa3d test(timetable): Phase C — 67 new tests (pure logic + composables + store + schemas)
apps/app/tests/unit/lib/timetable/:
  - snap.test.ts (5)            — rounding, clamp, edge cases
  - time-grid.test.ts (6)       — px↔min↔ISO roundtrips, formatTickLabel
  - conflict.test.ts (8)        — overlap, endpoint-touching, lane/stage scoping, cancelled exclusion
  - b2b.test.ts (6)             — 0min, 2:59, 3:01, overlap, side-set mapping, threshold constant
  - capacity.test.ts (7)        — null capacity, missing data, warn/critical, crew+guests preference
  - lane.test.ts (8)            — Pass 1 + Pass 2, cascade-bump preview, cancelled exclusion

apps/app/tests/unit/composables/:
  - useTimetableMutations.test.ts (5) — Idempotency-Key header, optimistic + cascade,
                                         409 VersionMismatch surfaced, park sends null,
                                         createStage POST path
  - useDragOrClick.test.ts (3)        — onClick fires under threshold, onDragStart+End
                                         above threshold, Esc cancels mid-flight

apps/app/tests/unit/schemas/timetable.test.ts (8) — payload + response zod parsers
apps/app/tests/unit/lib/idempotencyKey.test.ts (3) — 6-30 char range, 24-hex, uniqueness
apps/app/tests/unit/stores/useTimetableStore.test.ts (5) — defaults, toggleStatus, drag state, null guard

Refactor: useTimetableMutations.move now throws Error instances (no-throw-literal)
so AxiosError.message and the VersionMismatchError shape both bubble through .catch().

Test count: 252 → 319 (+67). All 42 files pass.

Out of scope this session (added to BACKLOG):
- ART-PERFORMANCEBLOCK-COMPONENT-TESTS — Vuetify intentionally not loaded in
  vitest.config.ts; a Vuexy-stub setup for component-mount tests is one PR of
  its own. Pure rendering logic (capacity, B2B, conflict) is fully covered at
  the lib/ layer.
- ART-AXE-CORE-A11Y-TESTS — axe-core not yet installed in the repo. The
  aria-label structure on PerformanceBlock + aria-live on the page entry are
  authored to pass an axe scan when added.
- ART-INTEGRATION-FLOW-TEST — full add → drag → resize → park flow needs
  Vuetify + router + msw setup; defer with the component tests above.

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