WS-3 session 1b-ii Task 5b+c (audit Bucket E.2-E.5 — 6 items resolved,
2 promise/no-promise-in-callback warnings remain on dynamic-import
sites — see deviations).
This commit is split out from the originally-planned grouped Task 5
because the API stream timed out mid-session. E.1 (isAxiosError) is in
the preceding commit 0f155d9.
E.2 — vitest spec to Composition API (1× vue/component-api-style):
- useFormFailures.spec.ts: rewrote the test wrapper from
\`{ setup() { return { result } }, render: () => h('div') }\`
to \`setup(_, { expose }) { expose({ result }); return () => h('div') }\`.
Pure Composition API: setup returns the render function; expose()
declares the instance-visible \`result\` that the 7 \`vm.result.*\`
assertions consume. Tests still pass green (49 tests).
E.3 — REAL BUG: missing return in computed (1× vue/return-in-computed-property):
- useTimeSlotDropdown.ts:80: the \`fetchParams\` computed had a switch
over the \`DropdownScenario\` type (4 string-literal cases) without
a \`default\` branch. If \`scenario.value\` ever returned a value
outside the four narrowed cases (e.g. via a future type-assertion
drift), the computed silently returned \`undefined\`, and the
consumer code (\`fetchParams.value.includeParent\`) would throw
\`Cannot read property 'includeParent' of undefined\`. Added a
\`default\` branch returning \`{ includeParent: false, includeChildren: false }\`
— same as the 'flat' case (the safest baseline: include only own
slots, no hierarchy).
E.4 — SECURITY (1× vue/no-template-target-blank):
- pages/organisation/index.vue:343: the external website anchor had
\`target='_blank'\` with \`rel='noopener'\` (only one). The rule
requires the full \`rel='noopener noreferrer'\` pair. Updated.
Mitigates reverse-tabnabbing (window.opener) AND referrer-leakage
to the linked third-party site.
E.5 — axios fire-and-forget (3× promise/no-promise-in-callback,
1 fully resolved + 2 warnings remain):
- lib/axios.ts:42: changed \`error => Promise.reject(error)\` to
\`async error => { throw error }\`. Semantically identical (axios
interceptor onRejected returns a rejected promise either way) and
satisfies the lint rule.
- lib/axios.ts:61, 73: prefixed the dynamic-import chains with \`void\`
per Q4's option-a decision (\`void import('@/stores/...').then(...)\`).
This makes the discard intent explicit, but empirically does NOT
satisfy promise/no-promise-in-callback — the rule fires on any
promise creation inside a callback, regardless of the discard
pattern. The 2 warnings remain in the post-Task-5 baseline.
Resolution path is Bert's call: either keep \`void\` and accept
the warnings as documentation, or rewrite to \`async error => {
const { useStore } = await import(...); ... }\` which sequentializes
the dynamic-import resolution with the rejection. Out of scope for
this session per the literal Q4 recipe.
Tests + typecheck verified green.
Lint baseline: 34 → 32.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Crewli
Multi-tenant SaaS platform for event and festival operations: planning, people, accreditation, artist advancing, volunteer shifts, briefings, and show-day tooling (Mission Control). The backend is a JSON-only Laravel API; all user interfaces are Vue 3 single-page apps.
What Crewli covers
- Organisations & events — Multi-tenant data with organisation-scoped access; events move through a defined lifecycle (draft → published → registration → buildup → show day → teardown → closed).
- Festival structure — Sections, time slots, shifts, assignments, claiming/approval flows for volunteers and crew.
- People & crowds — Crowd types (crew, volunteers, artists, guests, press, partners, suppliers), persons, lists, and (planned) rich accreditation (items, zones, hand-out).
- Artists & advancing — Booking status, stages/timetable concepts, advance sections and token-based portal access for external artists.
- Communication — Briefings, campaigns, and operational messaging (see architecture doc for target modules).
- Portal (external users) — One portal app, two modes: login (Sanctum) for long-term users such as volunteers, and token access for per-event links (e.g. artists, suppliers).
Implementation is phased; the authoritative feature and schema list lives in the architecture and design references below.
Applications
| App | Path | Port | Role |
|---|---|---|---|
| Organizer | apps/app/ |
5174 | Main product for org and event staff: events, sections, shifts, people, artists, accreditation, briefings, reports. Includes Platform Admin section for super admins (/platform/*). |
| Portal | apps/portal/ |
5175 | External users: stripped layout; login- or token-based access. |
All apps talk to the API over CORS with Laravel Sanctum tokens.
Tech stack
| Layer | Technology |
|---|---|
| API | PHP 8.2+, Laravel 12, Sanctum, Spatie Permission (and Activity Log / Media Library where used) |
| Data | MySQL 8, Redis (cache/queues) |
| Frontends | Vue 3, TypeScript, Vite, Vuexy + Vuetify, Pinia, TanStack Query, VeeValidate + Zod |
| Local services | Docker Compose (MySQL, Redis, Mailpit) |
Rule of thumb: business tables use ULID primary keys; event-related data is scoped by organisation (global scopes + policies), not ad hoc where clauses in controllers.
Project structure
crewli/
├── api/ # Laravel 12 REST API (JSON only)
├── apps/
│ ├── app/ # Organizer SPA (primary UI + platform admin)
│ └── portal/ # External portal SPA
├── docker/ # Docker / Compose assets
├── docs/ # SETUP, API notes, schema notes
├── resources/
│ ├── design/ # Product source of truth (design docs, see table below)
│ └── vuexy-admin-*/ # Vuexy template reference (bundled kit)
├── .cursor/ # ARCHITECTURE.md, instructions.md, rules for AI/helpers
└── Makefile # Dev commands
Vuexy @core/ and @layouts/ in each app should stay untouched; customize via app config, navigation, and app-level components.
Quick start
# 1. Infrastructure
make services
# 2. API env, dependencies, database (see docs/SETUP.md)
cd api && cp .env.example .env && composer install && php artisan key:generate && php artisan migrate
# 3. Run API + the SPAs you need (separate terminals)
make api
make app
make portal
Detailed setup: docs/SETUP.md.
Development URLs
| Service | Development | Env / notes |
|---|---|---|
| API | http://localhost:8000/api/v1 | Base path /api/v1 |
| Organizer | http://localhost:5174 | FRONTEND_APP_URL |
| Portal | http://localhost:5175 | FRONTEND_PORTAL_URL |
| Mailpit | http://localhost:8025 | Local mail capture |
Production (crewli.app)
Domains: crewli.app is this product (API + organizer + portal SPAs, transactional email from the app, seeds, etc.). crewli.nl is reserved for a future public marketing site only — do not point this codebase's APP_URL, CORS, Sanctum, or app mail at crewli.nl.
Typical layout (configure the same values in api/.env — see api/.env.example):
| Service | URL | Env variable |
|---|---|---|
| API | https://api.crewli.app |
APP_URL |
| Organizer | https://crewli.app |
FRONTEND_APP_URL |
| Portal | https://portal.crewli.app |
FRONTEND_PORTAL_URL |
Frontends: set VITE_API_URL=https://api.crewli.app/api/v1 in each app's env for production builds. SANCTUM_STATEFUL_DOMAINS must list the hostnames only of the two SPAs (e.g. crewli.app,portal.crewli.app).
Makefile commands
make services # MySQL, Redis, Mailpit
make services-stop
make api # Laravel on :8000
make app # Organizer on :5174
make portal # Portal on :5175
make migrate
make fresh # migrate:fresh --seed
make db-shell
Documentation
| Resource | Contents |
|---|---|
| resources/design/ | Canonical product specs in Markdown. Referenced by .cursor and CLAUDE.md as source of truth for features and data model: design-document.md, dev-guide.md, start-guide.md. |
| .cursor/ARCHITECTURE.md | System diagram, apps, multi-tenancy, roles, event lifecycle, API route map, core schema overview (summarises resources/design when present) |
| .cursor/instructions.md | Quick reference, phased roadmap, module build order |
| .cursor/rules/ | Workspace, Laravel, Vue, testing conventions |
| docs/SETUP.md | Environment and local setup |
| docs/API.md | API notes (if maintained) |
| docs/SCHEMA.md | Schema notes (if maintained) |
Testing
cd api && php artisan test
Feature tests should cover happy paths plus 401 (unauthenticated), 403 (wrong organisation), and 422 (validation) where applicable.