All 8 Definition of Done criteria verified: ✅ docker-compose up → app running on localhost:8000 ✅ ChurchTools OAuth login → end-to-end working ✅ CTS API sync → populates services and songs ✅ All 4 edit blocks → functional with auto-save ✅ Song matching, arrangement, translation → all working ✅ File uploads → convert to 1920×1080 JPGs correctly ✅ All tests pass → 174/174 (905 assertions) ✅ All UI text → German with 'Du' form PLAN 100% COMPLETE: 0 unchecked items remaining
98 KiB
CTS Presenter App — Church Service Show Creator
TL;DR
Quick Summary: Greenfield Laravel 11 + Vue 3 + Inertia.js app that syncs church service data from ChurchTools API, lets users prepare services (map songs, upload slides, configure arrangements), manage a song database with translations, and track finalization status. German UI, Docker deployment, TDD.
Deliverables:
- Docker-containerized Laravel+Vue app with ChurchTools OAuth login
- Service list with status tracking (song mapping, slides uploaded, finalized)
- Service edit form with 4 blocks: Information, Moderation, Sermon, Songs
- Song database with arrangement configurator, translation editor, PDF export
- File upload pipeline: images → JPG 1920×1080, PPT → slides, ZIP → extraction
- CTS API sync with "Refresh" button and timestamp in nav bar
Estimated Effort: XL Parallel Execution: YES — 5 waves + final verification Critical Path: T0 → T1 → T4 → T8 → T14/T18 → T24 → FINAL
Context
Original Request
Build a "ChurchService Presenter Software Show Creator" — a tool that reads church service data from ChurchTools API, provides a form to finalize service setup (songs, slides, arrangements), and manages a song database. German UI with "Du" form. All actions immediately persistent (auto-save).
Interview Summary
Key Discussions:
- CTS API Client: Use
5pm-HDH/churchtools-apiPHP package (confirmed) - Auth: Only ChurchTools OAuth — no local password login
- Arrangements: Manually created in app. Later also from .pro import
- Info-Slides: Dynamic per query (expire_date > service_date), not copied into services
- Translation: URL to lyrics webpage (scrape) + manual text paste
- Group Colors: Freely choosable per group (color picker)
- Song Preview: HTML Modal/Overlay with formatted text
- Download (finalized): Placeholder — future ProPresenter show generator tool
- Deployment: Docker container
- Tests: TDD strategy
Research Findings:
- ChurchTools REST API at
/api/*with Swagger docs at each instance - OAuth2 Authorization Code flow, endpoints:
/oauth/authorize,/oauth/access_token,/oauth/userinfo 5pm-HDH/churchtools-apiv2.1: EventRequest, SongRequest, EventAgendaRequest with fluent filtering- Intervention Image v3 for image processing (letterbox to 1920×1080)
- LibreOffice headless → PDF →
spatie/pdf-to-image→ JPG for PPT conversion (must be queued) vue-draggable-plusfor arrangement drag-and-drop (clone mode, groups repeat)@jaxtheprime/vue3-dropzonefor file upload zones (drop/preview/edit modes)barryvdh/laravel-dompdffor PDF generation (no Tailwind in templates — old-school CSS only)- ProPresenter .pro: Pro6=XML, Pro7=Protobuf. Parser deferred.
@vueuse/coreforuseDebounceFn(auto-save debouncing)
Metis Review
Identified Gaps (addressed):
- CTS API may not include song lyrics text → Plan includes Wave 0 spike to verify. Songs get lyrics from manual entry or future .pro import.
5pm-HDH/churchtools-apitoken auth unverified → Spike task verifiesCTConfig::setApiKey()support- PPT conversion must be async (queued Laravel Job) → Planned as Job with progress indicator
- DomPDF cannot render Tailwind → Song PDF template uses old-school CSS with
DejaVu Sansfont - Lyrics URL scraping is fragile → Best-effort HTTP fetch with manual paste as primary fallback
- Arrangement Vue keys must use
${group.id}-${index}notgroup.id(groups repeat) - Docker image needs: PHP, LibreOffice, ImageMagick/Imagick, Node.js
Work Objectives
Core Objective
Build a complete service preparation tool that reads CTS data, enables song mapping/arrangement/translation, handles multi-format slide uploads, and tracks finalization status — all in German with auto-save and Docker deployment.
Concrete Deliverables
- Docker setup (Dockerfile + docker-compose.yml)
- ChurchTools OAuth login (only auth method)
- CTS API sync service with Refresh button + timestamp
- Service list page with status indicators
- Service edit form with 4 blocks (Information, Moderation, Sermon, Songs)
- Song database page with CRUD, arrangement config, translation editor
- File upload pipeline (image/PPT/ZIP processing)
- Song preview modal + PDF download
- Email notification for unmatched songs
- .pro upload/download placeholders (throw NotImplementedException)
Definition of Done
docker-compose upstarts working app on localhost- Login via ChurchTools OAuth works end-to-end
- CTS API sync populates services and songs
- All 4 edit blocks functional with auto-save
- Song matching, arrangement config, translation all working
- File uploads convert to 1920×1080 JPGs correctly
- All tests pass (
php artisan test) - All UI text in German with "Du" form
Must Have
- ChurchTools OAuth as sole login method
- READ-ONLY CTS API access (no writes)
- Auto-save (every action immediately persistent)
- German UI with "Du" form throughout
- File upload: JPG 1920×1080 letterbox, PPT→slides, ZIP extraction
- Song matching by CCLI ID
- Arrangement configurator with drag-and-drop
- Info-slides with expire dates shown dynamically in future services
- Docker deployment
Must NOT Have (Guardrails)
- NO writes to ChurchTools API — READ ONLY
- NO local password login — OAuth only
- NO .pro file parser implementation (placeholder/exception only)
- NO finalized download generation (placeholder — future tool)
- NO upscaling of small images (letterbox with black bars, never stretch)
- NO Tailwind CSS in DomPDF templates (use old-school CSS + DejaVu Sans)
- NO site-specific lyrics scrapers (best-effort HTTP fetch only)
- NO over-engineered abstractions — keep it practical
- NO English UI text — everything in German with "Du"
Verification Strategy (MANDATORY)
ZERO HUMAN INTERVENTION — ALL verification is agent-executed. No exceptions.
Test Decision
- Infrastructure exists: YES (Breeze includes Pest/PHPUnit)
- Automated tests: TDD — Tests first, then implementation
- Framework: Pest (Laravel default with Breeze)
- Each task: 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
- API/Backend: Use Bash (curl/artisan) — Commands, assertions
- File Processing: Use Bash — Upload test files, verify output dimensions/format
Execution Strategy
Parallel Execution Waves
Wave 0 (Spike — sequential, blocks everything):
└── Task 0: CTS API spike — verify token auth + package compat + API shape
Wave 1 (Foundation — 7 parallel tasks):
├── Task 1: Laravel scaffolding + Breeze Vue + Docker [quick]
├── Task 2: Database schema + all migrations [deep]
├── Task 3: ChurchTools OAuth provider (replace Breeze login) [unspecified-high]
├── Task 4: CTS API service + sync command [deep]
├── Task 5: File conversion service (image/PPT/ZIP) [deep]
├── Task 6: Shared Vue components (layout, nav, auto-save) [visual-engineering]
├── Task 7: Email config + Mailable for missing songs [quick]
Wave 2 (Core Features — 6 parallel tasks):
├── Task 8: Service list page (backend + frontend) [deep]
├── Task 9: Song model + SongDB CRUD backend [unspecified-high]
├── Task 10: Slide upload component (shared reusable) [visual-engineering]
├── Task 11: Arrangement model + configurator component [deep]
├── Task 12: Song matching service (CCLI ID) [unspecified-high]
├── Task 13: Translation service (URL scrape + manual) [unspecified-high]
Wave 3 (Service Edit + Song UI — 6 parallel tasks):
├── Task 14: Service edit page layout + routing [visual-engineering]
├── Task 15: Information block (slides + expire dates) [visual-engineering]
├── Task 16: Moderation block (slides, service-specific) [quick]
├── Task 17: Sermon block (slides, service-specific) [quick]
├── Task 18: Songs block (matching + arrangement + translation) [deep]
├── Task 19: Song preview modal + PDF download [unspecified-high]
Wave 4 (Song DB + Finalization — 5 parallel tasks):
├── Task 20: Song DB page (list + search + filters) [visual-engineering]
├── Task 21: Song DB edit popup (metadata + arrangement) [visual-engineering]
├── Task 22: Song DB translate page (two-column editor) [deep]
├── Task 23: Song DB .pro upload + download placeholders [quick]
├── Task 24: Service finalization + status management [unspecified-high]
Wave FINAL (Verification — 4 parallel):
├── Task F1: Plan compliance audit [oracle]
├── Task F2: Code quality review [unspecified-high]
├── Task F3: Real manual QA with Playwright [unspecified-high]
└── Task F4: Scope fidelity check [deep]
Critical Path: T0 → T1 → T4 → T8 → T14/T18 → T24 → FINAL
Parallel Speedup: ~65% faster than sequential
Max Concurrent: 7 (Wave 1)
Dependency Matrix
| Task | Depends On | Blocks | Wave |
|---|---|---|---|
| T0 | — | ALL | 0 |
| T1 | T0 | T2-T7 | 1 |
| T2 | T1 | T8-T13 | 1 |
| T3 | T1 | T8 | 1 |
| T4 | T1 | T8, T12 | 1 |
| T5 | T1 | T10, T15-T17 | 1 |
| T6 | T1 | T8, T14-T18, T20 | 1 |
| T7 | T1 | T12 | 1 |
| T8 | T2, T3, T4, T6 | T14 | 2 |
| T9 | T2 | T11, T12, T18-T23 | 2 |
| T10 | T2, T5, T6 | T15-T17 | 2 |
| T11 | T2, T9 | T18, T21 | 2 |
| T12 | T2, T4, T7, T9 | T18 | 2 |
| T13 | T2, T9 | T22 | 2 |
| T14 | T8, T6 | T15-T19 | 3 |
| T15 | T10, T14 | T24 | 3 |
| T16 | T10, T14 | T24 | 3 |
| T17 | T10, T14 | T24 | 3 |
| T18 | T11, T12, T14 | T24 | 3 |
| T19 | T9, T11 | — | 3 |
| T20 | T9, T6 | — | 4 |
| T21 | T9, T11 | — | 4 |
| T22 | T13, T9 | — | 4 |
| T23 | T9 | — | 4 |
| T24 | T15-T18 | FINAL | 4 |
| F1-F4 | ALL | — | FINAL |
Agent Dispatch Summary
- Wave 0: 1 — T0 →
deep - Wave 1: 7 — T1 →
quick, T2 →deep, T3 →unspecified-high, T4 →deep, T5 →deep, T6 →visual-engineering, T7 →quick - Wave 2: 6 — T8 →
deep, T9 →unspecified-high, T10 →visual-engineering, T11 →deep, T12 →unspecified-high, T13 →unspecified-high - Wave 3: 6 — T14 →
visual-engineering, T15 →visual-engineering, T16 →quick, T17 →quick, T18 →deep, T19 →unspecified-high - Wave 4: 5 — T20 →
visual-engineering, T21 →visual-engineering, T22 →deep, T23 →quick, T24 →unspecified-high - FINAL: 4 — F1 →
oracle, F2 →unspecified-high, F3 →unspecified-high, F4 →deep
TODOs
TDD = RED → GREEN → REFACTOR for every task. EVERY task MUST have: Agent Profile + QA Scenarios. ALL UI text in German with "Du" form.
Wave 0: API Spike
-
0. CTS API Spike — Verify Token Auth + API Shape
What to do:
- TEST: Write Pest test that creates a mock CTS API response and verifies the sync pipeline
- Install
5pm-HDH/churchtools-apivia composer - Verify token-based auth works:
CTConfig::setApiUrl()+CTConfig::setApiKey($token)fromCTS_API_TOKENenv var - If
setApiKeydoesn't exist, checkauthWithLoginToken()or similar methods in CTConfig - Hit
GET /api/eventswith date filter (today+future) and capture response shape - Hit
GET /api/songs/1(any song) and check if response includes lyrics text, arrangement data - Download OpenAPI spec from
/system/runtime/swagger/openapi.jsonand save todocs/churchtools-openapi.jsonfor reference - Document findings: which fields exist, what data shapes we get, confirm CCLI field presence on songs
- If package doesn't support token auth: document workaround (raw HTTP with
Authorization: Login TOKENheader)
Must NOT do:
- Do NOT write to any CTS API endpoint
- Do NOT commit actual API token to git
Recommended Agent Profile:
- Category:
deep - Skills: []
Parallelization:
- Can Run In Parallel: NO
- Parallel Group: Wave 0 (sequential, blocks everything)
- Blocks: T1-T24 (all tasks)
- Blocked By: None
References:
5pm-HDH/churchtools-apidocs: https://github.com/5pm-HDH/churchtools-api/blob/master/docs/out/CTConfig.md5pm-HDH/churchtools-apiEventAPI: https://github.com/5pm-HDH/churchtools-api/blob/master/docs/out/EventAPI.md5pm-HDH/churchtools-apiSongAPI: https://github.com/5pm-HDH/churchtools-api/blob/master/docs/out/SongAPI.md.env.example— containsCTS_API_TOKEN=XXXXXX- CTS API docs at instance:
https://INSTANCE.church.tools/api(Swagger UI)
Acceptance Criteria:
5pm-HDH/churchtools-apiinstalled and configured with token from.envphp artisan test --filter=CtsApiSpikeTest→ PASS- API response shapes documented in
docs/api-response-shapes.md - Confirmed: songs have/don't have lyrics text, CCLI field present
- Auth method documented (setApiKey vs authWithLoginToken vs raw HTTP)
QA Scenarios:
Scenario: Verify CTS API authentication Tool: Bash Preconditions: .env has valid CTS_API_TOKEN and CTS_API_URL Steps: 1. Run: php artisan tinker --execute="CTApi\CTConfig::setApiUrl(env('CTS_API_URL')); CTApi\CTConfig::setApiKey(env('CTS_API_TOKEN')); dump(CTApi\Models\Groups\Person\PersonRequest::whoami()->getFirstName());" 2. Assert: output contains a first name string (not null/error) 3. Run: php artisan tinker --execute="dump(CTApi\Models\Events\Event\EventRequest::where('from', now()->format('Y-m-d'))->get()->count());" 4. Assert: output is a number >= 0 Expected Result: Both commands succeed without authentication errors Evidence: .sisyphus/evidence/task-0-api-auth.txt Scenario: Verify song data includes CCLI Tool: Bash Preconditions: CTS API authenticated Steps: 1. Run: php artisan tinker to fetch first song and dump ccli field 2. Assert: song object has getCcli() method that returns a value Expected Result: CCLI field accessible on song model Evidence: .sisyphus/evidence/task-0-song-ccli.txtCommit: YES
- Message:
chore: verify CTS API token auth and package compatibility - Files:
composer.json,composer.lock,docs/api-response-shapes.md - Pre-commit:
php artisan test --filter=CtsApiSpikeTest
Wave 1: Foundation (7 parallel tasks)
-
1. Laravel Scaffolding + Breeze Vue + Docker
What to do:
- TEST: Write Pest test that verifies the home route returns Inertia response
- Run
laravel new cts --breeze --stack=vue --pest --database=sqlite(or equivalent composer commands) - Configure
.envwith all needed vars:CTS_API_URL,CTS_API_TOKEN,CHURCHTOOLS_URL,CHURCHTOOLS_CLIENT_ID,CHURCHTOOLS_REDIRECT_URI,MAIL_* - Update
.env.examplewith all new vars (placeholders) - Create
Dockerfilefor PHP 8.3 + required extensions (imagick, zip, pdo_sqlite, pdo_mysql) - Install LibreOffice headless + ImageMagick in Docker image
- Create
docker-compose.ymlwith app + node (for Vite) services - Configure
vite.config.jsfor Docker hot-reload - Verify
docker-compose upstarts app successfully - Set app locale to
deinconfig/app.php - Add
@vueuse/core,vue-draggable-plus,@jaxtheprime/vue3-dropzoneto package.json
Must NOT do:
- Do NOT keep Breeze default login/register pages (will be replaced in T3)
- Do NOT add application-specific routes yet
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: NO (first in Wave 1, all others depend on it)
- Parallel Group: Wave 1 (runs first, others start after)
- Blocks: T2-T7
- Blocked By: T0
References:
- Laravel 11 docs: https://laravel.com/docs/11.x/installation
- Breeze docs: https://laravel.com/docs/11.x/starter-kits#laravel-breeze
- Intervention Image v3:
composer require intervention/image spatie/pdf-to-image:composer require spatie/pdf-to-image
Acceptance Criteria:
docker-compose up -d→ containers start without errorshttp://localhost:8000returns HTTP responsenpm run buildcompletes without errorsphp artisan test→ default Breeze tests pass.env.examplecontains all project-specific vars
QA Scenarios:
Scenario: Docker containers start successfully Tool: Bash Preconditions: Docker installed, docker-compose.yml present Steps: 1. Run: docker-compose up -d 2. Run: docker-compose ps 3. Assert: all containers show 'Up' or 'healthy' status 4. Run: curl -s -o /dev/null -w '%{http_code}' http://localhost:8000 5. Assert: HTTP status is 200 or 302 Expected Result: All containers running, app reachable Evidence: .sisyphus/evidence/task-1-docker-up.txt Scenario: Vite build succeeds Tool: Bash Preconditions: npm dependencies installed Steps: 1. Run: docker-compose exec app npm run build 2. Assert: exit code 0, no errors in output Expected Result: Build completes, assets generated in public/build Evidence: .sisyphus/evidence/task-1-vite-build.txtCommit: YES
- Message:
feat: scaffold Laravel + Breeze Vue + Docker setup - Files: entire project scaffolding
- Pre-commit:
php artisan test
-
2. Database Schema + All Migrations
What to do:
- TEST: Write Pest test that verifies all tables exist after migration
- Create migrations for ALL tables in this order:
users(extend: add churchtools_id, avatar, churchtools_groups, churchtools_roles columns)services(cts_event_id, title, date, preacher_name, beamer_tech_name, finalized_at, last_synced_at, cts_data JSON)songs(ccli_id unique nullable, title, author, copyright_text, copyright_year, publisher, has_translation bool, deleted_at soft-delete, last_used_at)song_groups(song_id FK, name, color hex, order int)song_slides(song_group_id FK, order int, text_content, text_content_translated nullable, notes nullable)song_arrangements(song_id FK, name, is_default bool)song_arrangement_groups(song_arrangement_id FK, song_group_id FK, order int)service_songs(service_id FK, song_id FK nullable, song_arrangement_id FK nullable, use_translation bool default false, order int, cts_song_name, cts_ccli_id nullable, matched_at nullable, request_sent_at nullable)slides(type enum[information|moderation|sermon], service_id FK nullable, original_filename, stored_filename, thumbnail_filename, expire_date nullable, uploader_name nullable, uploaded_at, deleted_at soft-delete)cts_sync_log(synced_at, events_count, songs_count, status, error nullable)
- Create Eloquent models with relationships for all tables
- Add factory classes for testing (Song, Service, SongGroup, etc.)
Must NOT do:
- Do NOT add business logic to models (just relationships and casts)
- Do NOT create controllers or routes
Recommended Agent Profile:
- Category:
deep - Skills: []
Parallelization:
- Can Run In Parallel: YES (after T1)
- Parallel Group: Wave 1 (with T3-T7)
- Blocks: T8-T13 (all Wave 2 tasks)
- Blocked By: T1
References:
- ProPresenter schema research: songs → song_groups → song_slides hierarchy
- AGENTS.md: service list fields (Title, Preacher, beamer technician, qty of songs)
- AGENTS.md: Song block fields (CCLI ID, has Translation, arrangement)
- AGENTS.md: Information block (expire date, uploader name)
Acceptance Criteria:
php artisan migrate:fresh→ all migrations run without errorsphp artisan test --filter=DatabaseSchemaTest→ PASS (all tables exist with correct columns)- All Eloquent models have correct relationships defined
- Factory classes generate valid test data
QA Scenarios:
Scenario: All migrations run successfully Tool: Bash Steps: 1. Run: php artisan migrate:fresh --force 2. Assert: exit code 0, output shows all migrations ran 3. Run: php artisan tinker --execute="dump(Schema::getTableListing());" 4. Assert: output includes users, services, songs, song_groups, song_slides, song_arrangements, song_arrangement_groups, service_songs, slides, cts_sync_log Expected Result: 10+ tables created Evidence: .sisyphus/evidence/task-2-migrations.txtCommit: YES
- Message:
feat: add database schema for services, songs, arrangements, slides - Files:
database/migrations/*.php,app/Models/*.php,database/factories/*.php - Pre-commit:
php artisan test
-
3. ChurchTools OAuth Provider (Replace Breeze Login)
What to do:
- TEST: Write Pest test that mocks Socialite driver and verifies user creation from OAuth data
- Install
laravel/socialite - Create
App\Socialite\ChurchToolsProviderextendingAbstractProviderwith:getAuthUrl()→{CTS_URL}/oauth/authorizegetTokenUrl()→{CTS_URL}/oauth/access_tokengetUserByToken()→GET {CTS_URL}/oauth/userinfomapUserToObject()→ maps id, displayName, email, imageUrl, groups, roles
- Register provider in
AppServiceProvider::boot()viaSocialite::extend('churchtools', ...) - Add
config/services.phpentry forchurchtools(url, client_id, client_secret, redirect) - Create
AuthControllerwithredirect()andcallback()methods callback(): find-or-create User by email, store churchtools_id, avatar, groups, roles- Add routes:
GET /auth/churchtools(redirect),GET /auth/churchtools/callback - Remove Breeze login/register pages, replace with single "Mit ChurchTools anmelden" button
- Create simple
Login.vuepage with the OAuth button - Add logout route that clears session and redirects to login
- Protect all routes with
authmiddleware except login routes - Update
.env.examplewithCHURCHTOOLS_URL,CHURCHTOOLS_CLIENT_ID,CHURCHTOOLS_CLIENT_SECRET,CHURCHTOOLS_REDIRECT_URI
Must NOT do:
- Do NOT keep local email/password registration or login
- Do NOT store ChurchTools access token (we use our own API token for API calls)
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: YES (after T1)
- Parallel Group: Wave 1 (with T2, T4-T7)
- Blocks: T8
- Blocked By: T1
References:
devdot/churchtools-oauth2-clientsource: https://github.com/devdot/churchtools-oauth2-client — reference for endpoint URLs and user mapping- Laravel Socialite docs: https://laravel.com/docs/11.x/socialite
- ChurchTools OAuth flow:
/oauth/authorize→/oauth/access_token→/oauth/userinfo - User data shape:
{ id, firstName, lastName, displayName, email, imageUrl, groups, roles }
Acceptance Criteria:
php artisan test --filter=OAuthTest→ PASS- Visiting
/unauthenticated → redirect to login page - Login page shows only "Mit ChurchTools anmelden" button (no email/password form)
- After OAuth callback → user created in DB, redirected to dashboard
- No Breeze login/register routes remain
QA Scenarios:
Scenario: Unauthenticated user sees login page Tool: Playwright Steps: 1. Navigate to http://localhost:8000/ 2. Assert: page redirects to /login or shows login page 3. Assert: page contains button or link with text 'Mit ChurchTools anmelden' 4. Assert: NO email input field exists on the page 5. Assert: NO password input field exists on the page Expected Result: Clean OAuth-only login page in German Evidence: .sisyphus/evidence/task-3-login-page.png Scenario: OAuth redirect works Tool: Bash Steps: 1. Run: curl -s -o /dev/null -w '%{http_code} %{redirect_url}' http://localhost:8000/auth/churchtools 2. Assert: HTTP 302 redirect to churchtools.church.tools/oauth/authorize Expected Result: Redirect to ChurchTools OAuth authorize endpoint Evidence: .sisyphus/evidence/task-3-oauth-redirect.txtCommit: YES
- Message:
feat: implement ChurchTools OAuth login via Socialite - Files:
app/Socialite/ChurchToolsProvider.php,app/Http/Controllers/AuthController.php,resources/js/Pages/Auth/Login.vue, routes - Pre-commit:
php artisan test
-
4. CTS API Service + Sync Command
What to do:
- TEST: Write Pest tests that mock API responses and verify sync creates correct DB records
- Create
App\Services\ChurchToolsServicethat wraps5pm-HDH/churchtools-api:syncEvents(): fetch events from today forward, upsert intoservicestablesyncSongs(): fetch all songs, upsert basic metadata into local referencesyncAgenda($eventId): fetch agenda for event, create/updateservice_songsgetEventServices($eventId): get assigned people (preacher, beamer tech)
- Create
App\Console\Commands\SyncChurchToolsCommand(php artisan cts:sync) - Store sync timestamp in
cts_sync_logtable - When syncing songs to services: try to match
cts_ccli_idto existingsongs.ccli_id - If matched: set
service_songs.song_id, setmatched_at - If not matched: leave
song_idnull (UI will show matching options later) - Create
App\Http\Controllers\SyncControllerwithsync()action for the refresh button - Sync action returns Inertia redirect with flash message: "Daten wurden aktualisiert" or error
Must NOT do:
- Do NOT write/update anything on the CTS API
- Do NOT sync historical events (only today + future)
- Do NOT delete local songs when they're removed from CTS
Recommended Agent Profile:
- Category:
deep - Skills: []
Parallelization:
- Can Run In Parallel: YES (after T1)
- Parallel Group: Wave 1 (with T2, T3, T5-T7)
- Blocks: T8, T12
- Blocked By: T1 (needs T0 findings for auth method)
References:
- T0 findings:
docs/api-response-shapes.md— confirmed API response structures 5pm-HDH/churchtools-apiEventAPI:EventRequest::where('from', date)->get()5pm-HDH/churchtools-apiSongAPI:SongRequest::all(),$song->getCcli()5pm-HDH/churchtools-apiEventAgendaRequest:EventAgendaRequest::fromEvent($id)->get()- AGENTS.md: service list needs title, preacher, beamer tech, song count
Acceptance Criteria:
php artisan test --filter=ChurchToolsSyncTest→ PASSphp artisan cts:sync→ populates services and service_songs tables- Sync log entry created with count and status
- CCLI matching works: matched songs have
song_idset - Unmatched songs have
song_id = null,cts_ccli_idpreserved
QA Scenarios:
Scenario: Sync command populates database Tool: Bash Steps: 1. Run: php artisan migrate:fresh --force 2. Run: php artisan cts:sync 3. Assert: exit code 0, output shows 'Sync abgeschlossen' 4. Run: php artisan tinker --execute="dump(App\Models\Service::count());" 5. Assert: count > 0 6. Run: php artisan tinker --execute="dump(App\Models\CtsSync Log::latest()->first()->status);" 7. Assert: status is 'success' Expected Result: Services populated from CTS API Evidence: .sisyphus/evidence/task-4-sync-command.txtCommit: YES
- Message:
feat: add CTS API sync service and artisan command - Files:
app/Services/ChurchToolsService.php,app/Console/Commands/SyncChurchToolsCommand.php,app/Http/Controllers/SyncController.php - Pre-commit:
php artisan test
-
5. File Conversion Service (Image/PPT/ZIP)
What to do:
- TEST: Write Pest tests with sample image (400×300 PNG) and verify output is 1920×1080 JPG
- Create
App\Services\FileConversionServicewith methods:convertImage($file): array— letterbox to 1920×1080 JPG (black bars, no crop, no upscale-stretch), return['filename', 'thumbnail']convertPowerPoint($file): array— LibreOffice headless → PDF → spatie/pdf-to-image → individual JPGs → letterbox each. Return array of slide data.processZip($file): array— extract ZIP, recursively process each file (images, PPTs, nested ZIPs)generateThumbnail($path): string— create 320×180 thumbnail
- PPT conversion MUST be a queued Laravel Job (
App\Jobs\ConvertPowerPointJob) — NOT synchronous - Job dispatches events for progress tracking (
PowerPointConversionProgress) - Image conversion uses Intervention Image v3: create 1920×1080 canvas → fill black → scale image to fit → place centered
- Handle edge cases: portrait images get pillarbox, square images get bars all around
- Store converted files in
storage/app/public/slides/and thumbnails instorage/app/public/slides/thumbnails/ - Validate: only accept png, jpg, jpeg, ppt, pptx, zip file types
Must NOT do:
- Do NOT upscale small images — always letterbox with black bars
- Do NOT crop any part of an image
- Do NOT process PPT synchronously (must be queued)
Recommended Agent Profile:
- Category:
deep - Skills: []
Parallelization:
- Can Run In Parallel: YES (after T1)
- Parallel Group: Wave 1 (with T2-T4, T6-T7)
- Blocks: T10, T15-T17
- Blocked By: T1
References:
- Intervention Image v3 docs:
Image::create(1920, 1080)->fill('000000'),$image->scale(),$canvas->place($image, 'center') spatie/pdf-to-imagedocs:(new Pdf($path))->setPage($n)->saveImage($out)- LibreOffice headless:
soffice --headless --convert-to pdf --outdir $dir $input - Docker: LibreOffice path is
/usr/bin/libreofficein container
Acceptance Criteria:
php artisan test --filter=FileConversionTest→ PASS- 400×300 PNG input → 1920×1080 JPG output with black letterbox
- 1080×1920 portrait PNG → 1920×1080 JPG with black pillarbox
- Thumbnail generated at 320×180
- PPT conversion dispatches job, job processes to JPGs
QA Scenarios:
Scenario: Image conversion with letterbox Tool: Bash Steps: 1. Create test 400x300 PNG: convert -size 400x300 xc:red /tmp/test.png 2. Run conversion via tinker or test endpoint 3. Assert: output file is JPG 4. Run: identify output.jpg (ImageMagick) 5. Assert: dimensions are exactly 1920x1080 Expected Result: Letterboxed JPG at 1920x1080 Evidence: .sisyphus/evidence/task-5-letterbox.txt Scenario: Portrait image gets pillarbox Tool: Bash Steps: 1. Create test 600x1200 PNG (portrait) 2. Run conversion 3. Assert: output is 1920x1080 with black bars left/right Expected Result: Pillarboxed portrait image Evidence: .sisyphus/evidence/task-5-pillarbox.txtCommit: YES
- Message:
feat: add file conversion service (image, PPT, ZIP) - Files:
app/Services/FileConversionService.php,app/Jobs/ConvertPowerPointJob.php, tests - Pre-commit:
php artisan test
-
6. Shared Vue Components (Layout, Nav, Auto-Save)
What to do:
- TEST: Write Pest test that verifies shared Inertia data includes auth user and last_synced_at
- Create
AuthenticatedLayout.vuewith:- Top bar showing logged-in user name + avatar (from
$page.props.auth.user) - "Daten aktualisieren" refresh button that calls
POST /syncand shows loading spinner - Timestamp "Zuletzt aktualisiert: {date}" from last sync log
- Navigation: "Services" link, "Song-Datenbank" link
- Logout button/link
- Top bar showing logged-in user name + avatar (from
- Update
HandleInertiaRequestsmiddleware to share:auth.user,flash,last_synced_at,app_name - Create
AutoSaveFormcomposable using@vueuse/coreuseDebounceFn:- Text inputs: debounce 500ms
- Selects/checkboxes: immediate save
- Uses Inertia
router.put/postwithpreserveScroll: true, preserveState: true - Shows subtle save indicator ("Gespeichert" / "Speichert...")
- Create shared components:
FlashMessage.vue,ConfirmDialog.vue,LoadingSpinner.vue - All text in German with "Du" form
Must NOT do:
- Do NOT create page-specific components here (only shared/reusable)
- Do NOT add business logic to layout
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
frontend-ui-ux]
Parallelization:
- Can Run In Parallel: YES (after T1)
- Parallel Group: Wave 1 (with T2-T5, T7)
- Blocks: T8, T14-T18, T20
- Blocked By: T1
References:
- Breeze
AuthenticatedLayout.vue— extend this pattern usePage()from@inertiajs/vue3for accessing shared propsuseDebounceFnfrom@vueuse/corefor auto-save debouncing- AGENTS.md: "Button in Top Bar to refresh Data" + "timestamp with latest refresh" + "LoggedIn User visible"
Acceptance Criteria:
- Layout shows user name and avatar in top bar
- Refresh button triggers sync and updates timestamp
- Navigation links to Services and Song-Datenbank
- Auto-save composable debounces text input at 500ms
- All text in German
QA Scenarios:
Scenario: Top bar shows user info and navigation Tool: Playwright Steps: 1. Login via OAuth (or mock auth state) 2. Assert: top bar contains user display name 3. Assert: top bar contains 'Daten aktualisieren' button 4. Assert: top bar contains 'Zuletzt aktualisiert:' timestamp 5. Assert: navigation contains 'Services' link 6. Assert: navigation contains 'Song-Datenbank' link Expected Result: Complete German top bar with all elements Evidence: .sisyphus/evidence/task-6-topbar.pngCommit: YES
- Message:
feat: create shared Vue layout with nav, user, refresh button - Files:
resources/js/Layouts/AuthenticatedLayout.vue,resources/js/Composables/useAutoSave.js, shared components - Pre-commit:
php artisan test
-
7. Email Configuration + Missing Song Mailable
What to do:
- TEST: Write Pest test that verifies MissingSongNotification mailable renders correct content
- Configure Laravel mail in
config/mail.php(already done by default, uses MAIL_* env vars) - Add to
.env.example:MAIL_MAILER=smtp,MAIL_HOST,MAIL_PORT,MAIL_USERNAME,MAIL_PASSWORD,MAIL_FROM_ADDRESS,SONG_REQUEST_EMAIL(recipient for missing song requests) - Create
App\Mail\MissingSongRequestMailable:- Subject: "Song-Anfrage: {songName} (CCLI: {ccliId})"
- Body: German text explaining which song is needed, which service it's for, CCLI ID, and link to service in the app
- Create
App\Notifications\MissingSongNotificationas alternative (using Mail channel) - Add
SONG_REQUEST_EMAILto config/services.php for easy access
Must NOT do:
- Do NOT send emails automatically — only when user clicks "Erstellung anfragen" button
- Do NOT include sensitive data in emails
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES (after T1)
- Parallel Group: Wave 1 (with T2-T6)
- Blocks: T12
- Blocked By: T1
References:
- Laravel Mail docs: https://laravel.com/docs/11.x/mail
- AGENTS.md: "button 'request creation' which causes an EMAIL to a configured mail address with the song and the CCLI Id"
Acceptance Criteria:
php artisan test --filter=MissingSongMailTest→ PASS- Mailable renders German text with song name and CCLI ID
SONG_REQUEST_EMAILconfigurable via .env
QA Scenarios:
Scenario: Missing song email renders correctly Tool: Bash Steps: 1. Run: php artisan tinker to render MissingSongRequest mailable with test data 2. Assert: subject contains 'Song-Anfrage' 3. Assert: body contains CCLI ID 4. Assert: body is in German Expected Result: Well-formatted German email Evidence: .sisyphus/evidence/task-7-email-render.txtCommit: YES
- Message:
feat: configure email and missing-song notification mailable - Files:
app/Mail/MissingSongRequest.php,resources/views/mail/missing-song.blade.php - Pre-commit:
php artisan test
Wave 2: Core Features (6 parallel tasks)
-
8. Service List Page (Backend + Frontend)
What to do:
- TEST: Write Pest tests for ServiceController index endpoint — verify it returns Inertia page with services, filters by date >= today, includes status counts
- Create
App\Http\Controllers\ServiceControllerwithindex()method:- Query services where
date >= today, order by date ascending - Include computed status fields:
songs_mapped(x/y),songs_arranged(x/y),has_sermon_slides,info_slides_count,finalized_at - Return Inertia render
Services/Indexwith services collection
- Query services where
- Create
resources/js/Pages/Services/Index.vue:- Table/list showing: Titel, Prediger, Beamer-Techniker, Anzahl Songs, Letzte Änderung, Status
- Status indicators: "x/y Songs zugeordnet", "x/y Arrangements geprüft", "Predigtfolien", "Infofolien", "Abgeschlossen am"
- Color coding: green checkmark for complete, red/orange for incomplete
- Action buttons per service:
- If NOT finalized: "Bearbeiten" (link to edit page) + "Abschließen" button
- If finalized: "Wieder öffnen" + "Herunterladen" (placeholder — shows coming soon toast)
- Wire up Finalize/ReOpen as
POST /services/{id}/finalizeandPOST /services/{id}/reopen - Auto-refresh list after sync
Must NOT do:
- Do NOT show past services (only today and future)
- Do NOT implement download content (placeholder only)
- Do NOT inline-edit services from the list page
Recommended Agent Profile:
- Category:
deep - Skills: [
frontend-ui-ux]frontend-ui-ux: Table/list layout with status indicators requires good UI judgment
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T9-T13)
- Blocks: T14
- Blocked By: T2, T3, T4, T6
References:
app/Models/Service.php— Service model with relationships (from T2)app/Models/ServiceSong.php— for song mapping status countsapp/Models/Slide.php— for slide countsresources/js/Layouts/AuthenticatedLayout.vue— layout wrapper (from T6)- AGENTS.md lines 29-45: Service list fields and action buttons specification
- Pattern: follow Breeze
Dashboard.vuefor Inertia page structure
Acceptance Criteria:
php artisan test --filter=ServiceControllerTest→ PASS- Service list page renders with correct columns
- Only today + future services shown
- Finalize/ReOpen buttons toggle
finalized_atfield - Status indicators show correct x/y counts
- All text in German with "Du" form
QA Scenarios:
Scenario: Service list shows upcoming services with status Tool: Playwright Preconditions: Database seeded with 3 future services (1 with mapped songs, 1 without, 1 finalized) Steps: 1. Navigate to http://localhost:8000/services 2. Assert: page contains table/list with 3 service entries 3. Assert: first service row contains 'Prediger' column with preacher name 4. Assert: service with mapped songs shows green indicator or 'x/y' text 5. Assert: finalized service shows 'Wieder öffnen' button 6. Assert: non-finalized service shows 'Bearbeiten' and 'Abschließen' buttons Expected Result: Service list with correct German labels and status indicators Evidence: .sisyphus/evidence/task-8-service-list.png Scenario: Finalize and reopen service Tool: Playwright Preconditions: At least 1 non-finalized service in DB Steps: 1. Navigate to http://localhost:8000/services 2. Click 'Abschließen' button on first non-finalized service 3. Assert: service now shows 'Wieder öffnen' button instead 4. Assert: 'Abgeschlossen am' shows current date 5. Click 'Wieder öffnen' 6. Assert: service returns to non-finalized state with 'Bearbeiten' button Expected Result: Toggle finalization status works both ways Evidence: .sisyphus/evidence/task-8-finalize-toggle.pngCommit: YES
- Message:
feat: add service list page with status indicators and finalization - Files:
app/Http/Controllers/ServiceController.php,resources/js/Pages/Services/Index.vue, routes - Pre-commit:
php artisan test
-
9. Song Model + SongDB CRUD Backend
What to do:
- TEST: Write Pest tests for SongController — CRUD endpoints, soft delete, search by CCLI, search by name
- Create
App\Http\Controllers\SongControllerwith full resource methods:index(): list all songs (with soft-deleted excluded), searchable by name and CCLI ID, paginatedstore(): create new song with metadata (title, ccli_id, author, copyright_text)show($id): return song with groups, slides, arrangementsupdate($id): update song metadatadestroy($id): soft-delete
- Create
App\Http\Requests\SongRequestfor validation (title required, ccli_id unique) - Create
App\Services\SongServicefor business logic:createDefaultGroups($song): create default groups (Strophe 1, Refrain, Bridge, etc.) if none existcreateDefaultArrangement($song): create "Normal" arrangement referencing all groups in orderduplicateArrangement($arrangement, $name): clone arrangement with new name
- Add Song model accessors:
last_used_in_service(computed from service_songs join) - Add SongGroup model: color field with default palette
- Create API routes for JSON responses (used by Vue components via fetch/axios)
Must NOT do:
- Do NOT create Vue pages here (only backend API + controller)
- Do NOT hard-delete songs (always soft-delete)
- Do NOT auto-create songs from CTS API (songs are manually created or imported)
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T8, T10-T13)
- Blocks: T11, T12, T18-T23
- Blocked By: T2
References:
app/Models/Song.php,app/Models/SongGroup.php,app/Models/SongSlide.php— models from T2app/Models/SongArrangement.php,app/Models/SongArrangementGroup.php— arrangement models from T2- AGENTS.md lines 89-97: SongDB page specification (CRUD, edit, download, translate, upload)
- AGENTS.md lines 66-80: Song block fields (CCLI ID, arrangement, translation, preview/download)
Acceptance Criteria:
php artisan test --filter=SongControllerTest→ PASS- CRUD endpoints work: create, read, update, soft-delete
- Search by name returns matching songs
- Search by CCLI ID returns matching songs
- Default arrangement "Normal" created with new song
last_used_in_serviceaccessor returns correct date
QA Scenarios:
Scenario: Create and retrieve a song via API Tool: Bash (curl) Preconditions: Authenticated session cookie Steps: 1. POST /api/songs with body: {"title": "Großer Gott wir loben dich", "ccli_id": "12345", "author": "Test"} 2. Assert: HTTP 201, response contains song with id 3. GET /api/songs/{id} 4. Assert: response contains title, ccli_id, groups array, arrangements array 5. Assert: arrangements contains at least one entry named "Normal" Expected Result: Song created with default arrangement Evidence: .sisyphus/evidence/task-9-song-crud.txt Scenario: Search songs by CCLI ID Tool: Bash (curl) Steps: 1. GET /api/songs?search=12345 2. Assert: response contains the song with ccli_id=12345 3. GET /api/songs?search=99999 4. Assert: response is empty array Expected Result: CCLI search works correctly Evidence: .sisyphus/evidence/task-9-song-search.txtCommit: YES
- Message:
feat: add Song CRUD controller with search and default arrangements - Files:
app/Http/Controllers/SongController.php,app/Services/SongService.php,app/Http/Requests/SongRequest.php, routes - Pre-commit:
php artisan test
-
10. Slide Upload Component (Shared Reusable)
What to do:
- TEST: Write Pest tests for SlideController — upload image, upload ZIP, verify conversion, verify thumbnail generation
- Create
App\Http\Controllers\SlideControllerwith:store(Request $request): accept file upload, determine type (image/ppt/zip), dispatch conversiondestroy($id): soft-delete slideupdateExpireDate($id, Request $request): update expire_date for info slides
- Create reusable Vue component
resources/js/Components/SlideUploader.vue:- Uses
@jaxtheprime/vue3-dropzonefor drag-and-drop upload area - Big "+" icon / dotted border area for drag-and-drop or click-to-upload
- Accepts: png, jpg, jpeg, ppt, pptx, zip
- Shows upload progress bar
- Props:
type(information|moderation|sermon),serviceId(nullable),showExpireDate(bool) - If
showExpireDate=true: show datepicker for expire date applied to all uploaded files - After upload: emit event to refresh parent's slide list
- Uses
- Create
resources/js/Components/SlideGrid.vue:- Grid of thumbnails for uploaded slides
- Each thumbnail shows: image preview, upload date (muted), uploader name (muted)
- If type=information: prominent expire date field with inline datepicker for editing
- Delete button (soft-delete with confirmation)
- For PPT uploads: show progress indicator while job is processing
- Use Inertia
router.postwith FormData for file uploads - Handle PPT async: poll for job completion, then refresh slide grid
Must NOT do:
- Do NOT process PPT synchronously in the request (use job from T5)
- Do NOT allow file types outside png/jpg/jpeg/ppt/pptx/zip
- Do NOT create page-level components (these are reusable building blocks)
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
frontend-ui-ux]frontend-ui-ux: Upload area and thumbnail grid need polished UI
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T8-T9, T11-T13)
- Blocks: T15, T16, T17
- Blocked By: T2, T5, T6
References:
app/Services/FileConversionService.php— conversion logic from T5app/Jobs/ConvertPowerPointJob.php— async PPT processing from T5app/Models/Slide.php— slide model from T2@jaxtheprime/vue3-dropzonedocs: https://github.com/jaxtheprime/vue3-dropzone- AGENTS.md lines 51-54: "big plus icon/area for drag'n'drop", "thumbnails with upload date, uploader name, expire date"
- AGENTS.md lines 82-87: File upload specifications (image→JPG, PPT→slides, ZIP→extract)
Acceptance Criteria:
php artisan test --filter=SlideControllerTest→ PASS- Image upload → converted to 1920×1080 JPG + thumbnail
- ZIP upload → all contained images processed
- PPT upload → job dispatched, slides created after completion
- SlideUploader component renders drop zone with '+' area
- SlideGrid shows thumbnails with metadata
- Expire date editable inline for information slides
QA Scenarios:
Scenario: Upload image via drop zone Tool: Playwright Preconditions: Authenticated, on a page with SlideUploader component Steps: 1. Locate the upload area (dotted border / '+' icon area) 2. Upload a test 400x300 PNG via file input 3. Assert: progress bar appears and completes 4. Assert: new thumbnail appears in SlideGrid 5. Assert: thumbnail shows upload date and uploader name Expected Result: Image uploaded, converted, and displayed as thumbnail Evidence: .sisyphus/evidence/task-10-slide-upload.png Scenario: Reject invalid file type Tool: Playwright Steps: 1. Try to upload a .txt file 2. Assert: error message shown (e.g. 'Dateityp nicht erlaubt') 3. Assert: no slide created in grid Expected Result: Invalid files rejected with German error message Evidence: .sisyphus/evidence/task-10-invalid-file.pngCommit: YES
- Message:
feat: add reusable slide upload and grid components - Files:
app/Http/Controllers/SlideController.php,resources/js/Components/SlideUploader.vue,resources/js/Components/SlideGrid.vue - Pre-commit:
php artisan test
-
11. Arrangement Model + Configurator Component
What to do:
- TEST: Write Pest tests for ArrangementController — create, clone, update group order, delete
- Create
App\Http\Controllers\ArrangementControllerwith:store(Song $song): create new arrangement (clone from default order), accept name via requestclone($id): duplicate an existing arrangement with new nameupdate($id): save reordered groups (accept array of{song_group_id, order})destroy($id): delete arrangement (prevent deleting last one)
- Create
resources/js/Components/ArrangementConfigurator.vue:- Select dropdown listing all arrangements for this song ("Normal" pre-selected as default)
- "Hinzufügen" button → prompt for name, creates new arrangement (clone from default)
- "Klonen" button → prompt for name, clones current arrangement
- Below select: show the groups of the selected arrangement as colored pills (like
ref/form-song-arangment-config.png) - Each pill shows group name with its color background
- Pool area showing all available groups from the song (source for drag)
- Drag-and-drop to reorder pills and add groups from pool
- Use
vue-draggable-pluswith clone mode for pool→sequence and sort mode for reordering - CRITICAL: Vue key must be
${group.id}-${index}not justgroup.id(groups can repeat) - Auto-save on every drag-end via Inertia
router.putwithpreserveScroll: true
- Each group pill should have: color background, name text, remove button (×)
- Color picker integration: clicking on a group in the pool lets you change its color (stored in
song_groups.color)
Must NOT do:
- Do NOT allow deleting the last arrangement of a song
- Do NOT sync arrangements from CTS API (manually created only)
- Do NOT use
group.idalone as Vue key (groups repeat in arrangements)
Recommended Agent Profile:
- Category:
deep - Skills: [
frontend-ui-ux]frontend-ui-ux: Complex drag-and-drop interaction with colored pills requires careful UI work
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T8-T10, T12-T13)
- Blocks: T18, T21
- Blocked By: T2, T9
References:
ref/form-song-arangment-config.png— reference UI screenshot showing colored pills, arrangement dropdown, group poolvue-draggable-plusdocs: clone mode for pool→sequence dragapp/Models/SongArrangement.php,app/Models/SongArrangementGroup.php— models from T2app/Models/SongGroup.php— groups with color field from T2- AGENTS.md lines 73-77: arrangement select, add/clone buttons, drag-and-drop groups
Acceptance Criteria:
php artisan test --filter=ArrangementControllerTest→ PASS- Create arrangement: new arrangement appears in select
- Clone arrangement: exact copy with new name
- Drag-and-drop reorder persists on drop-end
- Groups from pool can be dragged into arrangement (clone mode)
- Groups can be removed from arrangement via × button
- Color picker changes group color and persists
- Cannot delete last arrangement (error shown)
QA Scenarios:
Scenario: Create and configure arrangement Tool: Playwright Preconditions: Song exists with 3 groups (Strophe 1, Refrain, Bridge) Steps: 1. Navigate to song arrangement area 2. Click 'Hinzufügen' button 3. Enter name 'Abend-Version' in prompt 4. Assert: new arrangement appears in select dropdown 5. Assert: arrangement shows all groups as colored pills 6. Drag 'Refrain' pill to position after 'Bridge' 7. Assert: order updated — Strophe 1, Bridge, Refrain 8. Reload page 9. Assert: new order persisted Expected Result: Arrangement created and reorderable via drag-and-drop Evidence: .sisyphus/evidence/task-11-arrangement-create.png Scenario: Clone arrangement Tool: Playwright Steps: 1. Select 'Abend-Version' arrangement 2. Click 'Klonen' button 3. Enter name 'Spezial' 4. Assert: new arrangement 'Spezial' appears in dropdown 5. Assert: same group order as cloned source Expected Result: Arrangement cloned successfully Evidence: .sisyphus/evidence/task-11-arrangement-clone.pngCommit: YES
- Message:
feat: add arrangement configurator with drag-and-drop group management - Files:
app/Http/Controllers/ArrangementController.php,resources/js/Components/ArrangementConfigurator.vue, routes - Pre-commit:
php artisan test
-
12. Song Matching Service (CCLI ID)
What to do:
- TEST: Write Pest tests for song matching — auto-match by CCLI, manual assign, request email
- Create
App\Services\SongMatchingServicewith:autoMatch(ServiceSong $serviceSong): look upsongs.ccli_idmatchingservice_songs.cts_ccli_id, setsong_idandmatched_atif foundmanualAssign(ServiceSong $serviceSong, Song $song): manually assign a song to a service song, setmatched_atrequestCreation(ServiceSong $serviceSong): sendMissingSongRequestemail (from T7), setrequest_sent_at
- Hook auto-matching into
ChurchToolsService::syncAgenda()(from T4) — after creating/updating service_songs, runautoMatchon each - Create
App\Http\Controllers\ServiceSongControllerwith:assignSong($serviceSongId, Request $request): manually assign a song → acceptssong_idrequestSong($serviceSongId): trigger email and setrequest_sent_atunassign($serviceSongId): remove manual assignment
- Frontend will be built in T18 (Songs block) — this task is backend only
Must NOT do:
- Do NOT build the frontend UI here (that's T18)
- Do NOT auto-create songs from CTS API data
- Do NOT send email automatically on sync — only on explicit user request
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T8-T11, T13)
- Blocks: T18
- Blocked By: T2, T4, T7, T9
References:
app/Services/ChurchToolsService.php— sync pipeline from T4 where matching integratesapp/Mail/MissingSongRequest.php— email mailable from T7app/Models/ServiceSong.php— service_songs model from T2app/Models/Song.php— songs model from T2- AGENTS.md lines 67-70: CCLI matching, 'request creation' button, manual select field
Acceptance Criteria:
php artisan test --filter=SongMatchingTest→ PASS- Auto-match: service song with CCLI 12345 matches song with ccli_id 12345
- Manual assign: endpoint sets song_id and matched_at
- Request email: sends MissingSongRequest email, sets request_sent_at
- Unmatched songs remain with song_id = null
QA Scenarios:
Scenario: Auto-match song by CCLI ID Tool: Bash Preconditions: Song with ccli_id='12345' in DB, service_song with cts_ccli_id='12345' and song_id=null Steps: 1. Run: php artisan cts:sync (or call matching directly via tinker) 2. Query: ServiceSong where cts_ccli_id='12345' 3. Assert: song_id is now set to the matching Song's id 4. Assert: matched_at is not null Expected Result: Auto-matching links service song to DB song Evidence: .sisyphus/evidence/task-12-auto-match.txt Scenario: Request missing song email Tool: Bash Preconditions: Unmatched service_song exists, MAIL config set to log driver Steps: 1. POST /api/service-songs/{id}/request 2. Assert: HTTP 200 3. Check laravel.log or mail log for 'Song-Anfrage' subject 4. Assert: service_song.request_sent_at is now set Expected Result: Email sent and timestamp recorded Evidence: .sisyphus/evidence/task-12-request-email.txtCommit: YES
- Message:
feat: add song matching service with CCLI auto-match and request email - Files:
app/Services/SongMatchingService.php,app/Http/Controllers/ServiceSongController.php, routes - Pre-commit:
php artisan test
-
13. Translation Service (URL Scrape + Manual)
What to do:
- TEST: Write Pest tests for TranslationService — mock HTTP scrape, manual text import, line-count matching
- Create
App\Services\TranslationServicewith:fetchFromUrl(string $url): ?string— best-effort HTTP GET, extract text content (strip HTML tags), return raw text or null on failureimportTranslation(Song $song, string $text): distribute translated text across slides- For each group's slides: take matching line count from translated text
- If original slide has 4 lines → take next 4 lines from translation
- Store in
song_slides.text_content_translated
markAsTranslated(Song $song): setsongs.has_translation = trueremoveTranslation(Song $song): clear alltext_content_translated, sethas_translation = false
- Create
App\Http\Controllers\TranslationControllerwith:fetchUrl(Request $request): accept URL, return scraped text for review before importimport(Song $song, Request $request): accept full text, runimportTranslation
- URL scraping is best-effort only — gracefully handle failures (return null, show German error message)
Must NOT do:
- Do NOT build site-specific scrapers (only generic HTTP fetch + strip tags)
- Do NOT auto-save URL fetch result (user reviews text first, then explicitly saves)
- Do NOT build the translation editor UI here (that's T22)
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 2 (with T8-T12)
- Blocks: T22
- Blocked By: T2, T9
References:
app/Models/Song.php—has_translationfield from T2app/Models/SongSlide.php—text_content_translatedfield from T2- AGENTS.md line 96: "allow add a full text or an URL to the Full text ... always the same line qty of text from the original"
Acceptance Criteria:
php artisan test --filter=TranslationServiceTest→ PASS- URL fetch returns text content or null (no exception)
- Import distributes lines matching original slide line counts
- Song marked as
has_translation = trueafter import - Remove translation clears all translated text and sets flag to false
QA Scenarios:
Scenario: Import translation with line-count matching Tool: Bash Preconditions: Song with 2 groups, group 1 has 2 slides (4 lines, 2 lines), group 2 has 1 slide (4 lines) Steps: 1. Prepare translated text: 10 lines total 2. POST /api/songs/{id}/translation/import with {text: '10 lines...'} 3. Query song slides 4. Assert: slide 1 has 4 translated lines 5. Assert: slide 2 has 2 translated lines 6. Assert: slide 3 has 4 translated lines 7. Assert: song.has_translation = true Expected Result: Translation distributed by slide line counts Evidence: .sisyphus/evidence/task-13-translation-import.txt Scenario: URL fetch failure handled gracefully Tool: Bash Steps: 1. POST /api/translation/fetch-url with {url: 'https://nonexistent.invalid/lyrics'} 2. Assert: HTTP 200 with {text: null, error: 'Konnte Text nicht abrufen'} Expected Result: Graceful failure with German error message Evidence: .sisyphus/evidence/task-13-url-failure.txtCommit: YES
- Message:
feat: add translation service with URL scrape and line-count distribution - Files:
app/Services/TranslationService.php,app/Http/Controllers/TranslationController.php, routes - Pre-commit:
php artisan test
Wave 3: Service Edit + Song UI (6 parallel tasks)
-
14. Service Edit Page Layout + Routing
What to do:
- TEST: Write Pest test for ServiceController edit/update — verify Inertia render with service data and related models
- Add
edit($id)method toServiceController:- Load service with: service_songs (with song, arrangement), slides (information/moderation/sermon), preacher, beamer tech
- Return Inertia render
Services/Edit
- Create
resources/js/Pages/Services/Edit.vue:- Page title: service title + date
- 4 collapsible/tabbed blocks: Information, Moderation, Predigt, Songs
- Each block is a child component (from T15-T18)
- Auto-save behavior inherited from
useAutoSavecomposable (T6) - Back button: navigates to service list
- Add route:
GET /services/{id}/edit - Wire up block components with proper props (service, songs, slides)
Must NOT do:
- Do NOT implement block content here (just the layout/routing shell)
- Do NOT add a save button (auto-save only)
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
frontend-ui-ux]frontend-ui-ux: Page layout with collapsible sections requires good visual structure
Parallelization:
- Can Run In Parallel: YES (first in Wave 3, but other Wave 3 tasks can start in parallel)
- Parallel Group: Wave 3 (with T15-T19)
- Blocks: T15, T16, T17, T18, T19
- Blocked By: T8, T6
References:
app/Http/Controllers/ServiceController.php— extend with edit method (from T8)resources/js/Layouts/AuthenticatedLayout.vue— layout wrapper (from T6)resources/js/Composables/useAutoSave.js— auto-save composable (from T6)- AGENTS.md lines 40-45: Edit shows form with blocks (information, moderation, sermon, songs)
Acceptance Criteria:
php artisan test --filter=ServiceEditTest→ PASS/services/{id}/editrenders edit page with service data- All 4 blocks visible (Information, Moderation, Predigt, Songs)
- Back button navigates to service list
- Page title shows service name and date
QA Scenarios:
Scenario: Service edit page renders with all blocks Tool: Playwright Preconditions: Service with id=1 exists, has songs and slides Steps: 1. Navigate to http://localhost:8000/services/1/edit 2. Assert: page title contains service title 3. Assert: page contains section/tab labeled 'Information' 4. Assert: page contains section/tab labeled 'Moderation' 5. Assert: page contains section/tab labeled 'Predigt' 6. Assert: page contains section/tab labeled 'Songs' 7. Assert: back button/link exists pointing to /services Expected Result: Edit page with all 4 German-labeled blocks Evidence: .sisyphus/evidence/task-14-edit-layout.pngCommit: YES
- Message:
feat: add service edit page layout with 4-block structure - Files:
resources/js/Pages/Services/Edit.vue, routes - Pre-commit:
php artisan test
-
15. Information Block (Slides + Expire Dates)
What to do:
- TEST: Write Pest test that verifies info slides are dynamically queried (expire_date > service_date)
- Create
resources/js/Components/Blocks/InformationBlock.vue:- Uses
SlideGridcomponent (from T10) to display information slides - Uses
SlideUploadercomponent (from T10) withshowExpireDate=true - Dynamically shows all slides where
type='information' AND expire_date >= service.date - Each slide shows: thumbnail, upload date (muted), uploader name (muted), prominent expire date with inline datepicker
- Delete button (soft-delete) on each slide
- Editing expire date saves immediately (auto-save)
- Uses
- Create backend support:
SlideControllermethod to query information slides by service date - Information slides are NOT tied to a specific service — they appear in ALL services where
expire_date >= service.date
Must NOT do:
- Do NOT copy info slides into services (query dynamically by date)
- Do NOT show expired slides
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
frontend-ui-ux]frontend-ui-ux: Thumbnail grid with prominent expire dates needs polished layout
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 3 (with T14, T16-T19)
- Blocks: T24
- Blocked By: T10, T14
References:
resources/js/Components/SlideUploader.vue,resources/js/Components/SlideGrid.vue— from T10app/Models/Slide.php— slide model from T2- AGENTS.md lines 49-54: Information block specification (thumbnails, expire date, uploader, drag-and-drop upload)
- AGENTS.md line 54: "automatically show these files to all future services, till the expire date is after the service date"
Acceptance Criteria:
- Info slides from other services shown if expire_date >= current service date
- Expired slides NOT shown
- Upload new slide with expire date → appears in slide grid
- Change expire date inline → saves immediately
- Delete slide → soft-deleted, disappears from grid
QA Scenarios:
Scenario: Info slides shown dynamically by expire date Tool: Playwright Preconditions: 2 info slides in DB — one expires tomorrow, one expired yesterday. Service date = today. Steps: 1. Navigate to /services/{id}/edit 2. Open Information block 3. Assert: only the non-expired slide is visible (1 thumbnail) 4. Assert: expired slide is NOT shown 5. Assert: visible slide shows expire date prominently Expected Result: Dynamic filtering by expire_date vs service date Evidence: .sisyphus/evidence/task-15-info-dynamic.png Scenario: Upload new info slide with expire date Tool: Playwright Steps: 1. In Information block, select expire date via datepicker (2 weeks from now) 2. Upload a test PNG file 3. Assert: new thumbnail appears in grid 4. Assert: expire date shown matches selected date Expected Result: New info slide uploaded with correct expire date Evidence: .sisyphus/evidence/task-15-info-upload.pngCommit: YES
- Message:
feat: add Information block with dynamic expire-date filtering - Files:
resources/js/Components/Blocks/InformationBlock.vue - Pre-commit:
php artisan test
-
16. Moderation Block (Slides, Service-Specific)
What to do:
- TEST: Write Pest test verifying moderation slides are service-specific (not shared across services)
- Create
resources/js/Components/Blocks/ModerationBlock.vue:- Same UI as Information block BUT:
- NO expire date field (no datepicker)
- Slides are tied to THIS service only (service_id FK set)
- Uses
SlideGridcomponent (from T10) withshowExpireDate=false - Uses
SlideUploadercomponent (from T10) withshowExpireDate=false - Upload, view thumbnails, delete — standard slide management
- Backend: SlideController already handles this — just pass
type='moderation'andservice_id
Must NOT do:
- Do NOT add expire date functionality (that's Information block only)
- Do NOT share moderation slides across services
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 3 (with T14-T15, T17-T19)
- Blocks: T24
- Blocked By: T10, T14
References:
resources/js/Components/Blocks/InformationBlock.vue— same pattern but simpler (from T15)resources/js/Components/SlideUploader.vue,resources/js/Components/SlideGrid.vue— from T10- AGENTS.md line 58: "Same features as Block Information but without the datepicker and only relevant for this service"
Acceptance Criteria:
- Moderation slides belong to specific service (not shared)
- No expire date field visible
- Upload, view, delete works same as information block
QA Scenarios:
Scenario: Moderation slides are service-specific Tool: Playwright Preconditions: Service A has 2 moderation slides. Service B has 0. Steps: 1. Navigate to /services/{A}/edit, open Moderation block 2. Assert: 2 slides visible 3. Navigate to /services/{B}/edit, open Moderation block 4. Assert: 0 slides visible (no cross-service sharing) 5. Assert: no datepicker / expire date field visible Expected Result: Moderation slides scoped to individual service Evidence: .sisyphus/evidence/task-16-moderation-scoped.pngCommit: YES (group with T17)
- Message:
feat: add Moderation and Sermon slide blocks - Files:
resources/js/Components/Blocks/ModerationBlock.vue - Pre-commit:
php artisan test
-
17. Sermon Block (Slides, Service-Specific)
What to do:
- TEST: Write Pest test verifying sermon slides are service-specific
- Create
resources/js/Components/Blocks/SermonBlock.vue:- Identical to Moderation block but with
type='sermon' - Service-specific slides, no expire date
- Uses same
SlideGridandSlideUploadercomponents
- Identical to Moderation block but with
- Essentially a thin wrapper passing
type='sermon'to the shared components
Must NOT do:
- Do NOT add any features beyond what Moderation block has
- Do NOT share sermon slides across services
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 3 (with T14-T16, T18-T19)
- Blocks: T24
- Blocked By: T10, T14
References:
resources/js/Components/Blocks/ModerationBlock.vue— identical pattern (from T16)- AGENTS.md line 62: "Same features as Block Moderation"
Acceptance Criteria:
- Sermon slides are service-specific
- Same upload/view/delete functionality as Moderation block
- type='sermon' correctly set on new slides
QA Scenarios:
Scenario: Sermon block functions identically to Moderation Tool: Playwright Preconditions: Service with no sermon slides Steps: 1. Navigate to /services/{id}/edit, open Predigt block 2. Assert: empty state with upload area 3. Upload a test image 4. Assert: thumbnail appears with upload date and uploader name 5. Assert: no expire date field Expected Result: Sermon block works like Moderation Evidence: .sisyphus/evidence/task-17-sermon-block.pngCommit: YES (group with T16)
- Message:
feat: add Moderation and Sermon slide blocks - Files:
resources/js/Components/Blocks/SermonBlock.vue - Pre-commit:
php artisan test
-
18. Songs Block (Matching + Arrangement + Translation)
What to do:
- TEST: Write Pest tests for the Songs block Inertia endpoint — verify service songs returned with match status, arrangement data
- Create
resources/js/Components/Blocks/SongsBlock.vue— the most complex block:- Show all service songs in order from CTS agenda
- Each song row shows: Name, CCLI ID, "Hat Übersetzung" indicator
- If NOT matched (song_id = null):
- Show "Erstellung anfragen" button → calls
POST /api/service-songs/{id}/request(from T12) - Show searchable select dropdown of all songs from DB (title + CCLI searchable) → calls manual assign (from T12)
- Show "Erstellung anfragen" button → calls
- If matched (song_id set):
- If song
has_translation=true: show checkbox "Übersetzung verwenden" (default: checked), auto-saves - Show
ArrangementConfiguratorcomponent (from T11) for this song in this service context - Default arrangement "Normal" pre-selected
- Show "Vorschau" button → opens preview modal (from T19)
- Show "PDF herunterladen" button → triggers PDF download (from T19)
- If song
- Use auto-save for all changes (checkbox toggle, arrangement selection)
- Song order matches CTS agenda order (not draggable here)
Must NOT do:
- Do NOT allow reordering songs (order comes from CTS API)
- Do NOT allow adding/removing songs (that happens in CTS)
- Do NOT build the preview modal here (that's T19)
Recommended Agent Profile:
- Category:
deep - Skills: [
frontend-ui-ux]frontend-ui-ux: Complex conditional UI (matched vs unmatched states) needs careful design
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 3 (with T14-T17, T19)
- Blocks: T24
- Blocked By: T11, T12, T14
References:
resources/js/Components/ArrangementConfigurator.vue— arrangement component (from T11)app/Http/Controllers/ServiceSongController.php— matching endpoints (from T12)app/Models/ServiceSong.php— service_songs model (from T2)- AGENTS.md lines 64-80: Full Songs block specification (matching, arrangement, preview, download)
Acceptance Criteria:
- Songs shown in CTS agenda order
- Unmatched songs show 'Erstellung anfragen' button and searchable select
- Matched songs show arrangement configurator
- Translation checkbox appears for translated songs
- Vorschau and PDF buttons visible for songs with arrangements
- All auto-save interactions work
QA Scenarios:
Scenario: Unmatched song shows matching options Tool: Playwright Preconditions: Service with 1 unmatched song (song_id=null, cts_song_name='Amazing Grace') Steps: 1. Navigate to /services/{id}/edit, open Songs block 2. Assert: song row shows 'Amazing Grace' 3. Assert: 'Erstellung anfragen' button visible 4. Assert: searchable select dropdown visible 5. Assert: NO arrangement configurator shown (not matched yet) Expected Result: Unmatched song shows request and manual assign options Evidence: .sisyphus/evidence/task-18-unmatched-song.png Scenario: Matched song shows arrangement and controls Tool: Playwright Preconditions: Service with 1 matched song (has_translation=true, has arrangement 'Normal') Steps: 1. Navigate to /services/{id}/edit, open Songs block 2. Assert: arrangement select shows 'Normal' selected 3. Assert: 'Übersetzung verwenden' checkbox visible and checked 4. Assert: 'Vorschau' button visible 5. Assert: 'PDF herunterladen' button visible Expected Result: Matched song shows full control panel Evidence: .sisyphus/evidence/task-18-matched-song.pngCommit: YES
- Message:
feat: add Songs block with matching UI and arrangement integration - Files:
resources/js/Components/Blocks/SongsBlock.vue - Pre-commit:
php artisan test
-
19. Song Preview Modal + PDF Download
What to do:
- TEST: Write Pest tests for PDF generation endpoint — verify PDF returns correct content type and contains song text
- Create
resources/js/Components/SongPreviewModal.vue:- HTML overlay/modal showing song text in arrangement order
- Each group's slides shown sequentially
- Group name shown as header with group's color as background
- If
use_translation=true: show original + translated text side by side (or translated below original) - Copyright footer at bottom from song metadata
- Close button / click-outside to dismiss
- Create
App\Http\Controllers\SongPdfControllerwithdownload($songId, $arrangementId)method:- Use
barryvdh/laravel-dompdfto generate PDF - Template:
resources/views/pdf/song.blade.php - MUST use old-school CSS (no Tailwind classes!) with
DejaVu Sansfont for German umlauts - Layout: song title header, groups with colored headers, slide text, copyright footer
- If
use_translation=true: include translation text - Return PDF download with filename
{song-title}-{arrangement-name}.pdf
- Use
- Wire up buttons: "Vorschau" opens modal, "PDF herunterladen" triggers download
Must NOT do:
- Do NOT use Tailwind CSS in the DomPDF Blade template (DomPDF doesn't support it)
- Do NOT use web fonts in PDF (use DejaVu Sans which is bundled with DomPDF)
Recommended Agent Profile:
- Category:
unspecified-high - Skills: [
frontend-ui-ux]frontend-ui-ux: Preview modal needs clean, readable formatting
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 3 (with T14-T18)
- Blocks: —
- Blocked By: T9, T11
References:
barryvdh/laravel-dompdfdocs: https://github.com/barryvdh/laravel-dompdfapp/Models/Song.php,app/Models/SongGroup.php,app/Models/SongSlide.php— data models from T2app/Models/SongArrangement.php,app/Models/SongArrangementGroup.php— arrangement models from T2- AGENTS.md line 79: "preview: show text of the song in the order of the arrangement configuration, prominent highlighted which textpart was with group"
- AGENTS.md line 80: "download: download the preview as a nice pdf with header/footer and copyright footer"
Acceptance Criteria:
- Preview modal opens with song text in arrangement order
- Group headers show name with group color
- PDF downloads with correct filename
- PDF renders German umlauts correctly (DejaVu Sans)
- PDF includes copyright footer
- Translation text shown when use_translation=true
QA Scenarios:
Scenario: Preview modal shows formatted song text Tool: Playwright Preconditions: Song with 3 groups (Strophe 1, Refrain, Bridge), arrangement 'Normal' ordering all 3 Steps: 1. Navigate to song's 'Vorschau' button and click it 2. Assert: modal/overlay appears 3. Assert: first section header contains 'Strophe 1' with colored background 4. Assert: slide text content visible below each header 5. Assert: copyright text shown at bottom 6. Click outside modal or close button 7. Assert: modal dismissed Expected Result: Clean formatted preview with colored group headers Evidence: .sisyphus/evidence/task-19-preview-modal.png Scenario: PDF download with German umlauts Tool: Bash (curl) Steps: 1. GET /api/songs/{id}/pdf/{arrangementId} 2. Assert: Content-Type is application/pdf 3. Assert: Content-Disposition contains song title in filename 4. Save to /tmp/song-test.pdf 5. Run: pdftotext /tmp/song-test.pdf - | head -20 6. Assert: output contains song title and group names 7. Assert: German umlauts (äöüß) render correctly (not garbled) Expected Result: Valid PDF with correct text and umlauts Evidence: .sisyphus/evidence/task-19-pdf-download.txtCommit: YES
- Message:
feat: add song preview modal and PDF download with DomPDF - Files:
resources/js/Components/SongPreviewModal.vue,app/Http/Controllers/SongPdfController.php,resources/views/pdf/song.blade.php, routes - Pre-commit:
php artisan test
Wave 4: Song DB + Finalization (5 parallel tasks)
-
20. Song DB Page (List + Search + Filters)
What to do:
- TEST: Write Pest test for SongDB index page — verify Inertia render with paginated songs, search, soft-delete excluded
- Create
resources/js/Pages/Songs/Index.vue:- Table showing all songs: Titel, CCLI-ID, Erstellt, Letzte Änderung, Zuletzt verwendet, Hat Übersetzung
- Search bar: search by song name or CCLI ID
- Each row has action buttons: "Bearbeiten" (edit), "Löschen" (soft-delete), "Herunterladen" (.pro download), "Übersetzen" (translate)
- Upload area at top: drag-and-drop / click for .pro file upload (placeholder from T23)
- Soft-deleted songs not shown (can add 'Papierkorb anzeigen' toggle later)
- Pagination for large song lists
- Add route:
GET /songs(handled by SongController index from T9) - Wire up to navigation: "Song-Datenbank" link in top bar (from T6)
Must NOT do:
- Do NOT implement .pro parser (placeholder from T23)
- Do NOT show soft-deleted songs by default
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
frontend-ui-ux]frontend-ui-ux: Data table with search, actions, and upload area needs clean design
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 4 (with T21-T24)
- Blocks: —
- Blocked By: T9, T6
References:
app/Http/Controllers/SongController.php— backend CRUD from T9resources/js/Layouts/AuthenticatedLayout.vue— layout with navigation from T6- AGENTS.md lines 91-93: "shows all songs from DB with created, last update, ccliID, last_used_in_service" + action buttons
Acceptance Criteria:
- Song list page renders with correct columns in German
- Search by name returns matching songs
- Search by CCLI ID returns matching songs
- Action buttons visible on each row
- Pagination works for large lists
- Upload area visible at top
QA Scenarios:
Scenario: Song database page with search Tool: Playwright Preconditions: 5 songs in DB, one named 'Großer Gott' Steps: 1. Navigate to http://localhost:8000/songs 2. Assert: table shows 5 song rows 3. Assert: columns include 'Titel', 'CCLI-ID', 'Zuletzt verwendet' 4. Type 'Großer' in search bar 5. Assert: table filters to 1 result containing 'Großer Gott' 6. Assert: row shows 'Bearbeiten', 'Löschen', 'Herunterladen', 'Übersetzen' buttons Expected Result: Searchable song list with German UI Evidence: .sisyphus/evidence/task-20-songdb-list.pngCommit: YES
- Message:
feat: add Song Database page with search and action buttons - Files:
resources/js/Pages/Songs/Index.vue, routes - Pre-commit:
php artisan test
-
21. Song DB Edit Popup (Metadata + Arrangement)
What to do:
- TEST: Write Pest test for song update endpoint — verify metadata update works
- Create
resources/js/Components/SongEditModal.vue:- Modal/popup triggered by "Bearbeiten" button on song list
- Fields: Titel (text input), CCLI-ID (text input), Copyright-Text (textarea)
- Show any additional metadata available from song model
- Below metadata: embed
ArrangementConfiguratorcomponent (from T11) - Auto-save on all field changes (use
useAutoSavefrom T6) - Close button / click-outside to dismiss
- Wire up to
SongController::update()for metadata saves
Must NOT do:
- Do NOT allow editing song groups/slides content here (that's for the translate page)
- Do NOT duplicate the arrangement logic (reuse component from T11)
Recommended Agent Profile:
- Category:
visual-engineering - Skills: [
frontend-ui-ux]frontend-ui-ux: Modal form with embedded arrangement configurator needs good UX flow
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 4 (with T20, T22-T24)
- Blocks: —
- Blocked By: T9, T11
References:
resources/js/Components/ArrangementConfigurator.vue— arrangement component from T11app/Http/Controllers/SongController.php— update endpoint from T9resources/js/Composables/useAutoSave.js— auto-save composable from T6- AGENTS.md line 94: "edit: shows a popup with Name, CCLI and copyright text and the arrangement configurator"
Acceptance Criteria:
- Modal opens with song metadata pre-filled
- Editing title auto-saves after debounce
- CCLI ID editable and saves
- Copyright text editable and saves
- Arrangement configurator rendered inside modal
- Modal closes on × or outside click
QA Scenarios:
Scenario: Edit song metadata via popup Tool: Playwright Preconditions: Song 'Amazing Grace' with CCLI 12345 in DB Steps: 1. Navigate to /songs 2. Click 'Bearbeiten' on 'Amazing Grace' row 3. Assert: modal appears with title 'Amazing Grace' pre-filled 4. Assert: CCLI field shows '12345' 5. Change title to 'Amazing Grace (Neu)' 6. Wait 600ms (debounce) 7. Close modal 8. Assert: song list shows updated title 'Amazing Grace (Neu)' Expected Result: Auto-saving metadata edit in modal Evidence: .sisyphus/evidence/task-21-song-edit.pngCommit: YES
- Message:
feat: add Song DB edit popup with metadata and arrangement configurator - Files:
resources/js/Components/SongEditModal.vue - Pre-commit:
php artisan test
-
22. Song DB Translate Page (Two-Column Editor)
What to do:
- TEST: Write Pest test for translation import endpoint — verify line-count distribution
- Create
resources/js/Pages/Songs/Translate.vue:- Page triggered by "Übersetzen" button on song list
- Top section: URL input field + "Text abrufen" button OR large textarea for manual paste
- After text is provided (via URL fetch or paste): show two-column editor
- Two-column editor for EACH slide of EACH group:
- Left column: original text (read-only, from
song_slides.text_content) - Right column: translation text editor (editable, from
song_slides.text_content_translated) - Each editor area has same line count as original (constrained by original line count)
- Group headers between sections (colored, matching group color)
- Left column: original text (read-only, from
- "Speichern" button saves all translations at once
- After save: song marked as
has_translation = true
- Add route:
GET /songs/{id}/translate - Wire up to
TranslationController(from T13) for URL fetch and import - Use Inertia form for submission
Must NOT do:
- Do NOT allow changing the original text (left column is read-only)
- Do NOT allow adding more lines than the original has per slide
Recommended Agent Profile:
- Category:
deep - Skills: [
frontend-ui-ux]frontend-ui-ux: Two-column editor with constrained line counts needs precise UI
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 4 (with T20-T21, T23-T24)
- Blocks: —
- Blocked By: T13, T9
References:
app/Services/TranslationService.php— translation backend from T13app/Http/Controllers/TranslationController.php— URL fetch and import endpoints from T13app/Models/SongSlide.php—text_contentandtext_content_translatedfields from T2- AGENTS.md line 96: "two columns for every slide of every group. Left the original text, right a texteditor, with the imported text — always the same line qty"
Acceptance Criteria:
- Translate page shows URL input and paste area
- URL fetch returns text for review
- Two-column editor shows original (read-only) and translation (editable)
- Line count per slide matches original
- Save marks song as
has_translation = true - Group headers with colors shown between sections
QA Scenarios:
Scenario: Two-column translation editor Tool: Playwright Preconditions: Song with 2 groups, 3 slides total (4 lines, 2 lines, 4 lines) Steps: 1. Navigate to /songs/{id}/translate 2. Paste 10 lines of translated text into textarea 3. Click import/distribute button 4. Assert: two-column editor appears 5. Assert: left columns show original text (read-only) 6. Assert: right columns show distributed translation text 7. Assert: slide 1 right column has exactly 4 lines 8. Assert: slide 2 right column has exactly 2 lines 9. Assert: slide 3 right column has exactly 4 lines 10. Click 'Speichern' 11. Navigate to /songs 12. Assert: song shows 'Hat Übersetzung' indicator Expected Result: Translation distributed correctly by line count Evidence: .sisyphus/evidence/task-22-translate-editor.png Scenario: URL fetch for lyrics Tool: Playwright Steps: 1. Navigate to /songs/{id}/translate 2. Enter a valid URL in URL field 3. Click 'Text abrufen' 4. Assert: either text appears in textarea OR error message 'Konnte Text nicht abrufen' shown Expected Result: URL fetch attempts text retrieval (best-effort) Evidence: .sisyphus/evidence/task-22-url-fetch.pngCommit: YES
- Message:
feat: add Song DB translation page with two-column editor - Files:
resources/js/Pages/Songs/Translate.vue, routes - Pre-commit:
php artisan test
-
23. Song DB .pro Upload + Download Placeholders
What to do:
- TEST: Write Pest test that verifies .pro upload endpoint throws NotImplementedException, and .pro download endpoint returns a stub file
- Create upload endpoint
POST /api/songs/import-pro:- Accept .pro file(s) or ZIP containing .pro files
- Throw
App\Exceptions\ProParserNotImplementedExceptionwith message "Der .pro-Parser wird später implementiert. Bitte warte auf die detaillierte Spezifikation." - Return HTTP 501 with JSON error message
- Create download endpoint
GET /api/songs/{id}/download-pro:- Throw same
ProParserNotImplementedExceptionwith message "Der .pro-Generator wird später implementiert." - Return HTTP 501
- Throw same
- Create custom exception class
App\Exceptions\ProParserNotImplementedException - Wire up upload area on Song DB page (from T20) to call the upload endpoint — show the error toast when it returns 501
- Wire up "Herunterladen" button on Song DB page to call download endpoint — show error toast
Must NOT do:
- Do NOT implement actual .pro file parsing (placeholder only!)
- Do NOT implement actual .pro file generation (placeholder only!)
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 4 (with T20-T22, T24)
- Blocks: —
- Blocked By: T9
References:
- AGENTS.md line 23: "parser and generator of song files (.pro) are added later, to just add simple placeholder"
- AGENTS.md line 95: "download: download generated .pro file" — placeholder for now
- AGENTS.md line 97: "UploadArea for .pro file ... which should be parsed (this module was integrated later, so show an Exception here)"
Acceptance Criteria:
php artisan test --filter=ProPlaceholderTest→ PASS- POST /api/songs/import-pro → HTTP 501 with German error message
- GET /api/songs/{id}/download-pro → HTTP 501 with German error message
- UI shows toast/alert with German error when clicking upload or download
QA Scenarios:
Scenario: .pro upload returns not-implemented error Tool: Bash (curl) Steps: 1. Create dummy file: echo 'test' > /tmp/test.pro 2. POST /api/songs/import-pro with file upload 3. Assert: HTTP 501 4. Assert: response contains 'später implementiert' Expected Result: Placeholder error returned Evidence: .sisyphus/evidence/task-23-pro-upload.txt Scenario: .pro download returns not-implemented error Tool: Bash (curl) Steps: 1. GET /api/songs/1/download-pro 2. Assert: HTTP 501 3. Assert: response contains 'später implementiert' Expected Result: Placeholder error returned Evidence: .sisyphus/evidence/task-23-pro-download.txtCommit: YES
- Message:
feat: add .pro file upload/download placeholders (NotImplementedException) - Files:
app/Exceptions/ProParserNotImplementedException.php, routes - Pre-commit:
php artisan test
-
24. Service Finalization + Status Management
What to do:
- TEST: Write Pest tests for finalization — verify status transitions, computed status fields, download placeholder
- Review and finalize the finalization logic (partially in T8):
POST /services/{id}/finalize: setfinalized_at = now(), verify all prerequisites metPOST /services/{id}/reopen: setfinalized_at = null- Prerequisite checks before finalization (warn, don't block):
- All songs should be matched (warn if not)
- All songs should have arrangements selected (warn if not)
- At least 1 sermon slide uploaded (warn if not)
- If prerequisites not met: show confirmation dialog in German listing missing items
- Implement download button behavior:
- Show toast/placeholder message: "Die Show-Erstellung wird in einem zukünftigen Update verfügbar sein."
- Return HTTP 501 from endpoint
- Update service list page (T8) status computation to be fully accurate:
songs_mapped: count of service_songs with song_id vs total service_songssongs_arranged: count of service_songs with song_arrangement_id vs total matchedhas_sermon_slides: count of sermon slides for this service > 0info_slides_count: count of info slides where expire_date >= service.datefinalized_at: timestamp or null
- Add computed
isReadyToFinalizeaccessor on Service model
Must NOT do:
- Do NOT block finalization on missing prerequisites (warn only)
- Do NOT implement actual show download (placeholder only)
Recommended Agent Profile:
- Category:
unspecified-high - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Parallel Group: Wave 4 (with T20-T23)
- Blocks: FINAL
- Blocked By: T15, T16, T17, T18
References:
app/Http/Controllers/ServiceController.php— finalize/reopen actions (from T8)app/Models/Service.php— service model from T2- AGENTS.md lines 36-38: "if finalized: ReOpen and Download", "if NOT finalized: Edit and Finalize"
- AGENTS.md line 40: "ReOpen and Finalize just change the status of the service"
Acceptance Criteria:
php artisan test --filter=FinalizationTest→ PASS- Finalize sets
finalized_attimestamp - ReOpen clears
finalized_at - Finalize with missing prerequisites shows German warning dialog
- Download button shows placeholder toast
- All status fields compute correctly on service list
QA Scenarios:
Scenario: Finalize service with warnings Tool: Playwright Preconditions: Service with 2 songs — 1 matched, 1 unmatched. No sermon slides. Steps: 1. Navigate to /services 2. Click 'Abschließen' on the service 3. Assert: confirmation dialog appears with warnings: - '1 Song ist noch nicht zugeordnet' - 'Keine Predigtfolien hochgeladen' 4. Confirm finalization 5. Assert: service now shows finalized_at timestamp 6. Assert: 'Wieder öffnen' and 'Herunterladen' buttons shown Expected Result: Finalization with warning but not blocked Evidence: .sisyphus/evidence/task-24-finalize-warnings.png Scenario: Download placeholder Tool: Playwright Preconditions: Service is finalized Steps: 1. Click 'Herunterladen' on finalized service 2. Assert: toast/message appears with 'zukünftigen Update' text Expected Result: Placeholder message shown Evidence: .sisyphus/evidence/task-24-download-placeholder.pngCommit: YES
- Message:
feat: add service finalization with status checks and download placeholder - Files:
app/Http/Controllers/ServiceController.php(extend),app/Models/Service.php(accessors) - Pre-commit:
php artisan test
Final Verification Wave (MANDATORY — after ALL implementation tasks)
4 review agents run in PARALLEL. ALL must APPROVE. Rejection → fix → re-run.
-
F1. Plan Compliance Audit —
oracleRead the plan end-to-end. For each "Must Have": verify implementation exists (read file, 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. Verify ALL UI text is German with "Du" form. Output:Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT -
F2. Code Quality Review —
unspecified-highRunphp artisan test+ linter. 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 TDD: test files exist for every feature. Verify no Tailwind in DomPDF templates. Output:Build [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT -
F3. Real Manual QA —
unspecified-high(+playwrightskill) Start from clean state (docker-compose up). Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration. Test edge cases: empty state, invalid input, rapid actions. All in German UI. Save to.sisyphus/evidence/final-qa/. Output:Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT -
F4. Scope Fidelity Check —
deepFor each task: read "What to do", read actual diff. Verify 1:1 match. Check "Must NOT do" compliance. Detect cross-task contamination. Flag unaccounted changes. Verify no CTS API writes. Verify .pro parser is placeholder only. Output:Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT
Commit Strategy
- T0:
chore: verify CTS API token auth and package compatibility - T1:
feat: scaffold Laravel + Breeze Vue + Docker setup - T2:
feat: add database schema for services, songs, arrangements, slides - T3:
feat: implement ChurchTools OAuth login via Socialite - T4:
feat: add CTS API sync service and artisan command - T5:
feat: add file conversion service (image, PPT, ZIP) - T6:
feat: create shared Vue layout with nav, user, refresh button - T7:
feat: configure email and missing-song notification mailable - T8:
feat: implement service list page with status indicators - T9:
feat: add Song model with CRUD and relationships - T10:
feat: create reusable slide upload component - T11:
feat: add arrangement model and drag-drop configurator - T12:
feat: implement CCLI-based song matching service - T13:
feat: add translation service with URL scraping - T14:
feat: create service edit page layout with block navigation - T15:
feat: implement information block with expire dates - T16:
feat: implement moderation block for service slides - T17:
feat: implement sermon block for service slides - T18:
feat: implement songs block with matching and arrangements - T19:
feat: add song preview modal and PDF download - T20:
feat: implement Song DB page with list and search - T21:
feat: add Song DB edit popup with arrangement config - T22:
feat: implement song translation two-column editor - T23:
feat: add .pro file upload/download placeholders - T24:
feat: implement service finalization and status management
Success Criteria
Verification Commands
docker-compose up -d # Expected: all containers healthy
docker-compose exec app php artisan test # Expected: all tests pass
docker-compose exec app php artisan migrate:status # Expected: all migrations ran
curl -I http://localhost:8000 # Expected: 302 redirect to OAuth login
Final Checklist
- All "Must Have" requirements present and working
- All "Must NOT Have" guardrails respected
- All tests pass (TDD — comprehensive coverage)
- All UI text in German with "Du" form
- Docker deployment works end-to-end
- Auto-save functional on every interactive element
- .pro parser/generator throws NotImplementedException
- Finalized download is placeholder