pp-planer/.sisyphus/notepads/macros-and-labels-import/learnings.md
2026-05-04 07:41:39 +02:00

14 KiB

Learnings — macros-and-labels-import

[2026-05-03] Session ses_210cd1557ffeGs4SEGrt7hnvyS — Plan Created

Parser Library

  • Source at /Users/thorsten/AI/propresenter/src/ (NOT /Users/thorsten/AI/propresenter-work/php/ per stale AGENTS.md)
  • VCS repo: https://git.stadtmission-butzbach.de/public/propresenter-php.git (dev-master)
  • New classes (NOT yet in vendor/): MacrosFileReader, LabelsFileReader, Macro, MacroLibrary, MacroCollection, Label, LabelLibrary
  • MacrosFileReader::read(string $filePath): MacroLibrary — raw protobuf binary, no extension
  • LabelsFileReader::read(string $filePath): LabelLibrary — same
  • Label::getName() returns protobuf text field — name is the identity (no UUID for labels)
  • Macro::getColor() returns ?array{r,g,b,a} floats 0..1 — need MacroColorConverter to get hex
  • Label::getColorHex() already returns #RRGGBB — mirror its formula for macros
  • PHP 8.4 required by parser. App currently requires ^8.2 — BLOCKER for T0.1

DB Schema Key Facts

  • slides.type enum is [information, moderation, sermon] ONLY — no agenda_item
  • agenda_item part_type = slide where service_agenda_item_id IS NOT NULL at runtime
  • song_groups.color is NOT NULLABLE (migration says so) — new labels.color IS nullable
  • service_songs.song_id is cascadeOnDelete — wiping songs auto-cascades to service_songs

Export Flow

  • ProExportService::buildGroups() lines 38-69 — macro injection point
  • ProExportService::buildMacroData() lines 71-86 — reads 4 legacy settings keys
  • Currently injects macro ONLY when group name is "COPYRIGHT" (case-insensitive)
  • ProImportService::import(UploadedFile $file): array — method signature (NOT importFromFile)

Settings Pattern

  • Setting::get($key, $default) / Setting::set($key, $value) — simple key/value
  • settings table: key UNIQUE, value TEXT

Critical Decisions

  • song_groups → labels: global table, "drop all data" migration (no backwards compat)
  • Hybrid macro scope: global defaults in Settings; per-(service, part_type) override via "Anpassen"
  • Override = snapshot of globals at creation time; future global changes don't propagate
  • Stacking: all matching assignments fire, ordered by macro_assignments.order ASC
  • Hidden macros/labels: skip at export, warning badge in editor
  • Label colors: read-only in UI; Labels file import is sole authority; .pro auto-discovery only sets color on CREATE
  • FK rules: restrictOnDelete on macro/label refs (use hidden_at); cascadeOnDelete on service-scoped rows

Migration/Test Notes

  • tests/Pest.php already applies RefreshDatabase to all Feature tests; no extra setup needed for Feature/Migrations
  • SQLite unique constraint errors can be asserted with ->toThrow(\Exception::class) in migration tests
  • macro_collection_macros can safely use a reserved-ish order column name in SQLite/Laravel migrations; the schema + foreign keys passed migrate:fresh
  • foreignId()->constrained()->cascadeOnDelete() correctly cascades through the junction table under the current sqlite test setup

T2.1 — Models + Factories (label-based schema)

  • All new model conventions match house style: $fillable array, casts() method (not $casts property), typed return types on relations.
  • hidden_at semantics, NOT SoftDeletes: Label and Macro use hidden_at timestamp + isHidden() helper; SoftDeletes deliberately not used.
  • MacroCollection pivot ordering: belongsToMany(Macro::class, 'macro_collection_macros')->withPivot('order')->orderBy('macro_collection_macros.order') — must qualify the column with the pivot table name to avoid SQLite ambiguous column errors.
  • ServiceMacroOverride::assignments() uses composite-key relation: HasMany on service_id with explicit where('part_type', $this->part_type) filter (Eloquent has no native composite-FK support).
  • SongArrangement::arrangementLabels() ordered: hasMany(SongArrangementLabel::class)->orderBy('order') so consumers see labels in the correct slide order without re-sorting.
  • SongArrangementLabelFactory uses Label::factory() and SongArrangement::factory() directly — both have HasFactory trait.
  • Test gating: After T2.1 alone, 270/328 tests pass. The remaining 58 failures are all in app/Services/SongService.php, app/Services/ProImportService.php, and the test files that exercise those services; T4.4 owns those updates.
  • DatabaseSchemaTest passes cleanly (3 tests / 31 assertions): all expected tables exist, dropped tables gone, all factories produce valid rows.

