diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json index 70827e8..822696e 100644 --- a/.sisyphus/boulder.json +++ b/.sisyphus/boulder.json @@ -1,14 +1,31 @@ { - "active_plan": null, - "completed_plan": "/Users/thorsten/AI/cts-work/.sisyphus/plans/cts-bugfix-features.md", - "completed_at": "2026-03-02T11:20:00.000Z", - "started_at": "2026-03-02T09:38:25.624Z", + "active_plan": "/Users/thorsten/AI/cts-work/.sisyphus/plans/edit-page-restructure.md", + "started_at": "2026-03-29T09:26:31.630Z", "session_ids": [ - "ses_355fcc13effe4ksRKIO611tYSD" + "ses_2c7383fb5ffer5YzYhHHeCDk9a", + "ses_2c7140656ffeXuVsz4aZDp9jtT", + "ses_2c70cb563ffeuKjg4NciBBmm7R" ], - "plan_name": "cts-bugfix-features", - "worktree_path": "/Users/thorsten/AI/cts-work", - "status": "complete", - "tasks_completed": 20, - "tasks_total": 20 -} + "plan_name": "edit-page-restructure", + "agent": "atlas", + "task_sessions": { + "todo:1": { + "task_key": "todo:1", + "task_label": "1", + "task_title": "Create `service_agenda_items` Migration, Model + Factory", + "session_id": "ses_2c7140656ffeXuVsz4aZDp9jtT", + "agent": "Sisyphus-Junior", + "category": "quick", + "updated_at": "2026-03-29T09:32:40.580Z" + }, + "todo:3": { + "task_key": "todo:3", + "task_label": "3", + "task_title": "Create AgendaMatcherService (Wildcard Namesmatching)", + "session_id": "ses_2c70cb563ffeuKjg4NciBBmm7R", + "agent": "Sisyphus-Junior", + "category": "quick", + "updated_at": "2026-03-29T09:37:26.235Z" + } + } +} \ No newline at end of file diff --git a/.sisyphus/notepads/edit-page-restructure/decisions.md b/.sisyphus/notepads/edit-page-restructure/decisions.md new file mode 100644 index 0000000..e69de29 diff --git a/.sisyphus/notepads/edit-page-restructure/issues.md b/.sisyphus/notepads/edit-page-restructure/issues.md new file mode 100644 index 0000000..e69de29 diff --git a/.sisyphus/notepads/edit-page-restructure/learnings.md b/.sisyphus/notepads/edit-page-restructure/learnings.md new file mode 100644 index 0000000..188cfb7 --- /dev/null +++ b/.sisyphus/notepads/edit-page-restructure/learnings.md @@ -0,0 +1,88 @@ +# Learnings — edit-page-restructure + +## 2026-03-29 — Session start + +### Code Conventions +- Migrations: anonymous class `return new class extends Migration {`; methods `up(): void`, `down(): void` +- Models: `$fillable` array; `casts()` method (not `$casts` property); promoted properties in constructor +- SoftDeletes: used on Song and Slide models; `whereNull('deleted_at')` in manual queries +- PHP string concat: no spaces around `.` operator +- Return types always present; union types for multiple returns +- Relationships: typed return types (`HasMany`, `BelongsTo`) +- Error messages: German Du-form; flash `->with('success', '...')`; JSON `'message'` key + +### Codebase Facts +- `service_songs` table stores ONLY songs from CTS agenda (via `getSongs()`) +- `EventAgendaItem.type` actual values: UNKNOWN — Task 5 will discover +- `EventAgendaItem.getItems()` returns ALL agenda items (not just songs) +- `isBeforeEvent=true` items should be excluded from visible agenda +- Settings stored as plain strings in `settings` table (key-value) +- Settings controller uses `MACRO_KEYS` constant array for validation +- SlideUploader sends `type`, `service_id`, `expire_date` in FormData +- PlaylistExportService builds: info slides → matched songs → moderation → sermon (OLD ORDER) +- Backward compat needed: old services without agenda items fall back to block-based export + +### Test Patterns +- Pest v4 function-style: `test('description', function() { ... })` +- DB reset: `use RefreshDatabase;` in class-based, or implicit in Pest +- Inertia assertions: `$response->assertInertia(fn ($page) => $page->component('...')->has('...'))` +- Mocked API: injectable closures in ChurchToolsService constructor +- Storage: `Storage::fake('public')` in beforeEach for file tests +- Carbon: `Carbon::setTestNow(...)` for deterministic dates + +## 2026-03-29 — ServiceAgendaItem DB schema + +### Created +- Migration: `2026_03_29_100001_create_service_agenda_items_table.php` — contains all EventAgendaItem fields from CTS +- Migration: `2026_03_29_100002_add_service_agenda_item_id_to_slides_table.php` — links slides to agenda items +- Model: `ServiceAgendaItem` with `scopeVisible()`, relationships (service, serviceSong, slides) +- Factory: `ServiceAgendaItemFactory` with `withSong()` and `nonSong()` states +- Tests: `ServiceAgendaItemTest` — 14 tests covering schema, relationships, cascades, scopes + +### Key Fields +- `cts_agenda_item_id` (nullable, indexed): CTS API item ID for syncing +- `position` (string): CTS agenda item position (e.g. "1", "2", "1.1") +- `type` (string): CTS type value ("Song", "Default", "Header", etc.) +- `is_before_event` (boolean): filters with `scopeVisible()` scope +- `responsible` (json → array cast): responsible persons +- `service_song_id` (nullable FK): links agenda item to song (nullOnDelete) +- `sort_order` (unsignedInt): unique per service with `unique(['service_id', 'sort_order'])` + +### Relationships +- `service()` BelongsTo Service (cascadeOnDelete) +- `serviceSong()` BelongsTo ServiceSong (nullable, nullOnDelete) +- `slides()` HasMany Slide (new FK added to slides table) + +### Test Patterns +- Factory states: `->withSong(song)` for song items, `->nonSong()` for non-song +- Scope: `ServiceAgendaItem::visible()` filters `is_before_event=false` +- Migrations now always use anonymous class `return new class extends Migration {}` +- Unique constraint throws `QueryException` when violated (tested) + +### Slide Model Update +- Added `service_agenda_item_id` to fillable +- Added `serviceAgendaItem()` BelongsTo relationship (nullOnDelete) + +## 2026-03-29 — AgendaMatcherService + +### Implementation +- Created `AgendaMatcherService` with 4 public methods for glob-style wildcard matching +- Uses PHP's native `fnmatch($pattern, $title, FNM_CASEFOLD)` for * wildcard support and case-insensitive matching +- Key methods: + - `matches(string $itemTitle, string $pattern): bool` — single pattern match + - `matchesAny(string $itemTitle, array $patterns): bool` — match against array + - `findFirstMatch(array $agendaItems, string $patterns): ?ServiceAgendaItem` — comma-separated patterns, returns first match or null + - `filterBetween(array $items, ?string $startPattern, ?string $endPattern): array` — filters items between boundaries (boundaries excluded) + +### Test Coverage +- 17 tests in Pest function-style covering all methods +- Patterns tested: exact match, suffix wildcard, prefix wildcard, both sides, case-insensitive, no-match +- `filterBetween` edge cases: both boundaries, start-only, neither, no-matching-start, empty arrays +- Pattern parsing: comma-separated patterns with whitespace trimming +- All tests pass; Pint clean + +### Code Patterns +- Service class structure matches `SongMatchingService` (constructor, public methods, no state) +- Public method documentation via docstrings (necessary for API clarity) +- Test organization via section headers (necessary for readability) +- No regex used — just `fnmatch()` for simplicity and correctness diff --git a/.sisyphus/notepads/edit-page-restructure/problems.md b/.sisyphus/notepads/edit-page-restructure/problems.md new file mode 100644 index 0000000..e69de29 diff --git a/.sisyphus/plans/edit-page-restructure.md b/.sisyphus/plans/edit-page-restructure.md new file mode 100644 index 0000000..60b63c4 --- /dev/null +++ b/.sisyphus/plans/edit-page-restructure.md @@ -0,0 +1,1815 @@ +# Service Edit Page Restructure — Agenda-Based View + +## TL;DR + +> **Quick Summary**: Replace the 4-block accordion service edit page (Information, Moderation, Sermon, Songs) with a full CTS agenda view showing all agenda items in order, with inline slide uploads, song arrangement management, and agenda-ordered presentation export. +> +> **Deliverables**: +> - New `service_agenda_items` DB table storing ALL CTS agenda items (not just songs) +> - Restructured edit page showing full agenda with slide upload per item +> - Redesigned arrangement dialog (two-column: pill list + lyric preview) +> - Three new global settings (announcement position, sermon recognition, agenda boundaries) +> - Agenda-ordered .proplaylist export +> - Updated service list status columns +> - PHP Feature tests + Playwright E2E tests for all changes +> +> **Estimated Effort**: Large +> **Parallel Execution**: YES — 5 waves +> **Critical Path**: Task 1 → Task 4 → Task 7 → Task 12 → Task 16 → Task 18 → F1-F4 + +--- + +## Context + +### Original Request +Restructure the service edit page to show the complete CTS agenda instead of 4 separate blocks. Keep announcements at top; replace moderation/sermon/songs blocks with a unified agenda view. Every agenda element gets slide upload capability. Songs show arrangements inline. New arrangement dialog with two-column layout. Export follows agenda order. + +### Interview Summary +**Key Discussions**: +- **Titel-Konfiguration**: Global in App-Settings (two separate fields for start/end with * wildcard) +- **Slide-Zuordnung**: Service-specific (bound to service + agenda item) +- **Announcement-Position**: Embedded in agenda at matched position during export +- **Nicht-Song Items**: Skipped in export if no slides or songs +- **Moderation**: Now a normal agenda item with "+" slide upload +- **Predigt**: Recognized via namesmatching setting (like announcements), with special upload area +- **Export-Format**: .proplaylist stays, but order follows agenda +- **Service-Liste**: Adapt status — songs + predigt stay, add count of additional items with slides +- **Arrangement '+' Button**: Quick-select showing ALL song groups (including already used ones) +- **2x Button**: Duplicates element directly below +- **Arrangement Preview**: Shows actual lyric text, translation in gray/italic/small below +- **Tests**: PHP Feature + Playwright E2E tests for all changes (TDD) + +### Metis Review +**Identified Gaps** (addressed): +- **CTS API type values**: The actual `EventAgendaItem.type` values need real-world verification. Plan includes a diagnostic task to dump actual type values from the user's CTS instance before implementing filtering logic. +- **Data migration**: Existing finalized services must remain downloadable. Plan keeps backward compatibility — old export path preserved for services finalized before migration. +- **Slide type enum**: Instead of expanding the `slides.type` enum, plan uses a new `service_agenda_item_id` FK on slides table. +- **isBeforeEvent items**: Items with `isBeforeEvent=true` will be excluded from the visible agenda display. +- **Re-sync behavior**: If agenda items change in CTS, existing uploaded slides are preserved (orphaned but accessible). +- **Agenda item with song AND slides**: Song .pro file takes precedence; uploaded slides are appended as additional slides within the same presentation item. + +--- + +## Work Objectives + +### Core Objective +Replace the rigid 4-block service edit page with a dynamic agenda-based view that mirrors the actual CTS service plan, enabling slide uploads on any agenda item and exporting presentations in the correct agenda order. + +### Concrete Deliverables +- Migration: `service_agenda_items` table +- Migration: `service_agenda_item_id` FK on `slides` table +- Model: `ServiceAgendaItem` with relationships +- Service: `AgendaMatcherService` (wildcard namesmatching) +- Updated: `ChurchToolsService::syncServiceAgendaItems()` storing ALL items +- Updated: `SettingsController` + `Settings.vue` with 4 new setting fields +- Updated: `PlaylistExportService` for agenda-ordered export +- Updated: `ServiceController::edit()` passing agenda items +- Updated: `ServiceController::index()` with new status counts +- New Vue: `AgendaItemRow.vue`, `SongAgendaItem.vue`, `AnnouncementPositionIndicator.vue` +- Redesigned Vue: `ArrangementDialog.vue` (two-column with scroll-sync) +- Restructured Vue: `Edit.vue` (agenda list replacing accordion) +- Updated: `Service::finalizationStatus()` for new model +- Tests: PHP Feature tests for all backend changes +- Tests: Playwright E2E tests for all frontend changes + +### Definition of Done +- [ ] `php artisan test` — all tests pass (existing + new) +- [ ] `npx playwright test` — all E2E tests pass +- [ ] `./vendor/bin/pint --test` — no formatting issues +- [ ] Service edit page shows full CTS agenda +- [ ] Songs show arrangement pills inline +- [ ] Arrangement dialog has two-column layout with lyric preview +- [ ] Export .proplaylist follows agenda order with announcements at matched position +- [ ] Settings page has 4 new fields for agenda configuration +- [ ] Service list shows updated status columns + +### Must Have +- Full CTS agenda display (all item types, not just songs) +- Slide upload on any agenda item via "+" button +- Song arrangement pills (Vers1, Chorus, etc.) shown inline +- Redesigned arrangement dialog with lyric preview +- Announcement slides embedded at matched agenda position in export +- Sermon recognition via namesmatching +- Configurable agenda start/end boundaries +- Backward compatibility for existing finalized services + +### Must NOT Have (Guardrails) +- MUST NOT change the CTS API integration fetcher pattern (injectable closures for testing) +- MUST NOT modify the propresenter-parser package +- MUST NOT touch SongDB page, auth/login, or file conversion logic +- MUST NOT allow drag-and-drop reordering of agenda items (order comes from CTS API, immutable) +- MUST NOT write to the CTS API (READ-ONLY constraint) +- MUST NOT remove existing `service_songs` data — migrate gracefully +- MUST NOT change Slide file storage pattern (public disk, filenames) +- MUST NOT add English strings to UI — all German Du-form +- MUST NOT use `as any`, `@ts-ignore`, or `@ts-expect-error` +- MUST NOT add "save" buttons — all actions persist immediately +- MUST NOT use Sie-form — always Du-form + +--- + +## Verification Strategy (MANDATORY) + +> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions. + +### Test Decision +- **Infrastructure exists**: YES +- **Automated tests**: TDD (tests first) +- **Framework**: Pest v4 / PHPUnit for PHP, Playwright for E2E +- **TDD**: Each task follows RED (failing test) → GREEN (minimal impl) → REFACTOR + +### QA Policy +Every task MUST include agent-executed QA scenarios. +Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`. + +- **Frontend/UI**: Use Playwright — Navigate, interact, assert DOM, screenshot +- **Backend/API**: Use Bash (curl/artisan) — Run commands, assert output +- **DB**: Use `php artisan test` — Assert migrations, model relationships +- **Export**: Use PHP tests — Assert file content and order + +--- + +## Execution Strategy + +### Parallel Execution Waves + +``` +Wave 1 (Foundation — no UI changes, all independent): +├── Task 1: service_agenda_items migration + model + factory [quick] +├── Task 2: Slides table FK migration (service_agenda_item_id) [quick] +├── Task 3: AgendaMatcherService (wildcard namesmatching logic) [quick] +├── Task 4: Settings keys + SettingsController extension [quick] +├── Task 5: CTS API type discovery diagnostic [quick] + +Wave 2 (Backend services — depends on Wave 1): +├── Task 6: ChurchToolsService agenda sync (all items, not just songs) [deep] (depends: 1) +├── Task 7: ServiceController.edit() — pass agenda items to frontend [unspecified-high] (depends: 1, 2, 4) +├── Task 8: Service.finalizationStatus() update [quick] (depends: 1) +├── Task 9: PlaylistExportService — agenda-ordered export [deep] (depends: 1, 2, 3) + +Wave 3 (Frontend components — depends on Wave 2): +├── Task 10: ArrangementDialog.vue redesign (two-column + lyric preview) [visual-engineering] (depends: 7) +├── Task 11: AgendaItemRow.vue + SongAgendaItem.vue components [visual-engineering] (depends: 7) +├── Task 12: Edit.vue restructure (agenda list + announcements section) [visual-engineering] (depends: 10, 11) +├── Task 13: Slide upload on agenda items (+ button + drag-and-drop) [visual-engineering] (depends: 2, 11) + +Wave 4 (Settings UI + Service List + Export wiring): +├── Task 14: Settings.vue — add agenda configuration fields [quick] (depends: 4) +├── Task 15: ServiceController.index() + Index.vue — updated status columns [unspecified-high] (depends: 1, 8) +├── Task 16: Wire agenda-ordered export into download flow [unspecified-high] (depends: 9, 12) + +Wave 5 (Tests + Cleanup): +├── Task 17: Update existing PHP tests for new model [unspecified-high] (depends: 6, 7, 8, 9) +├── Task 18: Playwright E2E tests for restructured edit page [unspecified-high] (depends: 12, 13, 14, 16) +├── Task 19: Remove deprecated components (ModerationBlock, SermonBlock) [quick] (depends: 17, 18) + +Wave FINAL (After ALL tasks — 4 parallel reviews, then user okay): +├── Task F1: Plan compliance audit (oracle) +├── Task F2: Code quality review (unspecified-high) +├── Task F3: Real manual QA (unspecified-high) +└── Task F4: Scope fidelity check (deep) +-> Present results -> Get explicit user okay + +Critical Path: Task 1 → Task 6 → Task 7 → Task 12 → Task 16 → Task 18 → F1-F4 → user okay +Parallel Speedup: ~65% faster than sequential +Max Concurrent: 5 (Wave 1) +``` + +### Dependency Matrix + +| Task | Depends On | Blocks | +|------|-----------|--------| +| 1 | — | 6, 7, 8, 9, 15 | +| 2 | — | 7, 9, 13 | +| 3 | — | 9 | +| 4 | — | 7, 14 | +| 5 | — | — | +| 6 | 1 | 17 | +| 7 | 1, 2, 4 | 10, 11, 12 | +| 8 | 1 | 15, 17 | +| 9 | 1, 2, 3 | 16, 17 | +| 10 | 7 | 12 | +| 11 | 7 | 12, 13 | +| 12 | 10, 11 | 16, 18 | +| 13 | 2, 11 | 18 | +| 14 | 4 | 18 | +| 15 | 1, 8 | — | +| 16 | 9, 12 | 18 | +| 17 | 6, 7, 8, 9 | 19 | +| 18 | 12, 13, 14, 16 | 19 | +| 19 | 17, 18 | — | + +### Agent Dispatch Summary + +- **Wave 1**: **5** — T1→`quick`, T2→`quick`, T3→`quick`, T4→`quick`, T5→`quick` +- **Wave 2**: **4** — T6→`deep`, T7→`unspecified-high`, T8→`quick`, T9→`deep` +- **Wave 3**: **4** — T10→`visual-engineering`, T11→`visual-engineering`, T12→`visual-engineering`, T13→`visual-engineering` +- **Wave 4**: **3** — T14→`quick`, T15→`unspecified-high`, T16→`unspecified-high` +- **Wave 5**: **3** — T17→`unspecified-high`, T18→`unspecified-high`, T19→`quick` +- **FINAL**: **4** — F1→`oracle`, F2→`unspecified-high`, F3→`unspecified-high`, F4→`deep` + +--- + +## TODOs + +- [x] 1. Create `service_agenda_items` Migration, Model + Factory + + **What to do**: + - Create migration `create_service_agenda_items_table` with columns: + - `id` (bigIncrements) + - `service_id` (FK to services, cascadeOnDelete) + - `cts_agenda_item_id` (string, nullable) — CTS API item ID + - `position` (string) — CTS position value (e.g. "1", "2", "1.1") + - `title` (string) — item title from CTS + - `type` (string) — CTS type value (e.g. "Song", "Default", "Header" — actual values TBD from Task 5) + - `note` (text, nullable) — item note + - `duration` (string, nullable) — planned duration + - `start` (string, nullable) — planned start time + - `is_before_event` (boolean, default false) + - `responsible` (json, nullable) + - `service_song_id` (FK to service_songs, nullable, nullOnDelete) — links to existing service_song if this is a song item + - `sort_order` (unsignedInteger) — display order (derived from position) + - `created_at`, `updated_at` + - Unique index: `[service_id, sort_order]` + - Create `ServiceAgendaItem` model with: + - `$fillable` array with all columns + - Relationships: `belongsTo` service, `belongsTo` serviceSong (nullable), `hasMany` slides + - `casts()` method: `responsible` → array, `is_before_event` → boolean + - Scope: `scopeVisible($query)` — filters `is_before_event = false` + - Scope: `scopeBetweenTitles($query, $startTitle, $endTitle)` — filters items between matching header items using AgendaMatcherService + - Create factory `ServiceAgendaItemFactory` with reasonable defaults + - Write Pest tests: + - Test migration creates table with all columns + - Test model relationships (service, serviceSong, slides) + - Test factory creates valid records + - Test scopeVisible excludes is_before_event=true items + - Test unique constraint on [service_id, sort_order] + + **Must NOT do**: + - Do NOT modify `service_songs` table + - Do NOT add any UI code + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 2, 3, 4, 5) + - **Blocks**: Tasks 6, 7, 8, 9, 15 + - **Blocked By**: None + + **References**: + **Pattern References**: + - `database/migrations/2026_03_01_100700_create_service_songs_table.php` — Migration pattern with FK constraints and unique indices + - `app/Models/ServiceSong.php` — Model pattern with fillable, casts, relationships + - `app/Models/Slide.php` — SoftDeletes and nullable FK pattern + - `database/factories/` — Factory pattern for existing models + + **API/Type References**: + - `vendor/5pm-hdh/churchtools-api/src/Models/Events/Event/EventAgendaItem.php` — CTS API item fields: position, title, type, note, duration, start, isBeforeEvent, responsible, song + + **WHY Each Reference Matters**: + - `service_songs migration` — Follow exact FK constraint style and anonymous class migration pattern + - `ServiceSong model` — Copy the promoted constructor, fillable, casts, and relationship patterns + - `EventAgendaItem.php` — Source of truth for all fields the model must store + + **Acceptance Criteria**: + - [ ] Migration runs: `php artisan migrate` → no errors + - [ ] `php artisan test --filter=ServiceAgendaItemTest` → all tests pass + + **QA Scenarios**: + ``` + Scenario: Migration creates table correctly + Tool: Bash (php artisan) + Preconditions: Fresh database + Steps: + 1. Run `php artisan migrate:fresh` + 2. Run `php artisan test --filter=test_service_agenda_items_table_has_all_columns` + Expected Result: Test passes, asserting all columns exist + Evidence: .sisyphus/evidence/task-1-migration.txt + + Scenario: Model relationships work + Tool: Bash (php artisan test) + Preconditions: Migrated database with factories + Steps: + 1. Run `php artisan test --filter=test_service_agenda_item_belongs_to_service` + 2. Run `php artisan test --filter=test_service_agenda_item_belongs_to_service_song` + Expected Result: Both tests pass + Evidence: .sisyphus/evidence/task-1-relationships.txt + + Scenario: Unique constraint prevents duplicate sort_order per service + Tool: Bash (php artisan test) + Preconditions: Migrated database + Steps: + 1. Run `php artisan test --filter=test_unique_sort_order_per_service` + Expected Result: Test verifies that inserting duplicate [service_id, sort_order] throws exception + Evidence: .sisyphus/evidence/task-1-unique-constraint.txt + ``` + + **Commit**: YES + - Message: `feat(db): add service_agenda_items table and model` + - Files: `database/migrations/*_create_service_agenda_items_table.php`, `app/Models/ServiceAgendaItem.php`, `database/factories/ServiceAgendaItemFactory.php`, `tests/Feature/ServiceAgendaItemTest.php` + - Pre-commit: `php artisan test` + +--- + +- [x] 2. Add `service_agenda_item_id` FK to Slides Table + + **What to do**: + - Create migration `add_service_agenda_item_id_to_slides_table`: + - Add nullable `service_agenda_item_id` FK column (nullOnDelete) + - Add index on `service_agenda_item_id` + - Update `Slide` model: + - Add `service_agenda_item_id` to `$fillable` + - Add `agendaItem()` belongsTo relationship → `ServiceAgendaItem` + - Update `ServiceAgendaItem` model (from Task 1): + - Verify `slides()` hasMany relationship exists + - Write Pest tests: + - Test migration adds column + - Test Slide.agendaItem() relationship returns correct ServiceAgendaItem + - Test slides can be created with service_agenda_item_id + - Test nullOnDelete: when agenda item is deleted, slide.service_agenda_item_id becomes null + + **Must NOT do**: + - Do NOT change existing slide type enum + - Do NOT modify existing slide upload logic + - Do NOT remove existing service_id FK + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 3, 4, 5) + - **Blocks**: Tasks 7, 9, 13 + - **Blocked By**: None (Task 1 is parallel but this migration can run independently) + + **References**: + **Pattern References**: + - `database/migrations/2026_03_01_100800_create_slides_table.php` — Existing slides migration pattern + - `app/Models/Slide.php` — Current Slide model with fillable, relationships, SoftDeletes + - `database/migrations/2026_03_02_130249_add_cts_song_id_to_songs_and_service_songs_tables.php` — Pattern for adding columns to existing tables + + **WHY Each Reference Matters**: + - `slides migration` — See current column structure to ensure new FK is compatible + - `Slide model` — Must add to existing $fillable array and add new relationship method + - `add_cts_song_id migration` — Follow same pattern for "add column to existing table" migration + + **Acceptance Criteria**: + - [ ] Migration runs: `php artisan migrate` → no errors + - [ ] `php artisan test --filter=SlideAgendaItemRelationTest` → passes + + **QA Scenarios**: + ``` + Scenario: Slide links to agenda item correctly + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_slide_belongs_to_agenda_item` + Expected Result: Test creates a Slide with service_agenda_item_id, asserts ->agendaItem returns correct model + Evidence: .sisyphus/evidence/task-2-slide-relationship.txt + + Scenario: Null on delete works + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_slide_agenda_item_id_nulled_on_delete` + Expected Result: Deleting agenda item sets slide.service_agenda_item_id to null (not cascade delete) + Evidence: .sisyphus/evidence/task-2-null-on-delete.txt + ``` + + **Commit**: YES + - Message: `feat(db): add service_agenda_item_id FK to slides` + - Files: `database/migrations/*_add_service_agenda_item_id_to_slides_table.php`, `app/Models/Slide.php`, test file + - Pre-commit: `php artisan test` + +--- + +- [ ] 3. Create AgendaMatcherService (Wildcard Namesmatching) + + **What to do**: + - Create `app/Services/AgendaMatcherService.php` with: + - `matches(string $itemTitle, string $pattern): bool` — glob-style wildcard matching (`*` matches any chars, case-insensitive) + - `matchesAny(string $itemTitle, array $patterns): bool` — returns true if any pattern matches + - `findFirstMatch(array $agendaItems, array $patterns): ?ServiceAgendaItem` — returns first item whose title matches any pattern + - `filterBetween(array $items, ?string $startPattern, ?string $endPattern): array` — filters items between first match of start and end patterns + - Write comprehensive unit tests: + - Exact match: `"Predigt"` matches `"Predigt"` + - Wildcard suffix: `"Predigt*"` matches `"Predigt – Thema XY"` + - Wildcard prefix: `"*Predigt"` matches `"Die Predigt"` + - Wildcard both: `"*Predigt*"` matches `"Die Predigt heute"` + - Case insensitive: `"predigt*"` matches `"Predigt – Thema"` + - No match: `"Predigt*"` does NOT match `"Lobpreis"` + - Multiple patterns: `["Predigt*", "Sermon*"]` matches `"Predigt – Thema"` + - findFirstMatch: returns first matching item from array + - filterBetween with start only, end only, both, neither + - filterBetween excludes the boundary items themselves + + **Must NOT do**: + - Do NOT use regex — use simple glob-style `*` wildcard only + - Do NOT add any UI or controller code + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 2, 4, 5) + - **Blocks**: Task 9 + - **Blocked By**: None + + **References**: + **Pattern References**: + - `app/Services/SongMatchingService.php` — Existing service class pattern (constructor injection, single responsibility) + - `app/Services/ChurchToolsService.php:454-462` — normalizeCcli() for input sanitization pattern + + **WHY Each Reference Matters**: + - `SongMatchingService` — Follow same class structure (namespace, constructor, public methods) + - `normalizeCcli` — Same trim/normalize approach for pattern input sanitization + + **Acceptance Criteria**: + - [ ] `php artisan test --filter=AgendaMatcherServiceTest` → all tests pass (10+ test cases) + + **QA Scenarios**: + ``` + Scenario: Wildcard matching works correctly + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=AgendaMatcherServiceTest` + Expected Result: All wildcard matching test cases pass (exact, prefix*, *suffix, *both*, case-insensitive) + Evidence: .sisyphus/evidence/task-3-matching.txt + + Scenario: filterBetween returns correct subset + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_filter_between_start_and_end` + Expected Result: Given items [Header:Start, Item1, Item2, Header:End, Item3], returns [Item1, Item2] + Evidence: .sisyphus/evidence/task-3-filter-between.txt + + Scenario: Empty patterns return all items + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_filter_between_no_patterns` + Expected Result: When both start and end patterns are null/empty, returns all non-header items + Evidence: .sisyphus/evidence/task-3-no-filter.txt + ``` + + **Commit**: YES + - Message: `feat(service): add AgendaMatcherService with wildcard namesmatching` + - Files: `app/Services/AgendaMatcherService.php`, `tests/Feature/AgendaMatcherServiceTest.php` + - Pre-commit: `php artisan test` + +--- + +- [ ] 4. Add Agenda Settings Keys to SettingsController + + **What to do**: + - Add 4 new setting keys to `SettingsController::MACRO_KEYS` (rename to `SETTING_KEYS`): + - `agenda_announcement_position` — comma-separated patterns for announcement position matching (e.g. `"Information*,Hinweis*"`) + - `agenda_sermon_matching` — comma-separated patterns for sermon recognition (e.g. `"Predigt*,Sermon*"`) + - `agenda_start_title` — single pattern for agenda start boundary (e.g. `"Ablauf*"`) + - `agenda_end_title` — single pattern for agenda end boundary (e.g. `"Ende*"`) + - Update validation in `update()` to accept the new keys + - Update `index()` to pass all settings including new ones + - Write Pest tests: + - Test settings page returns new keys + - Test updating each new key + - Test validation rejects unknown keys + - Test comma-separated values stored correctly + + **Must NOT do**: + - Do NOT change the Settings.vue yet (Task 14) + - Do NOT change the Setting model + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 5) + - **Blocks**: Tasks 7, 14 + - **Blocked By**: None + + **References**: + **Pattern References**: + - `app/Http/Controllers/SettingsController.php:13-18` — Existing MACRO_KEYS constant array pattern + - `app/Http/Controllers/SettingsController.php:32-37` — Validation with Rule::in pattern + - `app/Models/Setting.php` — Setting::get() and Setting::set() API + + **WHY Each Reference Matters**: + - `SettingsController` — Add to existing constant array and follow same validation pattern + - `Setting model` — Understand get/set API for new keys + + **Acceptance Criteria**: + - [ ] `php artisan test --filter=SettingsControllerTest` → all tests pass including new key tests + + **QA Scenarios**: + ``` + Scenario: New setting keys are returned by index + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_settings_page_includes_agenda_keys` + Expected Result: Inertia page props include agenda_announcement_position, agenda_sermon_matching, agenda_start_title, agenda_end_title + Evidence: .sisyphus/evidence/task-4-settings-keys.txt + + Scenario: Updating agenda setting works + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_update_agenda_announcement_position` + Expected Result: PATCH request with key=agenda_announcement_position, value="Information*,Hinweis*" returns 200 + Evidence: .sisyphus/evidence/task-4-update-setting.txt + ``` + + **Commit**: YES + - Message: `feat(settings): add agenda configuration keys` + - Files: `app/Http/Controllers/SettingsController.php`, test file + - Pre-commit: `php artisan test` + +--- + +- [ ] 5. CTS Agenda Type Discovery Command + + **What to do**: + - Create artisan command `app/Console/Commands/DiscoverAgendaTypes.php`: + - Command: `cts:discover-agenda-types` + - Fetches agenda for the next upcoming service via `ChurchToolsService::syncAgenda()` + - Iterates `agenda.getItems()` and prints a table: + - Position | Title | Type | Has Song | Duration | IsBeforeEvent + - This is a diagnostic tool to discover actual CTS `EventAgendaItem.type` values + - Write basic test: + - Test command runs with mocked API data + - Test output contains expected columns + + **Must NOT do**: + - Do NOT use this to modify any data + - Do NOT make this part of the sync flow + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 4) + - **Blocks**: None (informational only) + - **Blocked By**: None + + **References**: + **Pattern References**: + - `app/Services/ChurchToolsService.php:145-159` — syncAgenda method with injectable fetcher + - `vendor/5pm-hdh/churchtools-api/src/Models/Events/Event/EventAgendaItem.php` — Item getters: getPosition(), getTitle(), getType(), getSong(), getIsBeforeEvent() + + **WHY Each Reference Matters**: + - `ChurchToolsService` — Reuse syncAgenda() method for fetching + - `EventAgendaItem` — Know which getter methods to call for output + + **Acceptance Criteria**: + - [ ] `php artisan cts:discover-agenda-types` runs without error (with valid API config) + - [ ] `php artisan test --filter=DiscoverAgendaTypesTest` → passes with mocked data + + **QA Scenarios**: + ``` + Scenario: Command outputs agenda items + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_discover_command_outputs_table` + Expected Result: Test verifies command outputs table with Position, Title, Type columns using mocked agenda data + Evidence: .sisyphus/evidence/task-5-discover-command.txt + ``` + + **Commit**: YES + - Message: `chore(debug): add CTS agenda type discovery command` + - Files: `app/Console/Commands/DiscoverAgendaTypes.php`, test file + - Pre-commit: `php artisan test` + +--- + +- [ ] 6. Extend ChurchToolsService to Sync ALL Agenda Items + + **What to do**: + - Rename `syncServiceAgendaSongs()` to `syncServiceAgendaItems()` (keep old method as deprecated wrapper for backward compat) + - New `syncServiceAgendaItems()`: + - Call `$agenda->getItems()` instead of `$agenda->getSongs()` + - For EACH `EventAgendaItem`: + - Upsert into `service_agenda_items` table using `[service_id, sort_order]` unique key + - Store all fields: cts_agenda_item_id, position, title, type, note, duration, start, is_before_event, responsible + - If item has a `getSong()` that is not null: + - Continue the existing `service_songs` upsert logic (create/update service_song row) + - Link: set `service_agenda_item.service_song_id` → the service_song ID + - Auto-match via `SongMatchingService` (existing behavior) + - Sort order: derive from item position or array index + - Remove orphaned agenda items: delete `service_agenda_items` where `service_id` matches but `sort_order` not in current sync batch + - Preserve slides: when removing orphaned agenda items, set their slides' `service_agenda_item_id` to null (don't delete slides) + - Update sync summary to include `agenda_items_count` + - Write comprehensive tests: + - Test all agenda items (songs + non-songs) are stored + - Test song items create both service_song AND service_agenda_item records with correct link + - Test non-song items don't create service_song records + - Test re-sync updates existing items + - Test orphaned items are cleaned up + - Test slides are preserved when orphaned items are removed + + **Must NOT do**: + - Do NOT change the `syncAgenda()` API call method + - Do NOT remove existing `service_songs` table or data + - Do NOT change the injectable closure pattern + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 7, 8, 9) + - **Blocks**: Task 17 + - **Blocked By**: Task 1 + + **References**: + **Pattern References**: + - `app/Services/ChurchToolsService.php:324-385` — Current `syncServiceAgendaSongs()` — the code being extended + - `app/Services/ChurchToolsService.php:145-159` — `syncAgenda()` returns agenda object + - `app/Services/SongMatchingService.php` — `autoMatch()` method used for song matching + - `vendor/5pm-hdh/churchtools-api/src/Models/Events/Event/EventAgenda.php:43-52` — `getSongs()` vs `getItems()` difference + - `vendor/5pm-hdh/churchtools-api/src/Models/Events/Event/EventAgendaItem.php` — Full item API + + **Test References**: + - `tests/Feature/ChurchToolsSyncTest.php` — Existing sync tests with mocked API data and fake event objects + + **WHY Each Reference Matters**: + - `syncServiceAgendaSongs` — This IS the method being refactored; understand every line + - `EventAgenda.getSongs()` — Shows current filtering; need to switch to `getItems()` + - `EventAgendaItem` — API surface for extracting all fields + - `ChurchToolsSyncTest` — Follow same mocking pattern for new tests + + **Acceptance Criteria**: + - [ ] `php artisan test --filter=ChurchToolsSyncTest` → all existing tests still pass + - [ ] `php artisan test --filter=test_sync_stores_all_agenda_item_types` → passes + - [ ] `php artisan test --filter=test_sync_links_song_items_to_service_songs` → passes + + **QA Scenarios**: + ``` + Scenario: Sync stores all agenda item types + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_sync_stores_all_agenda_item_types` + Expected Result: Given mock agenda with 8 items (3 songs, 2 headers, 3 default), all 8 appear in service_agenda_items table + Evidence: .sisyphus/evidence/task-6-all-types.txt + + Scenario: Song items link to service_songs + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_sync_links_song_items_to_service_songs` + Expected Result: Song agenda items have non-null service_song_id pointing to correct service_songs row + Evidence: .sisyphus/evidence/task-6-song-links.txt + + Scenario: Orphaned agenda items cleaned up on re-sync + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_resync_removes_orphaned_agenda_items` + Expected Result: After re-sync with fewer items, old items are removed but their slides retain null FK + Evidence: .sisyphus/evidence/task-6-orphan-cleanup.txt + ``` + + **Commit**: YES + - Message: `feat(sync): sync all CTS agenda items (not just songs)` + - Files: `app/Services/ChurchToolsService.php`, `tests/Feature/ChurchToolsSyncTest.php` + - Pre-commit: `php artisan test` + +--- + +- [ ] 7. Update ServiceController.edit() to Pass Agenda Items + + **What to do**: + - Update `ServiceController::edit()` to: + - Load `service.agendaItems` (ordered by sort_order) with eager-loaded relationships: + - `agendaItems.serviceSong.song.groups.slides` (for song items) + - `agendaItems.serviceSong.song.arrangements.arrangementGroups.group` + - `agendaItems.serviceSong.arrangement.arrangementGroups.group` (selected arrangement) + - `agendaItems.slides` (uploaded slides per agenda item) + - Load settings: `agenda_announcement_position`, `agenda_sermon_matching`, `agenda_start_title`, `agenda_end_title` + - Pass to Inertia: `agendaItems`, `agendaSettings`, plus existing `informationSlides`, `songsCatalog` + - Keep existing `serviceSongs` prop for backward compatibility during transition + - Apply `filterBetween` using `AgendaMatcherService` on the agenda items based on start/end title settings + - Mark items: add computed properties `is_announcement_position` (matches announcement pattern), `is_sermon` (matches sermon pattern) to each item + - Write tests: + - Test edit page returns agendaItems prop + - Test agendaItems are filtered by start/end titles + - Test is_announcement_position and is_sermon flags are set correctly + - Test settings are passed to frontend + + **Must NOT do**: + - Do NOT remove existing props yet (backward compat) + - Do NOT change the route definition + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 6, 8, 9) + - **Blocks**: Tasks 10, 11, 12 + - **Blocked By**: Tasks 1, 2, 4 + + **References**: + **Pattern References**: + - `app/Http/Controllers/ServiceController.php:88-224` — Current edit() method with Eloquent eager loading and Inertia::render + - `app/Models/Setting.php:15-20` — Setting::get() for retrieving settings + - `app/Services/AgendaMatcherService.php` — filterBetween() and findFirstMatch() (from Task 3) + + **Test References**: + - `tests/Feature/ServiceControllerTest.php` — Existing edit page tests with assertInertia + + **WHY Each Reference Matters**: + - `ServiceController::edit()` — This method is being modified; need to understand existing eager loading + - `Setting::get()` — How to retrieve the new settings for passing to frontend + - `AgendaMatcherService` — Used for filtering and marking items + + **Acceptance Criteria**: + - [ ] `php artisan test --filter=ServiceControllerTest` → all existing tests pass + - [ ] `php artisan test --filter=test_edit_returns_agenda_items` → passes + - [ ] `php artisan test --filter=test_edit_filters_agenda_by_title_settings` → passes + + **QA Scenarios**: + ``` + Scenario: Edit page returns agenda items + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_edit_returns_agenda_items` + Expected Result: Inertia page has 'agendaItems' prop with correct structure including type, title, serviceSong relation + Evidence: .sisyphus/evidence/task-7-agenda-items-prop.txt + + Scenario: Agenda items filtered by start/end title settings + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_edit_filters_agenda_by_title_settings` + Expected Result: With agenda_start_title="Ablauf*" and agenda_end_title="Ende*", only items between those headers are returned + Evidence: .sisyphus/evidence/task-7-filtered-items.txt + + Scenario: Sermon and announcement flags set correctly + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_edit_marks_sermon_and_announcement_items` + Expected Result: Items matching agenda_sermon_matching have is_sermon=true, items matching agenda_announcement_position have is_announcement_position=true + Evidence: .sisyphus/evidence/task-7-item-flags.txt + ``` + + **Commit**: YES + - Message: `feat(controller): pass agenda items to edit page` + - Files: `app/Http/Controllers/ServiceController.php`, test file + - Pre-commit: `php artisan test` + +--- + +- [ ] 8. Update Service.finalizationStatus() for Agenda Model + + **What to do**: + - Update `Service::finalizationStatus()` to check: + - All song-type agenda items have their service_song matched (song_id not null) + - All matched songs have an arrangement selected (song_arrangement_id not null) + - At least one agenda item flagged as sermon has slides uploaded (replaces "sermon slides exist" check) + - Announce if announcement position matching found a match (informational, not blocking) + - Keep the same return shape: `{ready: bool, warnings: string[]}` + - Warning messages in German Du-form + - Write tests: + - Test all songs matched → no warning + - Test unmatched song → warning "Song X wurde noch nicht zugeordnet" + - Test song without arrangement → warning "Song X hat kein Arrangement" + - Test no sermon slides → warning "Keine Predigt-Folien hochgeladen" + - Test all checks pass → ready=true + + **Must NOT do**: + - Do NOT remove existing checks — replace them to work with new model + - Do NOT change the finalization flow/endpoint + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 6, 7, 9) + - **Blocks**: Tasks 15, 17 + - **Blocked By**: Task 1 + + **References**: + **Pattern References**: + - `app/Models/Service.php` — Current finalizationStatus() method + - `tests/Feature/FinalizationTest.php` — Existing finalization tests + + **WHY Each Reference Matters**: + - `Service::finalizationStatus()` — The method being modified; understand current checks + - `FinalizationTest` — Update existing tests to use new model + + **Acceptance Criteria**: + - [ ] `php artisan test --filter=FinalizationTest` → all tests pass (updated for new model) + + **QA Scenarios**: + ``` + Scenario: Finalization warns about unmatched songs + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_finalization_warns_unmatched_songs` + Expected Result: Service with unmatched song agenda item returns warning + Evidence: .sisyphus/evidence/task-8-unmatched-warning.txt + + Scenario: All checks pass allows finalization + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_finalization_ready_when_all_checks_pass` + Expected Result: ready=true, warnings=[] + Evidence: .sisyphus/evidence/task-8-ready.txt + ``` + + **Commit**: YES + - Message: `refactor(model): update finalizationStatus for agenda model` + - Files: `app/Models/Service.php`, `tests/Feature/FinalizationTest.php` + - Pre-commit: `php artisan test` + +--- + +- [ ] 9. PlaylistExportService — Agenda-Ordered Export + + **What to do**: + - Refactor `PlaylistExportService::generatePlaylist()` to: + - Load service with `agendaItems` ordered by `sort_order` + - Build playlist in agenda order: + 1. For each agenda item in order: + - If item `is_announcement_position` → insert information slides as .pro presentation at this position + - If item has a matched song with arrangement → generate .pro via ProExportService + - If item has uploaded slides (via agendaItem.slides) → generate .pro presentation from those slides + - If item has both song AND slides → song .pro first, then slides .pro + - If item has nothing → skip + 2. If NO agenda item matched announcement position → insert information slides at the beginning (fallback) + - Keep the .proplaylist format (ProPlaylistGenerator) + - Handle backward compatibility: if service has no agenda items (old data), fall back to existing block-based export + - Write comprehensive tests: + - Test export follows agenda order + - Test announcements embedded at matched position + - Test announcements at beginning if no match + - Test items with slides are included + - Test items without slides or songs are skipped + - Test backward compat: old services use block-based export + - Test song + slides combination + + **Must NOT do**: + - Do NOT change the ProExportService (individual .pro generation) + - Do NOT change the propresenter-parser package + - Do NOT change the .proplaylist format + + **Recommended Agent Profile**: + - **Category**: `deep` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 6, 7, 8) + - **Blocks**: Tasks 16, 17 + - **Blocked By**: Tasks 1, 2, 3 + + **References**: + **Pattern References**: + - `app/Services/PlaylistExportService.php` — Current generatePlaylist() — the method being refactored + - `app/Services/ProExportService.php` — generateProFile() for individual song .pro files + - `app/Services/ProBundleExportService.php` — generateBundle() for slides-to-.pro conversion pattern + - `app/Services/AgendaMatcherService.php` — findFirstMatch() for announcement position detection + + **Test References**: + - `tests/Feature/PlaylistExportTest.php` — Existing export tests + + **WHY Each Reference Matters**: + - `PlaylistExportService` — The service being refactored; understand current item ordering and embedding + - `ProExportService` — Used unchanged for generating individual song .pro files + - `ProBundleExportService` — Pattern for converting uploaded slides into .pro presentation + - `AgendaMatcherService` — Used to find announcement insertion point + + **Acceptance Criteria**: + - [ ] `php artisan test --filter=PlaylistExportTest` → all tests pass + - [ ] `php artisan test --filter=test_export_follows_agenda_order` → passes + - [ ] `php artisan test --filter=test_export_embeds_announcements_at_matched_position` → passes + + **QA Scenarios**: + ``` + Scenario: Export follows agenda order + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_export_follows_agenda_order` + Expected Result: Given agenda [Begrüßung, Song1, Information, Predigt, Song2], export order is [Song1.pro, InfoSlides.pro, Predigt-Slides.pro, Song2.pro] + Evidence: .sisyphus/evidence/task-9-agenda-order.txt + + Scenario: Announcements at matched position + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_export_embeds_announcements_at_matched_position` + Expected Result: With announcement setting "Information*", announcements .pro is embedded right at the "Informationen" agenda item position + Evidence: .sisyphus/evidence/task-9-announcement-position.txt + + Scenario: Backward compat for old services + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_export_falls_back_to_block_order` + Expected Result: Service without agenda items exports in old block order (info→songs→moderation→sermon) + Evidence: .sisyphus/evidence/task-9-backward-compat.txt + ``` + + **Commit**: YES + - Message: `feat(export): agenda-ordered playlist export` + - Files: `app/Services/PlaylistExportService.php`, `tests/Feature/PlaylistExportTest.php` + - Pre-commit: `php artisan test` + +- [ ] 10. Redesign ArrangementDialog — Two-Column with Lyric Preview + + **What to do**: + - Create new `resources/js/Components/ArrangementDialog.vue` (replaces `ArrangementConfigurator.vue` in the arrangement popup context): + - **Top section**: Arrangement selection dropdown + "Neu" (create) + "Duplizieren" (clone) buttons + - **Two-column layout**: + - **Left column** (pills): Vertical list of arrangement group names as colored pills + - Drag-and-drop reorderable (VueDraggable) + - Each pill shows group name + group color as background/border + - Right side of each pill: "2x" button (duplicate this group element directly below) + "+" button (quick-select popup showing ALL available song groups to insert below) + - "+" button shows a dropdown/popup with all song groups (Vers1, Vers2, Chorus, Bridge, etc.) — clicking one inserts it below the current pill + - **Right column** (lyric preview): Same elements as left, scroll-synced + - Each element shows: + - Colored left border matching group color + - Small pill-style title (e.g. "Vers 1") inside the top-left of the element + - Actual lyric text from `SongSlide.text_content` — one line per slide + - If song has translation: `SongSlide.text_content_translated` shown below each original line in gray, italic, smaller font + - If a group has multiple slides: show all slides together with a thin gray line separator between them + - Scroll position synced with left column (when user scrolls left, right scrolls to matching position and vice versa) + - **Delete arrangement**: Button to delete selected arrangement (prevent deleting last one) + - API calls (reuse existing endpoints): + - POST `/songs/{songId}/arrangements` — create new + - POST `/arrangements/{arrangementId}/clone` — clone + - PUT `/arrangements/{arrangementId}` — update groups order (on drag-end, on 2x, on + add) + - DELETE `/arrangements/{arrangementId}` — delete + - Emits: `arrangement-selected(arrangementId)`, `close` + - Props: `songId`, `arrangements`, `selectedArrangementId`, `groups` (all song groups with slides) + - This is a modal/dialog component, opened by edit button on song agenda items + + **Must NOT do**: + - Do NOT delete `ArrangementConfigurator.vue` yet (used by SongEditModal — removed in Task 19) + - Do NOT change the API endpoints (reuse existing) + - Do NOT add English text — all German Du-form + + **Recommended Agent Profile**: + - **Category**: `visual-engineering` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 3 (with Tasks 11, 13) + - **Blocks**: Task 12 + - **Blocked By**: Task 7 + + **References**: + **Pattern References**: + - `resources/js/Components/ArrangementConfigurator.vue` — Current arrangement UI with VueDraggable, create/clone/delete buttons (270 lines). Reuse API call patterns and VueDraggable configuration. + - `resources/js/Components/SongEditModal.vue` — Modal dialog pattern with auto-save + - `resources/js/Components/Blocks/SongsBlock.vue:1-100` — How arrangements are currently displayed and selected + + **API/Type References**: + - `app/Models/SongGroup.php` — Group fields: name, color, order + - `app/Models/SongSlide.php` — Slide fields: text_content, text_content_translated, order + - `app/Models/SongArrangement.php` — Arrangement: name, is_default + - `app/Models/SongArrangementGroup.php` — ArrangementGroup: song_arrangement_id, song_group_id, order + + **External References**: + - VueDraggable: already used in project, see existing import pattern in ArrangementConfigurator.vue + + **WHY Each Reference Matters**: + - `ArrangementConfigurator.vue` — Core logic being redesigned; reuse API calls, VueDraggable config, error handling + - `SongGroup/SongSlide` — Data structure for rendering lyric preview and group colors + - `SongEditModal` — Follow same modal dialog UX patterns (overlay, close on escape, etc.) + + **Acceptance Criteria**: + - [ ] `npm run build` → no errors + - [ ] ArrangementDialog renders with two columns + - [ ] Drag-and-drop reordering saves via PUT endpoint + - [ ] "2x" duplicates element below + - [ ] "+" shows quick-select of all groups + + **QA Scenarios**: + ``` + Scenario: Two-column layout renders correctly + Tool: Playwright + Preconditions: Dev server running, authenticated, service with matched song that has groups/slides + Steps: + 1. Navigate to service edit page + 2. Find song agenda item, click edit button (data-testid="song-edit-arrangement") + 3. Assert dialog is visible + 4. Assert left column has pill elements (data-testid="arrangement-pill-list") + 5. Assert right column has lyric content (data-testid="arrangement-lyric-preview") + 6. Take screenshot + Expected Result: Dialog shows with two columns, pills on left, lyrics on right + Failure Indicators: Dialog not opening, missing columns, no content + Evidence: .sisyphus/evidence/task-10-two-column-layout.png + + Scenario: Drag-and-drop reorders elements + Tool: Playwright + Preconditions: Dialog open with 3+ arrangement elements + Steps: + 1. Note current order of pills + 2. Drag first pill to third position + 3. Wait for save indicator (auto-save) + 4. Assert pills are in new order + Expected Result: Pills reorder and save persists (PUT request sent) + Evidence: .sisyphus/evidence/task-10-drag-reorder.png + + Scenario: 2x button duplicates element + Tool: Playwright + Preconditions: Dialog open with arrangement elements + Steps: + 1. Count current pills (e.g. 4) + 2. Click "2x" button on second pill (data-testid="arrangement-duplicate-btn") + 3. Assert pill count is now 5 + 4. Assert new pill is same group name as the one duplicated + 5. Assert it appears directly after the duplicated pill + Expected Result: Duplicate appears below, same group name + Evidence: .sisyphus/evidence/task-10-duplicate.png + + Scenario: Lyric preview shows translation in gray/italic + Tool: Playwright + Preconditions: Song with has_translation=true and text_content_translated populated + Steps: + 1. Open arrangement dialog for translated song + 2. Assert original text is visible in normal font + 3. Assert translated text is visible with classes for gray, italic, small (check computed styles) + Expected Result: Translation text displayed below original in gray italic small font + Evidence: .sisyphus/evidence/task-10-translation-preview.png + ``` + + **Commit**: YES + - Message: `feat(ui): redesign ArrangementDialog with lyric preview` + - Files: `resources/js/Components/ArrangementDialog.vue` + - Pre-commit: `npm run build` + +--- + +- [ ] 11. Create AgendaItemRow and SongAgendaItem Components + + **What to do**: + - Create `resources/js/Components/AgendaItemRow.vue`: + - Generic agenda item display row with: + - Left: position number, title, optional note + - Center: slide thumbnails (if any uploaded) + - Right: "+" button for slide upload (opens drag-and-drop area below) + - When "+" clicked: toggles a `SlideUploader` area below the row (service-specific, type='agenda_item') + - Highlight behavior: + - Items with uploaded slides: success-style highlight (green border/bg) + - Items matching announcement position: special indicator/icon + - Items matching sermon pattern: sermon label/indicator + - Props: `agendaItem` (full object with slides, flags), `serviceId`, `serviceDate` + - Emits: `slides-updated` (triggers page reload) + - Create `resources/js/Components/SongAgendaItem.vue`: + - Extends/wraps AgendaItemRow with song-specific features: + - Song name from CTS + matched song name from DB + - If NOT matched: search dropdown to assign from catalog + "Erstellung anfragen" button (reuse from SongsBlock) + - If matched: + - Arrangement name shown as label + - Element pills (Vers1, Chorus, Bridge, etc.) from selected arrangement's groups — shown as small colored pills in a row + - Edit button → opens ArrangementDialog (Task 10) as modal + - Translation checkbox (if has_translation) + - Highlight: success (has arrangement), warning (no arrangement), neutral (not matched) + - Props: `agendaItem` (with serviceSong relation), `songsCatalog`, `serviceId` + - Emits: `arrangement-selected`, `slides-updated` + - Both components use `data-testid` attributes on all interactive elements + + **Must NOT do**: + - Do NOT implement the full upload logic (reuse existing `SlideUploader`) + - Do NOT change existing API endpoints + - Do NOT use Sie-form + + **Recommended Agent Profile**: + - **Category**: `visual-engineering` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 3 (with Tasks 10, 13) + - **Blocks**: Tasks 12, 13 + - **Blocked By**: Task 7 + + **References**: + **Pattern References**: + - `resources/js/Components/Blocks/SongsBlock.vue` — Song matching UI (search dropdown, request button, arrangement selector, translation checkbox). Reuse the matching/assign/request logic. + - `resources/js/Components/SlideUploader.vue` — Reusable uploader component (already built) + - `resources/js/Components/SlideGrid.vue` — Thumbnail display for uploaded slides + + **API/Type References**: + - `routes/api.php` — Service-song assign/request/update endpoints + - `app/Http/Controllers/ServiceSongController.php` — API for song matching operations + + **WHY Each Reference Matters**: + - `SongsBlock.vue` — Has the complete song matching UX; extract/reuse the match/assign/request logic + - `SlideUploader/SlideGrid` — Compose these into the agenda item row for slide management + - `ServiceSongController` — API endpoints to reuse for song operations + + **Acceptance Criteria**: + - [ ] `npm run build` → no errors + - [ ] AgendaItemRow renders with title, position, "+" button + - [ ] SongAgendaItem shows arrangement pills for matched songs + + **QA Scenarios**: + ``` + Scenario: Non-song agenda item shows with "+" upload button + Tool: Playwright + Preconditions: Service with non-song agenda items (e.g. "Gebet") + Steps: + 1. Navigate to service edit page + 2. Find non-song agenda item row (data-testid="agenda-item-row") + 3. Assert title "Gebet" is visible + 4. Assert "+" button exists (data-testid="agenda-item-add-slides") + Expected Result: Row displays with title and "+" button + Evidence: .sisyphus/evidence/task-11-agenda-item-row.png + + Scenario: Song shows arrangement pills + Tool: Playwright + Preconditions: Service with matched song that has arrangement with groups + Steps: + 1. Navigate to service edit page + 2. Find song agenda item (data-testid="song-agenda-item") + 3. Assert arrangement name label is visible + 4. Assert group pills (data-testid="arrangement-pill") exist with correct names (e.g. "Vers 1", "Chorus") + Expected Result: Song row shows arrangement name + colored pills for each group + Evidence: .sisyphus/evidence/task-11-song-pills.png + + Scenario: Unmatched song shows search dropdown + Tool: Playwright + Preconditions: Service with unmatched song (no song_id) + Steps: + 1. Navigate to service edit page + 2. Find unmatched song row + 3. Assert search dropdown (data-testid="song-search-select") is visible + 4. Assert "Erstellung anfragen" button (data-testid="request-song-btn") exists + Expected Result: Unmatched song shows assignment controls + Evidence: .sisyphus/evidence/task-11-unmatched-song.png + ``` + + **Commit**: YES + - Message: `feat(ui): add AgendaItemRow and SongAgendaItem components` + - Files: `resources/js/Components/AgendaItemRow.vue`, `resources/js/Components/SongAgendaItem.vue` + - Pre-commit: `npm run build` + +--- + +- [ ] 12. Restructure Edit.vue — Agenda View Replacing Accordion + + **What to do**: + - Major restructure of `resources/js/Pages/Services/Edit.vue`: + - **Top section** (keep): Announcements/Information block + - Keep the existing `InformationBlock` component at the top + - This is unchanged: global slides with expire dates, upload, delete, download bundle + - **Agenda section** (new): Replace the 3 block accordion (Moderation, Sermon, Songs) with: + - Header: "Ablauf" (Agenda) + - List of all agenda items in order (from `agendaItems` prop) + - For each item: + - If item is a header/title type: render as section divider (styled differently, no "+" button) + - If item is a song: render `SongAgendaItem` component + - If item is flagged as sermon (`is_sermon`): render `AgendaItemRow` with sermon-specific upload area and label + - If item is flagged as announcement position (`is_announcement_position`): render `AgendaItemRow` with announcement indicator + - Otherwise: render `AgendaItemRow` (generic with "+" button) + - Each item with uploaded slides or song arrangement: highlighted (success/green border) + - Song without arrangement: warning highlight (amber/orange border) + - **Footer** (keep, adapted): Finalization status + action buttons + - Keep Finalize, Reopen, Download buttons + - Update finalization status display for new model + - Remove: ModerationBlock, SermonBlock, SongsBlock imports and usage + - Add: imports for AgendaItemRow, SongAgendaItem, ArrangementDialog + + **Must NOT do**: + - Do NOT change InformationBlock (announcements stay as-is) + - Do NOT change finalization/reopen/download endpoints + - Do NOT add English strings + + **Recommended Agent Profile**: + - **Category**: `visual-engineering` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave 3 (after Tasks 10, 11) + - **Blocks**: Tasks 16, 18 + - **Blocked By**: Tasks 10, 11 + + **References**: + **Pattern References**: + - `resources/js/Pages/Services/Edit.vue` — Current 662-line edit page (the file being restructured) + - `resources/js/Components/Blocks/InformationBlock.vue` — Keep this component unchanged + - `resources/js/Layouts/AuthenticatedLayout.vue` — Layout pattern + + **WHY Each Reference Matters**: + - `Edit.vue` — Must understand current structure to preserve header, footer, navigation, and toast functionality while replacing the middle section + - `InformationBlock` — Must continue to work as-is in the new layout + + **Acceptance Criteria**: + - [ ] `npm run build` → no errors + - [ ] Edit page shows announcements section at top + - [ ] Edit page shows agenda items list below + - [ ] Songs show inline arrangement pills + - [ ] Finalize/Download buttons still work + + **QA Scenarios**: + ``` + Scenario: Edit page shows agenda instead of accordion + Tool: Playwright + Preconditions: Service with synced agenda items (mix of songs, default items, headers) + Steps: + 1. Navigate to /services/{id}/edit + 2. Assert InformationBlock (data-testid="information-block") is visible at top + 3. Assert agenda section (data-testid="agenda-section") is visible below + 4. Assert NO accordion blocks (data-testid="moderation-block") exist + 5. Assert agenda items are listed in order + 6. Take screenshot + Expected Result: Page shows announcements + agenda list, no accordion + Evidence: .sisyphus/evidence/task-12-edit-page-structure.png + + Scenario: Header items render as dividers + Tool: Playwright + Preconditions: Agenda with header-type items + Steps: + 1. Navigate to edit page + 2. Find header items (data-testid="agenda-header-item") + 3. Assert they have divider styling (larger text, different background) + 4. Assert they do NOT have a "+" upload button + Expected Result: Headers are visual dividers without interaction + Evidence: .sisyphus/evidence/task-12-header-dividers.png + + Scenario: Highlighted items show correct status + Tool: Playwright + Steps: + 1. Navigate to edit page with: 1 song with arrangement, 1 song without arrangement, 1 item with slides, 1 item without slides + 2. Assert song with arrangement has success highlight (data-testid contains class with green/emerald) + 3. Assert song without arrangement has warning highlight (amber/orange) + 4. Assert item with slides has success highlight + 5. Assert plain item has no highlight + Expected Result: Correct visual status for each item type + Evidence: .sisyphus/evidence/task-12-highlights.png + ``` + + **Commit**: YES + - Message: `feat(ui): restructure Edit.vue with agenda view` + - Files: `resources/js/Pages/Services/Edit.vue` + - Pre-commit: `npm run build` + +--- + +- [ ] 13. Slide Upload on Agenda Items — "+" Button + Drag-and-Drop + + **What to do**: + - Enhance `AgendaItemRow.vue` (from Task 11): + - When "+" button clicked: show/hide a `SlideUploader` component below the row + - SlideUploader configured with: `type='agenda_item'`, `serviceId`, and new prop `agendaItemId` + - After upload: reload page to show new slide thumbnails + - Update `SlideController::store()`: + - Accept optional `service_agenda_item_id` parameter + - When provided: set `service_agenda_item_id` on created Slide record + - Keep existing type parameter for backward compat, but allow new type value `'agenda_item'` + - Update `SlideUploader.vue`: + - Add optional `agendaItemId` prop + - When set: include `service_agenda_item_id` in upload FormData + - No expire-date picker for agenda item slides (they're service-specific) + - Show uploaded slides as small thumbnails inline in the agenda item row (reuse SlideGrid patterns) + - Update slide deletion to work for agenda-item slides + - Write tests: + - Test uploading slide with service_agenda_item_id + - Test slide appears linked to correct agenda item + - Test deletion works + + **Must NOT do**: + - Do NOT change file conversion logic (images, PPT processing) + - Do NOT add expire dates to agenda item slides + + **Recommended Agent Profile**: + - **Category**: `visual-engineering` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 3 (with Tasks 10, 11 — but after 11 creates base component) + - **Blocks**: Task 18 + - **Blocked By**: Tasks 2, 11 + + **References**: + **Pattern References**: + - `resources/js/Components/SlideUploader.vue` — Existing uploader component to extend + - `app/Http/Controllers/SlideController.php` — store() method handling uploads + - `resources/js/Components/SlideGrid.vue` — Thumbnail display pattern + + **Test References**: + - `tests/Feature/SlideControllerTest.php` — Existing slide upload tests + + **WHY Each Reference Matters**: + - `SlideUploader` — Adding agendaItemId prop, minimal change to existing component + - `SlideController::store()` — Add service_agenda_item_id to the validation and storage logic + - `SlideGrid` — Reuse for inline thumbnail display in agenda rows + + **Acceptance Criteria**: + - [ ] `npm run build` → no errors + - [ ] `php artisan test --filter=SlideControllerTest` → all tests pass + - [ ] Slides uploaded via agenda item "+" button are linked to correct agenda item + + **QA Scenarios**: + ``` + Scenario: Upload slide on agenda item + Tool: Playwright + Preconditions: Service with agenda items, dev server running + Steps: + 1. Navigate to service edit page + 2. Find a non-song agenda item (e.g. "Gebet") + 3. Click "+" button (data-testid="agenda-item-add-slides") + 4. Assert upload area appears below the row + 5. Upload a test image file + 6. Wait for upload to complete and page reload + 7. Assert thumbnail appears in the agenda item row + Expected Result: Slide uploaded and visible as thumbnail in the agenda item + Evidence: .sisyphus/evidence/task-13-upload-slide.png + + Scenario: Delete slide from agenda item + Tool: Playwright + Steps: + 1. Navigate to edit page with agenda item that has slides + 2. Click delete button on slide thumbnail + 3. Confirm deletion + 4. Assert slide is removed + Expected Result: Slide deleted (soft-delete), no longer visible + Evidence: .sisyphus/evidence/task-13-delete-slide.png + ``` + + **Commit**: YES + - Message: `feat(ui): add slide upload on agenda items` + - Files: `resources/js/Components/AgendaItemRow.vue`, `resources/js/Components/SlideUploader.vue`, `app/Http/Controllers/SlideController.php`, test file + - Pre-commit: `npm run build && php artisan test` + +--- + +- [ ] 14. Settings.vue — Add Agenda Configuration Fields + + **What to do**: + - Update `resources/js/Pages/Settings.vue`: + - Add new section below existing "ProPresenter Makro-Konfiguration": + - Section title: "Agenda-Konfiguration" + - Description: "Diese Einstellungen steuern, wie der Gottesdienst-Ablauf angezeigt und exportiert wird." + - Add 4 new fields (same pattern as existing macro fields): + - `agenda_start_title` — Label: "Ablauf-Start", Placeholder: "z.B. Ablauf*" + - `agenda_end_title` — Label: "Ablauf-Ende", Placeholder: "z.B. Ende*" + - `agenda_announcement_position` — Label: "Ankündigungen-Position", Placeholder: "z.B. Information*,Hinweis*" + - Add help text: "Komma-getrennte Liste. Das erste passende Element im Ablauf bestimmt die Position der Ankündigungen im Export. * als Platzhalter." + - `agenda_sermon_matching` — Label: "Predigt-Erkennung", Placeholder: "z.B. Predigt*,Sermon*" + - Add help text: "Komma-getrennte Liste. Erkannte Elemente bekommen einen speziellen Upload-Bereich für Predigt-Folien. * als Platzhalter." + - Reuse existing `saveField()` auto-save on blur pattern + - Add `data-testid` attributes on all new inputs + + **Must NOT do**: + - Do NOT change existing macro settings section + - Do NOT add validation beyond what SettingsController provides + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 4 (with Tasks 15, 16) + - **Blocks**: Task 18 + - **Blocked By**: Task 4 + + **References**: + **Pattern References**: + - `resources/js/Pages/Settings.vue` — Existing settings page (155 lines) with field rendering and auto-save pattern. Add new section following same structure. + + **WHY Each Reference Matters**: + - `Settings.vue` — The file being modified. Follow exact same field rendering pattern (v-for, auto-save on blur, saving/saved/error states) + + **Acceptance Criteria**: + - [ ] `npm run build` → no errors + - [ ] Settings page shows "Agenda-Konfiguration" section with 4 fields + + **QA Scenarios**: + ``` + Scenario: Agenda settings fields visible and functional + Tool: Playwright + Steps: + 1. Navigate to /settings + 2. Assert section "Agenda-Konfiguration" is visible + 3. Assert input (data-testid="setting-agenda_start_title") exists with placeholder "z.B. Ablauf*" + 4. Assert input (data-testid="setting-agenda_announcement_position") exists + 5. Type "Predigt*" into sermon matching field + 6. Click outside (blur) to trigger save + 7. Assert saved indicator (green checkmark) appears + 8. Reload page + 9. Assert value "Predigt*" persists + Expected Result: All 4 fields visible, auto-save works, values persist + Evidence: .sisyphus/evidence/task-14-agenda-settings.png + ``` + + **Commit**: YES + - Message: `feat(ui): add agenda settings to Settings page` + - Files: `resources/js/Pages/Settings.vue` + - Pre-commit: `npm run build` + +--- + +- [ ] 15. Update Service List — Status Columns + + **What to do**: + - Update `ServiceController::index()`: + - Add count of agenda items with uploaded slides (non-song items that have slides) + - Keep songs_mapped_count and songs_arranged_count + - Keep sermon slides check (now: "any agenda item matching sermon pattern has slides") + - Add: `agenda_slides_count` — count of non-song agenda items that have at least one slide + - Update `resources/js/Pages/Services/Index.vue`: + - Replace moderation/sermon column with new status indicators: + - Songs: "x/y Lieder zugeordnet" (keep) + - Arrangements: "x/y Arrangements" (keep) + - Predigt: "Predigt-Folien ✓/✗" (keep, but source changes to agenda item matching) + - New: "N weitere Folien" — count of non-song agenda items with slides + - Remove references to moderation slides count + - Write tests: + - Test index returns correct agenda_slides_count + - Test sermon detection works via agenda item matching + + **Must NOT do**: + - Do NOT redesign the service list layout + - Do NOT change finalize/reopen/download from list view + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 4 (with Tasks 14, 16) + - **Blocks**: None + - **Blocked By**: Tasks 1, 8 + + **References**: + **Pattern References**: + - `app/Http/Controllers/ServiceController.php:30-86` — Current index() method with status counts + - `resources/js/Pages/Services/Index.vue` — Current list page (512 lines) with status columns + + **Test References**: + - `tests/Feature/ServiceControllerTest.php` — Existing index tests + + **WHY Each Reference Matters**: + - `ServiceController::index()` — Modify count queries for new model + - `Index.vue` — Update display columns + + **Acceptance Criteria**: + - [ ] `php artisan test --filter=ServiceControllerTest` → all tests pass + - [ ] `npm run build` → no errors + + **QA Scenarios**: + ``` + Scenario: Service list shows updated status columns + Tool: Playwright + Steps: + 1. Navigate to /services + 2. Find a service row with known data + 3. Assert songs count is visible (e.g. "3/3 Lieder") + 4. Assert predigt status is visible + 5. Assert "N weitere Folien" count is visible + 6. Assert NO "Moderation" column exists + Expected Result: List shows songs, predigt, and additional slides count + Evidence: .sisyphus/evidence/task-15-service-list-status.png + ``` + + **Commit**: YES + - Message: `feat(ui): update service list status columns` + - Files: `resources/js/Pages/Services/Index.vue`, `app/Http/Controllers/ServiceController.php`, test file + - Pre-commit: `npm run build && php artisan test` + +--- + +- [ ] 16. Wire Agenda-Ordered Export into Download Flow + + **What to do**: + - Update `ServiceController::download()`: + - Use the updated `PlaylistExportService::generatePlaylist()` (from Task 9) which now follows agenda order + - Add agenda items and settings loading before calling export + - Pass announcement slides and their position to the export service + - Handle the fallback: if service has no agenda items, use old block-based export path + - Update `ServiceController::finalize()`: + - Use updated `finalizationStatus()` (from Task 8) + - Keep confirmation dialog behavior unchanged + - Write integration test: + - Test full flow: create service with agenda items → upload slides → finalize → download → verify .proplaylist content order + + **Must NOT do**: + - Do NOT change the download endpoint route + - Do NOT change the .proplaylist format + - Do NOT break existing downloadBundle() endpoint + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave 4 (after Tasks 9, 12) + - **Blocks**: Task 18 + - **Blocked By**: Tasks 9, 12 + + **References**: + **Pattern References**: + - `app/Http/Controllers/ServiceController.php:226-260` — Current download() and finalize() methods + - `app/Services/PlaylistExportService.php` — Updated service from Task 9 + + **Test References**: + - `tests/Feature/FinalizationTest.php` — Existing finalize/download integration tests + - `tests/Feature/PlaylistExportTest.php` — Export tests + + **WHY Each Reference Matters**: + - `ServiceController` — The methods being wired up to use the new export + - `PlaylistExportService` — The updated service that now supports agenda ordering + + **Acceptance Criteria**: + - [ ] `php artisan test --filter=FinalizationTest` → passes + - [ ] `php artisan test --filter=PlaylistExportTest` → passes + - [ ] `php artisan test --filter=test_download_follows_agenda_order` → passes + + **QA Scenarios**: + ``` + Scenario: Full finalize → download flow with agenda order + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_full_finalize_download_flow` + Expected Result: Service with agenda items finalizes and downloads .proplaylist with presentations in agenda order + Evidence: .sisyphus/evidence/task-16-full-flow.txt + + Scenario: Old service without agenda items still downloadable + Tool: Bash (php artisan test) + Steps: + 1. Run `php artisan test --filter=test_old_service_download_backward_compat` + Expected Result: Service finalized before migration downloads correctly using old block order + Evidence: .sisyphus/evidence/task-16-backward-compat.txt + ``` + + **Commit**: YES + - Message: `feat(export): wire agenda export into download flow` + - Files: `app/Http/Controllers/ServiceController.php`, test file + - Pre-commit: `php artisan test` + +--- + +- [ ] 17. Update Existing PHP Tests for Agenda Model + + **What to do**: + - Review and update ALL existing PHP tests that reference the old model: + - `tests/Feature/ServiceControllerTest.php` — Update edit page assertions for agendaItems prop + - `tests/Feature/FinalizationTest.php` — Already updated in Task 8, verify integration + - `tests/Feature/PlaylistExportTest.php` — Already updated in Task 9, verify integration + - `tests/Feature/ChurchToolsSyncTest.php` — Already updated in Task 6, verify integration + - `tests/Feature/ModerationBlockTest.php` — Mark tests as covering deprecated feature or update + - `tests/Feature/SermonBlockTest.php` — Mark tests as covering deprecated feature or update + - `tests/Feature/SongsBlockTest.php` — Update to test SongAgendaItem behavior + - `tests/Feature/SlideControllerTest.php` — Add tests for agenda_item slide type + - `tests/Feature/InformationBlockTest.php` — Verify still passes unchanged + - Run full test suite: `php artisan test` — ALL tests must pass + - Do NOT delete old tests yet — only update them to work with new model + + **Must NOT do**: + - Do NOT delete any test files (deprecation in Task 19) + - Do NOT skip tests — update them + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 5 (with Tasks 18) + - **Blocks**: Task 19 + - **Blocked By**: Tasks 6, 7, 8, 9 + + **References**: + **Pattern References**: + - All test files in `tests/Feature/` — each file listed above needs review + + **WHY Each Reference Matters**: + - Every test file may reference old props (moderationSlides, sermonSlides) or old model structure + + **Acceptance Criteria**: + - [ ] `php artisan test` → ALL tests pass (0 failures, 0 errors) + + **QA Scenarios**: + ``` + Scenario: Full PHP test suite passes + Tool: Bash + Steps: + 1. Run `php artisan test` + Expected Result: All tests pass with 0 failures and 0 errors + Evidence: .sisyphus/evidence/task-17-full-test-suite.txt + ``` + + **Commit**: YES + - Message: `test(php): update existing tests for agenda model` + - Files: `tests/Feature/*.php` (multiple) + - Pre-commit: `php artisan test` + +--- + +- [ ] 18. Playwright E2E Tests for Restructured Edit Page + + **What to do**: + - Update/create E2E test files: + - `tests/e2e/service-edit-agenda.spec.ts` (NEW): + - Test agenda items display in correct order + - Test song items show arrangement pills + - Test non-song items show "+" upload button + - Test header items render as dividers + - Test slide upload on agenda item + - Test arrangement dialog opens from song edit button + - Test arrangement drag-and-drop + - Test arrangement 2x duplicate + - Test announcement position indicator + - Test sermon item detection + - Test highlight states (success/warning/neutral) + - Update `tests/e2e/service-edit-information.spec.ts`: + - Verify information block still at top, working as before + - Update `tests/e2e/service-finalization.spec.ts`: + - Verify finalize/download flow with new agenda model + - Deprecate/remove: + - `tests/e2e/service-edit-moderation.spec.ts` — no longer relevant + - `tests/e2e/service-edit-sermon.spec.ts` — sermon is now part of agenda + - `tests/e2e/service-edit-songs.spec.ts` — songs are now part of agenda + - Update `tests/e2e/sync-and-pro.spec.ts`: + - Verify sync creates agenda items + - Run: `npx playwright test` — all tests pass + + **Must NOT do**: + - Do NOT change auth setup + - Do NOT change base URL configuration + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 5 (with Task 17) + - **Blocks**: Task 19 + - **Blocked By**: Tasks 12, 13, 14, 16 + + **References**: + **Pattern References**: + - `tests/e2e/service-edit-songs.spec.ts` — Existing song E2E tests with authentication and Playwright patterns + - `tests/e2e/auth.setup.ts` — Auth setup for E2E tests + - `tests/e2e/service-finalization.spec.ts` — Finalization E2E flow + + **WHY Each Reference Matters**: + - Existing E2E tests — Follow same patterns (authentication, selectors, assertions, German text) + + **Acceptance Criteria**: + - [ ] `npx playwright test` → all tests pass + + **QA Scenarios**: + ``` + Scenario: E2E test suite passes + Tool: Bash + Steps: + 1. Run `npx playwright test` + Expected Result: All E2E tests pass + Evidence: .sisyphus/evidence/task-18-e2e-results.txt + ``` + + **Commit**: YES + - Message: `test(e2e): Playwright tests for restructured edit page` + - Files: `tests/e2e/*.spec.ts` + - Pre-commit: `npx playwright test` + +--- + +- [ ] 19. Remove Deprecated Block Components + + **What to do**: + - Remove Vue components no longer used: + - `resources/js/Components/Blocks/ModerationBlock.vue` — replaced by AgendaItemRow + - `resources/js/Components/Blocks/SermonBlock.vue` — replaced by sermon-flagged AgendaItemRow + - `resources/js/Components/Blocks/SongsBlock.vue` — replaced by SongAgendaItem + - Update `ArrangementConfigurator.vue`: + - If still used by `SongEditModal.vue` (SongDB page): keep it + - If `SongEditModal.vue` now uses `ArrangementDialog.vue`: remove `ArrangementConfigurator.vue` + - Remove deprecated test files: + - `tests/Feature/ModerationBlockTest.php` — if fully replaced by agenda tests + - `tests/Feature/SermonBlockTest.php` — if fully replaced + - `tests/e2e/service-edit-moderation.spec.ts` + - `tests/e2e/service-edit-sermon.spec.ts` + - `tests/e2e/service-edit-songs.spec.ts` + - Run full test suite to verify nothing breaks: + - `php artisan test` → pass + - `npx playwright test` → pass + - `npm run build` → pass + + **Must NOT do**: + - Do NOT remove InformationBlock (still in use) + - Do NOT remove SlideGrid, SlideUploader (still in use) + - Do NOT remove ArrangementConfigurator if SongEditModal still uses it + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave 5 (after Tasks 17, 18) + - **Blocks**: None + - **Blocked By**: Tasks 17, 18 + + **References**: + **Pattern References**: + - `resources/js/Components/Blocks/` — Directory containing block components to review + - `resources/js/Components/SongEditModal.vue` — Check if it still uses ArrangementConfigurator + + **Acceptance Criteria**: + - [ ] `npm run build` → no errors (no missing imports) + - [ ] `php artisan test` → all tests pass + - [ ] `npx playwright test` → all tests pass + + **QA Scenarios**: + ``` + Scenario: Build succeeds after component removal + Tool: Bash + Steps: + 1. Run `npm run build` + 2. Run `php artisan test` + 3. Run `npx playwright test` + Expected Result: All three commands succeed with zero errors + Evidence: .sisyphus/evidence/task-19-cleanup-verification.txt + ``` + + **Commit**: YES + - Message: `refactor(cleanup): remove deprecated block components` + - Files: deleted Vue components, deleted test files + - Pre-commit: `npm run build && php artisan test` + +--- + +## Final Verification Wave (MANDATORY — after ALL implementation tasks) + +> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing. + +- [ ] F1. **Plan Compliance Audit** — `oracle` + Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan. + Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT` + +- [ ] F2. **Code Quality Review** — `unspecified-high` + Run `./vendor/bin/pint --test` + `php artisan test`. 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. Verify German Du-form in all UI text. + Output: `Pint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT` + +- [ ] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill) + Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration: create service → sync agenda → upload slides on agenda items → configure arrangement → finalize → download .proplaylist → verify order. Edge cases: empty agenda, no songs, no matching settings. + Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT` + +- [ ] 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, nothing beyond spec was built. Check "Must NOT do" compliance. Detect cross-task contamination. Flag unaccounted changes. + Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT` + +--- + +## Commit Strategy + +| Task | Commit Message | Files | Pre-commit | +|------|---------------|-------|------------| +| 1 | `feat(db): add service_agenda_items table and model` | migration, model, factory, test | `php artisan test` | +| 2 | `feat(db): add service_agenda_item_id FK to slides` | migration, Slide model, test | `php artisan test` | +| 3 | `feat(service): add AgendaMatcherService with wildcard namesmatching` | service, test | `php artisan test` | +| 4 | `feat(settings): add agenda configuration keys` | controller, test | `php artisan test` | +| 5 | `chore(debug): add CTS agenda type discovery command` | artisan command | `php artisan test` | +| 6 | `feat(sync): sync all CTS agenda items (not just songs)` | ChurchToolsService, test | `php artisan test` | +| 7 | `feat(controller): pass agenda items to edit page` | ServiceController, test | `php artisan test` | +| 8 | `refactor(model): update finalizationStatus for agenda model` | Service model, test | `php artisan test` | +| 9 | `feat(export): agenda-ordered playlist export` | PlaylistExportService, test | `php artisan test` | +| 10 | `feat(ui): redesign ArrangementDialog with lyric preview` | Vue component | `npm run build` | +| 11 | `feat(ui): add AgendaItemRow and SongAgendaItem components` | Vue components | `npm run build` | +| 12 | `feat(ui): restructure Edit.vue with agenda view` | Edit.vue | `npm run build` | +| 13 | `feat(ui): add slide upload on agenda items` | Vue component, SlideController | `npm run build && php artisan test` | +| 14 | `feat(ui): add agenda settings to Settings page` | Settings.vue | `npm run build` | +| 15 | `feat(ui): update service list status columns` | Index.vue, ServiceController | `npm run build && php artisan test` | +| 16 | `feat(export): wire agenda export into download flow` | ServiceController | `php artisan test` | +| 17 | `test(php): update existing tests for agenda model` | test files | `php artisan test` | +| 18 | `test(e2e): Playwright tests for restructured edit page` | E2E test files | `npx playwright test` | +| 19 | `refactor(cleanup): remove deprecated block components` | Vue components, test files | `npm run build && php artisan test` | + +--- + +## Success Criteria + +### Verification Commands +```bash +php artisan test # Expected: all tests pass +npx playwright test # Expected: all E2E tests pass +./vendor/bin/pint --test # Expected: no formatting issues +npm run build # Expected: no build errors +``` + +### Final Checklist +- [ ] All "Must Have" present +- [ ] All "Must NOT Have" absent +- [ ] All tests pass (PHP + E2E) +- [ ] Service edit page shows CTS agenda +- [ ] Arrangement dialog has two-column layout +- [ ] Export follows agenda order +- [ ] Settings page has agenda configuration +- [ ] Existing finalized services still downloadable diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 833a558..286fef3 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -15,6 +15,10 @@ class SettingsController extends Controller 'macro_uuid', 'macro_collection_name', 'macro_collection_uuid', + 'agenda_start_title', + 'agenda_end_title', + 'agenda_announcement_position', + 'agenda_sermon_matching', ]; public function index(): Response diff --git a/tests/Feature/SettingsControllerAgendaKeysTest.php b/tests/Feature/SettingsControllerAgendaKeysTest.php new file mode 100644 index 0000000..ea51e3e --- /dev/null +++ b/tests/Feature/SettingsControllerAgendaKeysTest.php @@ -0,0 +1,85 @@ +user = User::factory()->create(); +}); + +test('settings index includes all agenda keys in props', function () { + $response = $this->actingAs($this->user) + ->withoutVite() + ->get(route('settings.index')); + + $response->assertInertia( + fn ($page) => $page + ->component('Settings') + ->has('settings.agenda_start_title') + ->has('settings.agenda_end_title') + ->has('settings.agenda_announcement_position') + ->has('settings.agenda_sermon_matching') + ); +}); + +test('patch agenda_start_title setting returns 200', function () { + $response = $this->actingAs($this->user) + ->patchJson(route('settings.update'), [ + 'key' => 'agenda_start_title', + 'value' => 'Test Title', + ]); + + $response->assertOk() + ->assertJson(['success' => true]); + + expect(Setting::get('agenda_start_title'))->toBe('Test Title'); +}); + +test('patch agenda_end_title setting returns 200', function () { + $response = $this->actingAs($this->user) + ->patchJson(route('settings.update'), [ + 'key' => 'agenda_end_title', + 'value' => 'End Title', + ]); + + $response->assertOk() + ->assertJson(['success' => true]); + + expect(Setting::get('agenda_end_title'))->toBe('End Title'); +}); + +test('patch agenda_announcement_position setting returns 200', function () { + $response = $this->actingAs($this->user) + ->patchJson(route('settings.update'), [ + 'key' => 'agenda_announcement_position', + 'value' => 'pattern1,pattern2', + ]); + + $response->assertOk() + ->assertJson(['success' => true]); + + expect(Setting::get('agenda_announcement_position'))->toBe('pattern1,pattern2'); +}); + +test('patch agenda_sermon_matching setting returns 200', function () { + $response = $this->actingAs($this->user) + ->patchJson(route('settings.update'), [ + 'key' => 'agenda_sermon_matching', + 'value' => 'sermon,predigt', + ]); + + $response->assertOk() + ->assertJson(['success' => true]); + + expect(Setting::get('agenda_sermon_matching'))->toBe('sermon,predigt'); +}); + +test('patch with unknown key returns 422', function () { + $response = $this->actingAs($this->user) + ->patchJson(route('settings.update'), [ + 'key' => 'unknown_setting_key', + 'value' => 'some value', + ]); + + $response->assertUnprocessable(); +});