# Media Library — Improvement & Growth Plan

> Package: capell-app/media-library · Kind: package · Tier: free · Product group: Capell Foundation · Bundle: foundation · Status: Complete

## 1. Snapshot

Media Library swaps Capell's default media backend for Awcodes Curator. `MediaLibraryServiceProvider` sets `capell.media.backend = 'curator'`, points `capell.media.model` at `CuratorMedia`, and binds the `MediaFieldFactory` contract to `CuratorMediaFieldFactory` (a `CuratorPicker`). It ships one admin surface (`MediaHealthPage` + `MediaHealthTable`, slug `media-health`, navigation group `group_system`), one console command (`capell:media-migrate-to-curator`), and a set of report/signal Actions (`BuildMediaHealthQueryAction`, `BuildDuplicateMediaQueryAction`, `BuildMissingAltMediaQueryAction`, `BuildMediaUsageDrilldownAction`, `BuildOrphanMediaQueryAction`, `BuildMissingRightsMetadataQueryAction`, `DeleteOrphanMediaRecordsAction`, `DispatchMissingAltMediaSignalsAction`) plus the heavyweight `MigrateSpatieMediaToCuratorAction` (555 LOC). The `CuratorMedia` model (376 LOC) adapts Curator to Capell's `MediaContract`, mapping Glide thumbnail/medium/large URLs to conversion/srcset semantics and storing focal points + crop presets inside Curator `curations`. There are no package-owned migrations; it relies on Curator's `curator` table and host-app FK columns, with publishable config for owner FK discovery, upload policy, visibility, and stale-media threshold. Dependents: **media-ai** can consume missing-alt candidates through `MediaMissingAltDetected`, and **seo-suite** can consume consistent image URL/alt/caption metadata from the shared media contract. Marketplace summary positions the package as Curator-backed media foundation without promising generated responsive conversions. The current marketplace gallery keeps only the extension card because the committed light/dark PNGs show empty Media tables; `docs/screenshots.json` still defines the **4** required capture targets for future recapture.

## 2. Improvements (existing functionality)

Prioritized.

1. **Done/Shipped: Implemented the declared health checks.** `MediaLibraryHealthCheck` now verifies the Curator media backend, model, and `MediaFieldFactory` binding, the `curator` table, and owner foreign-key config against the live schema so usage/orphan reporting cannot false-green on invalid config. Diagnostics output is translated and covered. — `src/Health/MediaLibraryHealthCheck.php`, `resources/lang/en/package.php`, `tests/Integration/MediaLibraryHealthCheckTest.php` — **M**

2. **Done/Shipped: Ship useful owner-FK config defaults.** Explicit `owner_foreign_keys` config remains the authoritative override, and fresh installs can now auto-discover conventional Curator owner columns when that list is empty. Media health, orphan reports, and health diagnostics resolve owner keys through a shared resolver, so usage/orphan reports are not silently inert by default while host apps with nonstandard columns can still publish exact config. — `config/media-library.php`, `src/Actions/DashboardReports/DiscoverOwnerForeignKeysAction.php`, `src/Actions/DashboardReports/ResolveOwnerForeignKeysAction.php`, `src/Actions/DashboardReports/BuildOrphanMediaQueryAction.php`, `src/Actions/DashboardReports/BuildMediaHealthQueryAction.php` — **M**

3. **Done/Shipped: Orphan cleanup deletes files and is reachable from the health UI.** `DeleteOrphanMediaRecordsAction` already removes unshared storage blobs before deleting Curator rows, and now accepts selected media IDs while still re-validating them through the orphan query. `MediaHealthTable` exposes a translated bulk action so operators can delete selected unused media without deleting still-referenced rows. Evidence: `MediaGapQueryActionsTest` covers selected cleanup preserving used and unselected orphan records while deleting the selected orphan file; `MediaLibraryCoverageTest` covers the table bulk action. — `src/Actions/DashboardReports/DeleteOrphanMediaRecordsAction.php`, `src/Filament/Pages/Tables/MediaHealthTable.php` — **M**

4. **Focal point / crop preset API has no editor surface.** `CuratorMedia` has full `getFocalPoint`/`setFocalPoint`/`getCropPresets`/`setCropPresets`/`getFocalPointForConversion` logic (and tests), but grep shows these are referenced only in the model and its coverage test — never by `CuratorMediaFieldFactory`, the picker, or any Filament component. The `media-library-focal-points` capability is reachable as a PHP API but invisible to editors. Add a focal-point picker/overlay to the Curator field or a model edit action. — `src/Models/CuratorMedia.php`, `src/Filament/Components/CuratorMediaFieldFactory.php` — **L**

