pp-planer/.sisyphus/plans/cts-round5-features.md

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:

  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

  • All 182+ existing Pest tests pass (198 tests, 1108 assertions)
  • npm run build succeeds 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 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

  • 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:

    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
  • 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:15cts_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
  • 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
  • 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:23const 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
  • 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-163fetchEvents() 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-92syncEvents() 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
  • 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 <pre> 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 <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 — 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
  • 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:
      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:25const files = ref([]) — the files ref populated by Vue3Dropzone v-model
    • /Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:47-57processFiles() function — the upload trigger to call from watch
    • /Users/thorsten/AI/cts-work/resources/js/Components/SlideUploader.vue:59-118uploadNextFile() 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
  • 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-92syncEvents() 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
  • 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
  • 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.

  • F1. Plan Compliance Auditoracle — 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 Reviewunspecified-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

  • F3. Real Manual QAunspecified-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

  • F4. Scope Fidelity Checkdeep — 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 build succeeds
  • ProPresenter .pro import works with test files
  • ProPresenter .pro export generates valid files
  • Finalized service exports valid .proplaylist