1012 lines
52 KiB
Markdown
1012 lines
52 KiB
Markdown
# .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 `Setting` model + 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
|
||
- [x] `cd /Users/thorsten/AI/cts-work && php artisan test` — all tests pass (198+ tests)
|
||
- [x] `cd /Users/thorsten/AI/cts-work && npm run build` — build succeeds
|
||
- [x] Generated .pro files have no fill/stroke/shadow/feather/textScroller attributes
|
||
- [x] Translated song export has two textboxes with different bounds matching TestTranslated.pro
|
||
- [x] 'normal' arrangement selected by default in generated .pro files
|
||
- [x] Settings page accessible with macro configuration
|
||
- [x] .probundle download works for information/moderation/sermon blocks
|
||
- [x] Drag highlight visible when reordering slides
|
||
- [x] 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
|
||
|
||
- [x] 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.vue` has 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 here
|
||
- `resources/js/Components/SlideGrid.vue:453-465` — Existing scoped styles section, add new CSS classes here
|
||
- `resources/js/Components/SlideGrid.vue:219` — Current `.slide-card` class 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 build` succeeds
|
||
- [ ] `php 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.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `fix(ui): add drag highlight to slide grid`
|
||
- Files: `resources/js/Components/SlideGrid.vue`
|
||
- Pre-commit: `npm run build`
|
||
|
||
- [x] 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 call `setEnabled(false)` instead of `setEnabled(true)`
|
||
- For `buildTextScroller()`, set to disabled/inactive state
|
||
- Keep all method calls in `buildSlideElement()` and `buildCue()` — attributes are still created, just disabled
|
||
- Keep all `use` imports — classes are still used
|
||
- Update tests in `vendor/propresenter/parser/tests/ProFileGeneratorTest.php` to 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()` or `generateAndWrite()`
|
||
- 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 calls
|
||
- `vendor/propresenter/parser/src/ProFileGenerator.php:~170` — `buildCue()` — remove setTextScroller call
|
||
- `vendor/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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 3. Auto-Select Default Arrangement on Song Match
|
||
|
||
**What to do**:
|
||
- In `app/Services/SongMatchingService.php`, in `autoMatch()` method (lines 34-38), after setting `song_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;`
|
||
- Apply same logic in `manualAssign()` method (lines 47-54), but ONLY if `song_arrangement_id` is 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 after `song_id` assignment
|
||
- `app/Services/SongMatchingService.php:47-54` — `manualAssign()` — insert arrangement lookup if song_arrangement_id is null
|
||
- `resources/js/Components/ArrangementConfigurator.vue:27` — Shows `is_default` priority pattern: `props.arrangements.find((item) => item.is_default)?.id ?? props.arrangements[0]?.id`
|
||
|
||
**API/Type References**:
|
||
- `app/Models/Song.php` — `arrangements()` relationship
|
||
- `app/Models/SongArrangement.php` — has `is_default` boolean, `name` string
|
||
- `app/Models/ServiceSong.php` — has `song_arrangement_id` foreign 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.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(songs): auto-select default arrangement on song match`
|
||
- Files: `app/Services/SongMatchingService.php`, test file
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 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 to `services.finalize` with `{ confirmed: false }`, handle `needs_confirmation` response
|
||
- `confirmFinalize()` method (lines 97-119) — POST with `{ confirmed: true }`
|
||
- `reopenService(serviceId)` method (lines 127-132) — POST to `services.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_at` reactively (or reload the page)
|
||
- After successful reopen, clear `service.finalized_at` reactively
|
||
|
||
**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 response
|
||
- `resources/js/Pages/Services/Index.vue:97-119` — `confirmFinalize()` — Confirm with warnings
|
||
- `resources/js/Pages/Services/Index.vue:127-132` — `reopenService()` — Simple POST, toast on success
|
||
- `resources/js/Pages/Services/Index.vue:417-470` — Confirmation modal with warnings list
|
||
- `resources/js/Pages/Services/Index.vue:50-57` — `showToast()` utility
|
||
|
||
**API/Type References**:
|
||
- `routes/web.php:55` — `POST /services/{service}/finalize` named `services.finalize`
|
||
- `routes/web.php:56` — `POST /services/{service}/reopen` named `services.reopen`
|
||
- `routes/web.php:58` — `GET /services/{service}/download` named `services.download`
|
||
- `app/Http/Controllers/ServiceController.php:224-245` — finalize() returns JSON: `{ needs_confirmation, warnings, success }`
|
||
- `app/Http/Controllers/ServiceController.php:247-256` — reopen() returns redirect with flash
|
||
- `app/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 build` succeeds
|
||
- [ ] `php 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.png
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 5. Default Arrangement Selection in ProFileGenerator
|
||
|
||
**What to do**:
|
||
- In `vendor/propresenter/parser/src/ProFileGenerator.php`, in `generate()` method (lines 115-117):
|
||
- Currently: `if (isset($arrangementProtos[0])) { $presentation->setSelectedArrangement($arrangementProtos[0]->getUuid()); }`
|
||
- Change to: Loop through `$arrangementProtos`, find the one where `getName()` matches 'normal' (case-insensitive). If found, use its UUID. If not found, fall back to first arrangement.
|
||
- Implementation:
|
||
```php
|
||
$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());
|
||
}
|
||
```
|
||
- 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()` or `generateAndWrite()`
|
||
- 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 replace
|
||
- `vendor/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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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 tall
|
||
- `buildTranslationBounds()`: 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 uses `buildBounds()`
|
||
- Change: Pass a `$boundsMethod` parameter or create `buildSlideElementWithBounds($name, $text, $bounds)` variant
|
||
- For translated slides: first element uses `buildOriginalBounds()`, second uses `buildTranslationBounds()`
|
||
- For non-translated slides: single element uses `buildBounds()` (unchanged)
|
||
- 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 template
|
||
- `vendor/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 assertions
|
||
- `vendor/propresenter/parser/tests/SlideTest.php` — Shows element/textbox structure
|
||
|
||
**WHY Each Reference Matters**:
|
||
- `buildBounds()`: Template for creating new bounds methods — same pattern, different values
|
||
- `buildCue()` 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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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`
|
||
- **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
|
||
- **Controller**: Create `app/Http/Controllers/SettingsController.php`:
|
||
- `index()` — Inertia render `Settings` page with current macro settings
|
||
- `update(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')` and `Route::patch('/settings', [SettingsController::class, 'update'])->name('settings.update')` in `routes/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 from `Setting::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 rendering
|
||
- `resources/js/Pages/Songs/Index.vue` — Existing page pattern (Inertia props, layout usage)
|
||
- `resources/js/Layouts/AuthenticatedLayout.vue:95-126` — Navigation links to extend
|
||
- `app/Http/Middleware/HandleInertiaRequests.php` — Shared props pattern
|
||
- `config/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 slideData
|
||
- `vendor/propresenter/parser/src/ProFileGenerator.php:206-227` — buildMacroAction() data structure
|
||
- `vendor/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 created
|
||
- [ ] `php artisan test` — all tests pass
|
||
- [ ] `npm 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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 8. .probundle Export for Service Slide Blocks
|
||
|
||
**✅ DECISION RESOLVED**: .probundle is a flat ZIP containing one `.pro` file + 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
|
||
- **Controller**: Add `downloadBundle(Service $service, string $blockType)` method to `ServiceController.php`:
|
||
- Validate blockType is one of: information, moderation, sermon
|
||
- Call ProBundleExportService
|
||
- Return BinaryFileResponse with content-type `application/zip` and `.probundle` extension
|
||
- **Route**: Add `Route::get('/services/{service}/download-bundle/{blockType}', [ServiceController::class, 'downloadBundle'])->name('services.download-bundle')` in `routes/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 BinaryFileResponse
|
||
- `vendor/propresenter/parser/src/ProFileGenerator.php:180-195` — Media action support: `if (isset($slideData['media'])) { $actions[] = self::buildMediaAction(...); }`
|
||
|
||
**API/Type References**:
|
||
- `app/Models/Slide.php` — has `stored_filename`, `original_name`, `type` (information/moderation/sermon)
|
||
- `app/Models/Service.php` — has relationships to slides
|
||
- PHP `ZipArchive` class — 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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
---
|
||
|
||
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
|
||
|
||
> 4 review agents run in PARALLEL. ALL must APPROVE. Rejection → fix → re-run.
|
||
|
||
- [x] F1. **Plan Compliance Audit** — `oracle`
|
||
Read 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`
|
||
|
||
- [x] F2. **Code Quality Review** — `unspecified-high`
|
||
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 QA Walkthrough** — `unspecified-high` (+ `playwright` skill)
|
||
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`
|
||
|
||
- [x] F4. **Scope Fidelity Check** — `deep`
|
||
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. 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.vue
|
||
- `refactor(pro): remove visual attributes from slide generation` — ProFileGenerator.php
|
||
- `feat(songs): auto-select default arrangement on song match` — SongMatchingService.php
|
||
- `feat(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.php
|
||
- `feat(pro): correct translated textbox positioning` — ProFileGenerator.php
|
||
- **Wave 3**: 2 commits
|
||
- `feat(settings): add global settings UI with macro configuration` — multiple files
|
||
- `feat(export): add probundle export for service slide blocks` — multiple files
|
||
|
||
---
|
||
|
||
## Success Criteria
|
||
|
||
### Verification Commands
|
||
```bash
|
||
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
|
||
- [x] All "Must Have" present
|
||
- [x] All "Must NOT Have" absent
|
||
- [x] All tests pass (Pest + PHPUnit vendor)
|
||
- [x] Build succeeds
|
||
- [x] All QA evidence captured
|