5. **Done/Shipped: Media health rows expose a per-issue column and filter, with a config-driven stale threshold.** `BuildMediaHealthQueryAction` selects `media_health_issue` (`missing_alt`, `stale`, or `unused`) and reads `capell.media_library.stale_after_days`; `MediaHealthTable` shows the translated issue badge and filters by the same primary-issue precedence so operators can focus missing-alt, stale, or unused rows. Evidence: `MediaHealthTest` covers computed issue labels, configurable stale threshold, and the table issue filter; `MediaLibraryCoverageTest` covers the table column/filter registration. — `src/Filament/Pages/Tables/MediaHealthTable.php`, `src/Actions/DashboardReports/BuildMediaHealthQueryAction.php` — **S**

6. **Done/Shipped: Duplicate detection groups by content hash, not path.** `BuildDuplicateMediaQueryAction` now hashes readable storage files with SHA-256 and reports byte-identical Curator rows even when they live at different paths. Missing or unreadable files are skipped instead of being misclassified, and the report projects both `duplicate_count` and `duplicate_hash`. — `src/Actions/DashboardReports/BuildDuplicateMediaQueryAction.php`, `tests/Integration/MediaGapQueryActionsTest.php` — **M**

7. **Done/Shipped: Rights-metadata checks parse `exif` JSON structurally.** `BuildMissingRightsMetadataQueryAction` now decodes Curator `exif` JSON, recursively looks for configured metadata keys as actual object keys, and only treats non-empty values as present. Null, blank, malformed JSON, unrelated values such as `{"note":"copyright"}`, and empty metadata keys remain report candidates. — `src/Actions/DashboardReports/BuildMissingRightsMetadataQueryAction.php`, `tests/Integration/MediaRightsMetadataQueryTest.php` — **M**

8. **Done/Shipped: `InteractsWithCuratorMedia::getFirstMedia` memoizes repeated owner lookups.** `getFirstMedia()` now caches the resolved Curator row per owner instance and collection/media id, while upload and clear operations update or forget the local cache. Repeated `getFirstMedia()`, `getFirstMediaUrl()`, and `getMedia()` calls for the same owner no longer re-query the `curator` table. — `src/Concerns/InteractsWithCuratorMedia.php`, `tests/Feature/FrontendRenderTest.php` — **S**

9. **Done/Shipped: Media usage drill-down Action.** `BuildMediaUsageDrilldownAction` resolves configured or discovered Curator owner foreign keys and returns bounded table/column/record/label references for a media row, turning usage counts into a package-safe "where used" seam for Media Pro/DAM and SEO checks. — `src/Actions/DashboardReports/BuildMediaUsageDrilldownAction.php`, `src/Data/MediaUsageReferenceData.php`, `tests/Integration/MediaGapQueryActionsTest.php` — **M**

10. **Future media polish: recapture meaningful Media Library screenshots.** README and `docs/overview.md` describe the shipped Curator backend, media health page/table, upload policy, owner-FK config, migration command, and current boundaries without promising generated WebP/AVIF conversions or a visual focal editor. The manifest now keeps only the marketplace card because the committed captures are empty Media tables; recapture populated media-health, Curator field, and migration-report states before promoting product screenshots again. — `README.md`, `docs/overview.md`, `docs/README.md`, `capell.json`, `docs/screenshots.json`, `tests/Unit/MediaLibraryCoverageTest.php` — **S**

## 3. Missing Features (gaps)

Tied to `capabilities[]` (`media-library`, `-curator-backend`, `-focal-points`, `-missing-alt-signal`, `-responsive-metadata`, `-rights-metadata`, `-duplicate-detection`, `-usage-reports`, `-orphan-cleanup`) and DAM norms.

