From fa3162b2b789e24c152b4a9cb7f5633d04783313 Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Mon, 2 Mar 2026 23:03:14 +0100 Subject: [PATCH] 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. --- .sisyphus/evidence/task-2-no-attributes.txt | 8 + .../evidence/task-3-auto-arrangement.txt | 101 ++++++ .../evidence/task-6-translated-bounds.txt | 154 +++++++++ .sisyphus/evidence/task-8-bundle-contents.txt | 21 ++ .../pro-gen-and-ui-fixes/learnings.md | 49 +++ AGENTS.md | 112 +++++++ config/dompdf.php | 301 ++++++++++++++++++ vite.config.js | 3 + 8 files changed, 749 insertions(+) create mode 100644 .sisyphus/evidence/task-2-no-attributes.txt create mode 100644 .sisyphus/evidence/task-3-auto-arrangement.txt create mode 100644 .sisyphus/evidence/task-6-translated-bounds.txt create mode 100644 .sisyphus/evidence/task-8-bundle-contents.txt create mode 100644 .sisyphus/notepads/pro-gen-and-ui-fixes/learnings.md create mode 100644 config/dompdf.php diff --git a/.sisyphus/evidence/task-2-no-attributes.txt b/.sisyphus/evidence/task-2-no-attributes.txt new file mode 100644 index 0000000..fdb8f90 --- /dev/null +++ b/.sisyphus/evidence/task-2-no-attributes.txt @@ -0,0 +1,8 @@ +hasFill:YES +fillEnabled:NO +hasStroke:YES +strokeEnabled:NO +hasShadow:YES +shadowEnabled:NO +hasFeather:YES +featherEnabled:NO diff --git a/.sisyphus/evidence/task-3-auto-arrangement.txt b/.sisyphus/evidence/task-3-auto-arrangement.txt new file mode 100644 index 0000000..2bc4d3c --- /dev/null +++ b/.sisyphus/evidence/task-3-auto-arrangement.txt @@ -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" diff --git a/.sisyphus/evidence/task-6-translated-bounds.txt b/.sisyphus/evidence/task-6-translated-bounds.txt new file mode 100644 index 0000000..83c49c0 --- /dev/null +++ b/.sisyphus/evidence/task-6-translated-bounds.txt @@ -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 diff --git a/.sisyphus/evidence/task-8-bundle-contents.txt b/.sisyphus/evidence/task-8-bundle-contents.txt new file mode 100644 index 0000000..7df55bf --- /dev/null +++ b/.sisyphus/evidence/task-8-bundle-contents.txt @@ -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. diff --git a/.sisyphus/notepads/pro-gen-and-ui-fixes/learnings.md b/.sisyphus/notepads/pro-gen-and-ui-fixes/learnings.md new file mode 100644 index 0000000..8e14903 --- /dev/null +++ b/.sisyphus/notepads/pro-gen-and-ui-fixes/learnings.md @@ -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. diff --git a/AGENTS.md b/AGENTS.md index ace3ed3..7fa0ed8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,3 +95,115 @@ ## SongDB Import - 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`. - 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 ` 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 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, + ], + +]; diff --git a/vite.config.js b/vite.config.js index 779a896..472221d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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`.