14 KiB
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 extensionLabelsFileReader::read(string $filePath): LabelLibrary— sameLabel::getName()returns protobuftextfield — name is the identity (no UUID for labels)Macro::getColor()returns?array{r,g,b,a}floats 0..1 — needMacroColorConverterto get hexLabel::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.typeenum is[information, moderation, sermon]ONLY — noagenda_itemagenda_itempart_type = slide whereservice_agenda_item_id IS NOT NULLat runtimesong_groups.coloris NOT NULLABLE (migration says so) — newlabels.colorIS nullableservice_songs.song_idiscascadeOnDelete— wipingsongsauto-cascades toservice_songs
Export Flow
ProExportService::buildGroups()lines 38-69 — macro injection pointProExportService::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 (NOTimportFromFile)
Settings Pattern
Setting::get($key, $default)/Setting::set($key, $value)— simple key/valuesettingstable: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:
restrictOnDeleteon macro/label refs (usehidden_at);cascadeOnDeleteon service-scoped rows
Migration/Test Notes
tests/Pest.phpalready appliesRefreshDatabaseto allFeaturetests; no extra setup needed forFeature/Migrations- SQLite unique constraint errors can be asserted with
->toThrow(\Exception::class)in migration tests macro_collection_macroscan safely use a reserved-ishordercolumn name in SQLite/Laravel migrations; the schema + foreign keys passedmigrate:freshforeignId()->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:
$fillablearray,casts()method (not$castsproperty), typed return types on relations. hidden_atsemantics, NOT SoftDeletes:LabelandMacrousehidden_attimestamp +isHidden()helper; SoftDeletes deliberately not used.MacroCollectionpivot 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 onservice_idwith explicitwhere('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.SongArrangementLabelFactoryusesLabel::factory()andSongArrangement::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. DatabaseSchemaTestpasses 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_groups → labels, song_arrangement_groups → song_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: traverseSong -> arrangements -> arrangementLabels -> label -> songSlidesfor content.song_slidestable only haslabel_id(nosong_ideither) — 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 viaSongArrangementLabel.- Helper functions defined at file level in Pest tests work cleanly:
function makeSongWithDefaultArrangement(): array { ... }keeps test setup DRY. - Fixture
Test.prohas 4 groups but only 3 are referenced in any arrangement — assertion needs to countLabel::count()(post-import) to verify "all 4 groups created", not arrangement labels. MacroColorConverter::fromRgba()(assoc-keyedr,g,b) replaces the oldProImportService::rgbaToHex()for label color conversion in importer; the legacy hex helpers were preserved becauseProFileGenerator::colorFromArrayuses numeric-indexed RGBA.- Removing the "groups must belong to this song" check in
ArrangementController::updateis correct since labels are global;exists:labels,idvalidation 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
settingstable: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
wasHiddento differentiatereEnabledvsupdatedcounts - Collection sync: detach all → attach with order index from parser
- Warnings: any MacroAssignment whose macro is currently hidden
MacroResolutionService
- Override-vs-defaults:
ServiceMacroOverrideexistence check decides whether to use service-specific or global assignments - Hidden macros and hidden labels (for
by_label) are filtered via Collection->reject() macrosForSlideuses match() expression for position semantics- Default collection fallback:
--MAIN--with UUID8D02FC57-83F8-4042-9B90-81C229728426
Pint quirk
- DTO classes with empty body need
{}on same line as constructor closing paren —single_line_empty_bodyrule.
Test patterns
- Pest auto-applies
RefreshDatabaseviatests/Pest.phpfor all Feature tests, but explicituses(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']inProFileGenerator::buildCue(); no$slideData['macros']stacking support exists.Slide::setMacro()also updates/replaces the first macro action. ProExportServicenow 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
ServiceintogenerateProFile()/generateParserSong()soMacroResolutionService::macrosForSlide()can resolve global or service-specific assignments. - Full verification for T2.7:
ddev exec php artisan testpassed 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()within:lists forpart_type(information, moderation, sermon, song, agenda_item) andposition(all_slides, first_slide, last_slide, by_label). - Route ordering matters:
/settings/macro-assignments/reorderMUST be registered BEFORE/settings/macro-assignments/{macroAssignment}, elsereorderis captured as the model parameter. - Route-model binding works automatically for both
{macroAssignment}and{serviceMacroAssignment}— Laravel resolves snake_case → StudlyCase → Eloquent model. - Unused
$serviceparameter 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.binwork withnew UploadedFile(path, name, null, null, true)(5th arg$test=truekeeps the file at original path sogetPathname()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 passesmacros_per_partkeyed 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_warningchecks raw flags before resolver filters them — but since resolver already filters,has_warningwill normally be false. Acceptable for badge UI. ServiceMacroOverride::where(...)->exists()checks override status per part.ServicePartMacroPanel.vueis positionedabsolute right-0 top-8 z-50— wrapper must beclass="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 fromXSRF-TOKENcookie (URL-decoded).
Final Verification F4 (2026-05-04)
- Scope-fidelity verification passed:
Label/Macrousehidden_at(no SoftDeletes), label imports are additive with color overwrite, missing macros are hidden viahidden_at,MacroResolutionServiceresolves override/default assignments and filters hidden macros/labels,ProExportServiceinjectsMacroResolutionServicewith no legacybuildMacroData(), andSettingsControlleronly exposes the fourAGENDA_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.vueshould mirror hidden-macro warnings forby_labelrows witha.label?.hidden_at, using a red badge anddata-testid="warning-hidden-label".ProImportServicemust keep new label colors nullable:MacroColorConverter::fromRgba($color)should flow through unchanged so missing.procolors becomeNULL, 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 (
filter→map→values→all), bundle export macro injection, German UI labels, and test IDs on macro/label picker/icon components. - Unaccounted grep output for
ArrangementConfigurator.vue,ArrangementDialog.vue, andSongEditModal.vueis explained by planned SongGroup → Label rename work, not unrelated scope creep.