docs: add build commands and architecture overview to AGENTS.md
Add build/test/lint commands, architecture overview, PHP/Vue/test code style conventions, and key project constraints. Include dompdf config, vite HMR note, and sisyphus evidence files.
This commit is contained in:
parent
149389a382
commit
fa3162b2b7
8
.sisyphus/evidence/task-2-no-attributes.txt
Normal file
8
.sisyphus/evidence/task-2-no-attributes.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
hasFill:YES
|
||||||
|
fillEnabled:NO
|
||||||
|
hasStroke:YES
|
||||||
|
strokeEnabled:NO
|
||||||
|
hasShadow:YES
|
||||||
|
shadowEnabled:NO
|
||||||
|
hasFeather:YES
|
||||||
|
featherEnabled:NO
|
||||||
101
.sisyphus/evidence/task-3-auto-arrangement.txt
Normal file
101
.sisyphus/evidence/task-3-auto-arrangement.txt
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
TASK: Auto-select default arrangement on song match
|
||||||
|
DATE: 2026-03-02
|
||||||
|
STATUS: COMPLETE ✓
|
||||||
|
|
||||||
|
IMPLEMENTATION SUMMARY
|
||||||
|
======================
|
||||||
|
|
||||||
|
1. Modified SongMatchingService.php (app/Services/SongMatchingService.php)
|
||||||
|
- autoMatch() method (lines 34-40): Added arrangement lookup logic
|
||||||
|
- manualAssign() method (lines 65-76): Added conditional arrangement setting
|
||||||
|
|
||||||
|
2. Added 4 new tests to SongMatchingTest.php
|
||||||
|
- autoMatch setzt song_arrangement_id auf Standard-Arrangement
|
||||||
|
- autoMatch bevorzugt is_default=true Arrangement
|
||||||
|
- autoMatch nutzt erstes Arrangement wenn kein Standard vorhanden
|
||||||
|
- manualAssign setzt song_arrangement_id wenn null
|
||||||
|
- manualAssign behält bestehende song_arrangement_id bei
|
||||||
|
|
||||||
|
ARRANGEMENT SELECTION PRIORITY
|
||||||
|
==============================
|
||||||
|
1. is_default = true
|
||||||
|
2. name = 'normal'
|
||||||
|
3. first arrangement (any)
|
||||||
|
4. null (if no arrangements exist)
|
||||||
|
|
||||||
|
BEHAVIOR
|
||||||
|
========
|
||||||
|
|
||||||
|
autoMatch():
|
||||||
|
- ALWAYS sets song_arrangement_id after matching song
|
||||||
|
- Uses priority order: is_default → name='normal' → first
|
||||||
|
- Handles case where song has no arrangements (sets to null)
|
||||||
|
|
||||||
|
manualAssign():
|
||||||
|
- ONLY sets song_arrangement_id if currently null
|
||||||
|
- Preserves existing arrangement selection when reassigning song
|
||||||
|
- Uses same priority order as autoMatch()
|
||||||
|
|
||||||
|
TEST RESULTS
|
||||||
|
============
|
||||||
|
|
||||||
|
All 20 SongMatchingTest tests PASS:
|
||||||
|
✓ autoMatch ordnet Song per CCLI-ID zu
|
||||||
|
✓ autoMatch nutzt CTS-Song-ID als Fallback wenn keine CCLI passt
|
||||||
|
✓ autoMatch gibt false zurück wenn kein CCLI-ID vorhanden
|
||||||
|
✓ autoMatch gibt false zurück wenn kein passender Song in DB
|
||||||
|
✓ autoMatch überspringt bereits zugeordnete Songs
|
||||||
|
✓ autoMatch setzt song_arrangement_id auf Standard-Arrangement [NEW]
|
||||||
|
✓ autoMatch bevorzugt is_default=true Arrangement [NEW]
|
||||||
|
✓ autoMatch nutzt erstes Arrangement wenn kein Standard vorhanden [NEW]
|
||||||
|
✓ manualAssign ordnet Song manuell zu
|
||||||
|
✓ manualAssign überschreibt bestehende Zuordnung
|
||||||
|
✓ manualAssign setzt song_arrangement_id wenn null [NEW]
|
||||||
|
✓ manualAssign behält bestehende song_arrangement_id bei [NEW]
|
||||||
|
✓ requestCreation sendet E-Mail und setzt request_sent_at
|
||||||
|
✓ unassign entfernt Zuordnung
|
||||||
|
✓ POST /api/service-songs/{id}/assign ordnet Song zu
|
||||||
|
✓ POST /api/service-songs/{id}/assign validiert song_id
|
||||||
|
✓ POST /api/service-songs/{id}/request sendet Anfrage-E-Mail
|
||||||
|
✓ POST /api/service-songs/{id}/unassign entfernt Zuordnung
|
||||||
|
✓ API Endpunkte erfordern Authentifizierung
|
||||||
|
✓ API gibt 404 für nicht existierende ServiceSong
|
||||||
|
|
||||||
|
Duration: 0.47s
|
||||||
|
Tests: 20 passed (45 assertions)
|
||||||
|
|
||||||
|
CODE QUALITY
|
||||||
|
============
|
||||||
|
✓ No LSP errors in SongMatchingService.php
|
||||||
|
✓ Follows Laravel code style conventions
|
||||||
|
✓ Uses nullsafe operator (?->)
|
||||||
|
✓ Uses null coalescing (??)
|
||||||
|
✓ Proper type hints and return types
|
||||||
|
✓ Clear comments explaining logic
|
||||||
|
|
||||||
|
VERIFICATION CHECKLIST
|
||||||
|
======================
|
||||||
|
✓ autoMatch() sets song_arrangement_id to default/normal/first arrangement
|
||||||
|
✓ manualAssign() sets arrangement ONLY if currently null
|
||||||
|
✓ New tests verify auto-arrangement selection
|
||||||
|
✓ New tests verify arrangement preservation
|
||||||
|
✓ All 20 SongMatching tests pass
|
||||||
|
✓ No regressions in existing tests
|
||||||
|
✓ Code follows project conventions
|
||||||
|
✓ LSP diagnostics clean
|
||||||
|
|
||||||
|
FILES MODIFIED
|
||||||
|
==============
|
||||||
|
1. app/Services/SongMatchingService.php
|
||||||
|
- autoMatch() method: Added arrangement lookup (lines 34-40)
|
||||||
|
- manualAssign() method: Added conditional arrangement setting (lines 65-76)
|
||||||
|
|
||||||
|
2. tests/Feature/SongMatchingTest.php
|
||||||
|
- Added SongArrangement import
|
||||||
|
- Added 4 new test cases for arrangement selection
|
||||||
|
|
||||||
|
NEXT STEPS
|
||||||
|
==========
|
||||||
|
Ready for commit:
|
||||||
|
git add app/Services/SongMatchingService.php tests/Feature/SongMatchingTest.php
|
||||||
|
git commit -m "feat(songs): auto-select default arrangement on song match"
|
||||||
154
.sisyphus/evidence/task-6-translated-bounds.txt
Normal file
154
.sisyphus/evidence/task-6-translated-bounds.txt
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Task 6: Translated Textbox Positioning - QA Evidence
|
||||||
|
|
||||||
|
## Test 1: Translated Slide Has Correct Dual Bounds
|
||||||
|
|
||||||
|
### Command
|
||||||
|
```bash
|
||||||
|
cd /Users/thorsten/AI/cts-work && php -r "
|
||||||
|
require 'vendor/autoload.php';
|
||||||
|
use ProPresenter\Parser\ProFileGenerator;
|
||||||
|
use ProPresenter\Parser\ProFileWriter;
|
||||||
|
use ProPresenter\Parser\ProFileReader;
|
||||||
|
\$song = ProFileGenerator::generate('TranslateTest',
|
||||||
|
[['name'=>'V1','color'=>[0,0,0,1],'slides'=>[['text'=>'Amazing Grace','translation'=>'Erstaunliche Gnade']]]],
|
||||||
|
[['name'=>'normal','groupNames'=>['V1']]]
|
||||||
|
);
|
||||||
|
ProFileWriter::write(\$song, '/tmp/translate-test.pro');
|
||||||
|
\$readSong = ProFileReader::read('/tmp/translate-test.pro');
|
||||||
|
\$slides = \$readSong->getSlides();
|
||||||
|
\$elements = \$slides[0]->getAllElements();
|
||||||
|
echo 'count: ' . count(\$elements) . PHP_EOL;
|
||||||
|
echo 'name0: ' . \$elements[0]->getName() . PHP_EOL;
|
||||||
|
echo 'name1: ' . \$elements[1]->getName() . PHP_EOL;
|
||||||
|
\$b0 = \$elements[0]->getGraphicsElement()->getBounds();
|
||||||
|
\$b1 = \$elements[1]->getGraphicsElement()->getBounds();
|
||||||
|
echo 'height0: ' . round(\$b0->getSize()->getHeight(), 1) . PHP_EOL;
|
||||||
|
echo 'height1: ' . round(\$b1->getSize()->getHeight(), 1) . PHP_EOL;
|
||||||
|
echo 'y0: ' . round(\$b0->getOrigin()->getY(), 3) . PHP_EOL;
|
||||||
|
echo 'y1: ' . round(\$b1->getOrigin()->getY(), 3) . PHP_EOL;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output
|
||||||
|
```
|
||||||
|
count: 2
|
||||||
|
name0: Orginal
|
||||||
|
name1: Deutsch
|
||||||
|
height0: 182.9
|
||||||
|
height1: 113.9
|
||||||
|
y0: 99.543
|
||||||
|
y1: 303.166
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
✅ Element count: 2 (expected: 2)
|
||||||
|
✅ Element 0 name: "Orginal" (expected: "Orginal")
|
||||||
|
✅ Element 1 name: "Deutsch" (expected: "Deutsch")
|
||||||
|
✅ Element 0 height: 182.9px (expected: ~182.946px)
|
||||||
|
✅ Element 1 height: 113.9px (expected: ~113.889px)
|
||||||
|
✅ Element 0 Y position: 99.543 (expected: 99.543)
|
||||||
|
✅ Element 1 Y position: 303.166 (expected: 303.166)
|
||||||
|
|
||||||
|
**Result**: PASS - Translated slides have correctly positioned dual textboxes matching TestTranslated.pro reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test 2: Non-Translated Slide Has Single Full Bounds
|
||||||
|
|
||||||
|
### Command
|
||||||
|
```bash
|
||||||
|
cd /Users/thorsten/AI/cts-work && php -r "
|
||||||
|
require 'vendor/autoload.php';
|
||||||
|
use ProPresenter\Parser\ProFileGenerator;
|
||||||
|
use ProPresenter\Parser\ProFileWriter;
|
||||||
|
use ProPresenter\Parser\ProFileReader;
|
||||||
|
\$song = ProFileGenerator::generate('NoTranslateTest',
|
||||||
|
[['name'=>'V1','color'=>[0,0,0,1],'slides'=>[['text'=>'Amazing Grace']]]],
|
||||||
|
[['name'=>'normal','groupNames'=>['V1']]]
|
||||||
|
);
|
||||||
|
ProFileWriter::write(\$song, '/tmp/no-translate-test.pro');
|
||||||
|
\$readSong = ProFileReader::read('/tmp/no-translate-test.pro');
|
||||||
|
\$slides = \$readSong->getSlides();
|
||||||
|
\$elements = \$slides[0]->getAllElements();
|
||||||
|
echo 'count: ' . count(\$elements) . PHP_EOL;
|
||||||
|
echo 'name0: ' . \$elements[0]->getName() . PHP_EOL;
|
||||||
|
\$b = \$elements[0]->getGraphicsElement()->getBounds();
|
||||||
|
echo 'height: ' . round(\$b->getSize()->getHeight(), 1) . PHP_EOL;
|
||||||
|
echo 'width: ' . round(\$b->getSize()->getWidth(), 1) . PHP_EOL;
|
||||||
|
echo 'y: ' . round(\$b->getOrigin()->getY(), 1) . PHP_EOL;
|
||||||
|
echo 'x: ' . round(\$b->getOrigin()->getX(), 1) . PHP_EOL;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output
|
||||||
|
```
|
||||||
|
count: 1
|
||||||
|
name0: Orginal
|
||||||
|
height: 880
|
||||||
|
width: 1620
|
||||||
|
y: 100
|
||||||
|
x: 150
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
✅ Element count: 1 (expected: 1)
|
||||||
|
✅ Element 0 name: "Orginal" (expected: "Orginal")
|
||||||
|
✅ Height: 880px (expected: 880px)
|
||||||
|
✅ Width: 1620px (expected: 1620px)
|
||||||
|
✅ Y position: 100 (expected: 100)
|
||||||
|
✅ X position: 150 (expected: 150)
|
||||||
|
|
||||||
|
**Result**: PASS - Non-translated slides keep single full-size textbox
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHPUnit Tests
|
||||||
|
|
||||||
|
### Test: testTranslatedSlideHasCorrectDualBounds
|
||||||
|
```bash
|
||||||
|
./vendor/bin/phpunit vendor/propresenter/parser/tests/ProFileGeneratorTest.php --filter testTranslatedSlideHasCorrectDualBounds
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**: OK (1 test, 7 assertions)
|
||||||
|
|
||||||
|
### Test: testNonTranslatedSlideHasSingleFullBounds
|
||||||
|
```bash
|
||||||
|
./vendor/bin/phpunit vendor/propresenter/parser/tests/ProFileGeneratorTest.php --filter testNonTranslatedSlideHasSingleFullBounds
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**: OK (1 test, 6 assertions)
|
||||||
|
|
||||||
|
### All ProFileGeneratorTest Tests
|
||||||
|
```bash
|
||||||
|
./vendor/bin/phpunit vendor/propresenter/parser/tests/ProFileGeneratorTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**: OK (14 tests, 95 assertions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Laravel Tests
|
||||||
|
|
||||||
|
### Command
|
||||||
|
```bash
|
||||||
|
php -d memory_limit=512M artisan test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**: Tests: 203 passed (1115 assertions), Duration: 3.89s
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ All acceptance criteria met:
|
||||||
|
- [x] Two new methods created: buildOriginalBounds(), buildTranslationBounds()
|
||||||
|
- [x] Translated slides have 2 elements with different bounds (heights: ~183px, ~114px)
|
||||||
|
- [x] Non-translated slides keep single element with full bounds (1620×880)
|
||||||
|
- [x] Textbox names unchanged: "Orginal" (typo intentional), "Deutsch"
|
||||||
|
- [x] New test: translated slide has correct dual bounds
|
||||||
|
- [x] New test: non-translated slide has single full-size bounds
|
||||||
|
- [x] PHPUnit tests pass (14/14)
|
||||||
|
- [x] Laravel tests pass (203/203)
|
||||||
|
|
||||||
|
**Task Status**: COMPLETE
|
||||||
|
**Date**: 2026-03-02
|
||||||
21
.sisyphus/evidence/task-8-bundle-contents.txt
Normal file
21
.sisyphus/evidence/task-8-bundle-contents.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
Task 8 Manual Bundle Verification
|
||||||
|
Datum: 2026-03-02
|
||||||
|
|
||||||
|
Erzeugter Bundle-Pfad:
|
||||||
|
/var/folders/jf/qly82l3s6cg19v82rm_h0q9h0000gn/T/information-69a5fdfc20077.probundle
|
||||||
|
|
||||||
|
Befehl:
|
||||||
|
unzip -l /var/folders/jf/qly82l3s6cg19v82rm_h0q9h0000gn/T/information-69a5fdfc20077.probundle
|
||||||
|
|
||||||
|
Ausgabe:
|
||||||
|
Archive: /var/folders/jf/qly82l3s6cg19v82rm_h0q9h0000gn/T/information-69a5fdfc20077.probundle
|
||||||
|
Length Date Time Name
|
||||||
|
--------- ---------- ----- ----
|
||||||
|
1323 03-02-2026 22:15 information.pro
|
||||||
|
12 03-02-2026 22:15 manual-bundle-1.jpg
|
||||||
|
--------- -------
|
||||||
|
1335 2 files
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
- Enthalten: 1 .pro Datei + 1 Bilddatei
|
||||||
|
- Struktur ist flach (Root-Level), wie gefordert.
|
||||||
49
.sisyphus/notepads/pro-gen-and-ui-fixes/learnings.md
Normal file
49
.sisyphus/notepads/pro-gen-and-ui-fixes/learnings.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Learnings: ProPresenter Generator and UI Fixes
|
||||||
|
|
||||||
|
## Task 6: Translated Textbox Positioning (2026-03-02)
|
||||||
|
|
||||||
|
### Implementation Pattern
|
||||||
|
- **Dual bounds methods**: Created `buildOriginalBounds()` and `buildTranslationBounds()` following same pattern as existing `buildBounds()`
|
||||||
|
- **Conditional logic in buildCue()**: Check for translation presence, use different bounds for each element
|
||||||
|
- **Optional parameter pattern**: Modified `buildSlideElement()` to accept `?Rect $bounds = null` parameter, defaulting to `buildBounds()` for backward compatibility
|
||||||
|
|
||||||
|
### Exact Values from TestTranslated.pro
|
||||||
|
- Original textbox: origin(150, 99.543), size(1620×182.946) — top position, ~183px tall
|
||||||
|
- Translation textbox: origin(150, 303.166), size(1620×113.889) — below, ~114px tall
|
||||||
|
- Non-translated: origin(150, 100), size(1620×880) — full height
|
||||||
|
|
||||||
|
### Testing Approach
|
||||||
|
- **PHPUnit tests**: Added two new tests to verify bounds for translated and non-translated slides
|
||||||
|
- **QA scenarios**: Used PHP CLI to generate, write, read back, and verify exact positioning values
|
||||||
|
- **Method access**: Used `getGraphicsElement()->getBounds()` to access protobuf bounds from TextElement wrapper
|
||||||
|
|
||||||
|
### Git Workflow
|
||||||
|
- **Symlinked vendor**: `vendor/propresenter/parser` is symlink to `/Users/thorsten/AI/propresenter-work/php`
|
||||||
|
- **Commit location**: Changes committed in propresenter-work repo, not cts-work
|
||||||
|
- **Branch**: propresenter-parser
|
||||||
|
|
||||||
|
### Test Results
|
||||||
|
- PHPUnit: 14/14 tests pass (95 assertions)
|
||||||
|
- Laravel: 203/203 tests pass (1115 assertions)
|
||||||
|
- QA verification: All positioning values match TestTranslated.pro reference exactly
|
||||||
|
|
||||||
|
## Task 8: .probundle Export fuer Service-Slide-Bloecke (2026-03-02)
|
||||||
|
|
||||||
|
### Export-Pattern
|
||||||
|
- Fuer ZIP-basierte Exporte ist `Storage::disk("public")->path(...)` teststabiler als harte `storage_path(...)`-Pfade, weil `Storage::fake("public")` dann direkt funktioniert.
|
||||||
|
- `.probundle` wurde als flaches ZIP umgesetzt: genau eine `.pro` plus alle Bilddateien auf Root-Ebene.
|
||||||
|
- Medienreferenzen in der `.pro` funktionieren robust mit Dateinamen (Root-Datei im Bundle) statt absoluten Pfaden.
|
||||||
|
|
||||||
|
### Controller-Integration
|
||||||
|
- Route-Parameter fuer Blocktyp (`information|moderation|sermon`) lassen sich sauber per Request-Validation nach `merge()` pruefen und liefern bei Fehlern korrekt 422 in JSON-Requests.
|
||||||
|
- Download-Response fuer temporaere Bundle-Dateien mit `deleteFileAfterSend(true)` vermeidet Temp-Muell.
|
||||||
|
|
||||||
|
### Test-Pattern
|
||||||
|
- Fuer BinaryFileResponse in Feature-Tests: Datei aus Response in eigenen Temp-Pfad kopieren und dann mit `ZipArchive` validieren.
|
||||||
|
- Vollsuite hatte einen sporadischen bestehenden Flake in `SongPdfTest`; Re-Run lief voll gruen (206/206).
|
||||||
|
|
||||||
|
## Scope-Audit Learnings (F4, 2026-03-02)
|
||||||
|
|
||||||
|
- Fuer strikte Scope-Fidelity prueft man nicht nur Dateiliste pro Commit, sondern auch unerwartete Hunks innerhalb der erwarteten Dateien (z.B. Header-Navigation oder Bulk-Delete-Features in einem Export-Task).
|
||||||
|
- Wenn ein Task auf vendor-Dateien zielt, die im Repo nicht versioniert sind, ist der Task aus Sicht des Git-Diffs nicht nachweisbar umgesetzt ("missing", auch wenn QA-Evidence existiert).
|
||||||
|
- Commit-Message-Match allein reicht nicht: Task 1/4/7/8 zeigen, dass ein passender Commit-Titel trotzdem deutliche Scope-Creep enthalten kann.
|
||||||
112
AGENTS.md
112
AGENTS.md
|
|
@ -95,3 +95,115 @@ ## SongDB Import
|
||||||
- download: download generated .pro file from the songDB for this song
|
- download: download generated .pro file from the songDB for this song
|
||||||
- translate: allow add a full text or an URL to the Full text, and then start an editor, that shows two columns for every slide of every group. Left the original text, right a texteditor, with the imported text - always the same line qty of text from the original is used from the given translated text. Save this as translation for this song, and mark it as `with translation`.
|
- translate: allow add a full text or an URL to the Full text, and then start an editor, that shows two columns for every slide of every group. Left the original text, right a texteditor, with the imported text - always the same line qty of text from the original is used from the given translated text. Save this as translation for this song, and mark it as `with translation`.
|
||||||
- UploadArea for drag'n'drop and click for upload, to upload a .pro file, a zip file with multiple .pro files, or a bunch of .pro files, which should be parsed (this module was integrated later, so show an Exception here till this was finalized) and added into the song DB.
|
- UploadArea for drag'n'drop and click for upload, to upload a .pro file, a zip file with multiple .pro files, or a bunch of .pro files, which should be parsed (this module was integrated later, so show an Exception here till this was finalized) and added into the song DB.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build, Test, Lint Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup (first time)
|
||||||
|
composer setup
|
||||||
|
|
||||||
|
# Dev server (Laravel + Vite + Queue + Logs via concurrently)
|
||||||
|
composer dev
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run all PHP tests (clears config cache first)
|
||||||
|
composer test
|
||||||
|
# or directly:
|
||||||
|
php artisan test
|
||||||
|
|
||||||
|
# Run a single PHP test file
|
||||||
|
php artisan test tests/Feature/ServiceControllerTest.php
|
||||||
|
|
||||||
|
# Run a single test method
|
||||||
|
php artisan test --filter=test_service_kann_abgeschlossen_werden
|
||||||
|
|
||||||
|
# Run only Unit or Feature suite
|
||||||
|
php artisan test --testsuite=Unit
|
||||||
|
php artisan test --testsuite=Feature
|
||||||
|
|
||||||
|
# PHP code formatting (Laravel Pint - default Laravel preset)
|
||||||
|
./vendor/bin/pint
|
||||||
|
# Check only (no changes):
|
||||||
|
./vendor/bin/pint --test
|
||||||
|
|
||||||
|
# Run e2e tests (requires running dev server at http://cts-work.test)
|
||||||
|
npx playwright test
|
||||||
|
# Single e2e file:
|
||||||
|
npx playwright test tests/e2e/service-list.spec.ts
|
||||||
|
|
||||||
|
# Migrations
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
Http/Controllers/ # Inertia controllers, return Inertia::render() or JSON
|
||||||
|
Http/Requests/ # Form request validation
|
||||||
|
Http/Middleware/ # HandleInertiaRequests shares props
|
||||||
|
Models/ # Eloquent models with factories in database/factories/
|
||||||
|
Services/ # Business logic (ChurchToolsService, SongService, etc.)
|
||||||
|
Jobs/ # Queue jobs (PowerPoint conversion)
|
||||||
|
Mail/ # Mailable classes
|
||||||
|
Cts/ # CTS API spike/sync utilities
|
||||||
|
resources/js/
|
||||||
|
Pages/ # Vue page components (mapped via Inertia::render)
|
||||||
|
Components/ # Reusable Vue components
|
||||||
|
Composables/ # Vue composables (useAutoSave)
|
||||||
|
Layouts/ # AuthenticatedLayout, GuestLayout, MainLayout
|
||||||
|
tests/
|
||||||
|
Feature/ # HTTP/integration tests (class-based, PHPUnit style)
|
||||||
|
Unit/ # Unit tests
|
||||||
|
e2e/ # Playwright browser tests (TypeScript)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style — PHP
|
||||||
|
|
||||||
|
- **Formatter**: Laravel Pint (default Laravel preset, no custom config)
|
||||||
|
- **Indentation**: 4 spaces
|
||||||
|
- **Imports**: Fully qualified, one per line, grouped (PHP classes, then Laravel, then app)
|
||||||
|
- **Models**: Use `$fillable` array (not `$guarded`). Use `casts()` method (not `$casts` property). Relationships return typed `HasMany`/`BelongsTo`/etc.
|
||||||
|
- **Controllers**: Return type hints (`Response`, `JsonResponse`, `RedirectResponse`). Use route-model binding. Use `Inertia::render()` for page responses.
|
||||||
|
- **Migrations**: Anonymous class style: `return new class () extends Migration { ... }`
|
||||||
|
- **Error messages**: German. Flash via `->with('success', '...')`. JSON errors use `message` key.
|
||||||
|
- **Null safety**: Use nullsafe operator `?->` and null coalescing `??`
|
||||||
|
- **DB operations**: Prefer Eloquent, fall back to `DB::table()` for bulk upserts in sync code
|
||||||
|
- **SoftDeletes**: Used on `Song` model. Use `whereNull('deleted_at')` in manual queries.
|
||||||
|
|
||||||
|
## Code Style — Vue / Frontend
|
||||||
|
|
||||||
|
- **Vue 3 Composition API** only, always `<script setup>`. No Options API.
|
||||||
|
- **Props**: `defineProps({ propName: { type: Type, default: value } })`
|
||||||
|
- **Emits**: `defineEmits(['event-name'])`
|
||||||
|
- **Imports**: Use `@/` alias for `resources/js/`. Vue imports from `'vue'`, Inertia from `'@inertiajs/vue3'`.
|
||||||
|
- **Functions**: Prefer `function name() {}` declarations in components (not `const name = () => {}`)
|
||||||
|
- **Styling**: Tailwind CSS v4 utility classes inline. Scoped `<style>` only when necessary (e.g. drag-and-drop).
|
||||||
|
- **State**: `ref()` for reactive state, `computed()` for derived. Use `watch()` for side effects.
|
||||||
|
- **Routing**: Use `route('name', params)` (Ziggy) for URL generation. Use `router.post/get/delete` from Inertia.
|
||||||
|
- **Testing attributes**: Add `data-testid="..."` on interactive elements for Playwright e2e tests.
|
||||||
|
- **All user-facing text must be German** (Du-form, not Sie).
|
||||||
|
|
||||||
|
## Code Style — Tests
|
||||||
|
|
||||||
|
- **Framework**: Pest v4 (wraps PHPUnit). Feature tests are class-based extending `TestCase` with `RefreshDatabase`.
|
||||||
|
- **Naming**: `test_snake_case_german_description` (e.g. `test_service_kann_abgeschlossen_werden`)
|
||||||
|
- **Auth**: `$this->actingAs(User::factory()->create())`
|
||||||
|
- **Vite**: Call `$this->withoutVite()` before testing Inertia page renders
|
||||||
|
- **Time**: Use `Carbon::setTestNow('2026-03-01 10:00:00')` for deterministic time
|
||||||
|
- **Assertions**: `assertInertia(fn ($page) => $page->component('...')->has('...')->where('...'))` for Inertia responses
|
||||||
|
- **DB**: Tests use in-memory SQLite (configured in `phpunit.xml`)
|
||||||
|
- **e2e**: Playwright (TypeScript), `tests/e2e/`, baseURL `http://cts-work.test`, auth via `auth.setup.ts`
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
- **CTS API is READ-ONLY** — never write/modify data via ChurchTools API
|
||||||
|
- **Immediate persistence** — all user actions save instantly, no separate "save" button
|
||||||
|
- **German locale** — `APP_LOCALE=de`, all UI text in German, Du-form
|
||||||
|
- **File uploads** — images convert to JPG 1920x1080 (maintain aspect ratio, no cropping); PPT/PPTX convert to multiple JPGs
|
||||||
|
- **Named routes** — all routes have names, use `route('name')` everywhere
|
||||||
|
- **ProPresenter (.pro) parser** — placeholder only, not yet implemented
|
||||||
|
|
|
||||||
301
config/dompdf.php
Normal file
301
config/dompdf.php
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Set some default values. It is possible to add all defines that can be set
|
||||||
|
| in dompdf_config.inc.php. You can also override the entire config file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||||
|
|
||||||
|
'public_path' => null, // Override the public path if needed
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show € and £.
|
||||||
|
*/
|
||||||
|
'convert_entities' => true,
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
/**
|
||||||
|
* The location of the DOMPDF font directory
|
||||||
|
*
|
||||||
|
* The location of the directory where DOMPDF will store fonts and font metrics
|
||||||
|
* Note: This directory must exist and be writable by the webserver process.
|
||||||
|
* *Please note the trailing slash.*
|
||||||
|
*
|
||||||
|
* Notes regarding fonts:
|
||||||
|
* Additional .afm font metrics can be added by executing load_font.php from command line.
|
||||||
|
*
|
||||||
|
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
|
||||||
|
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
|
||||||
|
* increase file size unless font subsetting is enabled. Before embedding a font please
|
||||||
|
* review your rights under the font license.
|
||||||
|
*
|
||||||
|
* Any font specification in the source HTML is translated to the closest font available
|
||||||
|
* in the font directory.
|
||||||
|
*
|
||||||
|
* The pdf standard "Base 14 fonts" are:
|
||||||
|
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
|
||||||
|
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
|
||||||
|
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||||
|
* Symbol, ZapfDingbats.
|
||||||
|
*/
|
||||||
|
'font_dir' => storage_path('fonts'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of the DOMPDF font cache directory
|
||||||
|
*
|
||||||
|
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
||||||
|
* This directory can be the same as DOMPDF_FONT_DIR
|
||||||
|
*
|
||||||
|
* Note: This directory must exist and be writable by the webserver process.
|
||||||
|
*/
|
||||||
|
'font_cache' => storage_path('fonts'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of a temporary directory.
|
||||||
|
*
|
||||||
|
* The directory specified must be writeable by the webserver process.
|
||||||
|
* The temporary directory is required to download remote images and when
|
||||||
|
* using the PDFLib back end.
|
||||||
|
*/
|
||||||
|
'temp_dir' => sys_get_temp_dir(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ==== IMPORTANT ====
|
||||||
|
*
|
||||||
|
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
||||||
|
* files on the webserver. All local files opened by dompdf must be in a
|
||||||
|
* subdirectory of this directory. DO NOT set it to '/' since this could
|
||||||
|
* allow an attacker to use dompdf to read any files on the server. This
|
||||||
|
* should be an absolute path.
|
||||||
|
* This is only checked on command line call by dompdf.php, but not by
|
||||||
|
* direct class use like:
|
||||||
|
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||||
|
*/
|
||||||
|
'chroot' => realpath(base_path()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol whitelist
|
||||||
|
*
|
||||||
|
* Protocols and PHP wrappers allowed in URIs, and the validation rules
|
||||||
|
* that determine if a resouce may be loaded. Full support is not guaranteed
|
||||||
|
* for the protocols/wrappers specified
|
||||||
|
* by this array.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
'allowed_protocols' => [
|
||||||
|
'data://' => ['rules' => []],
|
||||||
|
'file://' => ['rules' => []],
|
||||||
|
'http://' => ['rules' => []],
|
||||||
|
'https://' => ['rules' => []],
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operational artifact (log files, temporary files) path validation
|
||||||
|
*/
|
||||||
|
'artifactPathValidation' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'log_output_file' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable font subsetting or not.
|
||||||
|
*/
|
||||||
|
'enable_font_subsetting' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PDF rendering backend to use
|
||||||
|
*
|
||||||
|
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
||||||
|
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
||||||
|
* fall back on CPDF. 'GD' renders PDFs to graphic files.
|
||||||
|
* {@link * Canvas_Factory} ultimately determines which rendering class to
|
||||||
|
* instantiate based on this setting.
|
||||||
|
*
|
||||||
|
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
||||||
|
* capabilities for dompdf, however additional features (e.g. object,
|
||||||
|
* image and font support, etc.) differ between backends. Please see
|
||||||
|
* {@link PDFLib_Adapter} for more information on the PDFLib backend
|
||||||
|
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
|
||||||
|
* on CPDF. Also see the documentation for each backend at the links
|
||||||
|
* below.
|
||||||
|
*
|
||||||
|
* The GD rendering backend is a little different than PDFLib and
|
||||||
|
* CPDF. Several features of CPDF and PDFLib are not supported or do
|
||||||
|
* not make any sense when creating image files. For example,
|
||||||
|
* multiple pages are not supported, nor are PDF 'objects'. Have a
|
||||||
|
* look at {@link GD_Adapter} for more information. GD support is
|
||||||
|
* experimental, so use it at your own risk.
|
||||||
|
*
|
||||||
|
* @link http://www.pdflib.com
|
||||||
|
* @link http://www.ros.co.nz/pdf
|
||||||
|
* @link http://www.php.net/image
|
||||||
|
*/
|
||||||
|
'pdf_backend' => 'CPDF',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* html target media view which should be rendered into pdf.
|
||||||
|
* List of types and parsing rules for future extensions:
|
||||||
|
* http://www.w3.org/TR/REC-html40/types.html
|
||||||
|
* screen, tty, tv, projection, handheld, print, braille, aural, all
|
||||||
|
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
|
||||||
|
* Note, even though the generated pdf file is intended for print output,
|
||||||
|
* the desired content might be different (e.g. screen or projection view of html file).
|
||||||
|
* Therefore allow specification of content here.
|
||||||
|
*/
|
||||||
|
'default_media_type' => 'screen',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default paper size.
|
||||||
|
*
|
||||||
|
* North America standard is "letter"; other countries generally "a4"
|
||||||
|
*
|
||||||
|
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||||
|
*/
|
||||||
|
'default_paper_size' => 'a4',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default paper orientation.
|
||||||
|
*
|
||||||
|
* The orientation of the page (portrait or landscape).
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'default_paper_orientation' => 'portrait',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default font family
|
||||||
|
*
|
||||||
|
* Used if no suitable fonts can be found. This must exist in the font folder.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
'default_font' => 'serif',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image DPI setting
|
||||||
|
*
|
||||||
|
* This setting determines the default DPI setting for images and fonts. The
|
||||||
|
* DPI may be overridden for inline images by explictly setting the
|
||||||
|
* image's width & height style attributes (i.e. if the image's native
|
||||||
|
* width is 600 pixels and you specify the image's width as 72 points,
|
||||||
|
* the image will have a DPI of 600 in the rendered PDF. The DPI of
|
||||||
|
* background images can not be overridden and is controlled entirely
|
||||||
|
* via this parameter.
|
||||||
|
*
|
||||||
|
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
|
||||||
|
* If a size in html is given as px (or without unit as image size),
|
||||||
|
* this tells the corresponding size in pt.
|
||||||
|
* This adjusts the relative sizes to be similar to the rendering of the
|
||||||
|
* html page in a reference browser.
|
||||||
|
*
|
||||||
|
* In pdf, always 1 pt = 1/72 inch
|
||||||
|
*
|
||||||
|
* Rendering resolution of various browsers in px per inch:
|
||||||
|
* Windows Firefox and Internet Explorer:
|
||||||
|
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
|
||||||
|
* Linux Firefox:
|
||||||
|
* about:config *resolution: Default:96
|
||||||
|
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
|
||||||
|
*
|
||||||
|
* Take care about extra font/image zoom factor of browser.
|
||||||
|
*
|
||||||
|
* In images, <img> size in pixel attribute, img css style, are overriding
|
||||||
|
* the real image dimension in px for rendering.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
'dpi' => 96,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable embedded PHP
|
||||||
|
*
|
||||||
|
* If this setting is set to true then DOMPDF will automatically evaluate embedded PHP contained
|
||||||
|
* within <script type="text/php"> ... </script> tags.
|
||||||
|
*
|
||||||
|
* ==== IMPORTANT ==== Enabling this for documents you do not trust (e.g. arbitrary remote html pages)
|
||||||
|
* is a security risk.
|
||||||
|
* Embedded scripts are run with the same level of system access available to dompdf.
|
||||||
|
* Set this option to false (recommended) if you wish to process untrusted documents.
|
||||||
|
* This setting may increase the risk of system exploit.
|
||||||
|
* Do not change this settings without understanding the consequences.
|
||||||
|
* Additional documentation is available on the dompdf wiki at:
|
||||||
|
* https://github.com/dompdf/dompdf/wiki
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_php' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rnable inline JavaScript
|
||||||
|
*
|
||||||
|
* If this setting is set to true then DOMPDF will automatically insert JavaScript code contained
|
||||||
|
* within <script type="text/javascript"> ... </script> tags as written into the PDF.
|
||||||
|
* NOTE: This is PDF-based JavaScript to be executed by the PDF viewer,
|
||||||
|
* not browser-based JavaScript executed by Dompdf.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_javascript' => true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable remote file access
|
||||||
|
*
|
||||||
|
* If this setting is set to true, DOMPDF will access remote sites for
|
||||||
|
* images and CSS files as required.
|
||||||
|
*
|
||||||
|
* ==== IMPORTANT ====
|
||||||
|
* This can be a security risk, in particular in combination with isPhpEnabled and
|
||||||
|
* allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
|
||||||
|
* This allows anonymous users to download legally doubtful internet content which on
|
||||||
|
* tracing back appears to being downloaded by your server, or allows malicious php code
|
||||||
|
* in remote html pages to be executed by your server with your account privileges.
|
||||||
|
*
|
||||||
|
* This setting may increase the risk of system exploit. Do not change
|
||||||
|
* this settings without understanding the consequences. Additional
|
||||||
|
* documentation is available on the dompdf wiki at:
|
||||||
|
* https://github.com/dompdf/dompdf/wiki
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_remote' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of allowed remote hosts
|
||||||
|
*
|
||||||
|
* Each value of the array must be a valid hostname.
|
||||||
|
*
|
||||||
|
* This will be used to filter which resources can be loaded in combination with
|
||||||
|
* isRemoteEnabled. If enable_remote is FALSE, then this will have no effect.
|
||||||
|
*
|
||||||
|
* Leave to NULL to allow any remote host.
|
||||||
|
*
|
||||||
|
* @var array|null
|
||||||
|
*/
|
||||||
|
'allowed_remote_hosts' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ratio applied to the fonts height to be more like browsers' line height
|
||||||
|
*/
|
||||||
|
'font_height_ratio' => 1.1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the HTML5 Lib parser
|
||||||
|
*
|
||||||
|
* @deprecated This feature is now always on in dompdf 2.x
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
'enable_html5_parser' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -31,3 +31,6 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// HMR: When using Herd, run `npm run build` to use production assets.
|
||||||
|
// The ws://localhost:5173 error occurs when dev assets are loaded without `npm run dev`.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue