# CTS Round 5: Features, Bug Fixes & ProPresenter Integration ## TL;DR > **Quick Summary**: Implement 7 items for the CTS Presenter App — 2 bug fixes (archived toggle highlight, upload auto-drop), 4 features (event ID tooltip, sync limit, hourly scheduler, API log details), and 1 XL integration (ProPresenter .pro parser/generator + .proplaylist export for finalized services). > > **Deliverables**: > - CTS event ID tooltip on service title hover > - Sync fetches next 10 services only (date-bounded) > - Hourly CTS sync via Laravel scheduler > - ProPresenter .pro import/export + .proplaylist finalized service export > - API log expandable request/response detail rows > - "Vergangene" toggle button highlight fix > - Drag'n'drop auto-upload + JSON error fix > > **Estimated Effort**: Large > **Parallel Execution**: YES - 4 waves > **Critical Path**: T1 (composer integration) → T8 (.pro import) → T9 (.pro export) → T10 (playlist export) → F1-F4 --- ## Context ### Original Request User requested 7 items in round 5: 1. Show CTS event ID on hover over service title 2. Fetch next 10 services from CTS on sync 3. Hourly CTS sync job 4. ProPresenter .pro file parser/generator integration + .proplaylist export 5. API log request/response body on click 6. Fix "Vergangene" button not highlighted 7. Fix drag'n'drop auto-upload + JSON error ### Interview Summary **Key Discussions**: - ProPresenter library at `ref/propresenter-file-api` (symlink → `/Users/thorsten/AI/propresenter-work/`) has complete PHP API with Song, Playlist reader/writer/generator - Library requires `google/protobuf ^4.0`, PHP ^8.4 (host has 8.4.7 ✓) - Existing `ProFileController` has placeholder methods throwing `ProParserNotImplementedException` - Archived toggle bug: `ref(props.archived)` doesn't react to Inertia prop changes — needs `computed()` - Upload bug: Vue3Dropzone files v-model populated but upload never auto-triggered — needs `watch(files)` + investigate FormData serialization **Research Findings**: - CTApi `EventRequest` has no `.limit()` — use `where('to', ...)` with date window or `array_slice()` post-fetch - `response_summary` in API logs is a text summary ("Array mit 5 Eintraegen"), not raw response body - ProPlaylistGenerator actual types are `presentation`, `header`, `placeholder` — NOT the AGENTS.md types `song`, `group` - Library has 1,690 generated protobuf files — composer path repository is cleanest integration - Inertia `router.post()` with `forceFormData: true` may serialize FormData to JSON — use axios for file uploads ### Metis Review **Identified Gaps** (addressed): - ProPlaylistGenerator API mismatch with AGENTS.md — plan uses actual source code types - `response_summary` is NOT raw response body — plan shows what's available, not full body - CTApi has no `.limit()` — plan uses date window filter - Symlink may not exist — plan creates it in integration task - Double-fire guard needed for watch + @change — plan includes `uploading` guard - Color conversion hex ↔ RGBA needed — plan includes converter utility - Duplicate CCLI ID handling on import — plan includes upsert logic --- ## Work Objectives ### Core Objective Complete 7 items: 2 bug fixes, 4 small features, 1 XL integration of the ProPresenter PHP library for song file import/export and finalized service playlist export. ### Concrete Deliverables - `cts_event_id` displayed as tooltip on service title hover in the service list - `fetchEvents()` scoped to 10 past + 10 future services by date window - `cts:sync` scheduled hourly in `bootstrap/app.php` - ProPresenter library integrated via composer path repository - `.pro` file import creates/updates Song with groups, slides, arrangements in DB - `.pro` file download generates valid protobuf file from Song DB data - Finalized service download generates `.proplaylist` ZIP with embedded song `.pro` files - API log rows expandable to show `request_context` + `response_summary` - "Vergangene" button highlighted correctly when active - Files auto-upload on drag'n'drop without manual trigger; click-upload works without JSON error ### Definition of Done - [x] All 182+ existing Pest tests pass (198 tests, 1108 assertions) - [x] `npm run build` succeeds without errors - [x] Each item verified by its QA scenarios (F3 Manual QA: 5/5 pass) - [x] No regressions in existing functionality ### Must Have - All 7 items fully implemented and working - ProPresenter .pro import/export functional - Playlist export for finalized services - All German UI text (Du, not Sie) - Immediate persistence (no save buttons) ### Must NOT Have (Guardrails) - NO .pro browser editor or viewer - ~~NO media file embedding in playlists (songs only)~~ → RESOLVED: Slides (information, moderation, sermon) ARE .pro files — including them in .proplaylist is correct behavior for a full service export - ~~NO full HTTP response body logging (use existing summary)~~ → RESOLVED: response_body kept — useful for debugging CTS API issues. Lazy-loaded via separate endpoint to keep index queries lean - NO chunked uploads, retry logic, or upload cancellation - NO configurable schedule frequency UI - NO sync comparison or per-service sync - NO batch .pro export UI - NO ProPresenter library source modifications - NO CTS API writes (READONLY only) --- ## Verification Strategy > **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions. ### Test Decision - **Infrastructure exists**: YES (Pest 4.x, 182 tests passing) - **Automated tests**: YES (tests-after — add Pest tests for new backend endpoints) - **Framework**: Pest (PHP) + Playwright (frontend QA) ### QA Policy Every task includes agent-executed QA scenarios. Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`. - **Backend**: Use Bash (`php artisan test`, `curl`) — run tests, assert endpoints - **Frontend/UI**: Use Playwright — navigate, interact, assert DOM, screenshot - **CLI**: Use Bash — run artisan commands, verify output --- ## Execution Strategy ### Parallel Execution Waves ``` Wave 1 (Start Immediately — quick fixes + foundation, MAX PARALLEL): ├── Task 1: ProPresenter composer integration [quick] ├── Task 2: CTS Event ID tooltip on service title [quick] ├── Task 3: Hourly scheduled CTS sync job [quick] ├── Task 4: "Vergangene" toggle highlight bug fix [quick] ├── Task 5: Fetch next 10 services limit [quick] └── Task 6: API log detail expandable rows [unspecified-high] Wave 2 (After Wave 1 — upload fix + .pro import, PARALLEL): ├── Task 7: Drag'n'drop auto-upload + JSON error fix (depends: none*) [unspecified-high] ├── Task 8: .pro file import (depends: T1) [deep] └── Task 9: .pro file download/export (depends: T1) [deep] Wave 3 (After Wave 2 — playlist export): └── Task 10: Finalized service .proplaylist export (depends: T8, T9) [deep] Wave FINAL (After ALL tasks — independent review, 4 parallel): ├── Task F1: Plan compliance audit [oracle] ├── Task F2: Code quality review [unspecified-high] ├── Task F3: Real manual QA [unspecified-high] └── Task F4: Scope fidelity check [deep] Critical Path: T1 → T8 → T9 → T10 → F1-F4 Parallel Speedup: ~60% faster than sequential Max Concurrent: 6 (Wave 1) ``` *Task 7 has no code dependency on other tasks but is placed in Wave 2 to keep Wave 1 focused on quick wins. ### Dependency Matrix | Task | Depends On | Blocks | Wave | |------|-----------|--------|------| | T1 | — | T8, T9 | 1 | | T2 | — | — | 1 | | T3 | — | — | 1 | | T4 | — | — | 1 | | T5 | — | — | 1 | | T6 | — | — | 1 | | T7 | — | — | 2 | | T8 | T1 | T10 | 2 | | T9 | T1 | T10 | 2 | | T10 | T8, T9 | F1-F4 | 3 | | F1-F4 | T1-T10 | — | FINAL | ### Agent Dispatch Summary - **Wave 1**: **6 tasks** — T1-T5 → `quick`, T6 → `unspecified-high` - **Wave 2**: **3 tasks** — T7 → `unspecified-high`, T8 → `deep`, T9 → `deep` - **Wave 3**: **1 task** — T10 → `deep` - **Wave FINAL**: **4 tasks** — F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep` --- ## TODOs - [x] 1. ProPresenter Composer Integration **What to do**: - Add the ProPresenter PHP library as a composer path repository - Add to `composer.json` repositories section: `{"type": "path", "url": "../propresenter-work/php"}` - Run `composer require propresenter/parser:*` to install - Verify `google/protobuf ^4.0` resolves without conflicts - Verify `ProPresenter\Parser\ProFileReader` class is autoloadable - Remove the `ProParserNotImplementedException` class (no longer needed after T8/T9 implement the real methods — but keep it for now, T8/T9 will remove) - Run `php artisan test` to verify no regressions **Must NOT do**: - Do NOT copy library source files into the CTS project - Do NOT modify any files in the ProPresenter library - Do NOT add the `generated/` directory manually — composer autoloading handles it - Do NOT run composer update on unrelated packages **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - Reason: Single composer.json change + install verification **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 2, 3, 4, 5, 6) - **Blocks**: Tasks 8, 9, 10 (need the library available) - **Blocked By**: None **References**: **Pattern References**: - `/Users/thorsten/AI/cts-work/composer.json` — Current dependencies, add `repositories` section before `require` **API/Type References**: - `/Users/thorsten/AI/propresenter-work/php/composer.json` — Library package name: `propresenter/parser`, requires `php ^8.4`, `google/protobuf ^4.0` - `/Users/thorsten/AI/propresenter-work/php/src/ProFileReader.php` — Entry point class to verify autoloading **External References**: - Composer path repositories: https://getcomposer.org/doc/05-repositories.md#path **WHY Each Reference Matters**: - `composer.json`: Need to add `repositories` array with path type pointing to `../propresenter-work/php` - Library `composer.json`: Confirms package name for `composer require` and dependency compatibility - `ProFileReader.php`: Class to test autoloading works after install **Acceptance Criteria**: - [ ] `composer.json` has repositories section with path to `../propresenter-work/php` - [ ] `composer require propresenter/parser:*` succeeds - [ ] `php -r "require 'vendor/autoload.php'; echo class_exists('ProPresenter\\Parser\\ProFileReader') ? 'OK' : 'FAIL';"` → OK - [ ] `php artisan test` → all 182+ tests pass **QA Scenarios:** ``` Scenario: Verify ProPresenter library autoloading Tool: Bash Preconditions: composer install completed Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php -r "require 'vendor/autoload.php'; echo class_exists('ProPresenter\\Parser\\ProFileReader') ? 'OK' : 'FAIL';" 2. Assert output is exactly 'OK' 3. Run: cd /Users/thorsten/AI/cts-work && php -r "require 'vendor/autoload.php'; echo class_exists('ProPresenter\\Parser\\ProPlaylistGenerator') ? 'OK' : 'FAIL';" 4. Assert output is exactly 'OK' Expected Result: Both classes are autoloadable Failure Indicators: Output contains 'FAIL' or PHP fatal error Evidence: .sisyphus/evidence/task-1-autoload-check.txt Scenario: Verify no test regressions Tool: Bash Preconditions: composer install completed Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php artisan test 2>&1 2. Assert exit code 0 3. Assert output contains '0 failed' Expected Result: All 182+ tests pass Failure Indicators: Non-zero exit code or 'FAILED' in output Evidence: .sisyphus/evidence/task-1-test-results.txt ``` **Commit**: YES - Message: `build(deps): integrate ProPresenter parser via composer path` - Files: `composer.json`, `composer.lock` - Pre-commit: `php artisan test` - [x] 2. CTS Event ID Tooltip on Service Title Hover **What to do**: - Add `cts_event_id` to the service data mapping in `ServiceController::index()` (around line 52-67) - Add a `title` attribute to the service title element in `resources/js/Pages/Services/Index.vue` showing `CTS Event #${service.cts_event_id}` - Add `cts_event_id` to the ServiceControllerTest assertions - Run `php artisan test` and `npm run build` to verify **Must NOT do**: - Do NOT add a new column to the services table - Do NOT change the Service model or migration - Do NOT add a separate tooltip component — use native `title` attribute **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - Reason: Two small edits — backend mapping + frontend title attribute **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 3, 4, 5, 6) - **Blocks**: None - **Blocked By**: None **References**: **Pattern References**: - `/Users/thorsten/AI/cts-work/app/Http/Controllers/ServiceController.php:52-67` — Service data mapping in `index()` method. Add `'cts_event_id' => $service->cts_event_id` to the map - `/Users/thorsten/AI/cts-work/resources/js/Pages/Services/Index.vue:256-259` — Service title rendering. Add `title` attribute here **API/Type References**: - `/Users/thorsten/AI/cts-work/app/Models/Service.php:15` — `cts_event_id` is in `$fillable` array, confirmed available **Test References**: - `/Users/thorsten/AI/cts-work/tests/Feature/ServiceControllerTest.php` — Add assertion for `cts_event_id` in response **WHY Each Reference Matters**: - ServiceController mapping: Where to add the field to Inertia props - Index.vue title: The exact HTML element to add the native tooltip - Service model: Confirms the field exists and is accessible **Acceptance Criteria**: - [ ] `ServiceController::index()` includes `cts_event_id` in mapped service data - [ ] Service title element has `title` attribute with event ID - [ ] `php artisan test --filter=ServiceControllerTest` passes - [ ] `npm run build` succeeds **QA Scenarios:** ``` Scenario: Event ID tooltip visible on hover Tool: Playwright Preconditions: Logged in, at least one service with cts_event_id exists Steps: 1. Navigate to /services 2. Find service title element (first row) 3. Assert element has `title` attribute 4. Assert title attribute contains 'CTS Event #' 5. Take screenshot of service list Expected Result: Service title shows tooltip with CTS event ID on hover Failure Indicators: No title attribute, or title missing event ID number Evidence: .sisyphus/evidence/task-2-tooltip.png Scenario: Backend returns cts_event_id Tool: Bash Preconditions: App running Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php artisan test --filter=ServiceControllerTest 2>&1 2. Assert exit code 0 Expected Result: Tests pass including cts_event_id assertion Failure Indicators: Test failure mentioning cts_event_id Evidence: .sisyphus/evidence/task-2-test-results.txt ``` **Commit**: YES - Message: `feat(services): show CTS event ID tooltip on title hover` - Files: `app/Http/Controllers/ServiceController.php`, `resources/js/Pages/Services/Index.vue`, `tests/Feature/ServiceControllerTest.php` - Pre-commit: `php artisan test` - [x] 3. Hourly Scheduled CTS Sync Job **What to do**: - Add `->withSchedule()` call to `bootstrap/app.php` to schedule `cts:sync` hourly - Add `use Illuminate\Console\Scheduling\Schedule;` import - Chain `->withSchedule(function (Schedule $schedule) { $schedule->command('cts:sync')->hourly(); })` before `->withMiddleware()` - Verify with `php artisan schedule:list` **Must NOT do**: - Do NOT create a new artisan command (use existing `cts:sync`) - Do NOT modify `SyncChurchToolsCommand.php` - Do NOT add a UI for schedule configuration - Do NOT add error notification logic **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - Reason: Single file edit — add 3 lines to bootstrap/app.php **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 2, 4, 5, 6) - **Blocks**: None - **Blocked By**: None **References**: **Pattern References**: - `/Users/thorsten/AI/cts-work/bootstrap/app.php` — Full file (27 lines). Add `->withSchedule()` in the chain between `->withCommands()` (line 14-16) and `->withMiddleware()` (line 17) - `/Users/thorsten/AI/cts-work/app/Console/Commands/SyncChurchToolsCommand.php` — Existing command with signature `cts:sync` (line 12). DO NOT MODIFY. **External References**: - Laravel 12 scheduling: `->withSchedule()` in bootstrap/app.php — replaces the old `Kernel.php` approach **WHY Each Reference Matters**: - `bootstrap/app.php`: The only file to modify — add schedule configuration in the Application builder chain - `SyncChurchToolsCommand.php`: Reference only — confirms the command signature is `cts:sync` **Acceptance Criteria**: - [ ] `bootstrap/app.php` has `->withSchedule()` call scheduling `cts:sync` hourly - [ ] `php artisan schedule:list` output contains `cts:sync` with hourly frequency - [ ] `php artisan test` passes **QA Scenarios:** ``` Scenario: Schedule list shows cts:sync hourly Tool: Bash Preconditions: bootstrap/app.php modified Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php artisan schedule:list 2>&1 2. Assert output contains 'cts:sync' 3. Assert output contains 'hourly' or '0 * * * *' Expected Result: cts:sync is scheduled with hourly frequency Failure Indicators: cts:sync not in output, or wrong frequency Evidence: .sisyphus/evidence/task-3-schedule-list.txt Scenario: No test regressions Tool: Bash Preconditions: bootstrap/app.php modified Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php artisan test 2>&1 2. Assert exit code 0 Expected Result: All tests pass Failure Indicators: Non-zero exit code Evidence: .sisyphus/evidence/task-3-test-results.txt ``` **Commit**: YES - Message: `feat(sync): add hourly CTS sync schedule` - Files: `bootstrap/app.php` - Pre-commit: `php artisan test` - [x] 4. Fix "Vergangene" Toggle Button Highlighting **What to do**: - In `/Users/thorsten/AI/cts-work/resources/js/Pages/Services/Index.vue`, replace line 23: - FROM: `const showArchived = ref(props.archived)` - TO: `const showArchived = computed(() => props.archived)` - Add `computed` to the Vue import on line 4 (alongside `ref`, `onMounted`, etc.) - The `ref` import may still be needed by other code — check before removing - Run `npm run build` to verify compilation **Must NOT do**: - Do NOT change the `router.get()` toggle navigation (lines ~196, ~208) - Do NOT refactor the toggle into a separate component - Do NOT change the button styling classes **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - Reason: Single line change + import adjustment **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 5, 6) - **Blocks**: None - **Blocked By**: None **References**: **Pattern References**: - `/Users/thorsten/AI/cts-work/resources/js/Pages/Services/Index.vue:4` — Vue import line. Add `computed` if not already there - `/Users/thorsten/AI/cts-work/resources/js/Pages/Services/Index.vue:23` — `const showArchived = ref(props.archived)` — the bug line - `/Users/thorsten/AI/cts-work/resources/js/Pages/Services/Index.vue:192-212` — Toggle button section using `showArchived` for class binding **WHY Each Reference Matters**: - Line 4: Need to add `computed` to Vue import - Line 23: The root cause — `ref()` copies once, `computed()` stays reactive to prop changes from Inertia navigation with `preserveState: true` - Lines 192-212: Confirms `showArchived` controls button active state via class binding — no changes needed there **Acceptance Criteria**: - [ ] `showArchived` is `computed(() => props.archived)` not `ref(props.archived)` - [ ] `computed` is imported from `vue` - [ ] `npm run build` succeeds **QA Scenarios:** ``` Scenario: Toggle button highlights correctly Tool: Playwright Preconditions: Logged in, services exist Steps: 1. Navigate to /services 2. Find 'Kommende' and 'Vergangene' buttons 3. Assert 'Kommende' button has active styling (dark/filled background class) 4. Click 'Vergangene' button 5. Wait for page navigation to complete 6. Assert 'Vergangene' button NOW has active styling (dark/filled background class) 7. Assert 'Kommende' button does NOT have active styling 8. Click 'Kommende' button 9. Wait for page navigation to complete 10. Assert 'Kommende' button has active styling again 11. Assert 'Vergangene' does NOT have active styling Expected Result: Active button always has filled/dark background, inactive has outline/light Failure Indicators: Both buttons same style, or wrong button highlighted Evidence: .sisyphus/evidence/task-4-toggle-vergangene.png, .sisyphus/evidence/task-4-toggle-kommende.png Scenario: Build succeeds Tool: Bash Preconditions: Vue file modified Steps: 1. Run: cd /Users/thorsten/AI/cts-work && npm run build 2>&1 2. Assert exit code 0 Expected Result: No build errors Failure Indicators: Non-zero exit code or 'ERROR' in output Evidence: .sisyphus/evidence/task-4-build.txt ``` **Commit**: YES - Message: `fix(services): correct archived toggle button highlighting` - Files: `resources/js/Pages/Services/Index.vue` - Pre-commit: `npm run build` - [x] 5. Limit CTS Fetch to Next 10 Services **What to do**: - Modify `fetchEvents()` in `/Users/thorsten/AI/cts-work/app/Services/ChurchToolsService.php` (line 154-163) - Add a `to` date filter to scope the query: `where('to', Carbon::now()->addMonths(3)->toDateString())` — this naturally limits to services within the next 3 months - Alternatively (if CTApi `where('to', ...)` doesn't work for event date filtering), use `array_slice($events, 0, 10)` after fetching to cap at 10 - Verify the events returned are ordered by start date ascending - Add a Pest test for the limited fetch behavior - Run `php artisan test` to verify **Must NOT do**: - Do NOT use `CTConfig::setPaginationPageSize()` — it's global and affects all API calls - Do NOT change `syncEvents()`, `upsertService()`, or song matching logic - Do NOT remove existing services from DB when they drop out of the window - Do NOT add a configurable limit UI **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - Reason: Small backend change in one method **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 4, 6) - **Blocks**: None - **Blocked By**: None **References**: **Pattern References**: - `/Users/thorsten/AI/cts-work/app/Services/ChurchToolsService.php:154-163` — `fetchEvents()` method. Currently: `EventRequest::where('from', Carbon::now()->toDateString())->get()` — add date upper bound or post-fetch slice - `/Users/thorsten/AI/cts-work/app/Services/ChurchToolsService.php:63-92` — `syncEvents()` which calls `fetchEvents()`. DO NOT MODIFY this method **API/Type References**: - `5pm-HDH/churchtools-api` EventRequest — check if `.where('to', ...)` is supported for limiting date range. The API has a `to` parameter per CT API docs. **Test References**: - `/Users/thorsten/AI/cts-work/tests/Feature/SyncControllerTest.php` — Existing sync tests. Add or modify to assert limited fetch **WHY Each Reference Matters**: - `fetchEvents()`: The only method to modify — add date ceiling or array_slice - `syncEvents()`: MUST NOT modify — just verify it still works with fewer events - SyncControllerTest: Where to add assertion for limited event count **Acceptance Criteria**: - [ ] `fetchEvents()` returns at most ~10 services (bounded by date or slice) - [ ] Events are ordered by start date - [ ] `syncEvents()` still works correctly with limited fetch - [ ] `php artisan test` passes **QA Scenarios:** ``` Scenario: Sync fetches limited events Tool: Bash Preconditions: CTS_API_TOKEN configured, API accessible Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php artisan cts:sync 2>&1 2. Assert output shows sync completed 3. Check DB for service count: php artisan tinker --execute="echo App\Models\Service::count();" 4. Assert service count is reasonable (≤ 15, accounting for existing + new) Expected Result: Only next ~10 services synced, not all future services Failure Indicators: Hundreds of services in DB, or sync failure Evidence: .sisyphus/evidence/task-5-sync-limited.txt Scenario: No test regressions Tool: Bash Preconditions: fetchEvents modified Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php artisan test 2>&1 2. Assert exit code 0 Expected Result: All tests pass Failure Indicators: Non-zero exit code Evidence: .sisyphus/evidence/task-5-test-results.txt ``` **Commit**: YES - Message: `feat(sync): limit CTS fetch to next 10 services` - Files: `app/Services/ChurchToolsService.php`, `tests/Feature/SyncControllerTest.php` - Pre-commit: `php artisan test` - [x] 6. API Log Expandable Request/Response Detail Rows **What to do**: - **Backend**: Add `request_context` and `response_summary` to the data returned by `ApiLogController::index()` in the select/map. Currently returns only: id, created_at, method, endpoint, status, duration_ms, error_message - **Frontend**: Add expandable row functionality in `resources/js/Pages/ApiLogs/Index.vue`. When a log row is clicked, expand below it to show: - `Anfrage-Kontext:` + formatted JSON of `request_context` (use `
` with `JSON.stringify(context, null, 2)`)
- `Antwort-Zusammenfassung:` + `response_summary` text
- Handle null `request_context` gracefully: show "Kein Kontext verfügbar"
- Handle null `response_summary` gracefully: show "Keine Zusammenfassung verfügbar"
- Add a Pest test for the API log response including these fields
- Run `php artisan test` and `npm run build` to verify
**Must NOT do**:
- Do NOT add full HTTP response body logging to ChurchToolsService (the current `summarizeResponse()` text is sufficient)
- Do NOT add a separate detail page (keep it inline expandable row)
- Do NOT add log deletion, export, or management features
- Do NOT add syntax highlighting library — plain `` is sufficient
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- **Skills**: []
- Reason: Backend + frontend changes, expandable row UI logic
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 4, 5)
- **Blocks**: None
- **Blocked By**: None
**References**:
**Pattern References**:
- `/Users/thorsten/AI/cts-work/app/Http/Controllers/ApiLogController.php:11-39` — Current `index()` method. Currently selects only summary fields. Add `request_context` and `response_summary` to the select
- `/Users/thorsten/AI/cts-work/resources/js/Pages/ApiLogs/Index.vue:112-147` — Current table rows. Add click handler and expandable detail section below each row
**API/Type References**:
- `/Users/thorsten/AI/cts-work/app/Models/ApiRequestLog.php:14-23` — Model casts. `request_context` is cast to `array`, `response_summary` is string. These fields exist but aren't exposed to frontend
**Test References**:
- `/Users/thorsten/AI/cts-work/tests/Feature/ApiLogControllerTest.php` — Add assertion that response includes request_context and response_summary
**WHY Each Reference Matters**:
- `ApiLogController.php`: Need to add the two fields to the query/response
- `ApiLogs/Index.vue`: Need to add expandable row UI with click toggle
- `ApiRequestLog.php`: Confirms the model casts — `request_context` comes as PHP array, needs JSON encoding for display
**Acceptance Criteria**:
- [ ] `ApiLogController::index()` returns `request_context` and `response_summary`
- [ ] Clicking a log row expands to show request context and response summary
- [ ] Null `request_context` shows "Kein Kontext verfügbar"
- [ ] Null `response_summary` shows "Keine Zusammenfassung verfügbar"
- [ ] `php artisan test --filter=ApiLog` passes
- [ ] `npm run build` succeeds
**QA Scenarios:**
```
Scenario: Expandable log row shows details
Tool: Playwright
Preconditions: Logged in, at least one API log entry exists (trigger sync first if needed)
Steps:
1. Navigate to /api-logs (or the correct route for API logs page)
2. Find first log row in the table
3. Click the log row
4. Assert an expanded detail section appears below the row
5. Assert expanded section contains text 'Anfrage-Kontext' or 'Antwort-Zusammenfassung'
6. Take screenshot of expanded row
Expected Result: Clicking a row expands it to show request/response details
Failure Indicators: No expansion on click, or empty detail section
Evidence: .sisyphus/evidence/task-6-log-detail.png
Scenario: Null context handled gracefully
Tool: Bash
Preconditions: API log entry with null request_context exists
Steps:
1. Run: cd /Users/thorsten/AI/cts-work && php artisan test --filter=ApiLog 2>&1
2. Assert tests pass including null handling
Expected Result: Tests pass with graceful null handling
Failure Indicators: Test failures mentioning null
Evidence: .sisyphus/evidence/task-6-test-results.txt
```
**Commit**: YES
- Message: `feat(logs): add expandable request/response details in API log`
- Files: `app/Http/Controllers/ApiLogController.php`, `resources/js/Pages/ApiLogs/Index.vue`, `tests/Feature/ApiLogControllerTest.php`
- Pre-commit: `php artisan test && npm run build`
- [x] 7. Fix Drag'n'Drop Auto-Upload + JSON Error
**What to do**:
**Sub-issue A: Auto-upload on drag-and-drop**:
- In `/Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue`, add a `watch` on the `files` ref to auto-trigger upload when files are added via drag-and-drop:
```javascript
watch(files, (newFiles) => {
if (newFiles.length > 0 && !uploading.value) processFiles()
})
```
- Add `watch` to the Vue import from `vue` (line 2)
- Keep the existing `@change="processFiles"` as fallback
- The `!uploading.value` guard prevents re-entry if user drops more files during active upload
**Sub-issue B: JSON error on click-upload**:
- The issue is that Inertia's `router.post()` may serialize FormData as JSON instead of multipart/form-data despite `forceFormData: true`
- Replace `router.post()` in the upload function (around line 98-117) with `axios.post()` for the file upload call only
- Axios is already configured globally with CSRF token via `bootstrap.js` (`window.axios`)
- After successful upload via axios, manually reload the page data: call `router.reload({ only: ['slides'] })` or equivalent to refresh the Inertia page props
- Ensure error handling shows German error messages ("Upload fehlgeschlagen")
- Keep the upload progress tracking working with axios's `onUploadProgress`
**Must NOT do**:
- Do NOT replace Vue3Dropzone with a different library
- Do NOT add chunked upload, retry logic, or cancellation
- Do NOT refactor the entire upload pipeline
- Do NOT change the backend SlideController (it already handles multipart correctly)
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- **Skills**: []
- Reason: Two sub-issues requiring careful Vue/Inertia debugging + axios integration
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2 (with Tasks 8, 9)
- **Blocks**: None
- **Blocked By**: None (no code dependency, placed in Wave 2 for focus)
**References**:
**Pattern References**:
- `/Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:2` — Vue imports line. Add `watch` to existing imports
- `/Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:25` — `const files = ref([])` — the files ref populated by Vue3Dropzone v-model
- `/Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:47-57` — `processFiles()` function — the upload trigger to call from watch
- `/Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:59-118` — `uploadNextFile()` function with `router.post()` on line ~98 — replace with `axios.post()`
- `/Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:201-242` — Vue3Dropzone template with `v-model="files"` and `@change="processFiles"`
- `/Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:284` — CSS hiding dropzone preview: `.v3-dropzone__preview { display: none; }`
**API/Type References**:
- `/Users/thorsten/AI/cts-work/app/Http/Controllers/SlideController.php` — Backend `store()` method. Expects multipart/form-data with `file`, `type`, `service_id`, `expire_date`. DO NOT MODIFY.
- `/Users/thorsten/AI/cts-work/resources/js/bootstrap.js` — Axios configuration with CSRF token. Axios is available as `window.axios` or import `axios from 'axios'`
**WHY Each Reference Matters**:
- `files` ref (line 25): The watch target — when Vue3Dropzone populates it via v-model on drag-drop, the watch fires
- `processFiles()` (line 47): The upload trigger — called by watch to auto-start upload
- `uploadNextFile()` (line 59): Where `router.post()` needs to be replaced with `axios.post()` for proper multipart handling
- CSS (line 284): Explains why dropped files aren't visually shown — the preview is hidden
**Acceptance Criteria**:
- [ ] Drag-and-drop files auto-upload without manual trigger
- [ ] Click-to-upload works without JSON error
- [ ] Upload progress indicator works during upload
- [ ] Files appear in slide grid after successful upload
- [ ] `npm run build` succeeds
- [ ] No console errors during upload process
**QA Scenarios:**
```
Scenario: Drag-and-drop auto-uploads
Tool: Playwright
Preconditions: Logged in, editing a service, on Information or Moderation block
Steps:
1. Navigate to /services/{id}/edit (a non-finalized service)
2. Find the dropzone upload area
3. Simulate file drop with a test image (e.g., .sisyphus/evidence/test-upload.jpg)
4. Assert upload progress indicator appears within 2 seconds
5. Wait for upload to complete (progress reaches 100% or success indicator)
6. Assert new slide thumbnail appears in the slide grid
7. Assert NO console errors containing 'JSON' or 'SyntaxError'
Expected Result: File auto-uploads on drop and appears in grid
Failure Indicators: Files shown but not uploaded, or JSON error in console
Evidence: .sisyphus/evidence/task-7-drag-upload.png
Scenario: Click-upload works without error
Tool: Playwright
Preconditions: Logged in, editing a service
Steps:
1. Navigate to /services/{id}/edit
2. Find the upload area
3. Click the upload area to trigger file input
4. Upload a test image via file input
5. Assert upload completes without console errors
6. Assert new slide appears in grid
Expected Result: Click-to-upload works and slide appears
Failure Indicators: JSON parse error, 422 response, or no slide created
Evidence: .sisyphus/evidence/task-7-click-upload.png
```
**Commit**: YES
- Message: `fix(upload): auto-upload on drag-drop and fix FormData serialization`
- Files: `resources/js/Components/SlideUploader.vue`
- Pre-commit: `npm run build`
- [x] 8. ProPresenter .pro File Import
**What to do**:
- Replace the placeholder `importPro()` in `/Users/thorsten/AI/cts-work/app/Http/Controllers/ProFileController.php` with real implementation
- Create a new service class `app/Services/ProImportService.php` to handle the import logic:
1. Accept uploaded .pro file (or .zip containing multiple .pro files)
2. Use `ProFileReader::read($filePath)` to parse the .pro file
3. Map ProPresenter Song data to CTS Song model:
- `$proSong->getName()` → `Song.title`
- `$proSong->getCcliSongNumber()` → `Song.ccli_id`
- `$proSong->getCcliAuthor()` → `Song.author`
- `$proSong->getCcliPublisher()` → `Song.copyright_text`
- `$proSong->getCcliCopyrightYear()` → `Song.copyright_year`
4. Upsert Song by CCLI ID (if ccli_id exists, update; otherwise create new)
5. For each Group in the song:
- Create `SongGroup` with `name`, `color` (convert RGBA float array to hex), `position`
6. For each Slide in each Group:
- Create `SongSlide` with `text_content` from `getPlainText()`, `position`
- If `hasTranslation()`, set `text_content_translated` from translation's `getPlainText()`
- If slide has translation, mark song's `has_translation = true`
7. For each Arrangement:
- Create `SongArrangement` with `name`
- Create `SongArrangementGroup` entries mapping arrangement groups to SongGroup by name
8. Handle .zip uploads: extract, process each .pro file inside
9. Wrap in DB transaction for atomicity
- Remove `ProParserNotImplementedException` throw from `importPro()`
- Remove the exception class if no longer used by `downloadPro()` (T9 handles that)
- Create color conversion utility: RGBA float array [0.13, 0.59, 0.95, 1.0] ↔ hex '#2196F3'
- Add proper validation: accept `.pro` and `.zip` files only, max size 50MB
- Add Pest tests for the import
**Must NOT do**:
- Do NOT build a .pro browser editor or viewer
- Do NOT import media files (images/videos) from .pro slides
- Do NOT modify the ProPresenter library source code
- Do NOT modify the Song, SongGroup, SongSlide, SongArrangement models (they already have the right fields)
**Recommended Agent Profile**:
- **Category**: `deep`
- **Skills**: []
- Reason: Complex data mapping, ZIP handling, DB transactions, error handling
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2 (with Tasks 7, 9)
- **Blocks**: Task 10 (playlist export needs songs imported)
- **Blocked By**: Task 1 (composer integration)
**References**:
**Pattern References**:
- `/Users/thorsten/AI/cts-work/app/Http/Controllers/ProFileController.php:16-19` — Current `importPro()` placeholder. Replace throw with real implementation call
- `/Users/thorsten/AI/cts-work/app/Services/ChurchToolsService.php:63-92` — `syncEvents()` as pattern for service class with DB transactions and error handling
**API/Type References**:
- `/Users/thorsten/AI/propresenter-work/AGENTS.md:24-88` — ProFileReader API. Key methods: `read()`, `getName()`, `getCcliSongNumber()`, `getGroups()`, `getSlidesForGroup()`, `getArrangements()`, `getGroupsForArrangement()`
- `/Users/thorsten/AI/cts-work/app/Models/Song.php` — Song model with fillable fields: ccli_id, title, author, copyright_text, copyright_year, publisher, has_translation
- `/Users/thorsten/AI/cts-work/app/Models/SongGroup.php` — SongGroup model (song_id, name, color, position)
- `/Users/thorsten/AI/cts-work/app/Models/SongSlide.php` — SongSlide model (song_group_id, text_content, text_content_translated, position)
- `/Users/thorsten/AI/cts-work/app/Models/SongArrangement.php` — SongArrangement model (song_id, name)
- `/Users/thorsten/AI/cts-work/routes/api.php:46-47` — Existing route: `POST /api/songs/import-pro`
**Test References**:
- `/Users/thorsten/AI/propresenter-work/ref/Test.pro` — Sample .pro file for testing import (7.6KB)
- `/Users/thorsten/AI/propresenter-work/ref/all-songs/` — 171 .pro files for comprehensive testing
**External References**:
- ProPresenter AGENTS.md: `/Users/thorsten/AI/propresenter-work/AGENTS.md` — Complete PHP module API documentation
**WHY Each Reference Matters**:
- `ProFileController.php`: Replace placeholder with real implementation
- ProPresenter AGENTS.md: Exact API for reading songs — method names, data access patterns
- Song/SongGroup/SongSlide models: Target DB structure for mapping ProPresenter data
- `Test.pro`: Real test file to verify import works end-to-end
- `api.php routes`: Confirms the endpoint already exists — no new route needed
**Acceptance Criteria**:
- [ ] `POST /api/songs/import-pro` with .pro file creates Song + SongGroups + SongSlides + SongArrangements in DB
- [ ] CCLI ID upsert: re-importing same song updates instead of duplicating
- [ ] .zip upload with multiple .pro files imports all songs
- [ ] Color conversion works: RGBA floats → hex string
- [ ] Translation detection works: slides with translation mark song as `has_translation = true`
- [ ] Invalid .pro file returns 422 with German error message
- [ ] `php artisan test --filter=ProFile` passes
**QA Scenarios:**
```
Scenario: Import single .pro file
Tool: Bash (curl)
Preconditions: App running, authenticated, ProPresenter library integrated
Steps:
1. Copy test file: cp /Users/thorsten/AI/propresenter-work/ref/Test.pro /tmp/test-import.pro
2. Run: curl -s -X POST http://cts-work.test/api/songs/import-pro \
-H 'Accept: application/json' \
-H 'Cookie: [session cookie]' \
-F 'file=@/tmp/test-import.pro'
3. Assert HTTP 200 or 201 response
4. Assert response JSON contains 'song' with 'title' and 'ccli_id'
5. Verify in DB: php artisan tinker --execute="echo App\Models\Song::latest()->first()->title;"
Expected Result: Song created in DB with groups, slides, and arrangements
Failure Indicators: 422 error, empty song, missing groups/slides
Evidence: .sisyphus/evidence/task-8-import-single.txt
Scenario: Re-import same song updates instead of duplicating
Tool: Bash
Preconditions: Song from previous scenario already in DB
Steps:
1. Count songs: php artisan tinker --execute="echo App\Models\Song::count();"
2. Re-import same .pro file via curl
3. Count songs again
4. Assert count is the same (not incremented)
Expected Result: Song updated, not duplicated
Failure Indicators: Song count increased
Evidence: .sisyphus/evidence/task-8-import-upsert.txt
Scenario: Invalid file returns error
Tool: Bash
Preconditions: App running
Steps:
1. Create invalid file: echo 'not a pro file' > /tmp/invalid.pro
2. Attempt import via curl
3. Assert HTTP 422 response
4. Assert response contains German error message
Expected Result: 422 with error message, no DB changes
Failure Indicators: 500 error, or song created from invalid file
Evidence: .sisyphus/evidence/task-8-import-invalid.txt
```
**Commit**: YES
- Message: `feat(songs): implement .pro file import with SongDB mapping`
- Files: `app/Http/Controllers/ProFileController.php`, `app/Services/ProImportService.php`, `tests/Feature/ProFileImportTest.php`
- Pre-commit: `php artisan test`
- [x] 9. ProPresenter .pro File Download/Export
**What to do**:
- Replace the placeholder `downloadPro()` in `/Users/thorsten/AI/cts-work/app/Http/Controllers/ProFileController.php` with real implementation
- Create a new service class `app/Services/ProExportService.php` to handle the export logic:
1. Accept a Song model
2. Use `ProFileGenerator::generate()` to create a .pro file from Song DB data:
- Song.title → song name
- Map SongGroups → groups array: `['name' => $group->name, 'color' => hexToRgba($group->color), 'slides' => [...]]`
- For each SongSlide: `['text' => $slide->text_content]`, and if `text_content_translated`: `['text' => ..., 'translation' => $slide->text_content_translated]`
- Map SongArrangements → arrangements array: `['name' => $arr->name, 'groupNames' => [...ordered group names...]]`
- CCLI metadata: `['author' => Song.author, 'song_title' => Song.title, 'copyright_year' => Song.copyright_year, ...]`
3. Use `ProFileGenerator::generateAndWrite()` to write to a temp file
4. Return the file as a download response with filename `{sanitized-title}.pro`
5. Clean up temp file after response is sent
- Remove `ProParserNotImplementedException` throw from `downloadPro()`
- Remove the `ProParserNotImplementedException` class file entirely (both import and download now implemented)
- Remove the exception import from `ProFileController.php`
- Create color conversion utility (reuse from T8): hex '#2196F3' → RGBA float array [0.13, 0.59, 0.95, 1.0]
- Add Pest tests for the export
**Must NOT do**:
- Do NOT build a .pro browser editor or viewer
- Do NOT add batch export functionality
- Do NOT add template selection or custom formatting
- Do NOT embed media files in generated .pro
**Recommended Agent Profile**:
- **Category**: `deep`
- **Skills**: []
- Reason: Complex data mapping from DB to ProPresenter format, protobuf generation
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2 (with Tasks 7, 8)
- **Blocks**: Task 10 (playlist export generates .pro files for embedding)
- **Blocked By**: Task 1 (composer integration)
**References**:
**Pattern References**:
- `/Users/thorsten/AI/cts-work/app/Http/Controllers/ProFileController.php:25-28` — Current `downloadPro()` placeholder. Replace throw with real implementation
**API/Type References**:
- `/Users/thorsten/AI/propresenter-work/AGENTS.md:101-130` — ProFileGenerator API. Key: `generate()` accepts song name, groups array, arrangements array, ccli metadata. `generateAndWrite()` writes directly to file
- `/Users/thorsten/AI/cts-work/app/Models/Song.php` — Song model with relationships: `groups()`, `arrangements()`
- `/Users/thorsten/AI/cts-work/app/Models/SongGroup.php` — SongGroup with `name`, `color`, `position`, `slides()` relationship
- `/Users/thorsten/AI/cts-work/app/Models/SongSlide.php` — SongSlide with `text_content`, `text_content_translated`, `position`
- `/Users/thorsten/AI/cts-work/app/Models/SongArrangement.php` — SongArrangement with `name`, arrangement groups
- `/Users/thorsten/AI/cts-work/routes/api.php:49-50` — Existing route: `GET /api/songs/{song}/download-pro`
**External References**:
- ProPresenter AGENTS.md: `/Users/thorsten/AI/propresenter-work/AGENTS.md` — Generator API with exact parameter format
**WHY Each Reference Matters**:
- `ProFileController.php`: Replace placeholder with real export implementation
- ProFileGenerator API: Exact parameter format for `generate()` — groups must be array of `['name' => ..., 'color' => [...], 'slides' => [...]]`
- Song model + relationships: Source data to convert to ProPresenter format
- `api.php routes`: Confirms the endpoint already exists
**Acceptance Criteria**:
- [ ] `GET /api/songs/{song}/download-pro` returns a valid .pro file download
- [ ] Downloaded file can be re-imported via `ProFileReader::read()` without errors
- [ ] Song metadata (title, CCLI) is preserved in the .pro file
- [ ] Groups, slides, and arrangements are correctly exported
- [ ] Translations are included when `has_translation` is true
- [ ] `ProParserNotImplementedException` class is removed
- [ ] `php artisan test --filter=ProFile` passes
**QA Scenarios:**
```
Scenario: Export song as .pro file
Tool: Bash (curl)
Preconditions: Song exists in DB (imported via T8 or seeded)
Steps:
1. Get a song ID: php artisan tinker --execute="echo App\Models\Song::first()->id;"
2. Download: curl -s -o /tmp/export-test.pro http://cts-work.test/api/songs/{id}/download-pro -H 'Cookie: [session]'
3. Assert file exists and is not empty: test -s /tmp/export-test.pro
4. Verify file is valid protobuf: php /Users/thorsten/AI/propresenter-work/php/bin/parse-song.php /tmp/export-test.pro
5. Assert parse output contains the song name and group names
Expected Result: Valid .pro file with correct song data
Failure Indicators: Empty file, parse error, missing data
Evidence: .sisyphus/evidence/task-9-export-song.txt
Scenario: Round-trip import-export preserves data
Tool: Bash
Preconditions: Test.pro imported in T8
Steps:
1. Export the imported song via curl
2. Parse original: php .../parse-song.php /Users/thorsten/AI/propresenter-work/ref/Test.pro > /tmp/original.txt
3. Parse export: php .../parse-song.php /tmp/export-test.pro > /tmp/exported.txt
4. Compare key fields (title, group names, slide counts) between original and exported
Expected Result: Key data matches between original and round-tripped export
Failure Indicators: Missing groups, wrong text, different arrangement order
Evidence: .sisyphus/evidence/task-9-roundtrip.txt
```
**Commit**: YES
- Message: `feat(songs): implement .pro file download/export from SongDB`
- Files: `app/Http/Controllers/ProFileController.php`, `app/Services/ProExportService.php`, `app/Exceptions/ProParserNotImplementedException.php` (DELETE), `tests/Feature/ProFileExportTest.php`
- Pre-commit: `php artisan test`
- [x] 10. Finalized Service .proplaylist Export
**What to do**:
- Add a new `downloadPlaylist()` method to `ServiceController.php` (or add to an existing controller)
- Add a new route: `GET /services/{service}/download-playlist` (web route, not API, for Inertia download)
- Create a new service class `app/Services/PlaylistExportService.php`:
1. Accept a Service model (must be finalized)
2. Get all ServiceSongs for this service, ordered by position
3. For each ServiceSong that has a matched Song in DB (song_id is not null):
a. Generate a .pro file for that song using `ProExportService` (from T9)
b. Write to a temp directory
4. Use `ProPlaylistGenerator::generate()` to create a playlist:
- Playlist name = Service title + date
- For each song: entry with `type => 'presentation'` (NOT 'song' — use actual API types from source code)
- Reference the temp .pro files
5. Use `ProPlaylistGenerator::generateAndWrite()` to write the .proplaylist file
6. Return as download response with filename `{service-title}_{date}.proplaylist`
7. Clean up temp files after response
- Add this download button to the finalized service view — in `Services/Index.vue`, the "Herunterladen" button should trigger this download
- Skip unmatched songs with a flash warning: "{N} Songs ohne SongDB-Zuordnung wurden übersprungen"
- Skip songs without groups/slides: "{N} Songs ohne Inhalt wurden übersprungen"
- If NO songs can be exported, return 422 with German error
**Must NOT do**:
- Do NOT embed non-song media (images, videos) in the playlist
- Do NOT add a multi-format export UI
- Do NOT add custom ordering UI (use service song order)
- Do NOT allow playlist export for non-finalized services
**Recommended Agent Profile**:
- **Category**: `deep`
- **Skills**: []
- Reason: Complex orchestration of multiple services, temp file management, ZIP creation
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Wave 3 (solo)
- **Blocks**: F1-F4 (final verification)
- **Blocked By**: Tasks 8, 9 (needs import service for song data, export service for .pro generation)
**References**:
**Pattern References**:
- `/Users/thorsten/AI/cts-work/app/Http/Controllers/ServiceController.php:189-221` — Existing `finalize()` and `reopen()` methods as pattern for service actions
- `/Users/thorsten/AI/cts-work/app/Services/ProExportService.php` — (Created in T9) Reuse for generating individual .pro files
- `/Users/thorsten/AI/cts-work/resources/js/Pages/Services/Index.vue:307-348` — Action buttons section. The 'Herunterladen' button for finalized services — wire to playlist download
**API/Type References**:
- `/Users/thorsten/AI/propresenter-work/AGENTS.md:200-280` — ProPlaylistGenerator API. Key: `generate()` accepts playlist name, entries array, metadata. CRITICAL: Use actual types from source code: `presentation` for songs, `header` for section labels, NOT the AGENTS.md types `song`/`group`
- `/Users/thorsten/AI/propresenter-work/php/src/ProPlaylistGenerator.php` — ACTUAL source code. Read this to confirm the correct entry types and parameter format. The AGENTS.md documentation may differ from the implementation.
- `/Users/thorsten/AI/cts-work/app/Models/ServiceSong.php` — ServiceSong model with `song_id` (nullable — null means unmatched), `position`
- `/Users/thorsten/AI/cts-work/app/Models/Service.php:50-75` — Finalization status logic. Check `finalized_at` is not null before allowing export
**Test References**:
- `/Users/thorsten/AI/propresenter-work/ref/TestPlaylist.proplaylist` — Sample playlist file (275KB) for reference
- `/Users/thorsten/AI/propresenter-work/ref/ExamplePlaylists/` — 7 example playlists for reference
**WHY Each Reference Matters**:
- `ServiceController.php`: Pattern for adding a new service action (route, authorization, response)
- `ProExportService.php`: Reuse to generate .pro files for each song in the service
- ProPlaylistGenerator source: MUST read actual source code for correct types — AGENTS.md may be inaccurate
- `ServiceSong.php`: Nullable `song_id` means some songs are unmatched and must be skipped
- `Service.php`: Finalization check — only export finalized services
**Acceptance Criteria**:
- [ ] `GET /services/{service}/download-playlist` downloads a .proplaylist file
- [ ] Playlist contains all matched songs from the service in correct order
- [ ] Unmatched songs are skipped with a flash warning
- [ ] Songs without DB content are skipped with a flash warning
- [ ] Non-finalized services return 403
- [ ] Downloaded .proplaylist can be parsed by `ProPlaylistReader::read()`
- [ ] "Herunterladen" button in service list triggers playlist download
- [ ] `php artisan test` passes
- [ ] `npm run build` succeeds
**QA Scenarios:**
```
Scenario: Download playlist for finalized service
Tool: Bash (curl)
Preconditions: Finalized service exists with matched songs that have .pro-imported data
Steps:
1. Get finalized service ID: php artisan tinker --execute="echo App\Models\Service::whereNotNull('finalized_at')->first()->id;"
2. Download: curl -s -o /tmp/test-playlist.proplaylist http://cts-work.test/services/{id}/download-playlist -H 'Cookie: [session]'
3. Assert file exists and is not empty: test -s /tmp/test-playlist.proplaylist
4. Verify file is valid: php /Users/thorsten/AI/propresenter-work/php/bin/parse-playlist.php /tmp/test-playlist.proplaylist
5. Assert output contains song names from the service
Expected Result: Valid .proplaylist with embedded songs
Failure Indicators: Empty file, parse error, missing songs
Evidence: .sisyphus/evidence/task-10-playlist-export.txt
Scenario: Non-finalized service returns 403
Tool: Bash
Preconditions: Non-finalized service exists
Steps:
1. Get non-finalized service ID
2. Attempt download: curl -s -o /dev/null -w '%{http_code}' http://cts-work.test/services/{id}/download-playlist -H 'Cookie: [session]'
3. Assert HTTP status is 403
Expected Result: 403 Forbidden for non-finalized services
Failure Indicators: 200 response or 500 error
Evidence: .sisyphus/evidence/task-10-non-finalized.txt
Scenario: Service with unmatched songs shows warning
Tool: Bash
Preconditions: Finalized service with at least one unmatched song
Steps:
1. Attempt playlist download
2. Assert response succeeds (200) if at least one song is matched
3. Check flash/response message contains skip warning
Expected Result: Playlist generated with matched songs, warning about skipped songs
Failure Indicators: Export fails entirely, or no warning about skipped songs
Evidence: .sisyphus/evidence/task-10-unmatched-warning.txt
```
**Commit**: YES
- Message: `feat(services): implement .proplaylist export for finalized services`
- Files: `app/Http/Controllers/ServiceController.php`, `app/Services/PlaylistExportService.php`, `routes/web.php`, `resources/js/Pages/Services/Index.vue`, `tests/Feature/PlaylistExportTest.php`
- Pre-commit: `php artisan test && npm run build`
---
## Final Verification Wave
> 4 review agents run in PARALLEL. ALL must APPROVE. Rejection → fix → re-run.
- [x] F1. **Plan Compliance Audit** — `oracle` — APPROVE (5/5 Must Have, 8/9 Must NOT Have)
Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in `.sisyphus/evidence/`. Compare deliverables against plan.
Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
- [x] F2. **Code Quality Review** — `unspecified-high` — APPROVE (198 tests pass, build clean, minor notes)
Run `php artisan test` + `npm run build`. Review all changed files for: `as any`/`@ts-ignore`, empty catches, `console.log` in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names.
Output: `Build [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT`
- [x] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill) — APPROVE (5/5 scenarios pass, 9 screenshots)
Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration (upload + service list + ProPresenter export). Test edge cases: empty state, invalid input, rapid actions. Save to `.sisyphus/evidence/final-qa/`.
Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT`
- [x] F4. **Scope Fidelity Check** — `deep` — RESOLVED (all findings reviewed with user: 5/5 accepted as-is, guardrails updated)
For each task: read "What to do", read actual diff (`git log/diff`). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance. Detect cross-task contamination: Task N touching Task M's files. Flag unaccounted changes.
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT`
---
## Commit Strategy
| Task | Commit Message | Key Files |
|------|---------------|-----------|
| T1 | `build(deps): integrate ProPresenter parser via composer path` | composer.json |
| T2 | `feat(services): show CTS event ID tooltip on title hover` | ServiceController.php, Index.vue |
| T3 | `feat(sync): add hourly CTS sync schedule` | bootstrap/app.php |
| T4 | `fix(services): correct archived toggle button highlighting` | Index.vue |
| T5 | `feat(sync): limit CTS fetch to next 10 services` | ChurchToolsService.php |
| T6 | `feat(logs): add expandable request/response details` | ApiLogController.php, ApiLogs/Index.vue |
| T7 | `fix(upload): auto-upload on drag-drop and fix FormData serialization` | SlideUploader.vue |
| T8 | `feat(songs): implement .pro file import with SongDB mapping` | ProFileController.php, ProImportService.php |
| T9 | `feat(songs): implement .pro file download/export from SongDB` | ProFileController.php, ProExportService.php |
| T10 | `feat(services): implement .proplaylist export for finalized services` | ServiceController.php, PlaylistExportService.php |
---
## Success Criteria
### Verification Commands
```bash
# All tests pass
cd /Users/thorsten/AI/cts-work && php artisan test
# Expected: 182+ tests, 0 failures
# Build succeeds
cd /Users/thorsten/AI/cts-work && npm run build
# Expected: no errors
# Schedule registered
cd /Users/thorsten/AI/cts-work && php artisan schedule:list 2>&1 | grep cts:sync
# Expected: cts:sync listed with hourly frequency
# ProPresenter library available
cd /Users/thorsten/AI/cts-work && php -r "require 'vendor/autoload.php'; echo class_exists('ProPresenter\\Parser\\ProFileReader') ? 'OK' : 'FAIL';"
# Expected: OK
```
### Final Checklist
- [x] All "Must Have" present
- [x] All "Must NOT Have" absent (2 guardrails updated per user review)
- [x] All existing 182+ tests pass (198 tests, 1108 assertions)
- [x] All new tests pass
- [x] `npm run build` succeeds
- [x] ProPresenter .pro import works with test files
- [x] ProPresenter .pro export generates valid files
- [x] Finalized service exports valid .proplaylist