- **Deferred to Media Pro: folders / collections.** Curator remains flat in the free foundation package; no folder, album, or tag taxonomy is exposed. The `InteractsWithCuratorMedia` concern is explicitly _single-FK, one row per collection, no galleries_ (its own docblock says "If you need multi-item collections, stay on the default Spatie backend"). Multi-item galleries are a DAM/product-layer scope, not a foundation package promise.
- **Deferred to Media Pro: responsive conversions / WebP / AVIF generation.** The manifest now advertises `media-library-responsive-metadata` instead of generated variants. `getSrcset()` reads a pre-existing `responsive_images` metadata array or falls back to Curator's three Glide presets (thumbnail/medium/large); the package intentionally generates no conversions, modern formats, or `<picture>`/format negotiation.
- **Done/Shipped: missing-alt-text signal hook for media-ai.** `BuildMissingAltMediaQueryAction` exposes image media with null, empty, or whitespace-only alt text as an ordered candidate query with `usage_count`; `DispatchMissingAltMediaSignalsAction` emits `MediaMissingAltDetected` events for downstream automation. This turns the health report's missing-alt row into a subscribeable queue for media-ai without coupling it to Filament. — `src/Actions/DashboardReports/BuildMissingAltMediaQueryAction.php`, `src/Actions/DispatchMissingAltMediaSignalsAction.php`, `src/Events/MediaMissingAltDetected.php`
- **Bulk operations.** The health table has no bulk actions (bulk delete orphans, bulk set alt, bulk apply rights metadata, bulk re-tag). `DeleteOrphanMediaRecordsAction` exists but is unreachable from UI (see §2.3).
- **Done/Shipped: usage drill-down depth.** Usage counts now have a companion `BuildMediaUsageDrilldownAction` that returns concrete owner table/column/record references for a media id. Media Pro can add richer owner labels/routes later without changing the foundation query contract.
- **Search / filter.** Only the table `name` column is searchable; no filter by type, size band, missing-metadata, unused, or date range.
- **CDN / remote disks & signed URLs.** No handling of private-disk signed/temporary URLs; `getUrl()` returns Glide/storage URLs assuming public visibility (see §4).
- **Video & non-image assets.** All conversion/srcset logic short-circuits on `image/*`; no poster-frame, duration, or transcode handling for video/audio/PDF.
- **Cropping UI.** Crop presets are stored but never edited visually (see §2.4).

Differentiators (premium-worthy): responsive WebP/AVIF generation, visual focal/crop editor, folders+galleries, richer where-used UI/routes, and DAM taxonomy. Foundation features shipped here include working orphan cleanup, per-issue filtering, bulk orphan deletion, media-ai alt-text signals, content-hash duplicate detection, structural rights metadata checks, and usage drill-down data.

## 4. Issues / Risks

