Media Library — Improvement & Growth Plan
Package: capell-app/media-library · Kind: package · Tier: free · Product group: Capell Foundation · Bundle: foundation · Status: Complete
1. Snapshot
Section titled “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)
Section titled “2. Improvements (existing functionality)”Prioritized.
-
Done/Shipped: Implemented the declared health checks.
MediaLibraryHealthChecknow verifies the Curator media backend, model, andMediaFieldFactorybinding, thecuratortable, 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 -
Done/Shipped: Ship useful owner-FK config defaults. Explicit
owner_foreign_keysconfig 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 -
Done/Shipped: Orphan cleanup deletes files and is reachable from the health UI.
DeleteOrphanMediaRecordsActionalready removes unshared storage blobs before deleting Curator rows, and now accepts selected media IDs while still re-validating them through the orphan query.MediaHealthTableexposes a translated bulk action so operators can delete selected unused media without deleting still-referenced rows. Evidence:MediaGapQueryActionsTestcovers selected cleanup preserving used and unselected orphan records while deleting the selected orphan file;MediaLibraryCoverageTestcovers the table bulk action. —src/Actions/DashboardReports/DeleteOrphanMediaRecordsAction.php,src/Filament/Pages/Tables/MediaHealthTable.php— M -
Focal point / crop preset API has no editor surface.
CuratorMediahas fullgetFocalPoint/setFocalPoint/getCropPresets/setCropPresets/getFocalPointForConversionlogic (and tests), but grep shows these are referenced only in the model and its coverage test — never byCuratorMediaFieldFactory, the picker, or any Filament component. Themedia-library-focal-pointscapability 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 -
Done/Shipped: Media health rows expose a per-issue column and filter, with a config-driven stale threshold.
BuildMediaHealthQueryActionselectsmedia_health_issue(missing_alt,stale, orunused) and readscapell.media_library.stale_after_days;MediaHealthTableshows the translated issue badge and filters by the same primary-issue precedence so operators can focus missing-alt, stale, or unused rows. Evidence:MediaHealthTestcovers computed issue labels, configurable stale threshold, and the table issue filter;MediaLibraryCoverageTestcovers the table column/filter registration. —src/Filament/Pages/Tables/MediaHealthTable.php,src/Actions/DashboardReports/BuildMediaHealthQueryAction.php— S -
Done/Shipped: Duplicate detection groups by content hash, not path.
BuildDuplicateMediaQueryActionnow 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 bothduplicate_countandduplicate_hash. —src/Actions/DashboardReports/BuildDuplicateMediaQueryAction.php,tests/Integration/MediaGapQueryActionsTest.php— M -
Done/Shipped: Rights-metadata checks parse
exifJSON structurally.BuildMissingRightsMetadataQueryActionnow decodes CuratorexifJSON, 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 -
Done/Shipped:
InteractsWithCuratorMedia::getFirstMediamemoizes 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. RepeatedgetFirstMedia(),getFirstMediaUrl(), andgetMedia()calls for the same owner no longer re-query thecuratortable. —src/Concerns/InteractsWithCuratorMedia.php,tests/Feature/FrontendRenderTest.php— S -
Done/Shipped: Media usage drill-down Action.
BuildMediaUsageDrilldownActionresolves 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 -
Future media polish: recapture meaningful Media Library screenshots. README and
docs/overview.mddescribe 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)
Section titled “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
InteractsWithCuratorMediaconcern 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-metadatainstead of generated variants.getSrcset()reads a pre-existingresponsive_imagesmetadata 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.
BuildMissingAltMediaQueryActionexposes image media with null, empty, or whitespace-only alt text as an ordered candidate query withusage_count;DispatchMissingAltMediaSignalsActionemitsMediaMissingAltDetectedevents 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).
DeleteOrphanMediaRecordsActionexists but is unreachable from UI (see §2.3). - Done/Shipped: usage drill-down depth. Usage counts now have a companion
BuildMediaUsageDrilldownActionthat 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
namecolumn 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
Section titled “4. Issues / Risks”- Done/Shipped: advertised health checks are real. The package now fails Diagnostics when Curator is not fully registered, the
curatortable 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, andmedia_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::addMediaFromUploadedFileaccepts explicitpublic/privatevisibility and otherwise usescapell.media_library.default_visibility; private uploads store to the local disk.MigrateSpatieMediaToCuratorActionpreserves private source disk visibility on migrated Curator rows. Evidence:MediaVisibilityTestcovers upload defaults/config/explicit private handling and private URL safety;MigrateSpatieToCuratorCommandTestcovers 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).
addMediaFromUploadedFilestores whateverUploadedFileit 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).
MediaUsageQueryExpressionsvalidates identifiers with^\w+$, wraps via the query grammar, and checkshasTable/hasColumnbefore composing the correlated-subqueryusageCountExpression. This is the right pattern; keep it. Note for reviewers so theselectRaw/whereRawusage isn’t flagged as injection. - Performance budget claims vs reality. Manifest sets
adminQueryBudget: 40andcacheSafety.cacheable: falsewith 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 shortreport_cache_ttl_secondscache 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/getSrcsetoutput safety, focal/crop metadata storage, srcset200w, field factory returnsCuratorPicker, manifest capability assertions, health checks, orphan file deletion, upload validation, and private media URL safety. Not covered:MediaHealthPageFilament Shield gating. —tests/. - i18n. All
MediaHealthPage/MediaHealthTablestrings resolve fromcapell-admin::translation keys (good), butresources/lang/en/package.phpis near-empty andMigrateSpatieToCuratorCommandconsole output ('Migration summary','Warnings:','[Dry run] ...') is hardcoded English. —src/Console/MigrateSpatieToCuratorCommand.php. - Manifest/doc mismatches (besides screenshots in §1).
MigrateSpatieMediaInputdocblock contains a corruption: “the action dashboard-dashboard_reports without writing” (should describe dry-run). —src/Data/MigrateSpatieMediaInput.php.
5. Marketplace & Positioning
Section titled “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
Section titled “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 |