T4.4 — PHP rename audit (2026-05-03)

After Wave 2's schema migration (song_groupslabels, song_arrangement_groupssong_arrangement_labels), the rename-audit cleanup turned out to span far more files than the plan listed (12 app files + 11 test files vs 7 listed). Key findings:

  • Song::groups() relation was completely removed; many call sites needed adaptation, not just rename. New pattern: traverse Song -> arrangements -> arrangementLabels -> label -> songSlides for content.
  • song_slides table only has label_id (no song_id either) — slides are now globally owned by labels. Tests that previously did $verse = $song->groups()->create(...) need to find/create a global Label and link it via SongArrangementLabel.
  • Helper functions defined at file level in Pest tests work cleanly: function makeSongWithDefaultArrangement(): array { ... } keeps test setup DRY.
  • Fixture Test.pro has 4 groups but only 3 are referenced in any arrangement — assertion needs to count Label::count() (post-import) to verify "all 4 groups created", not arrangement labels.
  • MacroColorConverter::fromRgba() (assoc-keyed r,g,b) replaces the old ProImportService::rgbaToHex() for label color conversion in importer; the legacy hex helpers were preserved because ProFileGenerator::colorFromArray uses numeric-indexed RGBA.
  • Removing the "groups must belong to this song" check in ArrangementController::update is correct since labels are global; exists:labels,id validation is sufficient.

Wave 2 — T2.3, T2.4, T2.5 (services)

LabelsImportService

  • Case-insensitive name lookup via whereRaw('LOWER(name) = ?', [strtolower($name)])
  • Always updates color on existing labels (additive policy, never disables)
  • Skips labels with empty names
  • Stores metadata in settings table: labels_last_imported_at, labels_last_imported_filename

MacrosImportService

  • UUID is normalized to UPPER before storage (matches parser convention)
  • Macros not in file get hidden_at = now() (soft-disable, not delete)
  • Re-import re-enables a previously hidden macro by setting hidden_at = null
  • Tracks wasHidden to differentiate reEnabled vs updated counts
  • Collection sync: detach all → attach with order index from parser
  • Warnings: any MacroAssignment whose macro is currently hidden

MacroResolutionService

  • Override-vs-defaults: ServiceMacroOverride existence check decides whether to use service-specific or global assignments
  • Hidden macros and hidden labels (for by_label) are filtered via Collection->reject()
  • macrosForSlide uses match() expression for position semantics
  • Default collection fallback: --MAIN-- with UUID 8D02FC57-83F8-4042-9B90-81C229728426

Pint quirk

  • DTO classes with empty body need {} on same line as constructor closing paren — single_line_empty_body rule.

Test patterns

  • Pest auto-applies RefreshDatabase via tests/Pest.php for all Feature tests, but explicit uses(RefreshDatabase::class) is harmless and matches spec.
  • All 354 tests pass (was 334 before Wave 2.3-2.5).

T2.7 ProExportService MacroResolutionService

  • ProPresenter parser package currently consumes only $slideData['macro'] in ProFileGenerator::buildCue(); no $slideData['macros'] stacking support exists. Slide::setMacro() also updates/replaces the first macro action.
  • ProExportService now keeps song downloads backward-compatible by accepting optional ?Service; exports without service context intentionally emit no macros.
  • Playlist/bundle service exports must pass the active Service into generateProFile() / generateParserSong() so MacroResolutionService::macrosForSlide() can resolve global or service-specific assignments.
  • Full verification for T2.7: ddev exec php artisan test passed with 357 tests / 1706 assertions; evidence in .sisyphus/evidence/task-2.7-pest.txt.