- **Done/Shipped: advertised health checks are real.** The package now fails Diagnostics when Curator is not fully registered, the `curator` table is missing, or configured owner FKs do not resolve against the live schema. — `src/Health/MediaLibraryHealthCheck.php`, `tests/Integration/MediaLibraryHealthCheckTest.php`.
- **Done/Shipped: usage/orphan reports are no longer inert by default.** Explicit owner-FK config still wins, and when it is empty the package can discover conventional schema columns such as `image_id`, `hero_image_id`, and `media_id`; disabling discovery preserves the old empty-report behaviour intentionally. — `config/media-library.php`, `src/Actions/DashboardReports/ResolveOwnerForeignKeysAction.php`.
- **Orphan cleanup leaks files (data/storage debt).** See §2.3. Row deleted, blob retained. — `src/Actions/DashboardReports/DeleteOrphanMediaRecordsAction.php:30`.
- **Done/Shipped: upload and migration visibility are no longer forced public.** `InteractsWithCuratorMedia::addMediaFromUploadedFile` accepts explicit `public`/`private` visibility and otherwise uses `capell.media_library.default_visibility`; private uploads store to the local disk. `MigrateSpatieMediaToCuratorAction` preserves private source disk visibility on migrated Curator rows. Evidence: `MediaVisibilityTest` covers upload defaults/config/explicit private handling and private URL safety; `MigrateSpatieToCuratorCommandTest` covers a real private-source migration row. — `src/Concerns/InteractsWithCuratorMedia.php`, `src/Actions/MigrateSpatieMediaToCuratorAction.php`, `tests/Integration/MediaVisibilityTest.php`, `tests/Integration/MigrateSpatieToCuratorCommandTest.php`.
- **No upload validation (mime / size / extension).** `addMediaFromUploadedFile` stores whatever `UploadedFile` it is handed — no mime allow-list, no size cap, `alt => null`. Any consumer calling this trait method uploads unchecked content. — `src/Concerns/InteractsWithCuratorMedia.php`.
- **No signed-URL support.** `CuratorMedia::getUrl()` returns Glide/public storage URLs only; there is no temporary/signed URL path for private disks. Combined with the hardcoded-public default, the package has no private-asset story.
- **SQL-building is guarded (positive — not a risk).** `MediaUsageQueryExpressions` validates identifiers with `^\w+$`, wraps via the query grammar, and checks `hasTable`/`hasColumn` before composing the correlated-subquery `usageCountExpression`. This is the right pattern; keep it. Note for reviewers so the `selectRaw`/`whereRaw` usage isn't flagged as injection.
- **Performance budget claims vs reality.** Manifest sets `adminQueryBudget: 40` and `cacheSafety.cacheable: false` with no public cache tags. The health/orphan queries use correlated subqueries (`(select count(*) ...) + (select count(*) ...)`) per FK per row, unindexed, with no pagination cap on the count expression. A short `report_cache_ttl_seconds` cache now avoids repeated report scans in the same operating window, but large media tables still need deeper query/index work. — `capell.json` (`performance`), `src/Support/MediaUsageQueryExpressions.php`, `config/media-library.php`.
- **Test gaps.** Covered: migration command (dry-run, idempotency, metadata mapping, FK population, private source visibility), content-hash duplicate/orphan/structural rights gap queries, missing-alt signal query/event behavior, anonymous/non-admin `getUrl`/`getSrcset` output safety, focal/crop metadata storage, srcset `200w`, field factory returns `CuratorPicker`, manifest capability assertions, health checks, orphan file deletion, upload validation, and private media URL safety. **Not covered:** `MediaHealthPage` Filament Shield gating. — `tests/`.
- **i18n.** All `MediaHealthPage`/`MediaHealthTable` strings resolve from `capell-admin::` translation keys (good), but `resources/lang/en/package.php` is near-empty and `MigrateSpatieToCuratorCommand` console output (`'Migration summary'`, `'Warnings:'`, `'[Dry run] ...'`) is hardcoded English. — `src/Console/MigrateSpatieToCuratorCommand.php`.
- **Manifest/doc mismatches (besides screenshots in §1).** `MigrateSpatieMediaInput` docblock contains a corruption: _"the action dashboard-dashboard_reports without writing"_ (should describe dry-run). — `src/Data/MigrateSpatieMediaInput.php`.

## 5. Marketplace & Positioning

This is a free, bundled **foundation** package and should be positioned as the _media backbone_ every Capell site turns on — not a feature add-on. Its real platform job is: standardise media behind one contract so other packages (media-ai, seo-suite, themes) can rely on consistent media fields, URLs, and metadata.

**Current `summary` status.** The marketplace summary and `composer.json` description now use the reconciled positioning: Curator as the Capell media backbone, a shared media field, the media-health dashboard, and safe Spatie migration. They intentionally avoid claiming generated responsive variants, WebP/AVIF conversion, folders/galleries, or a visual focal editor.

- **Marketplace summary:** _"Make Curator the media backbone of your Capell site. One consistent media field everywhere, a media-health dashboard that surfaces missing alt text and unused assets, and a safe, idempotent migration from Spatie Media Library."_
- **Composer description:** _"Curator-backed media foundation for Capell CMS: unified media field, media-health reporting, and Spatie-to-Curator migration."_

**Free/bundle vs premium.** Keep the backend swap, the migration command, alt/stale/unused health reporting, content-hash duplicate detection, and the media-ai alt-text hook in the free foundation tier — that drives platform adoption and cross-sell. Carve a premium **"Media Pro / DAM"** upsell for: responsive WebP/AVIF conversion generation, visual focal-point + crop editor, folders/galleries, and usage drill-down. These are the advanced-DAM items that justify a paid tier without weakening the foundation.

**Screenshot/media gaps.** The current buyer-facing gallery keeps only the marketplace card. The 8 committed light/dark PNGs remain runner evidence but were demoted after visual inspection showed empty Media tables rather than the media health page/table, Curator field, or migration output. Recapture those four screenshot-contract scenarios with seeded media records as future media polish, then add a focal-point editor screenshot if a visual editor ships in Media Pro.

**Platform-pitch contribution & cross-sell.** "Capell gives you a real media foundation, not per-field uploaders" is a strong platform line. Cross-sell hooks: **media-ai** (auto alt text through the missing-alt signal), **seo-suite** (alt text + image metadata feed structured data / image sitemaps), **themes** (responsive metadata and focal output once conversions exist), and **Media Pro/DAM** (where-used UI, visual focal/crop editor, folders/galleries, and conversion generation).

