feat(media): media recipe library bound to presets (#146) #151

Merged
gronod merged 1 commits from feat/146-media-library into milestone/m10-studio 2026-09-12 23:00:22 +01:00
Owner

Refs #146

Summary

  • MediaRecipe (snake_case Codable, validated() strict decode) + MediaLibraryStore actor persisting media_library.json (sibling of settings.json, cap 200 refuse-with-error, .throwCorrupt keeps bytes, atomic writes).
  • MediaLibraryViewModel (@MainActor ObservableObject): single apply path through the existing applyPreset (#82) mapping; wizard.printerName = recipe.printerDisplayName; queue re-enumerated via serialized PrintSessionViewModel.enumeratePrinters() (fixed ProcessIDs — overlapping listPrinters would throw duplicateID).
  • Literal CAL_ refusal on the bound .cal stem and live wizard.basename: applyCalibration forced off, buildPrinttargConfig().calibrationFile == nilprinttarg -K can never see a CAL_ file. Blocked apply returns success-with-warning (permanent refusal; re-clicking cannot unstick).
  • Missing preset / colour-space mismatch / missing queue / missing-or-unparseable .cal → apply returns false, manage sheet stays open, picker reverts (selectedRecipeID written only on success).
  • Capture: name+paper+ink required; no preset selected → auto-snapshot custom preset bound to the recipe; bound-preset colour-space mismatch refuses Save; .cal path stored verbatim, saveMediaApplyCal forced off for CAL_/missing paths (calApplyable).
  • Staleness badge mediaRecipeStale: .printer when printerID absent from a non-empty enumerated queue list (empty = indeterminate); .calibration = bound cal CREATED + calibration_stale_days < now (age-only isStale(), no descriptor name compare). Re-evaluates on settingsDidChange and printer-list changes.
  • Sidebar Media block between preset Save/Manage and Calibrate Printer; mediaSelectpresetSelect; no id on the sidebar container. Sheets live on RootView only; manage sheet is List (macOS 12), double-click/Return applies + dismisses on success.
  • docs/21 id roster (242 → 267), docs/22 media-library section.

Gates (local — no Gitea Actions on this branch)

Targeted:

xcodegen generate --spec project.yml && xcodebuild test -scheme ICCery \
  -destination 'platform=macOS' -derivedDataPath build/DerivedData-feat-146-media-library \
  ARCHS="$(uname -m)" CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY='-' \
  -only-testing:ICCeryCoreTests/MediaRecipeTests \
  -only-testing:ICCeryCoreTests/MediaLibraryStoreTests \
  -only-testing:ICCeryCoreTests/MediaLibraryViewModelTests \
  -only-testing:ICCeryCoreTests/PresetViewModelMappingTests \
  -only-testing:ICCeryCoreTests/CalibrationStoreTests \
  -only-testing:ICCeryUITests/Milestone10MediaLibraryUITests \
  -only-testing:ICCeryUITests/AboutHelpUITests

34 unit + 5 UI tests, 0 failures, every selector matched tests.

Full host-arch suite (same command minus -only-testing:):
ICCeryCoreTests: 365 executed, 0 failures. ICCeryUITests: 32 executed, 0 failures, real Automation attach (no skip). The known ArgyllRunnerStreamingLoopTests.testNonZeroExitThrowsToolFailed flake did not recur.

xcresult: build/DerivedData-feat-146-media-library/Logs/Test/Test-ICCery-2026.09.12_22-46-57-+0100.xcresult

Acceptance criteria

  • Library file is media_library.json (sibling of settings.json)
  • Apply uses PresetMapping; no second Stage 1 form
  • CAL_ / missing .cal cannot enable printtarg -K
  • Printer mismatch is a banner, not a crash
  • Names render through Text
  • Sidebar width remains 270 pt with the new block
  • mediaSelect and presetSelect are distinct identifiers
  • Capture and Manage are RootView sheets (preset-dialog style)
  • Help overlays do not change sidebar height (testHelpOverlaysDoNotChangeSidebarHeight green)

Hardware gate (real Epson/Canon + real .cal) is the milestone-level check, not this PR.

Refs #146 ## Summary - `MediaRecipe` (snake_case Codable, `validated()` strict decode) + `MediaLibraryStore` actor persisting `media_library.json` (sibling of `settings.json`, cap 200 refuse-with-error, `.throwCorrupt` keeps bytes, atomic writes). - `MediaLibraryViewModel` (`@MainActor` `ObservableObject`): single apply path through the existing `applyPreset` (#82) mapping; `wizard.printerName = recipe.printerDisplayName`; queue re-enumerated via serialized `PrintSessionViewModel.enumeratePrinters()` (fixed `ProcessID`s — overlapping `listPrinters` would throw `duplicateID`). - Literal `CAL_` refusal on the bound `.cal` stem **and** live `wizard.basename`: `applyCalibration` forced off, `buildPrinttargConfig().calibrationFile == nil` — `printtarg -K` can never see a `CAL_` file. Blocked apply returns success-with-warning (permanent refusal; re-clicking cannot unstick). - Missing preset / colour-space mismatch / missing queue / missing-or-unparseable `.cal` → apply returns `false`, manage sheet stays open, picker reverts (`selectedRecipeID` written only on success). - Capture: name+paper+ink required; no preset selected → auto-snapshot custom preset bound to the recipe; bound-preset colour-space mismatch refuses Save; `.cal` path stored verbatim, `saveMediaApplyCal` forced off for `CAL_`/missing paths (`calApplyable`). - Staleness badge `mediaRecipeStale`: `.printer` when `printerID` absent from a **non-empty** enumerated queue list (empty = indeterminate); `.calibration` = bound cal `CREATED + calibration_stale_days < now` (age-only `isStale()`, no descriptor name compare). Re-evaluates on `settingsDidChange` and printer-list changes. - Sidebar Media block between preset Save/Manage and Calibrate Printer; `mediaSelect` ≠ `presetSelect`; no id on the sidebar container. Sheets live on `RootView` only; manage sheet is `List` (macOS 12), double-click/Return applies + dismisses on success. - docs/21 id roster (242 → 267), docs/22 media-library section. ## Gates (local — no Gitea Actions on this branch) Targeted: ``` xcodegen generate --spec project.yml && xcodebuild test -scheme ICCery \ -destination 'platform=macOS' -derivedDataPath build/DerivedData-feat-146-media-library \ ARCHS="$(uname -m)" CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY='-' \ -only-testing:ICCeryCoreTests/MediaRecipeTests \ -only-testing:ICCeryCoreTests/MediaLibraryStoreTests \ -only-testing:ICCeryCoreTests/MediaLibraryViewModelTests \ -only-testing:ICCeryCoreTests/PresetViewModelMappingTests \ -only-testing:ICCeryCoreTests/CalibrationStoreTests \ -only-testing:ICCeryUITests/Milestone10MediaLibraryUITests \ -only-testing:ICCeryUITests/AboutHelpUITests ``` → **34 unit + 5 UI tests, 0 failures**, every selector matched tests. Full host-arch suite (same command minus `-only-testing:`): → **ICCeryCoreTests: 365 executed, 0 failures. ICCeryUITests: 32 executed, 0 failures**, real Automation attach (no skip). The known `ArgyllRunnerStreamingLoopTests.testNonZeroExitThrowsToolFailed` flake did **not** recur. xcresult: `build/DerivedData-feat-146-media-library/Logs/Test/Test-ICCery-2026.09.12_22-46-57-+0100.xcresult` ## Acceptance criteria - [x] Library file is `media_library.json` (sibling of `settings.json`) - [x] Apply uses `PresetMapping`; no second Stage 1 form - [x] `CAL_` / missing `.cal` cannot enable `printtarg -K` - [x] Printer mismatch is a banner, not a crash - [x] Names render through `Text` - [x] Sidebar width remains 270 pt with the new block - [x] `mediaSelect` and `presetSelect` are distinct identifiers - [x] Capture and Manage are `RootView` sheets (preset-dialog style) - [x] Help overlays do not change sidebar height (`testHelpOverlaysDoNotChangeSidebarHeight` green) Hardware gate (real Epson/Canon + real `.cal`) is the milestone-level check, not this PR.
gronod added 1 commit 2026-09-12 23:00:07 +01:00
Refs #146

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
gronod self-assigned this 2026-09-12 23:00:16 +01:00
gronod added this to the M10 — Studio workflow (media library, gamut compare, spot-read, projects) milestone 2026-09-12 23:00:16 +01:00
gronod merged commit 1931da8448 into milestone/m10-studio 2026-09-12 23:00:22 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gronod/iccery-v2-mac#151