T2.8 Controllers + Routes (2026-05-03)

  • 4 thin controllers, all JSON responses for mutations (Inertia only on MacroAssignmentController::index).
  • Validation via inline $request->validate() with in: lists for part_type (information, moderation, sermon, song, agenda_item) and position (all_slides, first_slide, last_slide, by_label).
  • Route ordering matters: /settings/macro-assignments/reorder MUST be registered BEFORE /settings/macro-assignments/{macroAssignment}, else reorder is captured as the model parameter.
  • Route-model binding works automatically for both {macroAssignment} and {serviceMacroAssignment} — Laravel resolves snake_case → StudlyCase → Eloquent model.
  • Unused $service parameter on update/destroyAssignment is intentional: route-model binding requires it in the signature even if the assignment binding alone does the work.
  • Generic 422 message for parser failures hides internal exception details from users; all messages German Du-form.
  • Test fixtures tests/fixtures/macros-sample.bin & labels-sample.bin work with new UploadedFile(path, name, null, null, true) (5th arg $test=true keeps the file at original path so getPathname() returns the fixture).
  • UploadedFile::fake()->create('x.bin', 1) generates a 1KB empty file that fails parser parsing → triggers the controller's catch block → 422 JSON.
  • Auth tests use plain post() (form-data) → assertRedirect(route('login')); JSON requests would return 401, but session-based auth redirects.
  • Final test count: 376 (was 357) → +19 new tests / +54 assertions.

T4.2: Service Edit Macro Panel

  • ServiceController::edit() now passes macros_per_part keyed by part_type (information, moderation, sermon, song, agenda_item).
  • Each entry: count, is_overridden, has_warning, assignments[] (with macro_id/name/color/hidden, position, label_id/name).
  • Uses MacroResolutionService::resolveAssignmentsForPart() (already filters hidden macros + by_label with hidden labels). has_warning checks raw flags before resolver filters them — but since resolver already filters, has_warning will normally be false. Acceptable for badge UI.
  • ServiceMacroOverride::where(...)->exists() checks override status per part.
  • ServicePartMacroPanel.vue is positioned absolute right-0 top-8 z-50 — wrapper must be class="relative".
  • Edit.vue page only has 2 visible block headers (Ablauf and Information). Placed agenda_item/moderation/sermon/song MacroIcons in the Ablauf header row; placed information MacroIcon in the Information block header.
  • MacroIcon renders only when count > 0, so empty parts gracefully hide their badge.
  • Routes used: services.macro-overrides.store (POST + body {part_type}), services.macro-overrides.destroy (DELETE + body). XSRF token sourced from XSRF-TOKEN cookie (URL-decoded).

Final Verification F4 (2026-05-04)

  • Scope-fidelity verification passed: Label/Macro use hidden_at (no SoftDeletes), label imports are additive with color overwrite, missing macros are hidden via hidden_at, MacroResolutionService resolves override/default assignments and filters hidden macros/labels, ProExportService injects MacroResolutionService with no legacy buildMacroData(), and SettingsController only exposes the four AGENDA_KEYS.
  • Forbidden-pattern grep suite returned no output for label CRUD, macro action runner/editor patterns, TS suppressions, Vue console logs, bulk operations, label/macro drag reorder, and export caching.

[2026-05-04] Session follow-up — hidden label badge + nullable import color

  • MacroAssignments.vue should mirror hidden-macro warnings for by_label rows with a.label?.hidden_at, using a red badge and data-testid="warning-hidden-label".
  • ProImportService must keep new label colors nullable: MacroColorConverter::fromRgba($color) should flow through unchanged so missing .pro colors become NULL, not #808080.

2026-05-04 F1 final compliance audit

  • Final verification commands passed: no Vue song_group_id references, MacroIcon and hidden-label test IDs present, ProImportService #808080 fallback removed, required macro/label deliverables and schema present, Label/Macro use hidden_at without SoftDeletes, routes present, ProBundleExportService resolves macros for information/moderation/sermon and agenda_item exports.
  • MacroResolutionService supports part types dynamically via part_type string; grep for literal part names can be empty without indicating non-support.

2026-05-04 F4 scope fidelity check

  • Must-NOT grep suite found one historical toast pattern in pre-existing service/song Vue files; no macro/label feature-specific forbidden patterns were found (no SoftDeletes/deleted_at, drag UI, runner/preview, bulk ops, optimistic markers, collection assignment, export caching, agenda_item slide enum, TS suppressions, or console.log).
  • Required macro/label evidence present: all 5 part types, position enum including by_label, hidden_at semantics, explicit restrict/cascade FKs, stacking resolver (filtermapvaluesall), bundle export macro injection, German UI labels, and test IDs on macro/label picker/icon components.
  • Unaccounted grep output for ArrangementConfigurator.vue, ArrangementDialog.vue, and SongEditModal.vue is explained by planned SongGroup → Label rename work, not unrelated scope creep.