60 KiB
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:
- Show CTS event ID on hover over service title
- Fetch next 10 services from CTS on sync
- Hourly CTS sync job
- ProPresenter .pro file parser/generator integration + .proplaylist export
- API log request/response body on click
- Fix "Vergangene" button not highlighted
- 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
ProFileControllerhas placeholder methods throwingProParserNotImplementedException - Archived toggle bug:
ref(props.archived)doesn't react to Inertia prop changes — needscomputed() - Upload bug: Vue3Dropzone files v-model populated but upload never auto-triggered — needs
watch(files)+ investigate FormData serialization
Research Findings:
- CTApi
EventRequesthas no.limit()— usewhere('to', ...)with date window orarray_slice()post-fetch response_summaryin 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 typessong,group - Library has 1,690 generated protobuf files — composer path repository is cleanest integration
- Inertia
router.post()withforceFormData: truemay 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_summaryis 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
uploadingguard - 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_iddisplayed as tooltip on service title hover in the service listfetchEvents()scoped to 10 past + 10 future services by date windowcts:syncscheduled hourly inbootstrap/app.php- ProPresenter library integrated via composer path repository
.profile import creates/updates Song with groups, slides, arrangements in DB.profile download generates valid protobuf file from Song DB data- Finalized service download generates
.proplaylistZIP with embedded song.profiles - 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
- All 182+ existing Pest tests pass (198 tests, 1108 assertions)
npm run buildsucceeds without errors- Each item verified by its QA scenarios (F3 Manual QA: 5/5 pass)
- 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 exportNO 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
-
1. ProPresenter Composer Integration
What to do:
- Add the ProPresenter PHP library as a composer path repository
- Add to
composer.jsonrepositories section:{"type": "path", "url": "../propresenter-work/php"} - Run
composer require propresenter/parser:*to install - Verify
google/protobuf ^4.0resolves without conflicts - Verify
ProPresenter\Parser\ProFileReaderclass is autoloadable - Remove the
ProParserNotImplementedExceptionclass (no longer needed after T8/T9 implement the real methods — but keep it for now, T8/T9 will remove) - Run
php artisan testto 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, addrepositoriessection beforerequire
API/Type References:
/Users/thorsten/AI/propresenter-work/php/composer.json— Library package name:propresenter/parser, requiresphp ^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 addrepositoriesarray with path type pointing to../propresenter-work/php- Library
composer.json: Confirms package name forcomposer requireand dependency compatibility ProFileReader.php: Class to test autoloading works after install
Acceptance Criteria:
composer.jsonhas repositories section with path to../propresenter-work/phpcomposer require propresenter/parser:*succeedsphp -r "require 'vendor/autoload.php'; echo class_exists('ProPresenter\\Parser\\ProFileReader') ? 'OK' : 'FAIL';"→ OKphp 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.txtCommit: YES
- Message:
build(deps): integrate ProPresenter parser via composer path - Files:
composer.json,composer.lock - Pre-commit:
php artisan test
-
2. CTS Event ID Tooltip on Service Title Hover
What to do:
- Add
cts_event_idto the service data mapping inServiceController::index()(around line 52-67) - Add a
titleattribute to the service title element inresources/js/Pages/Services/Index.vueshowingCTS Event #${service.cts_event_id} - Add
cts_event_idto the ServiceControllerTest assertions - Run
php artisan testandnpm run buildto 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
titleattribute
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 inindex()method. Add'cts_event_id' => $service->cts_event_idto the map/Users/thorsten/AI/cts-work/resources/js/Pages/Services/Index.vue:256-259— Service title rendering. Addtitleattribute here
API/Type References:
/Users/thorsten/AI/cts-work/app/Models/Service.php:15—cts_event_idis in$fillablearray, confirmed available
Test References:
/Users/thorsten/AI/cts-work/tests/Feature/ServiceControllerTest.php— Add assertion forcts_event_idin 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()includescts_event_idin mapped service data- Service title element has
titleattribute with event ID php artisan test --filter=ServiceControllerTestpassesnpm run buildsucceeds
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.txtCommit: 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
- Add
-
3. Hourly Scheduled CTS Sync Job
What to do:
- Add
->withSchedule()call tobootstrap/app.phpto schedulects:synchourly - 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 signaturects:sync(line 12). DO NOT MODIFY.
External References:
- Laravel 12 scheduling:
->withSchedule()in bootstrap/app.php — replaces the oldKernel.phpapproach
WHY Each Reference Matters:
bootstrap/app.php: The only file to modify — add schedule configuration in the Application builder chainSyncChurchToolsCommand.php: Reference only — confirms the command signature iscts:sync
Acceptance Criteria:
bootstrap/app.phphas->withSchedule()call schedulingcts:synchourlyphp artisan schedule:listoutput containscts:syncwith hourly frequencyphp artisan testpasses
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.txtCommit: YES
- Message:
feat(sync): add hourly CTS sync schedule - Files:
bootstrap/app.php - Pre-commit:
php artisan test
- Add
-
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)
- FROM:
- Add
computedto the Vue import on line 4 (alongsideref,onMounted, etc.) - The
refimport may still be needed by other code — check before removing - Run
npm run buildto 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. Addcomputedif 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 usingshowArchivedfor class binding
WHY Each Reference Matters:
- Line 4: Need to add
computedto Vue import - Line 23: The root cause —
ref()copies once,computed()stays reactive to prop changes from Inertia navigation withpreserveState: true - Lines 192-212: Confirms
showArchivedcontrols button active state via class binding — no changes needed there
Acceptance Criteria:
showArchivediscomputed(() => props.archived)notref(props.archived)computedis imported fromvuenpm run buildsucceeds
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.txtCommit: YES
- Message:
fix(services): correct archived toggle button highlighting - Files:
resources/js/Pages/Services/Index.vue - Pre-commit:
npm run build
- In
-
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
todate 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), usearray_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 testto 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 callsfetchEvents(). DO NOT MODIFY this method
API/Type References:
5pm-HDH/churchtools-apiEventRequest — check if.where('to', ...)is supported for limiting date range. The API has atoparameter 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_slicesyncEvents(): 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 fetchphp artisan testpasses
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.txtCommit: 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
- Modify
-
6. API Log Expandable Request/Response Detail Rows
What to do:
- Backend: Add
request_contextandresponse_summaryto the data returned byApiLogController::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 ofrequest_context(use<pre>withJSON.stringify(context, null, 2))Antwort-Zusammenfassung:+response_summarytext- Handle null
request_contextgracefully: show "Kein Kontext verfügbar" - Handle null
response_summarygracefully: show "Keine Zusammenfassung verfügbar"
- Add a Pest test for the API log response including these fields
- Run
php artisan testandnpm run buildto 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
<pre>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— Currentindex()method. Currently selects only summary fields. Addrequest_contextandresponse_summaryto 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_contextis cast toarray,response_summaryis 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/responseApiLogs/Index.vue: Need to add expandable row UI with click toggleApiRequestLog.php: Confirms the model casts —request_contextcomes as PHP array, needs JSON encoding for display
Acceptance Criteria:
ApiLogController::index()returnsrequest_contextandresponse_summary- Clicking a log row expands to show request context and response summary
- Null
request_contextshows "Kein Kontext verfügbar" - Null
response_summaryshows "Keine Zusammenfassung verfügbar" php artisan test --filter=ApiLogpassesnpm run buildsucceeds
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.txtCommit: 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
- Backend: Add
-
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 awatchon thefilesref to auto-trigger upload when files are added via drag-and-drop:watch(files, (newFiles) => { if (newFiles.length > 0 && !uploading.value) processFiles() }) - Add
watchto the Vue import fromvue(line 2) - Keep the existing
@change="processFiles"as fallback - The
!uploading.valueguard 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 despiteforceFormData: true - Replace
router.post()in the upload function (around line 98-117) withaxios.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. Addwatchto 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 withrouter.post()on line ~98 — replace withaxios.post()/Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:201-242— Vue3Dropzone template withv-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— Backendstore()method. Expects multipart/form-data withfile,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 aswindow.axiosor importaxios from 'axios'
WHY Each Reference Matters:
filesref (line 25): The watch target — when Vue3Dropzone populates it via v-model on drag-drop, the watch firesprocessFiles()(line 47): The upload trigger — called by watch to auto-start uploaduploadNextFile()(line 59): Whererouter.post()needs to be replaced withaxios.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 buildsucceeds- 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.pngCommit: YES
- Message:
fix(upload): auto-upload on drag-drop and fix FormData serialization - Files:
resources/js/Components/SlideUploader.vue - Pre-commit:
npm run build
- In
-
8. ProPresenter .pro File Import
What to do:
- Replace the placeholder
importPro()in/Users/thorsten/AI/cts-work/app/Http/Controllers/ProFileController.phpwith real implementation - Create a new service class
app/Services/ProImportService.phpto handle the import logic:- Accept uploaded .pro file (or .zip containing multiple .pro files)
- Use
ProFileReader::read($filePath)to parse the .pro file - 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
- Upsert Song by CCLI ID (if ccli_id exists, update; otherwise create new)
- For each Group in the song:
- Create
SongGroupwithname,color(convert RGBA float array to hex),position
- Create
- For each Slide in each Group:
- Create
SongSlidewithtext_contentfromgetPlainText(),position - If
hasTranslation(), settext_content_translatedfrom translation'sgetPlainText() - If slide has translation, mark song's
has_translation = true
- Create
- For each Arrangement:
- Create
SongArrangementwithname - Create
SongArrangementGroupentries mapping arrangement groups to SongGroup by name
- Create
- Handle .zip uploads: extract, process each .pro file inside
- Wrap in DB transaction for atomicity
- Remove
ProParserNotImplementedExceptionthrow fromimportPro() - 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
.proand.zipfiles 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— CurrentimportPro()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-endapi.php routes: Confirms the endpoint already exists — no new route needed
Acceptance Criteria:
POST /api/songs/import-prowith .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=ProFilepasses
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.txtCommit: 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
- Replace the placeholder
-
9. ProPresenter .pro File Download/Export
What to do:
- Replace the placeholder
downloadPro()in/Users/thorsten/AI/cts-work/app/Http/Controllers/ProFileController.phpwith real implementation - Create a new service class
app/Services/ProExportService.phpto handle the export logic:- Accept a Song model
- 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 iftext_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, ...]
- Use
ProFileGenerator::generateAndWrite()to write to a temp file - Return the file as a download response with filename
{sanitized-title}.pro - Clean up temp file after response is sent
- Remove
ProParserNotImplementedExceptionthrow fromdownloadPro() - Remove the
ProParserNotImplementedExceptionclass 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— CurrentdownloadPro()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 withname,color,position,slides()relationship/Users/thorsten/AI/cts-work/app/Models/SongSlide.php— SongSlide withtext_content,text_content_translated,position/Users/thorsten/AI/cts-work/app/Models/SongArrangement.php— SongArrangement withname, 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-proreturns 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_translationis true ProParserNotImplementedExceptionclass is removedphp artisan test --filter=ProFilepasses
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.txtCommit: 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
- Replace the placeholder
-
10. Finalized Service .proplaylist Export
What to do:
- Add a new
downloadPlaylist()method toServiceController.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:- Accept a Service model (must be finalized)
- Get all ServiceSongs for this service, ordered by position
- 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 - 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
- Use
ProPlaylistGenerator::generateAndWrite()to write the .proplaylist file - Return as download response with filename
{service-title}_{date}.proplaylist - 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— Existingfinalize()andreopen()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:presentationfor songs,headerfor section labels, NOT the AGENTS.md typessong/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 withsong_id(nullable — null means unmatched),position/Users/thorsten/AI/cts-work/app/Models/Service.php:50-75— Finalization status logic. Checkfinalized_atis 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: Nullablesong_idmeans some songs are unmatched and must be skippedService.php: Finalization check — only export finalized services
Acceptance Criteria:
GET /services/{service}/download-playlistdownloads 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 testpassesnpm run buildsucceeds
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.txtCommit: 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
- Add a new
Final Verification Wave
4 review agents run in PARALLEL. ALL must APPROVE. Rejection → fix → re-run.
-
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 -
F2. Code Quality Review —
unspecified-high— APPROVE (198 tests pass, build clean, minor notes) Runphp artisan test+npm run build. Review all changed files for:as any/@ts-ignore, empty catches,console.login 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 -
F3. Real Manual QA —
unspecified-high(+playwrightskill) — 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 -
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
# 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
- All "Must Have" present
- All "Must NOT Have" absent (2 guardrails updated per user review)
- All existing 182+ tests pass (198 tests, 1108 assertions)
- All new tests pass
npm run buildsucceeds- ProPresenter .pro import works with test files
- ProPresenter .pro export generates valid files
- Finalized service exports valid .proplaylist