52 KiB
.pro Generation Improvements + UI Fixes
TL;DR
Quick Summary: Improve ProPresenter .pro file generation (remove visual attributes, fix translated textbox positioning, add macro support, default arrangement selection, .probundle export) and complete three pending UI fixes (drag highlighting, arrangement auto-persist, finalize buttons on Edit page).
Deliverables:
- Cleaned .pro output (no fill/stroke/shadow/feather/textScroller)
- Correct dual-textbox layout for translated slides matching TestTranslated.pro reference
- 'normal' arrangement auto-selected in generator and on song match
- Global settings UI for macro configuration + macro on COPYRIGHT slide
- .probundle export for service slide blocks (information, moderation, sermon)
- Drag highlight CSS on SlideGrid
- Finalize + "Finalize & Download" buttons on Service Edit page
Estimated Effort: Medium-Large (8 tasks across 3 waves) Parallel Execution: YES - 3 waves Critical Path: Task 2 → Task 5/6 → Task 7
Context
Original Requests
Request #14 (from prior session, NOT STARTED): 5 improvements to .pro file generation — remove slide attributes, add macro to COPYRIGHT slide with global settings UI, set 'normal' arrangement as default, two textboxes for translated slides, export service slides as .probundle.
Request #12 (from prior session, analyzed but NOT IMPLEMENTED): 3 UI improvements — slide drag highlight, default arrangement auto-persist on song match, finalize/download buttons on Edit page.
Interview Summary
Key Discussions:
- User explicitly said "take attention of the 'naming' and the exact position of the textboxes" for translated slides
- User said "which macro, should be selectable in a global settings UI - tell me what you need, with examples from the Module tests/samples"
- User said "set arrangement 'normal' as selected if exist"
- User said service blocks (information, moderation, sermon) should be exportable as .probundle
Research Findings:
- Textbox positioning (CRITICAL): TestTranslated.pro uses DIFFERENT bounds — "Orginal" at origin(150, 99.543) size(1620×182.946) top; "Deutsch" at origin(150, 303.166) size(1620×113.889) below. Current generator puts BOTH at origin(150,100) size(1620×880) overlaid. Must fix to match reference.
- Macro structure: 4 fields needed — name, uuid, collectionName, collectionUuid. buildMacroAction() already exists in generator (lines 206-227). Collection UUID default:
8D02FC57-83F8-4042-9B90-81C229728426 - Settings: NO infrastructure exists. Need migration, model, controller, Vue page, nav link.
- .probundle: NOT implemented anywhere. Must build from scratch. ZIP with .pro + images.
- SlideGrid.vue: Uses vue-draggable-plus. Add ghost-class/chosen-class/drag-class props.
- Finalize flow: Fully implemented in Index.vue. Port to Edit.vue.
Metis Review
Identified Gaps (addressed):
- Textbox bounds differ from generator defaults — using exact values from TestTranslated.pro
- "Remove attributes" ambiguity — ✅ RESOLVED: Set enable=false (not remove entirely)
- COPYRIGHT slide doesn't exist in export flow — ✅ RESOLVED: Slide with group name 'COPYRIGHT', configurable via settings
- .probundle format undefined — ✅ RESOLVED: Flat ZIP with .pro file + image files
- Default arrangement selection needs API change — resolved: find 'normal' by name in generate()
- manualAssign() should also auto-set arrangement — resolved: yes, same pattern as autoMatch()
Work Objectives
Core Objective
Complete 5 .pro generation improvements and 3 UI fixes to bring the presenter app to full feature parity with the spec.
Concrete Deliverables
- Modified
ProFileGenerator.php— cleaned attributes, correct textbox bounds, arrangement selection - New
Settingmodel + migration + controller + Vue page for macro config - Modified
ProExportService.php— macro injection into COPYRIGHT slide - New
ProBundleExportService.php— .probundle ZIP export for image slides - Modified
SlideGrid.vue— drag highlight CSS - Modified
SongMatchingService.php— auto-arrangement on match - Modified
Edit.vue— finalize + download buttons
Definition of Done
cd /Users/thorsten/AI/cts-work && php artisan test— all tests pass (198+ tests)cd /Users/thorsten/AI/cts-work && npm run build— build succeeds- Generated .pro files have no fill/stroke/shadow/feather/textScroller attributes
- Translated song export has two textboxes with different bounds matching TestTranslated.pro
- 'normal' arrangement selected by default in generated .pro files
- Settings page accessible with macro configuration
- .probundle download works for information/moderation/sermon blocks
- Drag highlight visible when reordering slides
- Finalize buttons work on Edit page
Must Have
- Exact textbox names preserved: "Orginal" (typo is intentional) and "Deutsch"
- Non-translated slides keep full-size single textbox at origin(150,100) size(1620×880)
- Existing 198 tests continue passing
- German UI text (Du, not Sie)
- Immediate persistence on all actions
Must NOT Have (Guardrails)
- Generic settings CRUD framework — ONLY the macro fields needed
- Changes to ProExportService public API signature (used by PlaylistExportService and ProFileController)
- Changes to ArrangementConfigurator drag-and-drop behavior
- CTS API writes of any kind
- Unnecessary abstractions or over-engineering
- JSDoc or documentation bloat
Verification Strategy
ZERO HUMAN INTERVENTION — ALL verification is agent-executed. No exceptions.
Test Decision
- Infrastructure exists: YES
- Automated tests: Tests-after (add targeted tests for new behavior)
- Framework: Pest (Laravel) + PHPUnit (vendor module)
QA Policy
Every task includes agent-executed QA scenarios.
Evidence saved to .sisyphus/evidence/task-{N}-{scenario-slug}.{ext}.
- Backend: Use Bash — run test commands, curl endpoints, verify responses
- Frontend/UI: Use Playwright (playwright skill) — navigate, interact, assert DOM, screenshot
- Vendor module: Use Bash — run PHPUnit, verify generated file contents
Execution Strategy
Parallel Execution Waves
Wave 1 (Start Immediately — 4 independent tasks):
├── Task 1: SlideGrid drag highlight CSS [quick]
├── Task 2: Set slide attributes to enable=false in ProFileGenerator [quick]
├── Task 3: Auto-select arrangement on song match [quick]
└── Task 4: Finalize + Download buttons on Edit page [visual-engineering]
Wave 2 (After Task 2 — same file, avoid conflicts):
├── Task 5: Default arrangement selection in generator [quick]
└── Task 6: Translated textbox positioning [unspecified-low]
Wave 3 (After Waves 1-2 + user decisions):
├── Task 7: Settings infrastructure + macro on COPYRIGHT group slide [unspecified-high]
└── Task 8: .probundle export for service slides [deep]
Wave FINAL (After ALL tasks — 4 parallel reviews):
├── Task F1: Plan compliance audit [oracle]
├── Task F2: Code quality review [unspecified-high]
├── Task F3: Real QA walkthrough [unspecified-high]
└── Task F4: Scope fidelity check [deep]
Critical Path: Task 2 → Task 5/6 → Task 7
Parallel Speedup: ~60% faster than sequential
Max Concurrent: 4 (Wave 1)
Dependency Matrix
| Task | Depends On | Blocks | Wave |
|---|---|---|---|
| 1 (Drag highlight) | — | — | 1 |
| 2 (Remove attributes) | — | 5, 6 | 1 |
| 3 (Auto arrangement match) | — | — | 1 |
| 4 (Finalize buttons Edit) | — | — | 1 |
| 5 (Default arrangement gen) | 2 | 7 | 2 |
| 6 (Translated textbox) | 2 | 7 | 2 |
| 7 (Settings + macro) | 2, 5, 6 + USER | — | 3 |
| 8 (.probundle export) | 2, 5, 6 + USER | — | 3 |
| F1-F4 (Final verification) | ALL | — | FINAL |
Agent Dispatch Summary
- Wave 1: 4 tasks — T1 →
quick, T2 →quick, T3 →quick, T4 →visual-engineering - Wave 2: 2 tasks — T5 →
quick, T6 →unspecified-low - Wave 3: 2 tasks — T7 →
unspecified-high, T8 →deep - FINAL: 4 tasks — F1 →
oracle, F2 →unspecified-high, F3 →unspecified-high, F4 →deep
TODOs
-
1. Slide Grid Drag Highlight
What to do:
- In
resources/js/Components/SlideGrid.vue(line 207-215), add drag highlight props to the<VueDraggable>component:ghost-class="slide-drag-ghost",chosen-class="slide-drag-chosen",drag-class="slide-drag-active" - Add scoped CSS styles (after line 453) for these classes:
.slide-drag-ghost:opacity: 0.4; border: 2px dashed rgb(99, 102, 241);(indigo dashed border, reduced opacity — shows drop position).slide-drag-chosen:ring-2 ring-indigo-500 shadow-lg scale-105(highlight the picked-up element).slide-drag-active:opacity: 0.8; transform: rotate(2deg);(slight rotation on the dragged clone)
- Also check if
ArrangementConfigurator.vuehas a separate drag area that needs the same treatment — if so, apply same classes there
Must NOT do:
- Do NOT change the drag-and-drop behavior or reordering logic
- Do NOT change existing cursor styles (cursor-grab / cursor-grabbing)
Recommended Agent Profile:
- Category:
quick- Reason: Single component CSS addition, well-defined insertion points
- Skills: [
frontend-ui-ux]frontend-ui-ux: Visual styling task, needs design sensibility for drag feedback
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with Tasks 2, 3, 4)
- Blocks: Nothing
- Blocked By: None (can start immediately)
References:
Pattern References:
resources/js/Components/SlideGrid.vue:207-215— VueDraggable component, add ghost-class/chosen-class/drag-class props hereresources/js/Components/SlideGrid.vue:453-465— Existing scoped styles section, add new CSS classes hereresources/js/Components/SlideGrid.vue:219— Current.slide-cardclass with cursor-grab styling
API/Type References:
- vue-draggable-plus docs: ghostClass, chosenClass, dragClass are string props passed to underlying SortableJS
WHY Each Reference Matters:
- Line 207-215: This is the exact VueDraggable tag where props go — the only place drag config lives
- Line 453-465: This is where scoped styles are defined — new CSS goes after existing styles
- Line 219: Shows current Tailwind styling pattern to match
Acceptance Criteria:
npm run buildsucceedsphp artisan test— all existing tests pass
QA Scenarios (MANDATORY):
Scenario: Drag highlight visible on slide reorder Tool: Playwright (playwright skill) Preconditions: App running at http://cts-work.test, logged in, navigate to a service Edit page with multiple slides in Information block Steps: 1. Navigate to http://cts-work.test/services and click "Bearbeiten" on any service 2. Expand the "Informationen" block 3. If slides exist, mousedown on a slide thumbnail and start dragging 4. Assert: the ghost element has class `slide-drag-ghost` with reduced opacity 5. Assert: the chosen element has class `slide-drag-chosen` with ring highlight 6. Take screenshot during drag state Expected Result: Visual highlight visible on dragged slide — ghost at drop position, chosen element highlighted Failure Indicators: No CSS class applied, no visual change during drag Evidence: .sisyphus/evidence/task-1-drag-highlight.png Scenario: No drag highlight on non-draggable elements Tool: Playwright (playwright skill) Preconditions: Same as above Steps: 1. Attempt to drag the upload area (has .no-drag class) 2. Assert: no drag classes applied, upload area doesn't move Expected Result: Upload area is not draggable, no highlight classes applied Evidence: .sisyphus/evidence/task-1-no-drag-upload.pngCommit: YES
- Message:
fix(ui): add drag highlight to slide grid - Files:
resources/js/Components/SlideGrid.vue - Pre-commit:
npm run build
- In
-
2. Set Visual Attributes to enable=false in ProFileGenerator
What to do:
- In
vendor/propresenter/parser/src/ProFileGenerator.php:- DECISION APPLIED: Set attributes with
enable=false(not remove entirely). This matches how ProPresenter natively generates reference files. - Modify
buildFill(),buildStroke(),buildShadow(),buildFeather()to callsetEnabled(false)instead ofsetEnabled(true) - For
buildTextScroller(), set to disabled/inactive state - Keep all method calls in
buildSlideElement()andbuildCue()— attributes are still created, just disabled - Keep all
useimports — classes are still used
- DECISION APPLIED: Set attributes with
- Update tests in
vendor/propresenter/parser/tests/ProFileGeneratorTest.phpto assert attributes are PRESENT but DISABLED (enable=false)
Must NOT do:
- Do NOT remove
buildBounds()— bounds are still needed for textbox positioning - Do NOT remove
buildPath()— paths may still be needed - Do NOT change the public API signature of
generate()orgenerateAndWrite() - Do NOT touch ProFileReader (reading logic stays intact)
Recommended Agent Profile:
- Category:
quick- Reason: Deletion of known methods, well-defined scope
- Skills:
[]- No special skills needed — pure PHP method removal
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with Tasks 1, 3, 4)
- Blocks: Tasks 5, 6 (same file — must complete before they start)
- Blocked By: None (can start immediately)
References:
Pattern References:
vendor/propresenter/parser/src/ProFileGenerator.php:257-281—buildSlideElement()— remove setFill/setStroke/setShadow/setFeather callsvendor/propresenter/parser/src/ProFileGenerator.php:~170—buildCue()— remove setTextScroller callvendor/propresenter/parser/src/ProFileGenerator.php:300-386— Methods to delete: buildFill, buildStroke, buildShadow, buildFeather, buildTextScroller
Test References:
vendor/propresenter/parser/tests/ProFileGeneratorTest.php— Update assertions that check for fill/stroke/shadow/feather presence/Users/thorsten/AI/propresenter-work/ref/TestTranslated.pro— Reference file showing attributes with enable=false (for comparison)
WHY Each Reference Matters:
- Lines 257-281: The exact method where attribute setters are called — surgical removal needed
- Lines 300-386: Dead code after removal — must be deleted to keep codebase clean
- Tests: Must be updated to not expect removed attributes, otherwise test suite breaks
Acceptance Criteria:
cd /Users/thorsten/AI/cts-work && php -r "require 'vendor/autoload.php'; echo 'autoload ok';"— autoload works- Vendor PHPUnit tests pass (run from within vendor dir or via app test suite)
php artisan test— all app tests pass
QA Scenarios (MANDATORY):
Scenario: Generated .pro file has no visual attributes Tool: Bash Preconditions: ProFileGenerator available via autoload Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php -r " require 'vendor/autoload.php'; use ProPresenter\Parser\ProFileGenerator; use ProPresenter\Parser\ProFileWriter; use ProPresenter\Parser\ProFileReader; \$song = ProFileGenerator::generate('AttrTest', [['name'=>'V1','color'=>[0,0,0,1],'slides'=>[['text'=>'Hello World']]]], [['name'=>'normal','groupNames'=>['V1']]]); ProFileWriter::write('/tmp/attr-test.pro', \$song); \$reader = new ProFileReader('/tmp/attr-test.pro'); \$slides = \$reader->getSlides(); \$el = \$slides[0]->getAllElements()[0]; \$raw = \$el->getRawElement(); echo 'hasFill:' . (\$raw->getFill() ? 'YES' : 'NO') . PHP_EOL; echo 'hasStroke:' . (\$raw->getStroke() ? 'YES' : 'NO') . PHP_EOL; " 2. Assert: fill is present but enabled=false, stroke present but enabled=false Expected Result: Generated file has fill/stroke/shadow/feather present but ALL with enabled=false Failure Indicators: Any attribute returns YES Evidence: .sisyphus/evidence/task-2-no-attributes.txt Scenario: Existing tests still pass after removal Tool: Bash Preconditions: Vendor test suite available Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php artisan test 2. Assert: 0 failures, 0 errors Expected Result: All 198+ tests pass Evidence: .sisyphus/evidence/task-2-tests-pass.txtCommit: YES
- Message:
refactor(pro): remove visual attributes from slide generation - Files:
vendor/propresenter/parser/src/ProFileGenerator.php,vendor/propresenter/parser/tests/ProFileGeneratorTest.php - Pre-commit:
php artisan test
- In
-
3. Auto-Select Default Arrangement on Song Match
What to do:
- In
app/Services/SongMatchingService.php, inautoMatch()method (lines 34-38), after settingsong_id:- Find the default arrangement:
$defaultArrangement = $song->arrangements()->where('is_default', true)->first() ?? $song->arrangements()->where('name', 'normal')->first() ?? $song->arrangements()->first(); - If found, set
$serviceSong->song_arrangement_id = $defaultArrangement->id;
- Find the default arrangement:
- Apply same logic in
manualAssign()method (lines 47-54), but ONLY ifsong_arrangement_idis currently null (don't override existing selection) - Add a test in the appropriate test file verifying: after autoMatch, serviceSong has a non-null song_arrangement_id pointing to the 'normal' arrangement
Must NOT do:
- Do NOT change how arrangements are created or structured
- Do NOT change the ArrangementConfigurator component behavior
- Do NOT override an existing arrangement selection in manualAssign
Recommended Agent Profile:
- Category:
quick- Reason: Small service method change, ~10 lines added
- Skills:
[]- Pure PHP backend logic
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with Tasks 1, 2, 4)
- Blocks: Nothing
- Blocked By: None (can start immediately)
References:
Pattern References:
app/Services/SongMatchingService.php:34-38—autoMatch()— insert arrangement lookup aftersong_idassignmentapp/Services/SongMatchingService.php:47-54—manualAssign()— insert arrangement lookup if song_arrangement_id is nullresources/js/Components/ArrangementConfigurator.vue:27— Showsis_defaultpriority pattern:props.arrangements.find((item) => item.is_default)?.id ?? props.arrangements[0]?.id
API/Type References:
app/Models/Song.php—arrangements()relationshipapp/Models/SongArrangement.php— hasis_defaultboolean,namestringapp/Models/ServiceSong.php— hassong_arrangement_idforeign key
WHY Each Reference Matters:
- Lines 34-38: Exact insertion point in autoMatch — add arrangement lookup right after song_id is set
- Lines 47-54: Same for manualAssign — only set if currently null to not override user choice
- ArrangementConfigurator line 27: Shows the priority order to replicate in PHP: is_default → 'normal' → first
Acceptance Criteria:
php artisan test— all tests pass- New test: after autoMatch, serviceSong.song_arrangement_id is not null
- New test: after manualAssign with existing arrangement, song_arrangement_id unchanged
QA Scenarios (MANDATORY):
Scenario: Auto-match sets default arrangement Tool: Bash Preconditions: Database has a song with 'normal' arrangement and is_default=true Steps: 1. Run: php artisan test --filter=SongMatchingServiceTest 2. Assert: test for auto-arrangement passes 3. Alternatively, check via tinker: php artisan tinker --execute=" \$song = App\Models\Song::first(); \$arrangement = \$song->arrangements()->where('is_default', true)->first(); echo 'has_default_arrangement: ' . (\$arrangement ? 'YES' : 'NO'); " Expected Result: Song matching auto-selects the default (or 'normal') arrangement Failure Indicators: song_arrangement_id remains null after match Evidence: .sisyphus/evidence/task-3-auto-arrangement.txt Scenario: Manual assign preserves existing arrangement choice Tool: Bash Preconditions: ServiceSong already has song_arrangement_id set Steps: 1. Run test that manually assigns to same song 2. Assert: song_arrangement_id unchanged Expected Result: Existing arrangement selection is not overridden Evidence: .sisyphus/evidence/task-3-preserve-arrangement.txtCommit: YES
- Message:
feat(songs): auto-select default arrangement on song match - Files:
app/Services/SongMatchingService.php, test file - Pre-commit:
php artisan test
- In
-
4. Finalize + Download Buttons on Service Edit Page
What to do:
- In
resources/js/Pages/Services/Edit.vue:- Add a sticky bottom bar after the blocks accordion (after line ~344) with:
- If NOT finalized: "Abschließen" button (primary) + "Abschließen & Herunterladen" button (secondary)
- If finalized: "Wieder öffnen" button + "Herunterladen" button
- Port the following from
resources/js/Pages/Services/Index.vue:finalizeService(serviceId)method (lines 69-95) — POST toservices.finalizewith{ confirmed: false }, handleneeds_confirmationresponseconfirmFinalize()method (lines 97-119) — POST with{ confirmed: true }reopenService(serviceId)method (lines 127-132) — POST toservices.reopen- Confirmation dialog with warnings list (lines 417-470)
- Toast notification pattern (lines 50-57)
- Add "Abschließen & Herunterladen" flow: call finalize first, on success trigger download via
window.location.href = route('services.download', service.id) - After successful finalize, update the local
service.finalized_atreactively (or reload the page) - After successful reopen, clear
service.finalized_atreactively
- Add a sticky bottom bar after the blocks accordion (after line ~344) with:
Must NOT do:
- Do NOT change the finalize/reopen controller logic
- Do NOT change the routes
- Do NOT redesign the finalization UX (keep same flow as Index.vue)
- Do NOT add finalize before all blocks are loaded
Recommended Agent Profile:
- Category:
visual-engineering- Reason: Vue component with UI layout, modal dialog, button styling
- Skills: [
frontend-ui-ux]frontend-ui-ux: Sticky footer layout, button hierarchy, modal design in German
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 1 (with Tasks 1, 2, 3)
- Blocks: Nothing
- Blocked By: None (can start immediately)
References:
Pattern References:
resources/js/Pages/Services/Index.vue:69-95—finalizeService()— POST with confirmation flow, handle needs_confirmation JSON responseresources/js/Pages/Services/Index.vue:97-119—confirmFinalize()— Confirm with warningsresources/js/Pages/Services/Index.vue:127-132—reopenService()— Simple POST, toast on successresources/js/Pages/Services/Index.vue:417-470— Confirmation modal with warnings listresources/js/Pages/Services/Index.vue:50-57—showToast()utility
API/Type References:
routes/web.php:55—POST /services/{service}/finalizenamedservices.finalizeroutes/web.php:56—POST /services/{service}/reopennamedservices.reopenroutes/web.php:58—GET /services/{service}/downloadnamedservices.downloadapp/Http/Controllers/ServiceController.php:224-245— finalize() returns JSON:{ needs_confirmation, warnings, success }app/Http/Controllers/ServiceController.php:247-256— reopen() returns redirect with flashapp/Http/Controllers/ServiceController.php:269-289— download() returns BinaryFileResponse
WHY Each Reference Matters:
- Index.vue 69-132: Complete working implementation to port — copy these methods, adapt for Edit page context
- Index.vue 417-470: Confirmation dialog template — reuse HTML structure
- Routes: Needed for
route()helper calls in Vue - Controller: Understanding response format to handle correctly (JSON vs redirect)
Acceptance Criteria:
npm run buildsucceedsphp artisan test— all tests pass- Finalize button visible on Edit page when service is not finalized
- Reopen + Download buttons visible when service is finalized
QA Scenarios (MANDATORY):
Scenario: Finalize from Edit page Tool: Playwright (playwright skill) Preconditions: App running at http://cts-work.test, logged in, navigate to an un-finalized service Edit page Steps: 1. Navigate to http://cts-work.test/services 2. Click "Bearbeiten" on a service that is NOT finalized 3. Scroll to bottom — assert "Abschließen" button visible 4. Click "Abschließen" 5. If confirmation dialog appears, click confirm button 6. Assert: page shows "Wieder öffnen" button (finalized state) 7. Take screenshot Expected Result: Service is finalized, buttons switch to reopen/download state Failure Indicators: Button not visible, finalize fails, page errors Evidence: .sisyphus/evidence/task-4-finalize-edit.png Scenario: Reopen from Edit page Tool: Playwright (playwright skill) Preconditions: Service is finalized (from previous scenario) Steps: 1. On the same Edit page, click "Wieder öffnen" 2. Assert: buttons switch back to "Abschließen" / "Abschließen & Herunterladen" 3. Take screenshot Expected Result: Service is reopened, buttons revert to non-finalized state Failure Indicators: Reopen fails, buttons don't update Evidence: .sisyphus/evidence/task-4-reopen-edit.png Scenario: Finalize & Download from Edit page Tool: Playwright (playwright skill) Preconditions: Service is not finalized, has at least some content Steps: 1. Click "Abschließen & Herunterladen" 2. If confirmation appears, confirm 3. Assert: download starts (check network for download response) 4. Assert: buttons switch to finalized state Expected Result: Service finalized AND file download triggered Evidence: .sisyphus/evidence/task-4-finalize-download.pngCommit: YES
- Message:
feat(services): add finalize and download buttons to edit page - Files:
resources/js/Pages/Services/Edit.vue - Pre-commit:
npm run build && php artisan test
- In
-
5. Default Arrangement Selection in ProFileGenerator
What to do:
- In
vendor/propresenter/parser/src/ProFileGenerator.php, ingenerate()method (lines 115-117):- Currently:
if (isset($arrangementProtos[0])) { $presentation->setSelectedArrangement($arrangementProtos[0]->getUuid()); } - Change to: Loop through
$arrangementProtos, find the one wheregetName()matches 'normal' (case-insensitive). If found, use its UUID. If not found, fall back to first arrangement. - Implementation:
$selectedArrangement = null; foreach ($arrangementProtos as $arr) { if (strtolower($arr->getName()) === 'normal') { $selectedArrangement = $arr; break; } } $selectedArrangement = $selectedArrangement ?? ($arrangementProtos[0] ?? null); if ($selectedArrangement) { $presentation->setSelectedArrangement($selectedArrangement->getUuid()); }
- Currently:
- Add test: generate with arrangements
['other', 'normal'], verify 'normal' is selected (not 'other') - Add test: generate with arrangements
['custom']only (no 'normal'), verify 'custom' is selected as fallback
Must NOT do:
- Do NOT change the public API signature of
generate()orgenerateAndWrite() - Do NOT add new parameters to these methods
- Do NOT change how arrangements are built — only which one is selected
Recommended Agent Profile:
- Category:
quick- Reason: Small logic change in one method, ~15 lines
- Skills:
[]
Parallelization:
- Can Run In Parallel: YES (with Task 6)
- Parallel Group: Wave 2 (with Task 6)
- Blocks: Task 7 (depends on stable generator)
- Blocked By: Task 2 (same file, avoid merge conflicts)
References:
Pattern References:
vendor/propresenter/parser/src/ProFileGenerator.php:115-117— Current arrangement selection logic to replacevendor/propresenter/parser/src/ProFileGenerator.php:96-113— Arrangement protobuf building loop (context for understanding $arrangementProtos)
Test References:
vendor/propresenter/parser/tests/ProFileGeneratorTest.php— Existing arrangement tests to extend
WHY Each Reference Matters:
- Lines 115-117: The EXACT code to replace — currently selects first, need to find 'normal'
- Lines 96-113: Shows how arrangements are built and stored in $arrangementProtos — needed to understand getName()
Acceptance Criteria:
- Vendor PHPUnit tests pass
php artisan test— all app tests pass- New test: 'normal' arrangement selected when present among multiple arrangements
- New test: first arrangement selected as fallback when 'normal' not present
QA Scenarios (MANDATORY):
Scenario: 'normal' arrangement auto-selected Tool: Bash Preconditions: ProFileGenerator available Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php -r " require 'vendor/autoload.php'; use ProPresenter\Parser\ProFileGenerator; use ProPresenter\Parser\ProFileReader; use ProPresenter\Parser\ProFileWriter; \$song = ProFileGenerator::generate('SelectTest', [['name'=>'V1','color'=>[0,0,0,1],'slides'=>[['text'=>'Hello']]]], [['name'=>'other','groupNames'=>['V1']], ['name'=>'normal','groupNames'=>['V1']]] ); ProFileWriter::write('/tmp/select-test.pro', \$song); \$reader = new ProFileReader('/tmp/select-test.pro'); \$arrangements = \$reader->getArrangements(); \$selected = \$reader->getSelectedArrangement(); echo 'selected: ' . \$selected; " 2. Assert output contains: selected: normal Expected Result: 'normal' arrangement is selected, not 'other' Failure Indicators: 'other' is selected or no arrangement selected Evidence: .sisyphus/evidence/task-5-normal-selected.txt Scenario: Fallback to first when no 'normal' Tool: Bash Steps: 1. Generate with arrangements ['custom'] only 2. Read back and verify 'custom' is selected Expected Result: Falls back to first arrangement gracefully Evidence: .sisyphus/evidence/task-5-fallback-first.txtCommit: YES
- Message:
feat(pro): select normal arrangement by default in generator - Files:
vendor/propresenter/parser/src/ProFileGenerator.php,vendor/propresenter/parser/tests/ProFileGeneratorTest.php - Pre-commit:
php artisan test
- In
-
6. Correct Translated Textbox Positioning
What to do:
- In
vendor/propresenter/parser/src/ProFileGenerator.php:- Currently
buildBounds()returns a single fixed rect: origin(150,100) size(1620×880) used for ALL textboxes - Create two additional private methods:
buildOriginalBounds(): Returns rect with origin(150, 99.543) size(1620, 182.946) — top position, ~183px tallbuildTranslationBounds(): Returns rect with origin(150, 303.166) size(1620, 113.889) — below, ~114px tall
- Keep existing
buildBounds()unchanged for non-translated (single textbox) slides - Modify
buildCue()(lines 151-192) where it creates slide elements:- Currently:
$elements[] = self::buildSlideElement('Orginal', ...)and$elements[] = self::buildSlideElement('Deutsch', ...) - Both call
buildSlideElement()which usesbuildBounds() - Change: Pass a
$boundsMethodparameter or createbuildSlideElementWithBounds($name, $text, $bounds)variant - For translated slides: first element uses
buildOriginalBounds(), second usesbuildTranslationBounds() - For non-translated slides: single element uses
buildBounds()(unchanged)
- Currently:
- Currently
- Add test: generate translated song, read back, verify:
- Element 0 name is "Orginal", bounds height ≈ 183
- Element 1 name is "Deutsch", bounds height ≈ 114
- Element 0 origin.y ≈ 99.5, Element 1 origin.y ≈ 303.2
- Add test: generate non-translated song, verify single element at full bounds (150,100, 1620×880)
Must NOT do:
- Do NOT change textbox names — "Orginal" (with intentional typo) and "Deutsch" must stay exactly as-is
- Do NOT change non-translated slide bounds
- Do NOT change
buildBounds()default behavior
Recommended Agent Profile:
- Category:
unspecified-low- Reason: Precise positioning work, needs careful float handling, but small scope
- Skills:
[]
Parallelization:
- Can Run In Parallel: YES (with Task 5)
- Parallel Group: Wave 2 (with Task 5)
- Blocks: Task 7 (depends on stable generator)
- Blocked By: Task 2 (same file, avoid merge conflicts)
References:
Pattern References:
vendor/propresenter/parser/src/ProFileGenerator.php:283-298—buildBounds()— current single-bounds method to use as templatevendor/propresenter/parser/src/ProFileGenerator.php:151-192—buildCue()— where slide elements are created with names "Orginal" / "Deutsch"vendor/propresenter/parser/src/ProFileGenerator.php:257-281—buildSlideElement()— where bounds are applied
External References:
/Users/thorsten/AI/propresenter-work/ref/TestTranslated.pro— THE reference file with exact positioning values. Confirmed bounds: Original origin(150, 99.543) size(1620×182.946), Deutsch origin(150, 303.166) size(1620×113.889)
Test References:
vendor/propresenter/parser/tests/ProFileReaderTest.php— Shows how to read back slides and elements for assertionsvendor/propresenter/parser/tests/SlideTest.php— Shows element/textbox structure
WHY Each Reference Matters:
buildBounds(): Template for creating new bounds methods — same pattern, different valuesbuildCue()lines 151-192: Where the if(translation) branch creates two elements — need to pass different bounds here- TestTranslated.pro: THE source of truth for exact positioning values — user explicitly said to match this
Acceptance Criteria:
- Vendor PHPUnit tests pass
php artisan test— all app tests pass- New test: translated slide has two elements with DIFFERENT bounds heights
- New test: non-translated slide has one element with full-size bounds
QA Scenarios (MANDATORY):
Scenario: Translated slide has correctly positioned dual textboxes Tool: Bash Preconditions: ProFileGenerator available Steps: 1. Run: cd /Users/thorsten/AI/cts-work && php -r " require 'vendor/autoload.php'; use ProPresenter\Parser\ProFileGenerator; use ProPresenter\Parser\ProFileWriter; use ProPresenter\Parser\ProFileReader; \$song = ProFileGenerator::generate('TranslateTest', [['name'=>'V1','color'=>[0,0,0,1],'slides'=>[['text'=>'Amazing Grace','translation'=>'Erstaunliche Gnade']]]], [['name'=>'normal','groupNames'=>['V1']]] ); ProFileWriter::write('/tmp/translate-test.pro', \$song); \$reader = new ProFileReader('/tmp/translate-test.pro'); \$slides = \$reader->getSlides(); \$elements = \$slides[0]->getAllElements(); echo 'count: ' . count(\$elements) . PHP_EOL; echo 'name0: ' . \$elements[0]->getName() . PHP_EOL; echo 'name1: ' . \$elements[1]->getName() . PHP_EOL; // Check heights differ \$b0 = \$elements[0]->getRawElement()->getBounds(); \$b1 = \$elements[1]->getRawElement()->getBounds(); echo 'height0: ' . round(\$b0->getSize()->getHeight(), 1) . PHP_EOL; echo 'height1: ' . round(\$b1->getSize()->getHeight(), 1) . PHP_EOL; " 2. Assert: count=2, name0=Orginal, name1=Deutsch, height0≈182.9, height1≈113.9 Expected Result: Two textboxes with correct names and different heights matching TestTranslated.pro reference Failure Indicators: Same height for both, wrong names, or only 1 element Evidence: .sisyphus/evidence/task-6-translated-bounds.txt Scenario: Non-translated slide keeps full-size single textbox Tool: Bash Steps: 1. Generate song without translation 2. Read back, verify 1 element with height ≈ 880 Expected Result: Single element at origin(150,100) size(1620×880) Evidence: .sisyphus/evidence/task-6-single-bounds.txtCommit: YES
- Message:
feat(pro): correct translated textbox positioning - Files:
vendor/propresenter/parser/src/ProFileGenerator.php,vendor/propresenter/parser/tests/ProFileGeneratorTest.php - Pre-commit:
php artisan test
- In
-
7. Settings Infrastructure + Macro on COPYRIGHT Slide
✅ DECISION RESOLVED: The COPYRIGHT slide is a slide within a group named "COPYRIGHT" in the song data. The macro should be attached to slides in the COPYRIGHT group. The macro settings (name, UUID, collection) are configurable via the Settings page.
What to do:
- Migration: Create
database/migrations/xxxx_create_settings_table.php:- Schema:
id,key(string, unique index),value(text, nullable),created_at,updated_at
- Schema:
- Model: Create
app/Models/Setting.php:- Static helper:
Setting::get($key, $default = null)— returns value for key - Static helper:
Setting::set($key, $value)— upserts key-value pair
- Static helper:
- Controller: Create
app/Http/Controllers/SettingsController.php:index()— Inertia renderSettingspage with current macro settingsupdate(Request $request)— Validate and save macro fields
- Vue Page: Create
resources/js/Pages/Settings.vue:- Form with 4 fields: Macro-Name (text), Macro-UUID (text), Collection-Name (text, default "--MAIN--"), Collection-UUID (text, default "8D02FC57-83F8-4042-9B90-81C229728426")
- Auto-save on blur (immediate persistence pattern)
- German labels: "Makro-Name", "Makro-UUID", "Collection-Name", "Collection-UUID"
- Help text explaining these values come from ProPresenter's macro configuration
- Navigation: Add "Einstellungen" link in
resources/js/Layouts/AuthenticatedLayout.vue(after "API-Log" in nav, lines 95-126) - Routes: Add
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index')andRoute::patch('/settings', [SettingsController::class, 'update'])->name('settings.update')inroutes/web.php - Shared Props: In
app/Http/Middleware/HandleInertiaRequests.php, add macro settings to shared data so ProExportService can access them - Integration: In
app/Services/ProExportService.php, read macro settings fromSetting::get()and inject into slides in groups named 'COPYRIGHT' as'macro' => [...]array. If macro settings are empty/null, skip macro injection (no error). If no COPYRIGHT group exists in the song, no macro is added (no error).
Must NOT do:
- Do NOT build a generic settings framework — ONLY the 4 macro fields
- Do NOT move existing .env-based configs (song_request email) to the settings table
- Do NOT add settings to every page via shared props unless needed
Recommended Agent Profile:
- Category:
unspecified-high- Reason: Full-stack feature — migration, model, controller, Vue page, navigation, integration
- Skills: [
frontend-ui-ux]frontend-ui-ux: Settings page design, form layout, auto-save UX
Parallelization:
- Can Run In Parallel: YES (with Task 8)
- Parallel Group: Wave 3 (with Task 8)
- Blocks: Nothing
- Blocked By: Tasks 2, 5, 6 (generator must be stable) + user decision on COPYRIGHT slide
References:
Pattern References:
app/Http/Controllers/ServiceController.php— Controller pattern with Inertia renderingresources/js/Pages/Songs/Index.vue— Existing page pattern (Inertia props, layout usage)resources/js/Layouts/AuthenticatedLayout.vue:95-126— Navigation links to extendapp/Http/Middleware/HandleInertiaRequests.php— Shared props patternconfig/services.php:38-40— Existing config pattern for song_request email (context, NOT to migrate)
API/Type References:
vendor/propresenter/parser/src/ProFileGenerator.php:180-182— Where macro data is read from slideDatavendor/propresenter/parser/src/ProFileGenerator.php:206-227— buildMacroAction() data structurevendor/propresenter/parser/tests/ProFileGeneratorTest.php:334-339— Macro test data example
WHY Each Reference Matters:
- ServiceController: Copy the Inertia render pattern for the new SettingsController
- Songs/Index.vue: Template for a full page with layout, shows Inertia props pattern
- AuthenticatedLayout: Exact file and lines where nav link must be added
- ProFileGenerator macro lines: Shows exact data structure the macro config must produce
Acceptance Criteria:
php artisan migrate— settings table createdphp artisan test— all tests passnpm run build— succeeds- Settings page accessible at /settings with macro form
- Macro values persist on page reload
QA Scenarios (MANDATORY):
Scenario: Settings page accessible and saves macro config Tool: Playwright (playwright skill) Preconditions: App running, logged in, migration run Steps: 1. Navigate to http://cts-work.test/settings 2. Assert: page loads with "Einstellungen" heading 3. Fill "Makro-Name" with "Test Macro" 4. Fill "Makro-UUID" with "11111111-2222-3333-4444-555555555555" 5. Tab out (blur) to trigger auto-save 6. Reload page 7. Assert: "Makro-Name" field contains "Test Macro" 8. Assert: "Makro-UUID" field contains the UUID 9. Take screenshot Expected Result: Settings saved to DB and persist across reloads Failure Indicators: Fields empty after reload, save fails Evidence: .sisyphus/evidence/task-7-settings-save.png Scenario: Navigation link present Tool: Playwright (playwright skill) Steps: 1. Navigate to any page 2. Assert: "Einstellungen" link visible in top navigation 3. Click it 4. Assert: URL is /settings Expected Result: Settings accessible from navigation Evidence: .sisyphus/evidence/task-7-nav-link.png Scenario: Empty macro config doesn't break export Tool: Bash Steps: 1. Ensure settings table has no macro values 2. Run: curl -s http://cts-work.test/api/songs/1/download-pro -o /tmp/no-macro.pro 3. Assert: download succeeds (HTTP 200) Expected Result: Export works without macro — no error when settings empty Evidence: .sisyphus/evidence/task-7-no-macro-export.txtCommit: YES
- Message:
feat(settings): add global settings UI with macro configuration - Files:
database/migrations/xxxx_create_settings_table.php,app/Models/Setting.php,app/Http/Controllers/SettingsController.php,resources/js/Pages/Settings.vue,resources/js/Layouts/AuthenticatedLayout.vue,routes/web.php,app/Services/ProExportService.php - Pre-commit:
php artisan test && npm run build
- Migration: Create
-
8. .probundle Export for Service Slide Blocks
✅ DECISION RESOLVED: .probundle is a flat ZIP containing one
.profile + image files at root level. Structure:{block_name}.pro+image1.jpg,image2.jpg, etc. The .pro file references images via media actions. Each slide in the .pro is an image slide (no text, just media reference). Applies to blocks: information, moderation, sermon.What to do:
- Service: Create
app/Services/ProBundleExportService.php:- Method:
generateBundle(Service $service, string $blockType): string— returns path to generated .probundle file - For each slide in the block:
- Get the stored image file path
- Create a ProFileGenerator slide entry with media action:
'media' => 'file:///' . $absolutePath,'format' => 'JPG' - Text can be empty or the slide's original filename
- Use ProFileGenerator::generateAndWrite() to create the .pro file inside a temp dir
- ZIP the .pro file + all image files into a .probundle (extension is
.probundle, format is ZIP) - Return the .probundle path
- Method:
- Controller: Add
downloadBundle(Service $service, string $blockType)method toServiceController.php:- Validate blockType is one of: information, moderation, sermon
- Call ProBundleExportService
- Return BinaryFileResponse with content-type
application/zipand.probundleextension
- Route: Add
Route::get('/services/{service}/download-bundle/{blockType}', [ServiceController::class, 'downloadBundle'])->name('services.download-bundle')inroutes/web.php - UI: In each block component (InformationBlock, ModerationBlock, SermonBlock), add a ".probundle herunterladen" download button/link
- Tests: Add test for bundle generation — verify ZIP contains .pro file + image files
Must NOT do:
- Do NOT change existing slide storage or upload logic
- Do NOT modify ProFileGenerator's core behavior for this
- Do NOT include slides from other blocks in a block's bundle
- Do NOT require finalization for bundle download (any time, independent of finalize state)
Recommended Agent Profile:
- Category:
deep- Reason: New service with ZIP handling, file system operations, integration across multiple layers
- Skills:
[]
Parallelization:
- Can Run In Parallel: YES (with Task 7)
- Parallel Group: Wave 3 (with Task 7)
- Blocks: Nothing
- Blocked By: Tasks 2, 5, 6 (generator must be stable) + user decision on .probundle format
References:
Pattern References:
app/Services/ProExportService.php— Existing export service pattern to follow (temp file creation, generator call, return path)app/Http/Controllers/ServiceController.php:269-289—download()method pattern for BinaryFileResponsevendor/propresenter/parser/src/ProFileGenerator.php:180-195— Media action support:if (isset($slideData['media'])) { $actions[] = self::buildMediaAction(...); }
API/Type References:
app/Models/Slide.php— hasstored_filename,original_name,type(information/moderation/sermon)app/Models/Service.php— has relationships to slides- PHP
ZipArchiveclass — for creating .probundle (ZIP format)
External References:
/Users/thorsten/AI/propresenter-work/ref/TestMitBildernUndMakro.pro— Reference for .pro files with image/media actions (1.9KB)
WHY Each Reference Matters:
- ProExportService: Pattern for temp file handling, generator invocation — copy this structure
- ServiceController download: Exact pattern for returning file downloads — reuse for .probundle
- ProFileGenerator media action: Shows how to create image slides — need 'media' + 'format' in slide data
- Slide model: Need stored_filename to locate actual image files on disk for ZIP inclusion
Acceptance Criteria:
php artisan test— all tests pass (including new bundle test)npm run build— succeeds- New test: bundle contains .pro file + correct number of images
- Download button visible on each block
QA Scenarios (MANDATORY):
Scenario: Download .probundle for information block Tool: Bash Preconditions: Service has uploaded information slides Steps: 1. Run: curl -s -o /tmp/info-bundle.probundle http://cts-work.test/services/1/download-bundle/information --cookie "session_cookie" 2. Run: unzip -l /tmp/info-bundle.probundle 3. Assert: ZIP contains exactly 1 .pro file + N image files (matching slide count) 4. Assert: .pro file name contains block type Expected Result: Valid ZIP file with .pro + images Failure Indicators: Not a valid ZIP, missing .pro file, wrong image count Evidence: .sisyphus/evidence/task-8-bundle-contents.txt Scenario: .probundle download button visible in UI Tool: Playwright (playwright skill) Preconditions: Logged in, service has slides Steps: 1. Navigate to service Edit page 2. Expand "Informationen" block 3. Assert: ".probundle herunterladen" button/link visible 4. Take screenshot Expected Result: Download button present in each block that has slides Evidence: .sisyphus/evidence/task-8-bundle-button.png Scenario: Invalid block type returns error Tool: Bash Steps: 1. Run: curl -s -w "%{http_code}" http://cts-work.test/services/1/download-bundle/invalid 2. Assert: HTTP 422 or 404 Expected Result: Invalid block type rejected gracefully Evidence: .sisyphus/evidence/task-8-invalid-block.txtCommit: YES
- Message:
feat(export): add probundle export for service slide blocks - Files:
app/Services/ProBundleExportService.php,app/Http/Controllers/ServiceController.php,routes/web.php,resources/js/Components/Blocks/InformationBlock.vue,resources/js/Components/Blocks/ModerationBlock.vue,resources/js/Components/Blocks/SermonBlock.vue - Pre-commit:
php artisan test && npm run build
- Service: Create
Final Verification Wave (MANDATORY — after ALL implementation tasks)
4 review agents run in PARALLEL. ALL must APPROVE. Rejection → fix → re-run.
-
F1. Plan Compliance Audit —
oracleRead the plan end-to-end. For each "Must Have": verify implementation exists (read file, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan. Output:Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT -
F2. Code Quality Review —
unspecified-highRunphp 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 QA Walkthrough —
unspecified-high(+playwrightskill) Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration. Test edge cases: empty state, invalid input, rapid actions. Save to.sisyphus/evidence/final-qa/. Output:Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT -
F4. Scope Fidelity Check —
deepFor 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. Flag unaccounted changes. Output:Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT
Commit Strategy
- Wave 1: 4 separate commits (one per task)
fix(ui): add drag highlight to slide grid— SlideGrid.vuerefactor(pro): remove visual attributes from slide generation— ProFileGenerator.phpfeat(songs): auto-select default arrangement on song match— SongMatchingService.phpfeat(services): add finalize and download buttons to edit page— Edit.vue
- Wave 2: 2 commits
feat(pro): select normal arrangement by default in generator— ProFileGenerator.phpfeat(pro): correct translated textbox positioning— ProFileGenerator.php
- Wave 3: 2 commits
feat(settings): add global settings UI with macro configuration— multiple filesfeat(export): add probundle export for service slide blocks— multiple files
Success Criteria
Verification Commands
cd /Users/thorsten/AI/cts-work && php artisan test # Expected: 200+ tests, 0 failures
cd /Users/thorsten/AI/cts-work && npm run build # Expected: build succeeds
Final Checklist
- All "Must Have" present
- All "Must NOT Have" absent
- All tests pass (Pest + PHPUnit vendor)
- Build succeeds
- All QA evidence captured