**Keywords/tags (8–12):** `media-library`, `digital-asset-management`, `curator`, `filament-media`, `alt-text`, `responsive-images`, `focal-point`, `image-metadata`, `media-migration`, `spatie-media`, `media-health`, `dam`.

## 6. Prioritized Roadmap

| Item                                                                                                                                                                                                                                                                                                            | Bucket | Effort | Impact | Section       |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------ | ------------- |
| Done/Shipped: Implement the 3 declared health checks. Evidence: Curator backend/model/field-factory binding, `curator` table, and schema-valid owner FK diagnostics are translated and covered.                                                                                                                 | Done   | M      | High   | §2.1, §4      |
| Done/Shipped: Ship & publish `config/media-library.php` for `owner_foreign_keys` (+ auto-discover). Evidence: explicit config wins, default schema discovery handles conventional Curator FK columns, and health/orphan reports use the resolver.                                                               | Done   | M      | High   | §2.2, §4      |
| Done/Shipped: Fix orphan cleanup to delete files, not just rows; wire it to UI/command. Evidence: selected health-table bulk cleanup delegates to `DeleteOrphanMediaRecordsAction`, revalidates selected rows through the orphan query, and focused tests prove only unused selected records/files are deleted. | Done   | M      | High   | §2.3, §4      |
| Done/Shipped: Stop forcing `visibility=public` on upload + migration; preserve source visibility. Evidence: uploads respect explicit/configured visibility and migration preserves private source disk visibility on migrated Curator rows.                                                                     | Done   | M      | High   | §4            |
| Done/Shipped: Add mime/size/extension validation to `addMediaFromUploadedFile`; upload validation is config-driven via `allowed_mime_types`, `allowed_extensions`, and `max_upload_kb`, with focused tests for allowed uploads and actionable rejection errors.                                                 | Done   | S      | Med    | §4            |
| Done/Shipped: Per-issue column/filter on Media Health table; stale threshold is configured via `capell.media_library.stale_after_days`. Evidence: `MediaHealthTest` covers issue labels, filter behavior, and the configurable threshold; `MediaLibraryCoverageTest` covers table registration.                 | Done   | S      | Med    | §2.5          |
| Recapture meaningful Media Library screenshots before promoting product media. Evidence: current marketplace media keeps only the extension card; committed empty-table PNGs remain runner evidence until replaced with populated media-health, Curator field, and migration-report captures.                   | Future | S      | Med    | §2.10, §1, §5 |
| Done/Shipped: Extract missing-alt signal Action/event for media-ai                                                                                                                                                                                                                                              | Done   | S      | High   | §3, §5        |
| Done/Shipped: Add usage drill-down Action for where-used Media Pro/DAM seams                                                                                                                                                                                                                                    | Done   | M      | High   | §2.9, §3      |
| Done/Shipped: Add anonymous/non-admin output-safety tests for `getUrl`/`getSrcset`                                                                                                                                                                                                                              | Done   | S      | High   | §4            |
| Done/Shipped: Memoize/eager-load `getFirstMedia` repeated owner lookups                                                                                                                                                                                                                                         | Done   | S      | Med    | §2.8, §4      |
| Done/Shipped: Cache health/orphan report results                                                                                                                                                                                                                                                                | Done   | S      | Med    | §4            |
| Done/Shipped: Content-hash duplicate detection (replace disk+path keying)                                                                                                                                                                                                                                       | Done   | M      | Med    | §2.6, §3      |
| Done/Shipped: Parse JSON for rights-metadata check (kill false negatives)                                                                                                                                                                                                                                       | Done   | M      | Med    | §2.7, §4      |
| Deferred to Media Pro: responsive WebP/AVIF conversion generation. Evidence: the foundation manifest now advertises responsive metadata reading, not generated variants, and the plan records the Media Pro follow-up.                                                                                          | Done   | L      | High   | §3, §5        |
| Deferred to Media Pro: visual focal-point + crop editor in Curator field. Evidence: the foundation package documents the PHP focal/crop metadata API as shipped and keeps visual editing in the Media Pro scope.                                                                                                | Done   | L      | Med    | §2.4, §3      |
| Deferred to Media Pro: folders/galleries plus richer where-used UI/routes. Evidence: the foundation package keeps the single-FK Curator adapter honest, while `BuildMediaUsageDrilldownAction` supplies the data seam Media Pro can render.                                                                     | Done   | L      | Med    | §3            |