# 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