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
2115 lines
98 KiB
Markdown
2115 lines
98 KiB
Markdown
# 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-api` PHP 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-api` v2.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-plus` for arrangement drag-and-drop (clone mode, groups repeat)
|
||
- `@jaxtheprime/vue3-dropzone` for file upload zones (drop/preview/edit modes)
|
||
- `barryvdh/laravel-dompdf` for PDF generation (no Tailwind in templates — old-school CSS only)
|
||
- ProPresenter .pro: Pro6=XML, Pro7=Protobuf. Parser deferred.
|
||
- `@vueuse/core` for `useDebounceFn` (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-api` token auth unverified → Spike task verifies `CTConfig::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 Sans` font
|
||
- Lyrics URL scraping is fragile → Best-effort HTTP fetch with manual paste as primary fallback
|
||
- Arrangement Vue keys must use `${group.id}-${index}` not `group.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
|
||
- [x] `docker-compose up` starts working app on localhost
|
||
- [x] Login via ChurchTools OAuth works end-to-end
|
||
- [x] CTS API sync populates services and songs
|
||
- [x] All 4 edit blocks functional with auto-save
|
||
- [x] Song matching, arrangement config, translation all working
|
||
- [x] File uploads convert to 1920×1080 JPGs correctly
|
||
- [x] All tests pass (`php artisan test`)
|
||
- [x] 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
|
||
|
||
- [x] 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-api` via composer
|
||
- Verify token-based auth works: `CTConfig::setApiUrl()` + `CTConfig::setApiKey($token)` from `CTS_API_TOKEN` env var
|
||
- If `setApiKey` doesn't exist, check `authWithLoginToken()` or similar methods in CTConfig
|
||
- Hit `GET /api/events` with 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.json` and save to `docs/churchtools-openapi.json` for 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 TOKEN` header)
|
||
|
||
**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-api` docs: https://github.com/5pm-HDH/churchtools-api/blob/master/docs/out/CTConfig.md
|
||
- `5pm-HDH/churchtools-api` EventAPI: https://github.com/5pm-HDH/churchtools-api/blob/master/docs/out/EventAPI.md
|
||
- `5pm-HDH/churchtools-api` SongAPI: https://github.com/5pm-HDH/churchtools-api/blob/master/docs/out/SongAPI.md
|
||
- `.env.example` — contains `CTS_API_TOKEN=XXXXXX`
|
||
- CTS API docs at instance: `https://INSTANCE.church.tools/api` (Swagger UI)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `5pm-HDH/churchtools-api` installed and configured with token from `.env`
|
||
- [ ] `php 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.txt
|
||
```
|
||
|
||
**Commit**: 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)
|
||
|
||
- [x] 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 `.env` with all needed vars: `CTS_API_URL`, `CTS_API_TOKEN`, `CHURCHTOOLS_URL`, `CHURCHTOOLS_CLIENT_ID`, `CHURCHTOOLS_REDIRECT_URI`, `MAIL_*`
|
||
- Update `.env.example` with all new vars (placeholders)
|
||
- Create `Dockerfile` for PHP 8.3 + required extensions (imagick, zip, pdo_sqlite, pdo_mysql)
|
||
- Install LibreOffice headless + ImageMagick in Docker image
|
||
- Create `docker-compose.yml` with app + node (for Vite) services
|
||
- Configure `vite.config.js` for Docker hot-reload
|
||
- Verify `docker-compose up` starts app successfully
|
||
- Set app locale to `de` in `config/app.php`
|
||
- Add `@vueuse/core`, `vue-draggable-plus`, `@jaxtheprime/vue3-dropzone` to 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 errors
|
||
- [ ] `http://localhost:8000` returns HTTP response
|
||
- [ ] `npm run build` completes without errors
|
||
- [ ] `php artisan test` → default Breeze tests pass
|
||
- [ ] `.env.example` contains 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.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat: scaffold Laravel + Breeze Vue + Docker setup`
|
||
- Files: entire project scaffolding
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 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 errors
|
||
- [ ] `php 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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\ChurchToolsProvider` extending `AbstractProvider` with:
|
||
- `getAuthUrl()` → `{CTS_URL}/oauth/authorize`
|
||
- `getTokenUrl()` → `{CTS_URL}/oauth/access_token`
|
||
- `getUserByToken()` → `GET {CTS_URL}/oauth/userinfo`
|
||
- `mapUserToObject()` → maps id, displayName, email, imageUrl, groups, roles
|
||
- Register provider in `AppServiceProvider::boot()` via `Socialite::extend('churchtools', ...)`
|
||
- Add `config/services.php` entry for `churchtools` (url, client_id, client_secret, redirect)
|
||
- Create `AuthController` with `redirect()` and `callback()` 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.vue` page with the OAuth button
|
||
- Add logout route that clears session and redirects to login
|
||
- Protect all routes with `auth` middleware except login routes
|
||
- Update `.env.example` with `CHURCHTOOLS_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-client` source: 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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\ChurchToolsService` that wraps `5pm-HDH/churchtools-api`:
|
||
- `syncEvents()`: fetch events from today forward, upsert into `services` table
|
||
- `syncSongs()`: fetch all songs, upsert basic metadata into local reference
|
||
- `syncAgenda($eventId)`: fetch agenda for event, create/update `service_songs`
|
||
- `getEventServices($eventId)`: get assigned people (preacher, beamer tech)
|
||
- Create `App\Console\Commands\SyncChurchToolsCommand` (`php artisan cts:sync`)
|
||
- Store sync timestamp in `cts_sync_log` table
|
||
- When syncing songs to services: try to match `cts_ccli_id` to existing `songs.ccli_id`
|
||
- If matched: set `service_songs.song_id`, set `matched_at`
|
||
- If not matched: leave `song_id` null (UI will show matching options later)
|
||
- Create `App\Http\Controllers\SyncController` with `sync()` 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-api` EventAPI: `EventRequest::where('from', date)->get()`
|
||
- `5pm-HDH/churchtools-api` SongAPI: `SongRequest::all()`, `$song->getCcli()`
|
||
- `5pm-HDH/churchtools-api` EventAgendaRequest: `EventAgendaRequest::fromEvent($id)->get()`
|
||
- AGENTS.md: service list needs title, preacher, beamer tech, song count
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `php artisan test --filter=ChurchToolsSyncTest` → PASS
|
||
- [ ] `php artisan cts:sync` → populates services and service_songs tables
|
||
- [ ] Sync log entry created with count and status
|
||
- [ ] CCLI matching works: matched songs have `song_id` set
|
||
- [ ] Unmatched songs have `song_id = null`, `cts_ccli_id` preserved
|
||
|
||
**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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\FileConversionService` with 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 in `storage/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-image` docs: `(new Pdf($path))->setPage($n)->saveImage($out)`
|
||
- LibreOffice headless: `soffice --headless --convert-to pdf --outdir $dir $input`
|
||
- Docker: LibreOffice path is `/usr/bin/libreoffice` in 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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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.vue` with:
|
||
- Top bar showing logged-in user name + avatar (from `$page.props.auth.user`)
|
||
- "Daten aktualisieren" refresh button that calls `POST /sync` and shows loading spinner
|
||
- Timestamp "Zuletzt aktualisiert: {date}" from last sync log
|
||
- Navigation: "Services" link, "Song-Datenbank" link
|
||
- Logout button/link
|
||
- Update `HandleInertiaRequests` middleware to share: `auth.user`, `flash`, `last_synced_at`, `app_name`
|
||
- Create `AutoSaveForm` composable using `@vueuse/core` `useDebounceFn`:
|
||
- Text inputs: debounce 500ms
|
||
- Selects/checkboxes: immediate save
|
||
- Uses Inertia `router.put/post` with `preserveScroll: 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/vue3` for accessing shared props
|
||
- `useDebounceFn` from `@vueuse/core` for 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.png
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\MissingSongRequest` Mailable:
|
||
- 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\MissingSongNotification` as alternative (using Mail channel)
|
||
- Add `SONG_REQUEST_EMAIL` to 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_EMAIL` configurable 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.txt
|
||
```
|
||
|
||
**Commit**: 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)
|
||
|
||
- [x] 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\ServiceController` with `index()` 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/Index` with services collection
|
||
- 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}/finalize` and `POST /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 counts
|
||
- `app/Models/Slide.php` — for slide counts
|
||
- `resources/js/Layouts/AuthenticatedLayout.vue` — layout wrapper (from T6)
|
||
- AGENTS.md lines 29-45: Service list fields and action buttons specification
|
||
- Pattern: follow Breeze `Dashboard.vue` for 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_at` field
|
||
- [ ] 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.png
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\SongController` with full resource methods:
|
||
- `index()`: list all songs (with soft-deleted excluded), searchable by name and CCLI ID, paginated
|
||
- `store()`: create new song with metadata (title, ccli_id, author, copyright_text)
|
||
- `show($id)`: return song with groups, slides, arrangements
|
||
- `update($id)`: update song metadata
|
||
- `destroy($id)`: soft-delete
|
||
- Create `App\Http\Requests\SongRequest` for validation (title required, ccli_id unique)
|
||
- Create `App\Services\SongService` for business logic:
|
||
- `createDefaultGroups($song)`: create default groups (Strophe 1, Refrain, Bridge, etc.) if none exist
|
||
- `createDefaultArrangement($song)`: create "Normal" arrangement referencing all groups in order
|
||
- `duplicateArrangement($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 T2
|
||
- `app/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_service` accessor 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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\SlideController` with:
|
||
- `store(Request $request)`: accept file upload, determine type (image/ppt/zip), dispatch conversion
|
||
- `destroy($id)`: soft-delete slide
|
||
- `updateExpireDate($id, Request $request)`: update expire_date for info slides
|
||
- Create reusable Vue component `resources/js/Components/SlideUploader.vue`:
|
||
- Uses `@jaxtheprime/vue3-dropzone` for 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
|
||
- 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.post` with 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 T5
|
||
- `app/Jobs/ConvertPowerPointJob.php` — async PPT processing from T5
|
||
- `app/Models/Slide.php` — slide model from T2
|
||
- `@jaxtheprime/vue3-dropzone` docs: 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.png
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 11. Arrangement Model + Configurator Component
|
||
|
||
**What to do**:
|
||
- TEST: Write Pest tests for ArrangementController — create, clone, update group order, delete
|
||
- Create `App\Http\Controllers\ArrangementController` with:
|
||
- `store(Song $song)`: create new arrangement (clone from default order), accept name via request
|
||
- `clone($id)`: duplicate an existing arrangement with new name
|
||
- `update($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-plus` with clone mode for pool→sequence and sort mode for reordering
|
||
- CRITICAL: Vue key must be `${group.id}-${index}` not just `group.id` (groups can repeat)
|
||
- Auto-save on every drag-end via Inertia `router.put` with `preserveScroll: 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.id` alone 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 pool
|
||
- `vue-draggable-plus` docs: clone mode for pool→sequence drag
|
||
- `app/Models/SongArrangement.php`, `app/Models/SongArrangementGroup.php` — models from T2
|
||
- `app/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.png
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\SongMatchingService` with:
|
||
- `autoMatch(ServiceSong $serviceSong)`: look up `songs.ccli_id` matching `service_songs.cts_ccli_id`, set `song_id` and `matched_at` if found
|
||
- `manualAssign(ServiceSong $serviceSong, Song $song)`: manually assign a song to a service song, set `matched_at`
|
||
- `requestCreation(ServiceSong $serviceSong)`: send `MissingSongRequest` email (from T7), set `request_sent_at`
|
||
- Hook auto-matching into `ChurchToolsService::syncAgenda()` (from T4) — after creating/updating service_songs, run `autoMatch` on each
|
||
- Create `App\Http\Controllers\ServiceSongController` with:
|
||
- `assignSong($serviceSongId, Request $request)`: manually assign a song → accepts `song_id`
|
||
- `requestSong($serviceSongId)`: trigger email and set `request_sent_at`
|
||
- `unassign($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 integrates
|
||
- `app/Mail/MissingSongRequest.php` — email mailable from T7
|
||
- `app/Models/ServiceSong.php` — service_songs model from T2
|
||
- `app/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.txt
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\TranslationService` with:
|
||
- `fetchFromUrl(string $url): ?string` — best-effort HTTP GET, extract text content (strip HTML tags), return raw text or null on failure
|
||
- `importTranslation(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)`: set `songs.has_translation = true`
|
||
- `removeTranslation(Song $song)`: clear all `text_content_translated`, set `has_translation = false`
|
||
- Create `App\Http\Controllers\TranslationController` with:
|
||
- `fetchUrl(Request $request)`: accept URL, return scraped text for review before import
|
||
- `import(Song $song, Request $request)`: accept full text, run `importTranslation`
|
||
- 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_translation` field from T2
|
||
- `app/Models/SongSlide.php` — `text_content_translated` field 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 = true` after 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.txt
|
||
```
|
||
|
||
**Commit**: 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)
|
||
|
||
- [x] 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 to `ServiceController`:
|
||
- 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 `useAutoSave` composable (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}/edit` renders 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.png
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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 `SlideGrid` component (from T10) to display information slides
|
||
- Uses `SlideUploader` component (from T10) with `showExpireDate=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)
|
||
- Create backend support: `SlideController` method 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 T10
|
||
- `app/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.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat: add Information block with dynamic expire-date filtering`
|
||
- Files: `resources/js/Components/Blocks/InformationBlock.vue`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 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 `SlideGrid` component (from T10) with `showExpireDate=false`
|
||
- Uses `SlideUploader` component (from T10) with `showExpireDate=false`
|
||
- Upload, view thumbnails, delete — standard slide management
|
||
- Backend: SlideController already handles this — just pass `type='moderation'` and `service_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.png
|
||
```
|
||
|
||
**Commit**: YES (group with T17)
|
||
- Message: `feat: add Moderation and Sermon slide blocks`
|
||
- Files: `resources/js/Components/Blocks/ModerationBlock.vue`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 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 `SlideGrid` and `SlideUploader` components
|
||
- 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.png
|
||
```
|
||
|
||
**Commit**: YES (group with T16)
|
||
- Message: `feat: add Moderation and Sermon slide blocks`
|
||
- Files: `resources/js/Components/Blocks/SermonBlock.vue`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 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)
|
||
- **If matched** (song_id set):
|
||
- If song `has_translation=true`: show checkbox "Übersetzung verwenden" (default: checked), auto-saves
|
||
- Show `ArrangementConfigurator` component (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)
|
||
- 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.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat: add Songs block with matching UI and arrangement integration`
|
||
- Files: `resources/js/Components/Blocks/SongsBlock.vue`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 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\SongPdfController` with `download($songId, $arrangementId)` method:
|
||
- Use `barryvdh/laravel-dompdf` to generate PDF
|
||
- Template: `resources/views/pdf/song.blade.php`
|
||
- MUST use old-school CSS (no Tailwind classes!) with `DejaVu Sans` font 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`
|
||
- 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-dompdf` docs: https://github.com/barryvdh/laravel-dompdf
|
||
- `app/Models/Song.php`, `app/Models/SongGroup.php`, `app/Models/SongSlide.php` — data models from T2
|
||
- `app/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.txt
|
||
```
|
||
|
||
**Commit**: 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)
|
||
|
||
- [x] 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 T9
|
||
- `resources/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.png
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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 `ArrangementConfigurator` component (from T11)
|
||
- Auto-save on all field changes (use `useAutoSave` from 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 T11
|
||
- `app/Http/Controllers/SongController.php` — update endpoint from T9
|
||
- `resources/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.png
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat: add Song DB edit popup with metadata and arrangement configurator`
|
||
- Files: `resources/js/Components/SongEditModal.vue`
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 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)
|
||
- "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 T13
|
||
- `app/Http/Controllers/TranslationController.php` — URL fetch and import endpoints from T13
|
||
- `app/Models/SongSlide.php` — `text_content` and `text_content_translated` fields 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.png
|
||
```
|
||
|
||
**Commit**: 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`
|
||
|
||
- [x] 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\ProParserNotImplementedException` with 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 `ProParserNotImplementedException` with message "Der .pro-Generator wird später implementiert."
|
||
- Return HTTP 501
|
||
- 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.txt
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat: add .pro file upload/download placeholders (NotImplementedException)`
|
||
- Files: `app/Exceptions/ProParserNotImplementedException.php`, routes
|
||
- Pre-commit: `php artisan test`
|
||
|
||
- [x] 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`: set `finalized_at = now()`, verify all prerequisites met
|
||
- `POST /services/{id}/reopen`: set `finalized_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_songs
|
||
- `songs_arranged`: count of service_songs with song_arrangement_id vs total matched
|
||
- `has_sermon_slides`: count of sermon slides for this service > 0
|
||
- `info_slides_count`: count of info slides where expire_date >= service.date
|
||
- `finalized_at`: timestamp or null
|
||
- Add computed `isReadyToFinalize` accessor 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_at` timestamp
|
||
- [ ] 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.png
|
||
```
|
||
|
||
**Commit**: 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.
|
||
|
||
- [x] 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. 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`
|
||
|
||
- [x] F2. **Code Quality Review** — `unspecified-high`
|
||
Run `php 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`
|
||
|
||
- [x] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill)
|
||
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`
|
||
|
||
- [x] F4. **Scope Fidelity Check** — `deep`
|
||
For 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
|
||
```bash
|
||
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
|
||
- [x] All "Must Have" requirements present and working
|
||
- [x] All "Must NOT Have" guardrails respected
|
||
- [x] All tests pass (TDD — comprehensive coverage)
|
||
- [x] All UI text in German with "Du" form
|
||
- [x] Docker deployment works end-to-end
|
||
- [x] Auto-save functional on every interactive element
|
||
- [x] .pro parser/generator throws NotImplementedException
|
||
- [x] Finalized download is placeholder
|