Strict-regex sweep of apps/app/src/ confirms zero VeeValidate usage:
no `from 'vee-validate'` imports, no <Field|Form|ErrorMessage>,
no defineRule(), no useForm(). The 15 prior fuzzy matches were
false positives where /useForm/ matched useFormDraft/useFormSteps/
useFormSchemas/useFormFailures.
Changes:
- Remove `vee-validate` and `@vee-validate/zod` from apps/app/package.json
- Regenerate pnpm-lock.yaml (no other deps shifted)
- CLAUDE.md "Forms": replace VeeValidate prescription with the actual
ref + @core/utils/validators + Zod-payload-schema pattern that the
codebase already uses everywhere
- VUEXY_COMPONENTS.md: correct the stale "Registration uses VeeValidate"
claim (the page actually uses useFormDraft + validators); update the
"Form validation" reference row
- BACKLOG.md: close VEE-001 with the audit trail
All 319 existing tests still pass; vue-tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drift from Session 4 step 11 — unplugin-vue-components and unplugin-vue-router
regenerated their .d.ts files for the new timetable surface. Was missed in the
original commit because the test runner doesn't trigger regen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PerformancePopover.vue — teleported floating panel; closes on Esc; shows
status chip, advancing %, computed Buma/VAT/total cost; deal-summary +
delete + open-detail buttons. Position math (340px wide, 12px margin,
flip side if no room) ports prototype's pickPos verbatim.
- AddPerformanceDialog.vue — Vuetify VDialog + raw ref form pattern (matches
CreateShiftDialog and the rest of the codebase). Uses createPerformancePayloadSchema
for client-side validation; falls back to surface-level errors map per field.
- StageEditor.vue — single-stage CRUD modal with name + capacity + 10-swatch
palette picker. Window.confirm cascade-park warning on delete.
- LineupMatrix.vue — stages × sub-events checkbox matrix; only dirty stages
fire replaceStageDays (atomic per stage).
- Wachtrij.vue — sidebar with search + 9 toggleable status chips with counts;
reads/writes useTimetableStore.statusFilter and searchQuery.
- WachtrijCard.vue — initials avatar + status dot + dot label + cancelled
strike-through. role=button, tabindex=0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
usePointerDrag — PointerEvents primitive with capture, escape-cancel,
keyboard-cancel, and onBeforeUnmount cleanup. Replaces the legacy
mousedown stack the prototype used.
useDragOrClick — threshold-based drag/click disambiguation (4px Manhattan,
matches prototype audit §4.1). Emits onClick when the pointer never crossed
the threshold; otherwise enters drag mode and emits onDragStart / onDragMove /
onDragEnd. Installs the one-shot capture-phase click suppressor on drag-end
so the synthetic click never opens the popover.
RFC v0.2 D7 — implemented once instead of three times like the prototype.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PerformanceBlock is the heart of the canvas:
- Status palette via CSS tokens (D21) — one class per booking_status enum value
- Cancelled hatch overlay + line-through (D5)
- Trashed-artist dashed border + ⌂ overlay icon (D27)
- Conflict ring + glow when warnings.includes('overlap') (D5)
- Capacity icon driven by evaluateCapacity() with warn/critical levels (D25)
- B2B left/right dots (D26 — 3-min threshold)
- Cascade-pulse class fired by parent on cascaded[] non-empty (D18)
- aria-label structure per D20: artist, stage, time window, status, advancing
- tabindex 0 + Enter/Space → select; Delete → emit delete
StageRow positions blocks by lane_resolved (D19) — server is authoritative.
StageHeaderCell uses Vuexy VMenu pattern for the per-stage actions.
EmptyDayState routes the user to LineupMatrix when no stages are active.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Extract generateIdempotencyKey() from useFormDraft into reusable lib/
- New types/timetable.ts mirrors PerformanceResource, ArtistEngagementResource,
StageResource, GenreResource and the four enums verbatim
- New schemas/timetable.ts adds zod parsers for runtime validation of API
responses + form payloads (createPerformance, createStage, moveTimetable)
RFC v0.2 §10 contract surface for the upcoming timetable canvas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR-3 follow-up. Live smoke surfaced that the @sentry/vue SDK was
running correctly and emitting events, but Crewli's strict
connect-src directive blocked every POST at the browser layer. No
fallback — events evaporated silently with a CSP-violation log in
DevTools console only.
Updated locations (audited the CSP surface; only two locations actually
need the whitelist):
- apps/app/index.html — dev meta CSP, adds http://localhost:8200 to
connect-src so local dev hits the docker-compose GlitchTip stack.
- deploy/nginx/csp-spa.conf — prod organizer SPA CSP, adds
https://monitoring.hausdesign.nl to BOTH the report-only and enforce
add_header lines so a future flip between modes can't silently break
observability.
NOT updated (deviation from prompt):
- api/config/security.php — the API CSP is `default-src 'none';
frame-ancestors 'none'` for JSON responses. Browsers don't enforce
connect-src on JSON contexts (no document, no fetch origin). Adding
connect-src would be semantically a no-op and confuse the deny-by-
default policy.
Regression guard: tests/Feature/Security/CspConnectsToObservabilityTest.
Reads both the dev meta tag and the prod nginx conf directly (the SPA's
CSP is not Laravel-served, so $this->get() can't reach it). Apply-with-
revert verified: stashing both fixes makes both cases fail with a clear
"Refused to connect because it violates the following CSP directive"
hint; popping the stash restores green.
SECURITY_AUDIT.md A13-9 updated with a WS-7 follow-up note documenting
the GlitchTip whitelist as an explicit security control: outgoing
observability traffic restricted to a single known host.
Test count 1549 to 1551. Larastan + Pint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WS-7 PR-3 commit 2.
- scrubber.spec.ts (18 cases): mirrors backend PiiScrubbingTest semantics.
Body/header/query scrubbing, form_values wholesale replacement, all
SENSITIVE_BODY_KEYS at top + nested levels, max_depth guard, cookies +
storage + user.cookies sanitisation.
- contextBinding.spec.ts (11 cases): exercises the Vue Router beforeEach
guard against a real router with mocked Sentry scope (capturing every
setTag/setUser call into a per-test buffer). Cases:
- portal-token zone — actor_scope=portal, no user_id
- platform route + super_admin — actor_scope=platform
- platform route without super_admin — does NOT tag platform
- organizer route with active org — actor_scope=organisation +
organisation_id
- organizer route without active org — actor_scope=user, no org tag
- unauthenticated public — actor_scope=anonymous
- actor_type role hierarchy
- RFC §3.8 ULID-only user identity (no email leakage)
- route_name + app=app baseline tags
- cross-zone leak guard: navigating from organizer to portal-token
calls scope.clear() and does not bind user
Frontend test count 223 to 252. Typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WS-7 PR-3 commit 1. Frontend mirror of the backend SDK install
(commits bdb89a2..adab3be), wired against the existing apps/app SPA.
- pnpm add @sentry/vue@10.52.0 (pinned).
- src/observability/sentry.ts: initSentry() — empty DSN no-op (RFC §3.3),
errors-only (tracesSampleRate=0, profilesSampleRate=0; RFC §2 amend.B),
sendDefaultPii=false, Console integration off, beforeSend wired to the
scrubber, initial scope tag app=app for GlitchTip filtering.
- src/observability/scrubber.ts: TypeScript port of backend
SentryEventScrubber. RFC §3.7 frontend block — body / header / query
scrubbing, form_values wholesale replacement, cookies wholesale,
defensive strip of contexts.storage and user.cookies, max-depth guard.
- src/observability/contextBinding.ts: Vue Router beforeEach guard that
binds RFC §3.6 auth-scope tags per navigation. Three zones via
route.meta.public + route.path matching:
- portal token zone (meta.public + meta.context=portal) → actor_scope=
portal, no user_id (RFC §3.6 explicit)
- /platform/* with super_admin → actor_scope=platform, no org tag
- default authenticated → actor_scope=organisation when an active
organisation is selected (useOrganisationStore.activeOrganisationId),
otherwise actor_scope=user
- unauthenticated public pages → actor_scope=anonymous
Reads useAuthStore (user, appRoles, isSuperAdmin) and
useOrganisationStore (activeOrganisationId) — corrected vs. RFC's
speculative auth-store API.
- src/observability/index.ts: barrel.
- src/main.ts: initSentry runs before registerPlugins so Sentry's Vue
errorHandler hooks before any plugin or component initialises;
installContextBinding runs after registerPlugins so pinia is up.
- env.d.ts: VITE_SENTRY_DSN_FRONTEND + VITE_SENTRY_RELEASE typed.
- .env.example: new file (didn't exist before) documenting all SPA env
vars including the new Sentry pair.
- vite.config.ts: build.sourcemap=true (RFC §3.5 — generated, uploaded
to GlitchTip by deploy.sh, then stripped before nginx serves dist/).
Typecheck: green. Build: green, *.map files emitted alongside *.js
chunks as expected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the WS-3 PR-B2a minimum precaution (`startsWith('/') &&
!startsWith('//')`) with a layered validator that rejects every input
that is not a strict relative path.
isSafeRelativePath rejects:
- Empty / null / undefined input
- Non-`/`-prefixed paths (including leading whitespace)
- Protocol-relative URLs (`//evil.com`)
- Backslash anywhere (browsers normalise `\` → `/` in some contexts;
`/\evil.com` parses as `//evil.com`)
- ASCII control characters `\x00`–`\x1F` and `\x7F` (NUL, tab, LF, CR,
DEL, etc. — header-injection vectors)
- Anything the URL constructor parses to a different origin than the
synthetic invalid origin used as the resolution base
The URL-constructor check is the authoritative guard; the prefix and
character checks are fast pre-filters that short-circuit common
attack shapes without paying the URL allocation.
Test coverage expands from 6 → 16 cases. New cases pin the
backslash, control-character, leading-whitespace, and positive-
character-set contracts. The URL-encoded-slash-in-query case
documents that we don't false-positive on `%2F` in query strings.
Closes A13-3 (open-redirect on post-login).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The card consumed the API directly via useVerifyMfa() (TanStack Query
mutation). Per Decision F's intent (store owns business logic, the
component consumes typed results), the card now calls
useAuthStore.verifyMfa() and pattern-matches on the MfaVerifyResult
discriminated union.
Changes:
- MfaChallengeCard: drop useVerifyMfa import; call authStore.verifyMfa
with camelCase args (sessionToken, trustDevice, deviceFingerprint,
deviceName); local isVerifying ref replaces verifyMutation.isPending.
On result.kind === 'authenticated' emit `verified` (no payload —
the store has already refreshed user state); on 'failed' surface
result.reason with a generic fallback.
- emit signature: `verified: [data: unknown]` → `verified: []`.
- login.vue: onMfaVerified no longer calls authStore.refreshUser —
authStore.verifyMfa() refreshes internally. Page just routes to
resolvePostLoginTarget().
Adds 4 vitest specs in components/auth/__tests__/MfaChallengeCard.spec.ts
covering: success path emits `verified` with camelCase args, failure
path shows reason and suppresses emit, trustDevice toggle honours
fingerprint + device name, fallback message when reason is empty.
Test count 209 → 213. Lint + typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auth-store merge made portal_events available on the unified
/auth/me response (held in useAuthStore.portalEvents). usePortalStore
now sources userEvents from the auth store, eliminating the duplicate
fetch that the legacy slim usePortalAuthStore had compensated for.
Changes:
- types/auth.ts: add portal_events?: PortalEvent[] to MeResponse
- useAuthStore: add portalEvents ref, populated in setUser from
me.portal_events, cleared in clearState
- usePortalStore: replace loadUserEventsFromApiAndStorage (which
fetched /auth/me) with syncEventsFromAuthStore (which reads
authStore.portalEvents). A reactive watch keeps userEvents in sync
whenever the auth store updates (login, refresh, logout). The
sessionStorage merge stays as offline cache + post-registration
bridge.
- types/portal.ts: drop the now-unused AuthMeUser type — MeResponse
is the canonical shape post-merge.
Boundaries: usePortalStore (stores-portal) statically imports
useAuthStore (stores) — already allowed by the matrix
(stores-portal allow includes stores).
Adds 4 vitest specs covering: userEvents reflects auth.portalEvents,
no apiClient.get('/auth/me') call from the portal store,
sessionStorage fallback when auth has not hydrated, reactive update
on auth.portalEvents change.
Test count 205 → 209. Lint + typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
usePortalStore now persists state in sessionStorage instead of
localStorage. Tab-close clears the session implicitly; explicit logout
+ 401 paths invoke reset() which iterates the `crewli:portal:` prefix
and removes every key (forward-compatible for future portal-namespaced
state).
Storage keys are renamed under the canonical prefix:
- crewli_portal_user_events_v1 → crewli:portal:events
- crewli_portal_active_event_id_v1 → crewli:portal:activeEventId
The single new prefix-clear function (clearStoragePrefix) replaces the
hand-listed key removals, so future portal-namespaced state additions
need no reset() change.
useAuthStore.handleUnauthorized() (the 401 interceptor target) is now
async and invokes clearAll() — the canonical session-cleanup hub —
restoring the portal-storage cleanup that the deleted
usePortalAuthStore.handleUnauthorized previously owned. The merge in
Phase E left this gap; this commit closes it.
Adds 7 vitest specs in stores/portal/__tests__/usePortalStore.spec.ts
covering: sessionStorage persistence, reset() prefix-iteration,
non-prefixed-key isolation, reactive state reset, useAuthStore.clearAll
+ handleUnauthorized integration.
Test count 198 → 205. Lint + typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
login.vue is rewritten to consume useAuthStore.login()'s discriminated
union — no more direct apiClient calls or branching on raw API response
shapes. The page maps result.kind to UI/routing decisions only:
- mfa-required → swap to MfaChallengeCard with the typed payload
- authenticated → resolvePostLoginTarget() (?to= relative, else
auth.resolveLandingRoute())
- must-set-password → forward-compatible placeholder route
- failed → field-level errors + rate_limit message branch
resolveLandingRoute() now returns a string path instead of
RouteLocationRaw — the typed router accepts string-paths cleanly,
removes the cast at every call site, and lets useAuthStore.spec.ts +
guards.spec.ts assert the resolved path directly.
A13-3 minimum precaution lives in a new utility:
src/utils/postLoginRedirect.ts. The relative-only check
(`startsWith('/') && !startsWith('//')`) rejects absolute, protocol-
relative, javascript:, and data: schemes. Full domain validation lands
in WS-3 PR-B2b.
6 vitest specs in utils/__tests__/postLoginRedirect.spec.ts cover the
six rejection / passthrough scenarios.
Test count 192 → 198. Lint + typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds components/shared/ContextSwitcher.vue — a Vuetify menu-button
that renders only when useAuthStore.showContextSwitcher is true (i.e.
the user has both portal and organizer contexts available). Click
calls useAuthStore.setLastContext + resolveLandingRoute and pushes
the new route.
Wired into both layouts:
- PortalLayout.vue: navbar right section, before UserAvatarMenu
- DefaultLayoutWithVerticalNav.vue (organizer navbar host): before
NavbarThemeSwitcher (OrganizerLayout.vue itself is a 10-line
wrapper around DefaultLayoutWithVerticalNav, so the component
wires into the actual navbar host).
Boundaries matrix update: components-shared now allows `stores` so
canonical shared chrome (ContextSwitcher, future global indicators)
can read useAuthStore directly without re-homing to
components/layout/. stores-portal stays disallowed for components-
shared by design — portal-specific state has no place in shared
chrome.
Adds 3 vitest specs covering: visibility gated by
showContextSwitcher, click invokes setLastContext + router.push.
Test count 189 → 192. Frontend lint + typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
usePortalAuthStore is deleted — its 114 lines were a slim wrapper over
the same /auth/me endpoint useAuthStore already consumes. The merged
store gains the full set of additions Bert specified for B2a:
State:
- availableContexts / defaultContext (from /auth/me contexts block)
- lastContext (localStorage-persisted)
- portalToken (in-memory only, for the bearer-axios flavour)
Getters: isPortalUser, isOrganizerUser, isPlatformAdmin (alias of
isSuperAdmin), showContextSwitcher, hasRole(), hasAnyRole().
Actions: login(), verifyMfa() — both return typed discriminated
unions so login.vue (Phase H) consumes results without branching on
raw API response shapes. setLastContext, setPortalToken,
resolveLandingRoute, clearAll. clearAll dynamically imports
usePortalStore.reset() to clear portal sessionStorage on session-end —
this is the canonical session-cleanup hub now that the merge has
happened.
5 source files migrated from usePortalAuthStore → useAuthStore. The
PortalLayout.spec.ts mock follows. The boundaries matrix gains a
single new edge (`stores → stores-portal`) replacing the deleted
stores-portal/usePortalAuthStore which previously owned that
cross-zone call.
Adds 16 vitest specs in src/stores/__tests__/useAuthStore.spec.ts
covering setUser context hydration, hasRole/hasAnyRole, lastContext
localStorage persistence, resolveLandingRoute precedence
(portal/organizer/super_admin/multi-role/forceContext/forbidden
fallback), portalToken state, and clearAll cleanup.
Test count 162 → 178 (16 new). Frontend lint + typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The single axios.ts file becomes a directory with:
- factory.ts — createApiClient + the registerDefaultInterceptors /
registerPortalTokenInterceptors seam (preserves the
TECH-AXIOS-STORE-COUPLING decoupling — no store imports inside)
- default.ts — cookie-authenticated client (organizer + cookie-auth
portal flows; existing 45 call sites resolve unchanged)
- portal-token.ts — Bearer-auth client for the artist-advance /
supplier-intake flows (forward-compatible groundwork; no active
consumers today)
- index.ts — re-exports apiClient + portalApiClient + the register* /
createApiClient surface; the existing `import { apiClient } from
'@/lib/axios'` continues to work directory-resolved.
The bindings plugin (plugins/3.axios-bindings.ts) now wires both
clients with a shared deps base + flavour-specific overrides. The
`getPortalToken` callback returns null until Phase E surfaces
`portalToken` on useAuthStore — no current consumers exercise the
Bearer path, so the null-return is intentional placeholder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Additive enrichment to MeResource — existing fields untouched, MeTest stays green.
New fields:
- contexts.available: list<'portal'|'organizer'> derived from Person + Organisation memberships
- contexts.default: precedence super_admin > organizer > portal > fallback portal
- platform.is_super_admin: bool promoted from app_roles
- organisations[].roles: 1-element array form alongside the legacy scalar role,
forward-compatible for the multi-role pivot work tracked in TECH-PIVOT-ROLES-MULTI
UserFactory gains volunteer(), orgAdmin(), volunteerAndOrganizer(), superAdmin()
state methods — codified role categories for reuse across future workstreams.
Adds forbidden.vue placeholder (PublicLayout) for the context-failure landing in
the upcoming guard rewrite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes WS-3 PR-B1 charter §4.2: portal is fully consumed by
apps/app under /portal/** (authenticated portal routes) and
/register/** (public token-based form-fill). All portal source has
moved or been merged in earlier commits in this PR.
Adaptations from the original prompt's Phase F:
- pnpm-workspace.yaml does not exist at the repo root (the monorepo
isn't a pnpm workspace; each app has its own package.json /
node_modules / scripts). No edit needed.
- Root package.json has no `dev:portal` / `build:portal` scripts.
No cleanup needed.
- Skipped `pnpm -w build` — apps/app builds via its own scripts.
Deletes 384 portal files (build configs, layouts, plugins, vendored
@layouts, public/, dev/prod Dockerfiles, nginx.conf, env.d.ts,
themeConfig, tsconfig, package.json, lockfile, etc.). All authentic
portal logic is preserved in apps/app/src — verified by:
- Vitest 23 / 162 passing
- vue-tsc --noEmit clean
- eslint clean (zero new errors / warnings)
NOT verified at this point: `pnpm build`. The build fails on a
pre-existing missing `flatpickr` stylesheet import in
src/@core/components/app-form-elements/AppDateTimePicker.vue —
present on main pre-PR, unrelated to this work, and tracked
separately. Reproduced on plain `main` without any of these changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the WS-3 §4.2 sub-zone classification to the apps/app
boundaries matrix:
- components-{shared,portal,organizer} alongside the legacy
components type. components/{auth,settings} are folded into
components-shared as the legacy cross-context home for MFA dialogs
+ PasswordRequirements (used by both organizer reset-password and
portal wachtwoord-instellen / profiel).
- composables-forms (src/composables/forms/**) — pure form-runtime
helpers reusable from organizer Form Builder later.
- stores-portal (src/stores/portal/**) — keeps the portal auth +
portal store walled off from the organizer auth surface.
- pages-{register,portal,platform,organizer} alongside the legacy
pages type — register pages cannot reach into stores or
components-portal/-organizer; portal pages cannot reach
components-organizer; organizer + platform pages cannot reach
stores-portal or components-portal.
Cross-context edges are forbidden (organizer ↛ portal,
shared ↛ portal/organizer). Two pragmatic exceptions are documented
inline:
- components-shared accepts the legacy auth/ + settings/ paths
until PR-B2 cleanup re-homes them under shared/{auth,settings}/.
- pages-register may read stores-portal because success.vue
optionally enriches with the portal user when authenticated.
PR-B2 may move success.vue into pages-portal so this drops.
Lint: 0 errors / 0 new warnings (only the pre-existing
boundaries v5→v6 deprecation warnings, which apply to all 19 rules
now). Tests: 23 / 162 pass. Typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routing wiring (Phase D of WS-3 PR-B1):
- apps/app/src/plugins/1.router/guards.ts: add a single early-return
carve-out before the org-selection redirect — `if (to.meta.context
=== 'portal') return`. Per ARCH-CONSOLIDATION-2026-04 §4.3,
meta.context is the canonical contract; PR-B2 evolves the guards
from this key to full context-aware logic (post-login landing,
context-switcher, role checks).
- apps/app/env.d.ts: extend RouteMeta with the new layout names
('OrganizerLayout' | 'PortalLayout' | 'PublicLayout'), context,
requiresAuth, requiresToken, navMode, navTitle.
- apps/app/typed-router.d.ts: regenerated by unplugin-vue-router to
pick up portal/* and register/* route names.
- Page meta finalisation: portal pages have layout: 'PortalLayout',
context: 'portal', preserving original requiresAuth + nav fields;
register pages have layout: 'PublicLayout' + public: true (the
apps/app guard convention for public routes, since meta.public is
what the existing guard recognises).
Form-types restructure (boundaries cleanup):
- apps/app/src/composables/forms/types/formBuilder.ts → src/types/forms/
- apps/app/src/composables/forms/utils/{formValidation,validators}.ts
→ src/utils/forms/
- All `@/composables/forms/{types,utils}/*` imports rewritten across
pages, components, composables, tests.
- This avoids a `types → composables` boundaries violation at
src/types/formSchema.ts which re-exports primitives from the
inlined form-schema. types/formSchema.ts now imports from
@/types/forms/formBuilder which is in the same boundaries zone.
Lint cleanup for moved portal sources (apps/portal had no
.eslintrc.cjs; the migrated code now has to pass apps/app's stricter
config):
- axios.isAxiosError → named import { isAxiosError }
(ClaimenTab, RoosterTab, profiel.vue)
- void schemaQuery.refetch() → schemaQuery.refetch()
(register/[public_token].vue)
- if-then-else collapsed to single boolean return (formatFieldValue)
- :delay-on-touch-only="true" → delay-on-touch-only shorthand
(FieldSectionPriority)
- ml-2 class → ms-2 (FieldAvailabilityPicker)
- multi-statement-per-line splits in profiel.vue + spec files
- unused emailConfigured ref removed (profiel.vue)
- one-component-per-file disabled with TODO TECH-WS3-PORTAL-LINT-CLEANUP
ref (FieldOptionsLocale.spec.ts — multi-Wrapper test pattern)
- restored `import Draggable from 'vuedraggable'` after lint:fix
removed it (template-only usage; the import IS needed)
- camelcase param renamed in FieldOptionsLocale harness factory
- typecheck nudge: spec state.data typed via PublicFormSectionOption[] /
PublicFormTimeSlot[] aliases instead of Record<string, unknown>
- PortalLayout.vue: explicit `import { useRoute, useRouter }` so the
vitest mock can intercept (the trimmed AutoImport set doesn't pull
vue-router's auto-imports)
Vitest: 23 / 162 passing. Lint: 0 errors / 0 new warnings (only the
pre-existing boundaries v5→v6 deprecation warnings remain). Typecheck:
clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates the navbar (event/platform two-mode toggle), mobile drawer
with avatar header + logout, RouterView Suspense wrapper, and footer
from apps/portal/src/layouts/portal.vue into the PortalLayout.vue
skeleton from PR-A. The skeleton's structure (VApp / VAppBar / VMain
/ VFooter) is preserved as the outer shell.
Notable adaptations:
- useAuthStore → usePortalAuthStore (renamed in C.3)
- usePortalStore import path → @/stores/portal/usePortalStore
- mobile nav links now point at /portal/evenementen and /portal/profiel
(the new sub-zone paths) instead of /evenementen and /profiel
- explicit `import { useRoute, useRouter }` from vue-router so the
vitest mock can intercept (auto-import not configured for these in
the trimmed test config)
Updated PortalLayout.spec.ts to mock the two pinia stores plus
useSkins, vue-router, UserAvatarMenu, and AppLoadingIndicator. Tests
now assert the auth-conditional rendering: header + drawer hidden
when unauthenticated, main + footer always present.
Also pulls in the @form-schema → @/composables/forms/* import
rewrites in the C.4-moved composables that the previous commit's
rename-only diff left unstaged.
Vitest: 23 files / 162 tests, no errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composables (apps/portal/src/composables → apps/app/src/composables/):
- useFormDraft, publicFormInjection → composables/ (root, used by
shared/public-form components)
- api/usePublicForm, api/usePublicFormSections,
api/usePublicFormTimeSlots → composables/api/ (no collisions)
- api/usePortalShifts, api/usePortalProfile, api/useVolunteerRegistration
→ composables/api/portal/ (subfolder per WS-3 PR-B1 charter to
leave room for organizer-side namesakes without clashes)
- api/useMfa → DELETED (apps/app version is a strict superset
with extra invalidateQueries calls and the admin-reset mutation)
Types (apps/portal/src/types → apps/app/src/types/):
- api, portal-shift, portal, registration → moved
- mfa → DELETED (byte-identical to apps/app/src/types/mfa.ts)
Schemas:
- apps/portal/src/schemas/registrationSchema.ts → apps/app/src/schemas/
Utils:
- deviceFingerprint, paginationMeta → DELETED (byte-identical
duplicates already in apps/app/src/utils/)
Lib:
- apps/portal/src/lib/{axios,query-client}.ts → DELETED. apps/app's
callback-bound axios (post-PR-A) and query-client are the
canonical versions. Portal pages currently importing
`@/lib/axios#apiClient` resolve to apps/app's apiClient with no
behavioral change for cookie-based requests.
Tests: 4 composable specs (useFormDraft x2, usePublicFormSections,
usePublicFormTimeSlots) moved into __tests__/ subdirs alongside
their composables.
@form-schema imports inside the moved files rewritten to
@/composables/forms/*.
Vitest now: 23 files / 162 tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- apps/portal/src/stores/useAuthStore.ts →
apps/app/src/stores/portal/usePortalAuthStore.ts. The export and
defineStore id are renamed (useAuthStore → usePortalAuthStore,
'auth' → 'portalAuth') so it can coexist with the organizer's
apps/app/src/stores/useAuthStore. Lazy import inside
resetPortalStoresSync() updated to the new path.
- apps/portal/src/stores/usePortalStore.ts →
apps/app/src/stores/portal/usePortalStore.ts (no name change —
apps/app does not have a usePortalStore).
All call sites in moved pages/components now import from
@/stores/portal/{usePortalStore,usePortalAuthStore} and call
usePortalAuthStore() instead of useAuthStore().
PR-B2 will merge this back into a single context-aware auth store.
Also includes the C.1 page meta-block updates (layout: 'PortalLayout'
| 'PublicLayout', context: 'portal') that were left unstaged after
the page-rename commit picked up only the path change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- public-form/** (18 files + 7 component tests) → shared/public-form/**
This is the runtime form-renderer; goes into shared/ because it will
be reused by the organizer-app Form Builder preview (S3b).
- event/{Claimen,Informatie,Overzicht,Rooster}Tab.vue → portal/event/**
- portal/{StatusCard,EventCard,UserAvatarMenu}.vue → portal/** (no
path change — both apps had a portal/ subfolder).
- AppLoadingIndicator.vue, auth/{PasswordRequirements,MfaChallengeCard}.vue,
settings/Mfa{Disable,Email,Totp}SetupDialog.vue: portal copies
deleted as duplicates of pre-existing apps/app components (diffs
were trivial formatting only).
Inside the moved files: rewrote @form-schema/* → @/composables/forms/*
and @/components/{public-form,event/[Tab]} → new sub-zone paths.
Updated apps/app/tsconfig.json to drop the @form-schema path alias
and the packages/form-schema include path. Updated formSchema.ts to
import from @/composables/forms/types/formBuilder. Carried the
crypto polyfill from apps/portal/tests/setup.ts into
apps/app/tests/setup.ts (needed by useFormDraft tests landing in C.4).
NOTE: Some moved tests still fail because they reference portal
composables (usePublicFormSections, useFormDraft) that move in C.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per WS-3 PR-B1 charter §4.2: portal pages relocate into the
single-SPA layout under apps/app/src/pages/portal/** (authenticated
portal context) and apps/app/src/pages/register/** (public
token-based form-fill / confirmation).
Updated meta blocks:
- Portal pages: layout: 'PortalLayout', context: 'portal'
(preserving original requiresAuth + nav fields)
- Register pages: layout: 'PublicLayout' (drop requiresAuth)
Skipped (apps/portal duplicates of pages already in apps/app):
index.vue, login.vue, wachtwoord-{vergeten,resetten}.vue,
verify-email-change.vue. Deleted: [...path].vue (apps/app already
has [...error].vue catch-all).
NOTE: Component/store/composable imports inside these files still
point at apps/portal-relative paths and will be rewritten in the
next commits. Build will not be green again until commit 6
(composables/lib).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inlines the form-schema source folder (no package.json, alias-only)
into apps/app/src/composables/forms. Drops the @form-schema alias
from apps/app/vite.config.ts (replaced by @/composables/forms via
the existing @ alias). apps/portal vite + vitest configs keep
@form-schema as a temporary alias pointing at the new location so
portal tests/build keep working until apps/portal is removed at the
end of this PR. Two pure-logic form-schema tests moved alongside.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Creates portal/register/shared/forms sub-folders ahead of the moves
in subsequent commits. Empty .gitkeep markers will be replaced by
real content as the moves land.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both interceptor error handlers in lib/axios.ts were declared
`async` but contain zero `await` calls — the request handler
just rethrows, and the response handler walks a synchronous
status-code branching tree before rethrowing. axios accepts both
sync and async handler signatures, so dropping the keyword is
mechanical and behavior-neutral.
Co-Authored-By: Claude <noreply@anthropic.com>
Chose Option A from the follow-up brief: useImpersonationStore
already holds an `ImpersonationState` ref hydrated from
sessionStorage at store-init and exposes the active impersonation
target user as a public `targetUserId` computed. The store is the
canonical source; sessionStorage is just its persistence sidecar.
Adds a fifth callback `getImpersonationTargetUserId: () => string
| null` to AxiosBindingsDeps and replaces the
sessionStorage.getItem('crewli_impersonation') + JSON.parse block
in the request interceptor with a single `deps.getImpersonationTargetUserId()`
call. The bindings plugin wires it to
`useImpersonationStore().targetUserId`.
After this commit lib/axios.ts has zero references to
sessionStorage and zero magic strings about impersonation
persistence — the only persistence-mechanism knowledge left is in
useImpersonationStore (where it belongs) and in
plugins/3.axios-bindings.ts (allowed to know about stores). The
HTTP module is now unambiguously pure infrastructure.
Behavior preserved 1:1: the store hydrates from sessionStorage
synchronously inside the defineStore factory, so the very first
HTTP request after page load sees the same target user id as
before.
Co-Authored-By: Claude <noreply@anthropic.com>
Supplies the runtime closures that the registerInterceptors seam
needs. The plugin imports the four stores
(`useOrganisationStore`, `useNotificationStore`, `useAuthStore`,
`useImpersonationStore`) — allowed by the boundaries matrix
(`plugins → stores`) — and passes them as lazy callbacks so the
store factories only resolve when an HTTP call actually fires.
Numeric prefix `3.` runs after `2.pinia.ts` (auto-loaded by
`@core/utils/plugins.ts` in alphabetical-path order), so Pinia is
guaranteed active before the bindings register. No change to
`main.ts` is required — the file is picked up by the existing
`import.meta.glob('./plugins/*.{ts,js}')` glob.
Two redirects previously inside axios.ts now live where they
belong:
- `window.location.href = '/platform'` on impersonation
revocation, in the `onImpersonationRevoked` closure.
- `handleUnauthorized()` (which itself redirects to `/login`)
on 401, gated by `isInitialized` inside the `onAuthFail`
closure — preserves the race-condition fix from sessie 1b-iii.
With this commit the two Vite mixed-import warnings
(useAuthStore + useImpersonationStore being both statically and
dynamically imported) disappear from `pnpm build`. Lint stays at
0 problems, typecheck clean, 49/49 tests pass.
Refs TECH-AXIOS-STORE-COUPLING.
Co-Authored-By: Claude <noreply@anthropic.com>
Closes the lib → stores boundary violations that WS-3 sessie 1c
flagged. lib/axios.ts is now pure HTTP infrastructure: it exports
the configured `apiClient` plus a `registerInterceptors(client,
deps)` function that takes a typed `AxiosBindingsDeps` callback
bag (`getActiveOrgId`, `notify`, `onAuthFail`,
`onImpersonationRevoked`). All four `eslint-disable-next-line
boundaries/element-types` comments referencing
TECH-AXIOS-STORE-COUPLING are removed in the same change because
the imports they suppressed are gone — they would otherwise be
orphan disables.
Behavior is preserved 1:1: same status-code branching, same toast
messages, same DEV-only console logs, same sessionStorage-driven
X-Impersonate-User header (which never depended on a store and
stays in lib/axios.ts as before). The two redirects that used to
live in axios.ts (`/platform` on impersonation revocation,
`/login` on auth fail) move into the bindings-plugin closures so
the HTTP module stops knowing about routing.
The `apiClient` singleton is now exported without interceptors
attached — the bindings plugin
(`plugins/3.axios-bindings.ts`, follow-up commit) wires them up
during plugin-init, before `app.mount`.
Refs TECH-AXIOS-STORE-COUPLING.
Co-Authored-By: Claude <noreply@anthropic.com>
unplugin-vue-router regenerates this file at build time. Missed in an
earlier merge — probably during a WS-6 admin-UI consolidation. The
form-failures pages and tests are already in main; only the typed
declaration was stale.
Routes added to the typed declaration:
- /organisation/form-failures
- /organisation/form-failures/:id
- /platform/form-failures
- /platform/form-failures/:id
Co-Authored-By: Claude <noreply@anthropic.com>
src/views/ contained a single Vuexy-template file
(views/pages/authentication/AuthProvider.vue) with zero importers
in the repo. Vendored dead code from the original Vuexy template;
the §4.2 post-consolidation target layout drops views/ entirely.
Removed:
- apps/app/src/views/ (recursive)
- 'src/views/**' line from boundaries/ignore in .eslintrc.cjs
Closes TECH-DELETE-DEAD-VIEWS.
Co-Authored-By: Claude <noreply@anthropic.com>
WS-3 session 1c — Phase B Q1=B-revised (Bert's call after the
plugin-reality discovery).
eslint-plugin-boundaries treats both static `import` and dynamic
`await import(...)` as boundary edges. The original Q1=B mechanism
("convert static→dynamic to satisfy the rule") doesn't actually
satisfy the rule — all 4 store accesses in lib/axios.ts trip
boundaries/element-types: lines 3, 4 (static, pre-1c) and lines
61, 72 (dynamic, from 1b-iii).
Three options were on the table; Bert chose B-revised:
- A-reversal (allow lib→stores in matrix) was rejected because it
permanently loosens the boundary for 4 imports — exactly the
silent exception the zero-compromise principle forbids.
- B-extract (decouple axios.ts from stores via callback-injection)
is real architectural work and deserves a focused session, not
the tail-end of a tooling sprint. Filed as TECH-AXIOS-STORE-
COUPLING in the next docs commit; the four sites carry per-line
TODO references to it.
- B-revised (this commit) preserves the strict matrix:
boundaries/element-types stays at 'error' globally; the four
axios.ts sites are explicit per-line exceptions, not a rule
loosening. Future lib/X.ts writers still hit the wall.
Behavior unchanged. Only lint visibility changed — 4 disable
comments added at:
- src/lib/axios.ts:3 (static useNotificationStore import)
- src/lib/axios.ts:5 (static useOrganisationStore import; was line 4)
- src/lib/axios.ts:63 (dynamic useImpersonationStore await import; was line 61)
- src/lib/axios.ts:75 (dynamic useAuthStore await import; was line 72)
Each comment is exactly:
// eslint-disable-next-line boundaries/element-types -- TECH-AXIOS-STORE-COUPLING: deliberate HTTP↔state seam, refactor scheduled per backlog.
Commit verb is `chore` not `refactor` per Bert: the code's behavior
doesn't change, only its lint-visibility does. Honest naming.
Tests + typecheck + build verified green:
- apps/app vitest: 49 passed
- apps/app vue-tsc: clean
- apps/app pnpm build: succeeded in 11.24s
Lint baseline: 4 → 0 errors. WS-3 1c acceptance criterion satisfied.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds 'boundaries' to plugins, the layered-architecture matrix to
rules, and the boundaries/elements + boundaries/ignore + boundaries/
include settings per the WS-3 1c audit (Phase A:
dev-docs/WS-3-SESSION-1C-AUDIT.md). Phase B sign-off (Bert):
- Q1=B — `lib → stores` is DISALLOWED in the matrix; lib/axios.ts is
refactored in the next commit.
- Q2 — src/views/** added to boundaries/ignore (dead Vuexy file;
TECH-DELETE-DEAD-VIEWS backlog item lands with the docs commit).
- Q3 — `navigation` allowed to import `types`, `utils` (forward
headroom).
- Q4 — sub-zone enforcement deferred to TECH-WS3-BOUNDARIES-SUBZONES
(lands when WS-3 PR-B brings the §4.2 components/{organizer,portal,
shared} + pages/{(auth),portal,…} structure).
Forward-flag carried into the inline comment: when src/plugins/1.router/
migrates to a top-level src/router/ in a later WS-3 PR, add a
{ type: 'router', pattern: 'src/router/**' } element and a
{ from: 'router', allow: ['types','utils','lib','plugins','stores'] }
rule. Doc-side flag also lands in the ARCH-CONSOLIDATION 1c entry.
Boundaries plugin v6 emits a deprecation warning that the
'element-types' selector format is legacy (v5 syntax); the rule
still works on v5-compatible config and migrating to v6 object-
selector syntax is out of scope per the prompt's "only the two
.eslintrc.cjs changes listed are permitted" constraint. Filing a
TECH-BOUNDARIES-V6-SELECTOR-MIGRATION backlog item (in the docs
commit) so the migration happens deliberately.
Lint count after this commit: 4 errors, all in lib/axios.ts (lines
3, 4, 61, 72 — the 2 static + 2 dynamic store imports). The plugin
treats both static AND dynamic `await import('@/stores/...')` as
boundary edges; this is a deliberate intermediate state. The next
commit (refactor) resolves all 4 to land at lint = 0.
Tests + typecheck verified green (boundary errors are lint-only).
Co-Authored-By: Claude <noreply@anthropic.com>
Adds eslint-plugin-boundaries@6.0.2 (MIT, peerDeps eslint>=6,
engines node>=18.18) as a direct devDep in apps/app/package.json,
matching the exact-pin style of the other 14 eslint-plugin-* deps.
Direct dep — not hoisted transitive — per the
TECH-PORTAL-ESLINT-DEPS lesson (Cursor's ESLint extension uses
strict module resolution and silently fails on plugins reachable
only via pnpm hoisting).
Plugin not yet enabled in .eslintrc.cjs; enabling lands in the next
commit per WS-3 1c sequence (audit Phase A → install → enable →
refactor axios.ts → docs).
Tests + typecheck verified green post-install.
Co-Authored-By: Claude <noreply@anthropic.com>
Surfaced during WS-3 1c-prep follow-up: Cursor's ESLint extension uses
strict module resolution and crashed on every plugin in the
@antfu/eslint-config-vue extends-chain that was only resolvable via
pnpm-hoisting in terminal.
Direct deps added (versions match what was already in pnpm store —
zero version shifts):
- 12 unscoped ESLint plugins (eslint-plugin-{antfu,es-x,html,i,jest,
jsdoc,jsonc,markdown,n,no-only-tests,unused-imports,yml,
eslint-comments})
- vue-eslint-parser
- @antfu/eslint-config-basic + @antfu/eslint-config-ts (extends targets)
- @stylistic/eslint-plugin-js + @stylistic/eslint-plugin-ts
.vscode/settings.json: removed redundant root-level
editor.defaultFormatter (per-language overrides do the job).
ESLint extension now activates correctly, server runs, save-on-format
works for TS/Vue files. Verified via smoke test: double quote in
useImpersonationStore.ts:1 was auto-corrected to single quote on Cmd+S.
Note: package.json declares some deprecated dependencies that pnpm
warns about (@antfu/eslint-config-vue@0.43.1, eslint@8.57.1,
eslint-plugin-i@2.28.1, eslint-plugin-markdown@3.0.1). Those are
pre-existing — not introduced here. Migration to ESLint v9 + flat
config + @antfu/eslint-config (modern) is a separate workstream.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WS-3 session 1b-iii follow-up — sonarjs/no-collapsible-if.
useImpersonationStore.ts:103: collapsed nested 'if (state.value)'
into the parent 'else if (data.data.session)' clause. Both legs
are AND-conditions on the same path, so the merge is semantically
identical. Brings the apps/app lint baseline to 0 problems.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WS-3 session 1b-iii Task 3.
Rewrites the response-interceptor error handler from
\`error => { ... void import(...).then(...) }\` to
\`async error => { ... await import(...) }\`.
Motivation: session 1b-ii's Q4 chose option-a (\`void\` prefix on
the dynamic-import chains), but empirically that doesn't satisfy
the promise/no-promise-in-callback rule — the rule fires on any
promise creation inside a callback, regardless of discard pattern.
Two warnings remained on lib/axios.ts:61, 73.
The async/await rewrite is semantically identical:
- Both call sites already end in window.location.href = ... which
navigates away, so the few ms of \`await\` resolution latency is
unobservable.
- The original return Promise.reject(error) becomes throw error in
an async function (async wraps throws in rejected promises).
Verified preserved byte-for-byte:
- 403 + impersonation_ended branch: clearState + redirect to /platform
+ rejection (now via throw)
- 401 branch: handleUnauthorized when authStore.isInitialized
- 403 / 404 / 422 / 503 / 5xx / !response notification branches
(untouched in diff — all still in same order, same messages)
- Final rejection so calling code's catch fires (now via throw)
- Request interceptor not touched
- No imports added or removed
Tests + typecheck verified green. Build smoke: pnpm build succeeded
in 11.13s, zero warnings.
Lint baseline: 3 → 1 (the 2 promise/no-promise-in-callback warnings
on axios.ts:61, 73 are gone). The remaining 1 item is a pre-existing
sonarjs/no-collapsible-if at useImpersonationStore.ts:103 — see the
1b-iii final report.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>