--- goal: Vertical Slice Implementation Plan for 24HRS Messe-Lotse (AI-Optimized) version: 1.0 date_created: 2026-06-10 last_updated: 2026-06-10 owner: 24HRS Team status: Planned tags: architecture, vertical-slices, ai-optimized, migration --- # Introduction ![Status: Planned](https://img.shields.io/badge/status-Planned-blue) This plan supersedes `architecture-rewrite-1.0.md` by re-scoping the 12 sequential phases into 6 AI-sized vertical slices. Each slice delivers a complete, independently shippable user capability across all layers (Pimcore data model → API → React UI → auth). No slice requires a later slice to function. Context: zero developers, AI-only execution. Scale: ~300 jobs/year, peaks at 113 jobs per fair. ## 1. Requirements & Constraints ### Inherited from architecture-rewrite-1.0.md - **REQ-001**: Multi-user with OAuth2 authentication (Pimcore password grant) - **REQ-002**: Four roles — Admin, Editor, Viewer, Customer — with scoped visibility - **REQ-003**: All file uploads stored in Pimcore DAM (not base64 in localStorage) - **REQ-004**: PDF export via Gotenberg (already running) - **REQ-005**: Frontend served from Pimcore domain (no CORS) - **REQ-006**: Feature parity with `index.html` mockup (8 tabs + dashboard + calendar) - **REQ-007**: Symfony Workflow-driven phase progression (8-state state machine) - **REQ-008**: Normalized 4-entity data model (fairLocation → fair → job ← customer) - **REQ-009**: Date inheritance (job overrides nullable, fall back to fair dates) - **REQ-010**: Fair Detail view — primary grouping of jobs - **REQ-011**: Resource pool management (11 entities, deferred to Slice 5) - **REQ-012**: AI document processing skills (deferred to Slice 4) - **REQ-013**: Tiered document storage (customer/fair/job levels) - **CON-001**: Must run in existing Docker Compose environment - **CON-002**: No legacy data migration needed (greenfield) - **CON-003**: Pimcore ≥ 11.x, Gotenberg, OAuth2 already in place - **SEC-001**: Booking platform passwords stored encrypted (Pimcore encrypted field type) - **SEC-002**: All API routes protected by OAuth2, role-checked per endpoint - **GUD-001**: React 18 + TypeScript + Vite + TanStack Query + Zustand + shadcn/ui ### Slice-Specific Constraints - **CON-SLICE-001**: Each slice must produce working, testable, demonstrable software - **CON-SLICE-002**: No slice may depend on a later slice for core functionality - **CON-SLICE-003**: Slices 1-3 deliver minimal viable product; Slices 4-6 add intelligence/automation - **CON-SLICE-004**: All AI-generated code must be verifiable via Playwright E2E tests per slice - **CON-SLICE-005**: Timestamped slice completion tracking per attached date column ### Decisions from Architecture Review (2026-06-10) - **DEC-001**: SketchUp 3D bridge is async (message queue worker), not synchronous HTTP call from Pimcore controller. Job-level mutual exclusion prevents duplicate generations. Health monitoring required for SketchUp + MCP availability. - **DEC-002**: Workflow must support admin "force transition" with justification log. Rigid guards alone are insufficient for real-world trade fair prep entropy. Card created: **INVESTIGATE workflow guard flexibility**. - **DEC-003**: Tiered document model must degrade gracefully when no brandbooks or multi-fair docs exist. If a customer has no `brandDoc`, the UI shows nothing collapsed — no empty inheritance indicators. - **DEC-004**: LLM operational cost and accuracy risk require investigation before Slice 4 begins. Card created: **INVESTIGATE monthly LLM cost at full scale (10,000+ calls/year)** and **INVESTIGATE SetupProgressAnalyzer auto-check confidence floor**. ## 2. Implementation Steps --- ### Slice 1 — Create and manage a trade fair - GOAL-001: Full-stack capability to authenticate, create venues/customers/fairs, and view them on a dashboard. Proves the entire technology stack works end-to-end: Pimcore data objects, OAuth2, REST API, React SPA, role-based access. | Task | Description | Completed | Date | |------|-------------|-----------|------| | TASK-001 | Scaffold `MessebauBundle` in `bundles/MessebauBundle/` with Symfony bundle class, `services.yaml`, `routes.yaml` | | | | TASK-002 | Create Data Object `FairLocation` (venue) — text: name, address, googleMapsUrl; contact: contactName, contactPhone, contactEmail; textarea: notes | | | | TASK-003 | Create Data Object `Customer` (company) — text: company, address; contact: contactName, contactPhone, contactEmail; asset: brandDoc (document, nullable), consultationDocs (gallery, nullable), contractDocs (gallery, nullable); textarea: notes | | | | TASK-004 | Create Data Object `Fair` (trade fair) — text: name, hallsUsed; relation: fairLocation (1→1 to `FairLocation`); datetime: setupStart, setupEnd, eventStart, eventEnd, teardownStart, teardownEnd; asset: constructionBriefing (gallery), floorPlan (gallery), regulations (gallery), protocols (gallery); textarea: notes | | | | TASK-005 | Create `FairLocationController` — `GET /api/messebau/fairlocations` (list), `GET/POST/PUT/DELETE /api/messebau/fairlocations/{id}` | | | | TASK-006 | Create `CustomerController` — `GET /api/messebau/customers` (list, searchable by company name), `GET/POST/PUT/DELETE /api/messebau/customers/{id}` | | | | TASK-007 | Create `FairController` — `GET /api/messebau/fairs` (list, filterable by fairLocation, date range), `GET/POST/PUT/DELETE /api/messebau/fairs/{id}` | | | | TASK-008 | Create `AssetController` (generalized) — `POST /api/messebau/{entity}/{id}/upload/{fieldName}` handles `customer` and `fair` entity uploads; `DELETE /api/messebau/{entity}/{id}/asset/{fieldName}` | | | | TASK-009 | Configure OAuth2 firewall — protect `/api/messebau/*` with `pimcore.api` firewall; define 4 roles: `ROLE_MESSEBAU_CUSTOMER`, `ROLE_MESSEBAU_VIEWER`, `ROLE_MESSEBAU_EDITOR`, `ROLE_MESSEBAU_ADMIN`; Customer role auto-filters by `user.messebauCustomer` relation; add `messebauCustomer` field to Pimcore User class | | | | TASK-010 | Scaffold Vite + React + TypeScript in `frontend/` with Tailwind CSS v3, shadcn/ui, react-router-dom, TanStack Query, Zustand, Axios | | | | TASK-011 | Configure `vite.config.ts` — build output to `../public/build/messebau/`, base path `/build/messebau/`, proxy `/api` to Pimcore in dev mode | | | | TASK-012 | Create `src/api/client.ts` — Axios instance, request interceptor for Bearer token, response interceptor for 401 → refresh token → retry; implement refresh token queue | | | | TASK-013 | Create `src/stores/authStore.ts` — Zustand: accessToken, refreshToken, user (id, username, roles), login/logout/refresh, persist to sessionStorage | | | | TASK-014 | Create `src/stores/uiStore.ts` — Zustand: currentView, sidebarCollapsed, toastQueue | | | | TASK-015 | Create `src/hooks/usePermissions.ts` — reads role from authStore, returns boolean flags: canEdit, canDelete, canCreateJob, canManageResources, isCustomer, etc. | | | | TASK-016 | Create `src/components/auth/LoginPage.tsx` — username + password form, calls authStore.login(), error display, redirect on success | | | | TASK-017 | Create `src/components/auth/ProtectedRoute.tsx` — checks auth token, redirects to /login if missing | | | | TASK-018 | Create `src/components/layout/AppShell.tsx` — flex: Sidebar (260px) + main (flex-1, 32px padding) | | | | TASK-019 | Create `src/components/layout/Sidebar.tsx` — "24HRS" logo + "Messe-Lotse" subtitle (yellow on black); nav items: Dashboard, Kalender (all roles); logout at bottom; role-gated nav items reserved for later slices | | | | TASK-020 | Create `src/components/dashboard/FairCard.tsx` — card per fair: name, location name, date range, job count placeholder (0); click navigates to /fair/{id} | | | | TASK-021 | Create `src/components/dashboard/FairCardGrid.tsx` — responsive grid of FairCards; search bar filters by name or location | | | | TASK-022 | Create `src/components/dashboard/EmptyState.tsx` — internal roles: "Keine Messen vorhanden" + create button; customer: "Keine Projekte vorhanden" (no create button) | | | | TASK-023 | Create `src/components/fair/CreateFairFlow.tsx` — wizard: Step 1: select or create FairLocation (combobox), Step 2: enter fair details (name, dates, halls), Step 3: confirm | | | | TASK-024 | Create `src/components/fair/FairDetailView.tsx` — fair header (name, location, dates), stats placeholder (0 jobs), "Neuen Job hinzufügen" button (gated by usePermissions) | | | | TASK-025 | Create `src/App.tsx` — routes: /login → LoginPage, / → ProtectedRoute → AppShell → Dashboard, /fair/:id → FairDetailView, /fair/new → CreateFairFlow, 404 | | | | TASK-026 | Create Pimcore controller `MessebauController::appAction()` — serves `public/build/messebau/index.html` via Twig; catch-all route for SPA routing | | | | TASK-027 | Write Playwright test `slice-1-auth-create-fair.spec.ts` — login as editor, create fairLocation, create fair, verify fair appears on dashboard, logout; login as customer, verify dashboard loads (empty), deny access to /fair/new | | | | TASK-028 | Write Vitest tests: `authStore.test.ts` (login, logout, token refresh), `usePermissions.test.ts` (role → flag mapping) | | | --- ### Slice 2 — Add jobs to a fair - GOAL-002: Create and view booth projects (jobs) linked to customers and fairs. File upload works via Pimcore DAM. Basic master data editing with role scoping. Proves entity relations, asset management, and role-gated UI. | Task | Description | Completed | Date | |------|-------------|-----------|------| | TASK-101 | Create Data Object `Job` (booth project) — **core entity**: standType (select: Systemstand/Reihstand/Eckstand/Kopfstand/Blockstand/Inselstand), standSize, standHeight, hall, area, project; relations: customer (n→1 to Customer), fair (n→1 to Fair); date overrides: setupStartOverride, setupEndOverride, eventStartOverride, eventEndOverride, teardownStartOverride, teardownEndOverride (all nullable — if null, inherit from fair); workflowState (input, managed by Symfony Workflow); textarea: notes | | | | TASK-102 | Create Data Object `ChecklistItem` — text, checked (boolean), order (integer); 1:n relation to Job | | | | TASK-103 | Create Data Object `Material` — type (select: flooring/standsystem/hardware/furniture), name, quantity (integer), notes; 1:n relation to Job | | | | TASK-104 | Create field collections for `Job`: `BookingPlatformInfo` (portalLink, email, password[encrypted], deadline), `PowerInfo` (kw, distributor, deadline), `WlanInfo` (type, deadline), `CleaningInfo` (deadline), `RiggingInfo` (supplierFirstname, supplierLastname, supplierEmail, supplierPhone, messeFirstname, messeLastname, messeEmail, messePhone, deadline), `DruckproduktionInfo` (qm, blockout, forexplatten, deadline), `CateringInfo` (baristaEnabled, baristaPackage, days, drinksEnabled, drinksDetails, foodEnabled, foodDetails), `CostEstimate` (standConstruction, activation, furniture, logistics — all integer EUR) | | | | TASK-105 | Add asset fields to Job: standImage (image), standGallery (image gallery), briefingCustomerDoc (document, nullable — if null, inherits from customer), briefingPlanDoc (document, nullable — if null, inherits from fair), briefingAdditionalDoc (document), powerConfirmation (document), wlanConfirmation (document), cleaningConfirmation (document), riggingQuote (document), riggingDrawing (document), riggingMesseConfirmation (document), standApprovalDoc (document), acceptancePhotos (image gallery), acceptanceSignature (image), weeklyProtocols (document gallery) | | | | TASK-106 | Create `JobController` — `GET /api/messebau/jobs` (paginated, filterable by fairId/customerId/workflowState/search), `GET /api/messebau/jobs/{id}` (full serialization with relations + asset URLs + resolved effectiveDates), `POST /api/messebau/jobs` (create — requires customer and fair relation IDs, initializes default checklist items), `PUT /api/messebau/jobs/{id}`, `PATCH /api/messebau/jobs/{id}`, `DELETE /api/messebau/jobs/{id}`. Customer role auto-filtered to own company's jobs; 403 on DELETE | | | | TASK-107 | Extend `AssetController` to handle job entity uploads via existing generalized endpoint | | | | TASK-108 | Extend `FairController` — `GET /api/messebau/fairs/{id}/jobs` lists all jobs for this fair (the "todo" view); fair serialization includes job count | | | | TASK-109 | Create `src/types/job.ts` — TypeScript interfaces for all entities and field collections from TASK-101 through TASK-105 | | | | TASK-110 | Create `src/api/fairLocations.ts` + `src/api/customers.ts` + `src/api/fairs.ts` + `src/api/jobs.ts` — typed API functions for all CRUD operations | | | | TASK-111 | Create `src/hooks/useFairs.ts` + `src/hooks/useJobs.ts` + `src/hooks/useJob.ts` + `src/hooks/useFair.ts` — TanStack Query hooks | | | | TASK-112 | Create `src/components/job/CreateJobFlow.tsx` — wizard: Step 1: select or create Customer (combobox with search), Step 2: select Fair (combobox grouped by fairLocation), Step 3: enter booth details (project, standType, standSize, hall). Navigate to job detail on creation | | | | TASK-113 | Create `src/components/job/JobCard.tsx` — stand image, customer company name, fair name, hall, workflow progress placeholder; click navigates to /jobs/{id} | | | | TASK-114 | Create `src/components/job/JobCardGrid.tsx` — responsive grid, maps jobs to JobCards | | | | TASK-115 | Update `FairDetailView` — show JobCardGrid filtered to fair; "Neuen Job hinzufügen" button triggers CreateJobFlow (gated by usePermissions: true for editor/admin, false for customer/viewer); customer sees their own jobs only | | | | TASK-116 | Create `src/components/job-detail/DetailHeader.tsx` — black bar: breadcrumb (fair.name → customer.company → job.project), back button; action buttons gated by usePermissions: save (editor/admin), duplicate (editor/admin), export PDF (all), delete (admin); customer: save + export only | | | | TASK-117 | Create `src/components/job-detail/TabContainer.tsx` — dynamic tab list based on role: internal sees all 8 tabs; customer sees 4 tabs (Tab 0, 1, 4, 5+6); active tab in search params (?tab=0); inactive tabs hidden, not just disabled | | | | TASK-118 | Create `src/components/job-detail/tabs/MasterDataTab.tsx` — two-column form using react-hook-form + zod: **entity selectors** (Customer + Fair read-only, with Google Maps link for fairLocation), **Stammdaten** (Projekt, Standart, Standgröße, Standhöhe, Halle/Stand, Standfläche — all editable by editor/admin, read-only for viewer, select fields editable by customer), **Zeitplanung** (fair dates shown read-only as reference; editor/admin sees override fields with inherited defaults; customer sees resolved effective dates read-only), **Status** (read-only badge), **Messestand-Bild** (file input, image preview, download/remove), **Notizen** (full-width textarea) | | | | TASK-119 | Create `src/components/job-detail/tabs/DokumenteTab.tsx` — checklist with 8 default items from index.html prototype; checkbox + strikethrough; editor/admin can add/remove items; customer can only check/uncheck existing items (usePermissions.canAddChecklistItems) | | | | TASK-120 | Write Playwright test `slice-2-jobs-crud.spec.ts` — login as editor, open fair detail, create job for customer A at that fair, verify job appears in fair detail grid, open job detail, edit master data fields, save, reload, verify persistence; login as customer A, verify only own jobs visible, verify edit fields limited to select fields, verify delete button absent | | | | TASK-121 | Write Vitest tests: `useJobs.test.ts` (fetch, filter, pagination), `MasterDataTab.test.tsx` (form validation, inherited dates display) | | | --- ### Slice 3 — Complete a job's documents and workflow - GOAL-003: Full job lifecycle with all 8 tabs operational, Symfony Workflow with manual transitions, file uploads for all tab fields, and PDF export via Gotenberg. At this point the system is a complete replacement for the index.html prototype. **Workflow note per DEC-002:** Guards are implemented as described in TASK-202, but an admin-only `POST /api/messebau/jobs/{id}/workflow/{transition}?force=true&reason=...` endpoint is provided. Force transitions log the admin user + reason to transition history. This is a safety valve for the gap between ideal workflow and real-world fair prep. | Task | Description | Completed | Date | |------|-------------|-----------|------| | TASK-201 | Create Symfony Workflow config in `bundles/MessebauBundle/Resources/config/workflows/job.yml` — 8 places (angebot, planung, organisation, produktion, aufbau, messe, abbau, abgeschlossen); initial marking: `angebot`; transitions with guards: `angebot→planung` requires customer+fair relations, `planung→organisation` requires briefingCustomerDoc uploaded, `organisation→produktion` requires power/wlan/cleaning deadlines set if their checkboxes enabled, `produktion→aufbau` requires all checklist items checked, `abbau→abgeschlossen` requires acceptanceSignature+acceptanceDate set; date-based transitions (aufbau→messe, messe→abbau) auto-fire via cron checking effective dates | | | | TASK-202 | Create `WorkflowController` — `GET /api/messebau/jobs/{id}/workflow` returns current place, available transitions (with guard status: allowed/blocked/reason), transition history; `POST /api/messebau/jobs/{id}/workflow/{transition}` applies manual transition; `POST ...?force=true&reason=...` (admin only) bypasses guards, logs justification to transition history | | | | TASK-203 | Create `WorkflowTransitionListener` — on `workflow.enter`: log timestamp to transition history (JSON array); on `workflow.enter.aufbau`: auto-generate PDF export for setup team; on `workflow.enter.messe`: send customer email with booth details; on `workflow.enter.abgeschlossen`: mark job as read-only | | | | TASK-204 | Create `src/components/job-detail/tabs/BriefingTab.tsx` — 3 collapsible sections with scope badges: "☰ Kunde" (customer.brandDoc, customer.consultationDocs, customer.contractDocs), "🌐 Messe" (fair.constructionBriefing, fair.floorPlan, fair.regulations, fair.protocols), "📋 Job" (job.briefingCustomerDoc override with "inherited from customer" indicator if null, job.briefingPlanDoc override with "inherited from fair" indicator if null, job.briefingAdditionalDoc). Each section has upload area. Follows DEC-003: empty levels show nothing collapsed — no empty inheritance indicators if parent has no docs | | | | TASK-205 | Create `src/components/job-detail/tabs/OrganisationTab.tsx` — collapsible sections for Buchungsplattform, Strombuchung, WLAN, Reinigungsservice, Rigging/Truss, Standfreigabe; each section has enable checkbox + fields + file upload with preview; resource summary (read-only placeholder, filled in Slice 5) | | | | TASK-206 | Create `src/components/job-detail/tabs/ProduktionTab.tsx` — material lists (4 sections: Bodenbeläge, Standsystem, Hardware, Möbel) with add/remove; Druckproduktion section (qm, blockout, forexplatten, deadline); Catering section (barista, drinks, food); Leadmagneten section (9 checkboxes) | | | | TASK-207 | Create `src/components/job-detail/tabs/StandfotosTab.tsx` — upload zone (dashed border, drag-and-drop, image/*, multiple, max 10), gallery grid with download + delete, click opens Lightbox | | | | TASK-208 | Create `src/components/shared/Lightbox.tsx` — portal-based (ReactDOM.createPortal), dark overlay, centered image, close button, prev/next arrows, keyboard navigation (arrow keys, escape), click-outside-to-close | | | | TASK-209 | Create `src/components/job-detail/tabs/StandabnahmeTab.tsx` — acceptance photo gallery (separate from Standfotos), signature pad (react-signature-canvas, 600x200, clear + save), remarks textarea, Abnahmedatum (datetime-local) | | | | TASK-210 | Create `src/components/job-detail/tabs/WeeklyTab.tsx` — multi-file upload for PDFs, list of uploaded protocols with filename/size/download/delete, empty state with PDF icon | | | | TASK-211 | Create `src/components/shared/WorkflowProgress.tsx` — horizontal stepper: 8 phases with current/active/complete states, phase icon + label; "Nächster Schritt: [Phase]" button triggers workflow transition; blocked transitions show tooltip with guard reason; force transition button (admin only) with reason text input modal; transition history expandable timeline | | | | TASK-212 | Create `JobExportService` — accepts job ID, loads full data with joined customer/fair/fairLocation, resolves effective dates, renders Twig template (`templates/messebau/pdf/export.html.twig`), POSTs HTML to Gotenberg, returns PDF binary | | | | TASK-213 | Create job PDF export endpoint — `GET /api/messebau/jobs/{id}/export-pdf` invokes JobExportService, returns PDF with Content-Disposition: attachment | | | | TASK-214 | Create `templates/messebau/pdf/export.html.twig` — print-optimized HTML: 24HRS header, customer + fair + location info grid, stand details, material tables, checklist with checkmarks, acceptance info + signature; CSS @page, yellow accents, Inter font, page-break-inside: avoid | | | | TASK-215 | Create `CalendarController` — `GET /api/messebau/calendar?year=&month=&fairId=` — aggregate effective dates (override or inherited from fair) plus all deadline fields across jobs; return flat event list with type+color+jobId+fairId | | | | TASK-216 | Create `src/hooks/useCalendar.ts` — TanStack Query hook fetching calendar endpoint; store current month/year/fairId in local state | | | | TASK-217 | Create `src/components/calendar/CalendarToolbar.tsx` — prev/next month buttons, month-year heading, "Heute" button, fair selector dropdown, color legend | | | | TASK-218 | Create `src/components/calendar/CalendarGrid.tsx` — 7-column grid, day headers (Mo-So), correct day cells, leading/trailing grayed out | | | | TASK-219 | Create `src/components/calendar/CalendarDay.tsx` — day number (bold + today border), event pills with color-coded types: setup=#3498db, event=#FFE000, teardown=#e74c3c, deadline=#FF6B35; truncated text with customer+fair name, click navigates to /jobs/{jobId}; span events render on each day in range | | | | TASK-220 | Write Playwright test `slice-3-full-workflow.spec.ts` — create fair+job, upload briefing doc, transition angebot→planung→organisation→produktion, verify guards block (try to skip to aufbau), fill required fields, complete checklist, transition to aufbau, add acceptance photo+signature, transition to abgeschlossen, verify read-only; force-transition from abgeschlossen to abbau (admin override), verify reason logged in history; export PDF, verify download | | | | TASK-221 | Write Playwright test `slice-3-calendar.spec.ts` — create 2 jobs at same fair with different dates, open calendar scoped to that fair, verify events render on correct days, click event pill navigates to job detail | | | --- ### Slice 4 — AI document intelligence - GOAL-004: Pluggable AI skills triggered on asset upload to Pimcore DAM. Common chain (Classifier → MismatchDetector → VersionDetector → DocumentScopeDetector) runs on every upload. Field-specific skills extract data and surface non-destructive suggestions. User explicitly applies or ignores each suggestion. **Precondition:** INVESTIGATE cards from DEC-004 must be resolved before Slice 4 work begins: 1. Monthly LLM cost estimate at 10,000+ calls/year 2. SetupProgressAnalyzer auto-check confidence threshold and false-positive safety measures | Task | Description | Completed | Date | |------|-------------|-----------|------| | TASK-301 | **INVESTIGATE:** Estimate monthly LLM cost — audit expected call volume per skill type × model pricing per token (gpt-4o-mini for most skills, gpt-4o for LeadMagnetIdeator, gpt-4o vision for SetupProgressAnalyzer + BrandConsistencyChecker); produce budget range; if >500€/month at full scale, recommend cost optimization strategy | | | | TASK-302 | **INVESTIGATE:** Define confidence floor for auto-actions — what confidence threshold prevents SetupProgressAnalyzer from auto-checking checklist items that weren't actually done; propose: auto-check only if confidence ≥ 0.90 AND the corresponding checklist item text semantically matches the detected change by cosine similarity ≥ 0.80; all lower-confidence matches surface as suggestions only | | | | TASK-303 | Create `SuggestionService` in `bundles/MessebauBundle/Service/` — stores AI suggestions as JSON field `aiSuggestions` on Job DO (array of `{id, skill, field, suggestedValue, confidence, sourceAssetId, status(pending/applied/ignored), createdAt}`); methods: add(), apply() writes to actual field, ignore(); API endpoints: `GET /api/messebau/jobs/{id}/suggestions`, `POST .../suggestions/{suggestionId}/apply`, `POST .../suggestions/{suggestionId}/ignore` | | | | TASK-304 | Create `DocumentClassifier` (SKILL-001) — Symfony service triggered on `asset.postAdd`; analyzes file extension, MIME type, filename patterns, first 4KB of content; classifies into: briefing, brandbook, floorplan, confirmation, confirmation-quote, stand-photo, acceptance-photo, weekly-protocol, consultation-summary, other. Pure regex + metadata — no LLM | | | | TASK-305 | Create `DocumentMismatchDetector` (SKILL-008) — triggered by common chain; compares classified type against upload field context; if mismatch (e.g. floorplan uploaded to briefing area), returns `{suggestedField, reason}`; blocks skill chain execution until user resolves (Move / Keep / Cancel) | | | | TASK-306 | Create `DocumentVersionDetector` (SKILL-008a) — triggered if field has ≥1 prior asset; filename Levenshtein distance + content comparison; if newer version detected: diff extracted dimensions/budgets/dates, surface warning with severity (info/warning/critical); user chooses Replace or Keep both | | | | TASK-307 | Create `DocumentScopeDetector` (SKILL-001a) — triggered on briefing-type uploads; detects customer/fair/job scope via heuristics (multi-fair mentions → customer, "alle Stände" + booth counts → fair, single booth + company → job); if scope mismatches upload field, suggests relocation with target entity preview; if user moves: asset relocated, skills re-run on correct entity | | | | TASK-308 | Create `AiSkillTriggerListener` — Symfony event listener on `asset.postAdd`; runs Classifier + MismatchDetector inline (sync, <5ms); dispatches one `AnalyzeDocumentMessage` per applicable skill to Symfony Messenger queue `ai_skills` (Redis or Doctrine transport) | | | | TASK-309 | Configure Symfony Messenger — `ai_skills` transport, multiple `messenger:consume` workers for parallel skill execution | | | | TASK-310 | Create `ContactExtractor` (SKILL-002) — triggered on briefing/confirmation docs; regex for email/phone → LLM fallback for name/role; output: `{name, email, phone, role}[]` → targets customer.contactName/Phone/Email | | | | TASK-311 | Create `DeadlineDetector` (SKILL-003 + SKILL-004a) — triggered on any document; regex for date patterns (DD.MM.YYYY, German relative dates, date ranges like "01.–02.07.2026"); field mapping heuristics (Aufbau→setupStart, Strom→power.deadline, etc.); conflict detection if document date differs from stored deadline | | | | TASK-312 | Create `MultiFairDetector` (SKILL-004b) — triggered on briefing/consultation-summary; LLM extraction of fair name, size, standType, location, budget; deduplication against existing fairs; suggestion: "Create N new fair records for customer X?" | | | | TASK-313 | Create `BudgetExtractor` (SKILL-004c) — triggered on any doc with currency amounts; regex for monetary patterns (€/EUR/k€) with context mapping (Standbau→costEstimate.standConstruction, Aktivierung→activation, etc.); fallback to regex-only if no LLM available | | | | TASK-314 | Create `PainPointSummarizer` (SKILL-004d) — triggered on consultation-summary/briefing; LLM extraction of painPoints[], expectations[], structuredNotes; suggestion: append/replace job.notes | | | | TASK-315 | Create `ChecklistGenerator` (SKILL-004) — triggered on briefing; LLM extraction of actionable tasks in German; deduplication against existing checklist items via cosine similarity of text embeddings | | | | TASK-316 | Create `StandDataExtractor` (SKILL-005) — triggered on floorplan; PDF/DWG text layer parsing for dimensions, standType, hall, standNumber; image-based fallback: LLM vision; comparison >10% discrepancy flags warning | | | | TASK-317 | Create `ConfirmationParser` (SKILL-006) — triggered on confirmation; LLM extraction of serviceType, details, confirmationNumber, bookingDate, status; auto-confirm if confirmation number found in text; discrepancy flag if booked values differ from stored | | | | TASK-318 | Create `BrandConsistencyChecker` (SKILL-007) — triggered on 2nd+ stand photo AND brandbook exists; extract brand colors from brandbook, analyze photo color histogram, Delta-E comparison; LLM vision: "Is logo visible? Position? Deviations from guidelines?"; result: green checkmark or amber warning overlay in photo gallery | | | | TASK-319 | Create `AcceptanceReportGenerator` (SKILL-010) — manual trigger only (button on Tab 6); LLM vision: analyze acceptance photos + remarks → structured report in German → Gotenberg PDF; includes photo gallery with annotations, issue summary, sign-off block | | | | TASK-320 | Create `SetupProgressAnalyzer` (SKILL-011) — triggered on 2nd+ stand photo; LLM vision: compare two most recent photos chronologically; detect structural changes (walls erected, flooring laid, graphics mounted, furniture placed, equipment installed, cleaning completed); auto-check mapping per DEC-302 confidence floor; all lower-confidence detections → suggestion only | | | | TASK-321 | Create `WeeklyProtocolSummarizer` (SKILL-012) — triggered on weekly-protocol PDF upload; LLM extract: date, status update, open items, decisions, next steps; renders as structured summary card on Tab 7 above file list | | | | TASK-322 | Create `MaterialListExtractor` (SKILL-009) — triggered on briefing/floorplan; LLM extraction of materials categorized as flooring/standsystem/hardware/furniture with name + suggested quantity | | | | TASK-323 | Create `LeadMagnetIdeator` (SKILL-009a) — triggered on briefing/consultation-summary with activation requirements; LLM (gpt-4o, T=0.7): "Generate 3 creative lead magnet ideas tied to company product, fitting budget, for event type"; context consumed: company description, product info, previous activations, budget constraints, event atmosphere | | | | TASK-324 | Create `src/components/shared/SuggestionBadge.tsx` — yellow notification banner near affected form field; shows suggested value, confidence %, source document name; [Apply] / [Ignore] buttons; per-tab [Apply All]; tab headers show pending suggestion count badge; low-confidence suggestions (< configurable threshold) hidden by default with toggle | | | | TASK-325 | Integrate SuggestionBadge into all affected tabs (Tab 0 MasterData, Tab 1 Briefing, Tab 3 Produktion, Tab 4 Checklist, Tab 5 Standfotos, Tab 7 Weekly) | | | | TASK-326 | Write Playwright test `slice-4-ai-pipeline.spec.ts` — upload Brevo consultation doc to briefingCustomerDoc; verify DocumentClassifier identifies it; verify MismatchDetector passes (correct field); verify ContactExtractor surfaces Lisa Reinhardt suggestion; apply it; verify job.contactName updated; upload floor plan to briefingCustomerDoc; verify MismatchDetector suggests move to briefingPlanDoc; move it; verify asset relocated | | | | TASK-327 | Write Playwright test `slice-4-setup-progress.spec.ts` — upload first stand photo; verify no analysis; upload second (different angle, more construction); verify SetupProgressAnalyzer compares images; verify low-confidence matches (<0.90) become suggestions, not auto-checks; verify high-confidence match (>0.90) auto-checks corresponding checklist item | | | --- ### Slice 5 — Fair-level resource planning - GOAL-005: Resource pool management with demand planning, worker/vehicle/equipment/accommodation assignment, travel management, and fill-gap visualization at fair level. Replaces per-job ad-hoc team/vehicle/hotel lists from the prototype. | Task | Description | Completed | Date | |------|-------------|-----------|------| | TASK-401 | Create Data Object `WorkerRole` — name (e.g. Barista, Rigger, LorryDriver, FairWorker, Electrician, Cleaner) | | | | TASK-402 | Create Data Object `Worker` — firstName, lastName, email, phone; role (n→1 to WorkerRole); state (internal/hired/toHire); notes | | | | TASK-403 | Create Data Object `PoolVehicle` — name, type, licensePlate; state (owned/hired/toHire); notes | | | | TASK-404 | Create Data Object `Equipment` — name, description; state (owned/rented/toRent); notes | | | | TASK-405 | Create Data Object `WorkerDemand` — fair (n→1 to Fair, required), job (n→1 to Job, nullable), workerRole (n→1 to WorkerRole, required); quantity (integer); notes | | | | TASK-406 | Create Data Object `WorkerAssignment` — fair (n→1 to Fair, required), job (n→1 to Job, nullable), worker (n→1 to Worker, required); startDate, endDate (datetime); notes | | | | TASK-407 | Create Data Object `PoolVehicleAssignment` — fair, job (nullable), poolVehicle; startDate, endDate; notes | | | | TASK-408 | Create Data Object `EquipmentAssignment` — fair, job (nullable), equipment; startDate, endDate; notes | | | | TASK-409 | Create Data Object `Accommodation` — name, address, bookingReference; type (hotel/apartment/other); notes | | | | TASK-410 | Create Data Object `AccommodationAssignment` — fair, accommodation, worker (nullable); checkIn, checkOut (date); notes. Multiple workers can share same accommodation | | | | TASK-411 | Create Data Object `Travel` — worker (n→1 to Worker, required), fair (n→1 to Fair, required); type (oneWay/roundTrip); origin, destination, transportMethod, bookingReference; departureDateTime, arrivalDateTime; returnDepartureDateTime, returnArrivalDateTime (nullable); notes | | | | TASK-412 | Create controllers: `WorkerRoleController`, `WorkerController`, `PoolVehicleController`, `EquipmentController`, `AccommodationController` — full CRUD for each pool entity | | | | TASK-413 | Create `FairResourceController` — `GET /api/messebau/fairs/{id}/resources` returns all demands+assignments grouped by type with gap computed (needed − assigned); `POST/PUT/DELETE /api/messebau/fairs/{id}/demands` manages demand slots; `POST/PUT/DELETE /api/messebau/fairs/{id}/assignments` manages worker/vehicle/equipment assignments | | | | TASK-414 | Create `JobResourceController` — `GET /api/messebau/jobs/{id}/resources` returns demands+assignments scoped to this job | | | | TASK-415 | Create `FairAccommodationController` — `GET /api/messebau/fairs/{id}/accommodations` returns accommodations + assignments + occupancy; `POST/PUT/DELETE /api/messebau/fairs/{id}/accommodations/{accId}/assign` | | | | TASK-416 | Create `WorkerTravelController` — `GET /api/messebau/workers/{id}/travel` (filterable by fairId); `POST/PUT/DELETE /api/messebau/workers/{id}/travel` | | | | TASK-417 | Create `src/types/resources.ts` — TypeScript interfaces for all 11 resource pool entities | | | | TASK-418 | Create `src/api/` files for all resource API endpoints (workerroles, workers, poolvehicles, equipment, accommodations, fairResources, jobResources, travel) | | | | TASK-419 | Create `src/hooks/useFairResources.ts` + `src/hooks/useJobResources.ts` — TanStack Query hooks | | | | TASK-420 | Create `src/components/fair/FairResourcesTab.tsx` — four grids: **Workers by Role** (role, needed qty, assigned count, gap, assigned workers with job labels, Add Demand / Assign Worker buttons), **Trucks** (vehicle name, state badge, assigned to job/dates), **Equipment** (item name, state badge, assigned to job/dates), **Accommodation** (name, type badge, occupancy, assigned workers with check-in/out, Assign Worker button); modals for add-demand and add-assignment | | | | TASK-421 | Create `src/components/resources/WorkerPoolView.tsx` — list all workers filterable by role+state, CRUD, "Add Worker" form; each worker row shows assigned fairs + travel count | | | | TASK-422 | Create `src/components/resources/PoolVehicleView.tsx` + `src/components/resources/EquipmentView.tsx` + `src/components/resources/AccommodationPoolView.tsx` — same CRUD pattern | | | | TASK-423 | Create `src/components/resources/WorkerDetailView.tsx` — worker info + role + state; travel records list (outbound/return info, transport method, booking reference, Add Travel button, one-way toggle); accommodation assignments at each fair; fair assignments | | | | TASK-424 | Update Sidebar — add "Ressourcen" nav item (collapsible sub-items: Worker Rollen, Worker Pool, Fahrzeug Pool, Equipment Pool, Unterkünfte); hidden for customer role | | | | TASK-425 | Update OrganisationTab (from Slice 3) — replace placeholder resource summary with live data from `GET /api/messebau/jobs/{id}/resources`; each item shows name + role/type + state badge + dates; "[Manage all resources for this fair →]" links to FairResourcesTab | | | | TASK-426 | Write Playwright test `slice-5-fair-resources.spec.ts` — open fair detail → Resources tab → add demand "3 Baristas" → verify gap=3 → create worker Anna (Barista, internal) → assign Anna → verify assigned=1, gap=2 → assign Anna to specific job → verify job Resources tab shows Anna; remove Anna → verify gap returns to 3 | | | | TASK-427 | Write Playwright test `slice-5-accommodation-travel.spec.ts` — create accommodation "Hotel Central" → assign worker Anna (check-in Jun 14, check-out Jun 18) → verify occupancy=1 → add travel for Anna: one-way Berlin→Hannover, Bahn, Jun 14 08:00→10:30 → verify worker detail shows travel + accommodation | | | --- ### Slice 6 — 3D booth generation - GOAL-006: Automated 3D booth model generation via SketchUp + Aluvision + MCP bridge. Driven by job data from the 24HRS application. Delivers .skp model, PNG preview, and BOM back to the job. **Architecture note per DEC-001:** Pimcore dispatches an async message to generate-3d queue. An MCP server worker pulls the message, invokes the SketchUp Ruby bridge via HTTP, stores results in Pimcore DAM, and updates job fields. Job-level lock prevents duplicate generations. Health endpoint on MCP server enables monitoring. **Precondition:** SketchUp Pro with Aluvision 3D Library installed on a dedicated machine. WEBrick bridge extension loaded. MCP server accessible to both Pimcore (for dispatching) and SketchUp bridge (localhost:9494). | Task | Description | Completed | Date | |------|-------------|-----------|------| | TASK-501 | **AUDIT:** Examine Aluvision 3D Library in SketchUp — catalog component naming conventions, folder structure, parameter patterns; document findings in `graphify-out/aluvision-audit.json` | | | | TASK-502 | Build SketchUp Ruby bridge — `aluvision_bridge.rb` extension entry point (WEBrick server on port 9494, auto-starts on SketchUp launch via UI.start_timer); implement `/health`, `/component/list`, `/booth/generate`, `/booth/export`, `/booth/bom` endpoints | | | | TASK-503 | Build `AluvisionRegistry` — Ruby module mapping logical component names → SketchUp library paths; includes all profile types, connectors, accessories, furniture, flooring as documented in 3d-booth-generation.md § Aluvision Registry Pattern | | | | TASK-504 | Build `BoothBuilder` — Ruby module: LAYOUT_WALLS mapping (6 stand types → wall topology), WALL_POSITIONS (position calculation per side), WALL_SIZES (dimension calculation); clears model, places walls, connectors, flooring, furniture, lead magnets; commits operation, zooms to fit | | | | TASK-505 | Build `WallBuilder`, `ConnectorBuilder`, `FlooringBuilder`, `FurniturePlacer`, `StorageBuilder` — individual component placement Ruby modules loaded by BoothBuilder | | | | TASK-506 | Build `BomExtractor` — enumerates placed components in active model, maps to Aluvision part numbers, computes totals (component count, profile length, connector count, flooring area); outputs JSON | | | | TASK-507 | Build `ModelExporter` — exports .skp (native save), renders PNG (SketchUp viewport capture), optional .glb/.dae export | | | | TASK-508 | Scaffold `aluvision-mcp-server/` — Node.js/TypeScript project with `@modelcontextprotocol/sdk`; implement `server.ts` entry point with StdioServerTransport | | | | TASK-509 | Implement `SketchUpClient` — HTTP client (fetch wrapper) targeting `http://localhost:9494`; GET for /health, /component/list, /booth/bom; POST for /booth/generate, /booth/export | | | | TASK-510 | Implement `LayoutEngine` — STAND_TYPE_TO_LAYOUT mapping (6 German standType strings → layout type); parseStandSize ("6x4" → {width:6000, depth:4000}); resolve() returns BoothSpec with layout, width, depth, height, finish | | | | TASK-511 | Implement all 12 MCP tool definitions (`aluvision_info`, `aluvision_build_booth`, `aluvision_create_wall`, `aluvision_create_corner`, `aluvision_create_storage`, `aluvision_create_door`, `aluvision_add_backlit`, `aluvision_arrange_layout`, `aluvision_export_bom`, `aluvision_export_model`, `aluvision_export_drawing`, `aluvision_get_component_info`) with parameter schemas in `aluvision_tools.ts` | | | | TASK-512 | Implement tool call handlers — Map each tool name to SketchUpClient method; aluvision_build_booth: resolve layout via LayoutEngine, POST to /booth/generate; aluvision_export_bom: GET /booth/bom; etc. | | | | TASK-513 | Create `Booth3dService` in Pimcore — buildParams(job) collects standType, size, height, flooring, furniture, lead magnets, storage rooms from job; mapStandType (6 German→English layout mapping); getFlooring/getFurniture/getLeadMagnets read from job Materials and field collections | | | | TASK-514 | Create `GenerateBooth3dMessage` — Symfony Messenger message class with jobId, params (serialized BoothSpec); dispatched by Pimcore controller | | | | TASK-515 | Create `GenerateBooth3dHandler` — Symfony Messenger handler: receives message, checks job-level lock (Redis key `booth3d:lock:{jobId}` with TTL), calls MCP server (spawns node process with stdio), receives result, stores .skp + PNG in Pimcore DAM, updates job.3dModelPath + job.3dPreviewPath + job.3dBom, releases lock | | | | TASK-516 | Create `Booth3dController` — `POST /api/messebau/jobs/{id}/generate-3d` dispatches GenerateBooth3dMessage; `GET /api/messebau/jobs/{id}/3d-status` returns generation status (queued/generating/complete/failed) + result paths if complete; `GET /api/messebau/jobs/{id}/3d-preview` returns PNG; `GET /api/messebau/jobs/{id}/3d-model` download link for .skp | | | | TASK-517 | Add 3D fields to Job DO — `3dModelPath`, `3dPreviewPath`, `3dBom` (JSON); `3dGenerationStatus` (queued/generating/complete/failed); `3dLastGenerated` (datetime) | | | | TASK-518 | Create `src/components/job-detail/Generate3dButton.tsx` — "3D Booth generieren" button on Tab 0 / DetailHeader; disabled if generation in progress; polling 3d-status every 5s while queued/generating; shows PNG preview in modal on complete; "Regenerieren" button for parameter tweaks; download .skp link; BOM summary alongside preview | | | | TASK-519 | Configure MCP server in Docker Compose — `aluvision-mcp` service linking to SketchUp bridge URL; environment variables for bridge URL and Aluvision library path | | | | TASK-520 | Implement MCP server health endpoint — `GET /health` returns MCP status, SketchUp bridge connectivity, last generation timestamp, queue depth | | | | TASK-521 | Configure Prometheus metrics or simple health check script — `curl http://localhost:9494/health` + `curl http://mcp-server/health` monitored via existing Docker health checks; alert if SketchUp bridge unreachable > 5 minutes | | | | TASK-522 | Write integration test `slice-6-booth-generation.spec.ts` — create job with Eckstand, 6x4, height 2.5; click "3D Booth generieren"; verify status transitions: queued → generating → complete; verify PNG preview displays in modal; verify BOM shows component count + profile lengths; download .skp file; regenerate with different standType (Inselstand), verify new model created | | | --- ### Cross-Cutting — Polish & Deployment (after core slices) | Task | Description | Completed | Date | |------|-------------|-----------|------| | TASK-901 | Install `sonner` for toast notifications; create `` in App.tsx | | | | TASK-902 | Add loading skeletons (shadcn/ui) for stats cards, job card grid, detail header, form fields, gallery items | | | | TASK-903 | Add error boundaries wrapping each major route/view; error message + retry button (queryClient.invalidateQueries) | | | | TASK-904 | Responsive design — sidebar collapses to hamburger on <768px; form grids stack single column; calendar days reduce height; gallery grid reduces columns | | | | TASK-905 | Install `vite-plugin-pwa` — service worker for static asset caching; offline indicator; PWA manifest (24HRS Messe-Lotse, yellow+black theme) | | | | TASK-906 | Create `frontend/Dockerfile` — multi-stage: Node.js build → nginx or Pimcore-served assets | | | | TASK-907 | Create `.github/workflows/ci.yml` — checkout, setup Node 20, npm ci, lint, typecheck, Vitest, Playwright (Pimcore container as service), build | | | | TASK-908 | Full manual QA — create job, fill all 8 tabs, upload files, save, reload, verify persistence; test calendar; test PDF export; test duplicate/delete; test mobile viewport; test all 4 roles (viewer, editor, admin, customer) | | | ## 3. Alternatives - **ALT-001**: The original `architecture-rewrite-1.0.md` plan with 12 sequential phases. Rejected because sequential phasing (backend-complete-before-frontend) is unsuitable for AI-only execution where each task must produce a verifiable working increment. - **ALT-002**: Monorepo with separate backend/frontend deployment. Rejected in favor of Pimcore-served React SPA (REQ-005) for simplicity — no CORS, session cookie reuse, single domain. - **ALT-003**: Direct SketchUp bridge calls from Pimcore (synchronous). Rejected per DEC-001 — requires async message queue for reliability, concurrent job locking, and health monitoring. ## 4. Dependencies - **DEP-001**: Pimcore ≥ 11.x with OAuth2, Gotenberg already running (CON-003) - **DEP-002**: Symfony Messenger with Redis or Doctrine transport (bundled with Pimcore) - **DEP-003**: Docker Compose environment (CON-001) — Slice 6 adds MCP server service - **DEP-004**: SketchUp Pro + Aluvision 3D Library (Slice 6 only) - **DEP-005**: OpenAI API key for gpt-4o-mini / gpt-4o / gpt-4o vision (Slice 4 only) - **DEP-006**: `@modelcontextprotocol/sdk` npm package (Slice 6 only) - **DEP-007**: `react-signature-canvas` npm package (Slice 3 only) ## 5. Files ### Plan Documents (this directory) - **FILE-001**: `plan/architecture-vertical-slices-1.0.md` — this file - **FILE-002**: `plan/architecture-rewrite-1.0.md` — original plan (superseded, retained for reference) - **FILE-003**: `plan/ai-document-skills.md` — AI skill specifications (referenced by Slice 4) - **FILE-004**: `plan/3d-booth-generation.md` — 3D booth generation specification (referenced by Slice 6) ### Backend (Pimcore) - **FILE-005**: `bundles/MessebauBundle/` — Symfony bundle directory (created in Slice 1) - **FILE-006**: `bundles/MessebauBundle/Resources/config/workflows/job.yml` — workflow configuration (created in Slice 3) - **FILE-007**: `bundles/MessebauBundle/Service/SuggestionService.php` — AI suggestion storage (created in Slice 4) - **FILE-008**: `bundles/MessebauBundle/EventListener/AiSkillTriggerListener.php` — AI skill trigger (created in Slice 4) - **FILE-009**: `bundles/MessebauBundle/Service/Booth3dService.php` — 3D parameter builder (created in Slice 6) - **FILE-010**: `templates/messebau/pdf/export.html.twig` — PDF export template (created in Slice 3) ### Frontend (React) - **FILE-011**: `frontend/` — Vite + React project directory (created in Slice 1) - **FILE-012**: `frontend/src/App.tsx` — application routes - **FILE-013**: `frontend/src/api/client.ts` — Axios client with auth interceptors - **FILE-014**: `frontend/src/stores/authStore.ts` — authentication state - **FILE-015**: `frontend/src/stores/uiStore.ts` — UI state - **FILE-016**: `frontend/src/hooks/usePermissions.ts` — role-based permission flags - **FILE-017**: `frontend/src/components/` — all React components per slice ### 3D Pipeline (Slice 6 only) - **FILE-018**: `sketchup-aluvision-bridge/` — SketchUp Ruby extension - **FILE-019**: `aluvision-mcp-server/` — Node.js MCP server ## 6. Testing ### E2E (Playwright) - **TEST-001**: `slice-1-auth-create-fair.spec.ts` — auth flow, entity creation, role gating - **TEST-002**: `slice-2-jobs-crud.spec.ts` — job CRUD, relations, file upload, role scoping - **TEST-003**: `slice-3-full-workflow.spec.ts` — 8-state workflow, all tabs, PDF export, calendar - **TEST-004**: `slice-3-calendar.spec.ts` — calendar event rendering, navigation - **TEST-005**: `slice-4-ai-pipeline.spec.ts` — document upload → classification → extraction → suggestion → apply/ignore - **TEST-006**: `slice-4-setup-progress.spec.ts` — photo comparison, confidence threshold, auto-check safety - **TEST-007**: `slice-5-fair-resources.spec.ts` — demand/assignment/gap flow, job-scoped resources - **TEST-008**: `slice-5-accommodation-travel.spec.ts` — accommodation + travel assignment - **TEST-009**: `slice-6-booth-generation.spec.ts` — async 3D generation, preview, BOM, regeneration ### Unit (Vitest) - **TEST-010**: `authStore.test.ts` — login, logout, token refresh - **TEST-011**: `usePermissions.test.ts` — role → flag mapping - **TEST-012**: `MasterDataTab.test.tsx` — form validation, date inheritance display - **TEST-013**: `useJobs.test.ts` — fetch, filter, pagination ### Backend (PHPUnit) - **TEST-014**: FairLocationControllerTest, CustomerControllerTest, FairControllerTest, JobControllerTest - **TEST-015**: WorkflowTest, WorkflowTransitionListenerTest (force transition guard bypass) - **TEST-016**: SuggestionServiceTest (apply, ignore, deduplication) ## 7. Risks & Assumptions - **RISK-001**: AI-only execution without human developers — all code must be independently verifiable by Playwright tests. Each slice ships with E2E tests that prove functionality. Slice quality gates on test pass. - **RISK-002**: LLM operational cost may exceed budget (DEC-004). Mitigation: resolve INVESTIGATE card before Slice 4 begins. If cost is prohibitive, defer non-critical vision skills (BrandConsistencyChecker, LeadMagnetIdeator) to a later release. - **RISK-003**: SketchUp must be running on a dedicated machine for 3D generation (Slice 6). Mitigation: health monitoring + alerting (TASK-521); generation is a nice-to-have, not blocking earlier slices. - **RISK-004**: Aluvision 3D Library component naming may not match expected patterns. Mitigation: TASK-501 audits the library before any implementation begins; AluvisionRegistry acts as adapter layer. - **ASSUMPTION-001**: Pimcore, Gotenberg, and OAuth2 are already operational in the Docker Compose environment (CON-003). - **ASSUMPTION-002**: The index.html prototype accurately represents the required feature set for Slices 1-3. Any deviations discovered during implementation are documented as changelog entries against the prototype. - **ASSUMPTION-003**: All 12 AI skills defined in `plan/ai-document-skills.md` are in scope for Slice 4. If INVESTIGATE card reveals cost concerns, Tier 3 skills may be deferred. ## 8. Related Specifications / Further Reading - [Architecture Rewrite 1.0 (Original Plan)](architecture-rewrite-1.0.md) — superseded by this document, retained for detailed backend task breakdowns - [AI Document Processing Skills](ai-document-skills.md) — full skill catalog with extraction strategies, prompts, and routing matrix - [3D Booth Generation Plan](3d-booth-generation.md) — SketchUp + Aluvision + MCP architecture, tool catalog, layout engine spec - [CFC2026 Briefing Documents](documents/) — source material for AI extraction testing - [AI Skill Prompts](prompts/) — LLM prompt templates for each skill