# Learnings & Conventions Convention log for CTS Presenter App development. ## 2026-03-01 - Task 0 CTS API Spike - `5pm-hdh/churchtools-api` v2.1.0 bietet sowohl `CTConfig::setApiKey()` als auch `CTConfig::authWithLoginToken()`; `setApiKey` ist als deprecated markiert, funktioniert aber weiterhin fuer Token-Auth. - Events-Fetch fuer heute+zukunft laeuft ueber `EventRequest::where('from', 'YYYY-MM-DD')->get()` und sendet Query-Parameter `from` plus `page`. - Song-Response enthaelt im verwendeten Shape `ccli`, `arrangements` und optional `lyrics` als nested Objekt. - Ohne gesetzte Runtime-Variablen `CTS_API_URL`/`CTS_API_TOKEN` ist nur ein Mock-basierter Spike moeglich; Live-Auth muss spaeter mit echten Env-Werten nachgezogen werden. ## 2026-03-01 - Task 1 Laravel Scaffolding + Breeze Vue + Docker - Laravel 11 + Breeze (Vue stack) + Pest scaffolding completed successfully - Inertia.js integration required explicit app.js setup with createInertiaApp() and resolvePageComponent() - Vite v7 requires @vitejs/plugin-vue to be explicitly configured in vite.config.js - Tailwind CSS v4 requires @tailwindcss/vite v4 (not v3) - version mismatch caused build failures - npm install requires --legacy-peer-deps flag due to Vite v7 + @vitejs/plugin-vue v5 compatibility - Docker build: PHP 8.4-fpm-alpine chosen over 8.3 due to composer.lock requiring PHP 8.4+ dependencies - Docker build: Removed Imagick PECL installation (requires autoconf) - can be added later if needed - Locale set to 'de' in config/app.php via env() with fallback - Vue packages (@vueuse/core, vue-draggable-plus, @jaxtheprime/vue3-dropzone) added to package.json - docker-compose.yml v2 syntax used (no version field needed, but warning appears) - Vite server configured for Docker with host: '0.0.0.0' and hmr settings for hot-reload - QA Scenario 1: docker compose up -d successfully starts app + node containers - QA Scenario 2: npm run build succeeds with 784 modules transformed, ~255KB gzipped app bundle - All Pest tests pass (home route returns 200 with Inertia response) - Task 0 spike files (CtsApiSpikeTest.php, CtsApiSpikeSync.php) preserved in new structure ## [2026-03-01 19:25] Task 1: Laravel Scaffolding + Breeze Vue + Docker ### Package Version Compatibility - Laravel 12 requires specific package versions: - `vite`: `^7.0.0` (matches laravel-vite-plugin@^2.0.0 peer dependency) - `@vitejs/plugin-vue`: `^6.0.0` (compatible with Vite 7) - Avoid version mismatches - check peer dependencies before install ### Docker Setup - Multi-stage Dockerfile with PHP 8.3-fpm-alpine base - LibreOffice + ImageMagick installed successfully in Alpine - App container starts successfully on port 8000 - Node container requires `npm install` to be run inside container for first-time setup - docker-compose.yml `version` attribute is obsolete (warning can be ignored) ### Laravel Autoload - Spike files relocated from `src/Cts/` to `app/Cts/` to match Laravel PSR-4 autoload - Laravel expects application code in `app/` directory - Custom `src/` directory requires composer.json autoload configuration ### Vite Configuration - Configured for Docker with `host: '0.0.0.0'` and `port: 5173` - HMR configured for localhost access from host machine - Build succeeds with 784 modules transformed ### Tests - 5 tests passing (14 assertions) - CTS API spike tests preserved from Task 0 - Home route test added for Inertia verification - Breeze default tests included ### Locale - App locale set to `'de'` in `config/app.php` - All UI text must be in German with "Du" form (per project spec) ## [2026-03-01 19:45] Wave 1 Complete (T2-T7) ### Database Schema (T2) - 10 migrations created in correct dependency order - All Eloquent models have proper relationships (hasMany, belongsTo, belongsToMany) - Factory classes generate valid test data - Soft deletes on songs and slides tables working correctly - JSON casts for cts_data, churchtools_groups, churchtools_roles ### ChurchTools OAuth (T3) - Custom Socialite provider successfully replaces Breeze auth - All Breeze register/password-reset pages removed - OAuth-only login with German UI ("Mit ChurchTools anmelden") - User creation from OAuth callback with churchtools_id, avatar, groups, roles - 9 tests passing (54 assertions) ### CTS API Sync (T4) - ChurchToolsService wraps 5pm-HDH/churchtools-api correctly - CCLI-based song matching works (matched_at timestamp set) - Unmatched songs preserved with song_id=null for manual matching - Sync log entries created with counts and status - German flash messages ("Daten wurden aktualisiert") ### File Conversion (T5) - Intervention Image v3 letterbox/pillarbox working perfectly - 400×300 PNG → 1920×1080 JPG with black bars (no upscaling) - Portrait images get pillarbox (black bars left/right) - PPT conversion queued via ConvertPowerPointJob (NOT synchronous) - Thumbnail generation at 320×180 - ZIP extraction and recursive processing implemented ### Shared Vue Components (T6) - AuthenticatedLayout with sticky nav, sync button, user avatar - useAutoSave composable with 500ms debounce for text inputs - FlashMessage, ConfirmDialog, LoadingSpinner components - HandleInertiaRequests middleware shares auth.user, flash, last_synced_at, app_name - All German UI text with "Du" form - 7 tests passing ### Email Configuration (T7) - MissingSongRequest mailable with German template - Email includes song name, CCLI ID, service info, link to service - SONG_REQUEST_EMAIL configurable via .env - Subject: "Song-Anfrage: {songName} (CCLI: {ccliId})" - 2 tests passing (10 assertions) ### Test Suite Health - All 30 tests passing (233 assertions) - No LSP errors (PHP LSP not configured, but tests validate correctness) - Clean git history with atomic commits ### Blockers Resolved - Fixed ChurchToolsSyncTest: changed `post()` to `$this->post()` (trivial fix) - No other blockers encountered ### Next Steps - Wave 2 ready to start (T8-T13): Service List, Song CRUD, Slide Upload, Arrangement Configurator, Song Matching, Translation Service - All Wave 2 tasks can run in parallel (no dependencies between them) ## [2026-03-01] Task T12: Song Matching Service (CCLI ID) ### SongMatchingService Pattern - Dedicated service class for all song matching operations: autoMatch, manualAssign, requestCreation, unassign - autoMatch checks: (1) not already assigned, (2) has cts_ccli_id, (3) Song with matching ccli_id exists in DB - manualAssign overwrites any existing assignment (no guard needed) - requestCreation sends MissingSongRequest mailable to configurable SONG_REQUEST_EMAIL address - unassign clears both song_id and matched_at ### ChurchToolsService Integration - Refactored syncServiceAgendaSongs to use SongMatchingService::autoMatch instead of inline DB matching - updateOrInsert creates the record first (without song_id), then Eloquent lookup + autoMatch - Preserves existing manual assignments during re-sync (only autoMatches if song_id is null) - app(SongMatchingService::class) used inside the method to avoid constructor injection on the sync service ### API Routes - ServiceSongController with 3 POST endpoints under auth:sanctum middleware - Routes: /api/service-songs/{id}/assign, /request, /unassign - German response messages: 'Song erfolgreich zugeordnet', 'Anfrage wurde gesendet', 'Zuordnung entfernt' - Validation on assign: song_id required + exists:songs,id - findOrFail gives automatic 404 for missing ServiceSong records ### Testing - 14 tests (33 assertions) covering service methods + API endpoints + auth + validation - Factory-based tests with ServiceSong::factory(), Song::factory(), User::factory() - Mail::fake() for email assertion without actually sending - Unique constraint gotcha: Song factory generates ccli_id with 80% probability, need explicit overrides in tests ### Pre-existing Test Failures (NOT caused by T12) - TranslationServiceTest: TranslationService class not yet created (future task) - ArrangementControllerTest: Routes not yet defined (future task) - ServiceControllerTest: Vite manifest missing Vue component (frontend task) ## [2026-03-01] Task T10: Slide Upload & Grid Components ### SlideController - SlideController handles 3 routes: POST /slides, DELETE /slides/{slide}, PATCH /slides/{slide}/expire-date - Image uploads processed synchronously via FileConversionService (1920×1080 JPG + 320×180 thumbnail) - PPT uploads stored to temp location then dispatched via ConvertPowerPointJob (async) - ZIP uploads processed recursively via processZip() — images sync, PPTs async - Moderation & sermon slides require service_id; information slides can be global (service_id nullable) - Extension validation done manually after Laravel validation (controller checks getClientOriginalExtension) ### Vue Components - @jaxtheprime/vue3-dropzone v1.1.0: v-model for files, @change event when files selected, slots for placeholder-img/title/description - CSS variables for theming: --v3-dropzone--primary, --v3-dropzone--border, etc. - Must hide default preview with `.v3-dropzone__preview { display: none }` since we use SlideGrid - SlideUploader emits 'uploaded' after all files processed sequentially - SlideGrid emits 'deleted' and 'updated' for parent refresh - Inline expire date editing: click-to-edit pattern with save/cancel buttons ### Testing - PPTX controller test requires mocking FileConversionService since fake files aren't real PowerPoint - Use `$this->postJson()` for validation tests expecting 422 (otherwise Laravel redirects with 302) - Mockery mock with `app()->instance()` works for service container injection - All 15 SlideControllerTest tests pass (37 assertions) - Full suite: 103 tests, 488 assertions ### Design Patterns - Amber/orange color palette matches AuthenticatedLayout nav gradient (from-amber-500 to-orange-600) - Dropzone uses dashed border + amber gradient background for hover states - Slide cards: rounded-xl, subtle shadow, hover lift (-translate-y-0.5), overlay controls - Expire date color coding: red=expired, amber=expires within 7 days, emerald=active, gray=no date