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