Dashboard Reports — Improvement & Growth Plan
Package: capell-app/dashboard-reports · Kind: package · Tier: premium · Product group: Capell Operations · Bundle: operations · Status: Draft
1. Snapshot
Section titled “1. Snapshot”Admin-only reporting package (surfaces: ["admin"]) that registers two Filament dashboard widgets into DashboardEnum::Main: ContentHealthWidget (scheduled / expired / URL-less / stale page counts) and PublishingTrendChartWidget (7-bucket published-vs-scheduled line chart). All domain logic lives in two reachable Actions — BuildDefaultContentHealthAction and BuildPublishingTrendAction (src/Actions/Dashboard/) — both reading core Page records through SiteScope::applyForCurrentActor(); the package owns no migrations, settings, or tables (database.migrations: false, requiredTables: []). It also binds a ContentHealthDataProvider (replacing the admin null provider) and tags a DashboardSettingsContributor for show/hide toggles. Deps: capell-app/admin, capell-app/core, lorisleiva/laravel-actions, spatie/laravel-data, spatie/laravel-package-tools. Marketplace summary now names the content-health and publishing-activity outcome directly. Screenshots declared in capell.json: 4 committed marketplace assets (extension-card.jpg plus three route-backed PNG captures for the required screenshot targets). docs/screenshots.json specifies the same 3 deployment captures (publishing-trend widget, content-health widget, settings) with runner fixture URLs.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”- Stop building content-health twice per render —
ContentHealthWidget::canView()callshasContentHealthData()which runs->build()(4COUNTqueries), thendata()runs->build()again (another 4). Same page load = 8 content-health queries plus 14 from the trend widget (7 buckets × published + scheduled). Cache the provider result for the request, or fold the “has issues” check into the cacheddata()result. —src/Filament/Widgets/ContentHealthWidget.php:33,38,45— S - Collapse the 14-query publishing-trend loop into grouped aggregates —
BuildPublishingTrendAction::handle()issues 2COUNTs per bucket inside a 7-iteration loop, plus 1 fortotalScheduled(15 round-trips). Replace with two grouped queries (one published, one scheduled) bucketed in SQL, or a singleselectRawwith conditional sums, then map rows to buckets. Material on largepagestables. —src/Actions/Dashboard/BuildPublishingTrendAction.php:25-42,58-78— M - Done/Shipped: date-range mapping is owned by the widget trait.
PublishingTrendChartWidgetusesHasDashboardDateRange::getDashboardDateRange()and passes resolvedCarbonImmutablestart/end values intoBuildPublishingTrendAction, so the Action no longer re-derives raw period strings. Focused publishing-trend coverage calls the Action with explicit ranges. —src/Filament/Widgets/PublishingTrendChartWidget.php,src/Actions/Dashboard/BuildPublishingTrendAction.php— S - Done/Shipped:
totalScheduledfollows the selected period.BuildPublishingTrendActioncalculates scheduled counts through the same bounded buckets used by the chart and setstotalScheduledtoarray_sum($scheduledCounts), matchingtotalPublishedwindow semantics. Existing boundary coverage asserts scheduled pages count once inside the selected range. —src/Actions/Dashboard/BuildPublishingTrendAction.php,tests/Feature/Actions/Dashboard/BuildPublishingTrendActionTest.php— S - Done/Shipped: long-range publishing trend labels show date ranges.
BuildPublishingTrendActionkeeps the existing 7 bounded buckets but now labels multi-day buckets as ranges (for example,Jan 1 - Feb 22) while preserving single-dayM jlabels. This fixes the misleading long-range chart copy without changing aggregate counts. —src/Actions/Dashboard/BuildPublishingTrendAction.php— M - Done/Shipped: CSV export per widget.
capell:dashboard-reports:exportnow exports eithercontent-healthorpublishing-trendwidget data as CSV, with--path, publishing-trend--from/--to, and content-health--stale-daysoptions. CSV formatting lives in Actions so command output and future package operations can reuse the same contract. —src/Actions/Dashboard/ExportContentHealthCsvAction.php,src/Actions/Dashboard/ExportPublishingTrendCsvAction.php,src/Console/Commands/ExportDashboardReportCommand.php— M - Done/Shipped: theme-token chart colours + dark-mode/i18n polish.
PublishingTrendChartWidgetnow uses Filament/Capell CSS token colours for published and scheduled datasets instead of hard-coded hex values, and coverage asserts the human-readable dashboard settings group label resolves toDashboard Reports. —src/Filament/Widgets/PublishingTrendChartWidget.php,resources/lang/en/dashboard.php,tests/Feature/Filament/DashboardReportsCoverageTest.php,tests/Feature/Filament/DashboardReportsDashboardSettingsContributorTest.php - Done/Shipped: Per-issue filtered deep-links are wired. Content-health issues link to the Page resource with the package-owned
dashboard_reports_healthtable filter preselected forscheduled_pages,expired_pages,pages_without_urls, orstale_pages. Evidence:BuildDefaultContentHealthAction::PAGE_TABLE_FILTER_KEY,pageIndexUrl(),DashboardReportsPageTableExtender, overview docs, and existing coverage inBuildDefaultContentHealthActionTest. — M - Done/Shipped: configurable stale-day threshold is wired.
DashboardReportsContentHealthDataProviderresolvesDashboardReportsSettingsResolver::settings()and passesstalePageThresholdDaysintoBuildDefaultContentHealthAction. The config keycapell-dashboard-reports.stale_page_threshold_daysis documented, clamped to 1-3650 days, shared by the widget and Page resource drill-down filter, and covered by focused provider/widget tests. —src/Support/Dashboard/DashboardReportsContentHealthDataProvider.php,src/Support/Dashboard/DashboardReportsSettingsResolver.php,docs/overview.md— S
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”Manifest capabilities are only ["dashboard-reports", "dashboard-reports-admin"] — deliberately generic, so the gap list defines what would make this a credible “reports” product rather than two widgets.
- Export PDF / print snapshot. CSV export is shipped for both widgets; a PDF/print snapshot of the dashboard remains open. (table-stakes)
- Scheduled email digests. No console command (
commands.install/setup/demo/doctorallnull), no queued job. A weekly “content health + publishing trend” email to editors/owners is the highest-value differentiator and fits theoperationsbundle. Would require its own Action + scheduled command + mailable. (differentiator) - Configurable thresholds & report toggles beyond visibility. Settings contributor only toggles widget show/hide; no stale-day threshold, no per-issue enable/disable, no bucket-count choice. (table-stakes)
- Drill-down / filtered deep-links. Content-health issue deep-links are shipped (see §2). Trend chart points should still become clickable to the pages published in that bucket. (table-stakes)
- More health signals. Current set is 4 page-state counts. The stale
ContentHealthData::from([...])fixture in the widget test (missingMetaDescriptionCount,duplicateTitleCount,emptyContentCount) hints at intended SEO/content-quality checks that were never built. Add missing-meta, duplicate-title, empty-body, orphaned-page checks. (differentiator) - Custom / composable report builder. Capabilities advertise generic “dashboard-reports” but there is no registry for host apps or sibling packages to contribute additional report widgets/cards through this package —
contributionTraceability.deferredContributions: ["dashboard-widget"]is declared but no extension point is implemented here. Expose a small report-registry soinsights,ga4-reports,login-auditcan plug cards in. (differentiator) - Role-scoped dashboards / saved views. Widgets gate by role (
editor/admin/super_admin) but there are no per-role report presets or saved filter views. (differentiator) - Real diagnostics health check. The advertised health check is a stub (see §4) — a genuine check (providers bound, widgets registered, settings reachable) is a missing capability the manifest already promises. (table-stakes for a “premium/first-party” tier)
4. Issues / Risks
Section titled “4. Issues / Risks”- Health check is a stub but advertised as
critical.DashboardReportsHealthCheck implements ChecksExtensionHealth, and that contract (vendor/capell-app/core/.../ExtensionContribution.php) only requirescompatibleCapellApiVersion(): string. The class returns'^4.0'and asserts nothing else. Yetcapell.jsonlabels it “package surfaces, providers, and install health are discoverable by Diagnostics” with"severity": "critical". This is a manifest/code mismatch: a green “critical” check that verifies an API-version string, not surfaces/providers/install. —src/Health/DashboardReportsHealthCheck.php:9-15,capell.json:58-66 - Stale Data shape in test fixture (misleading, not failing).
DashboardReportsDashboardWidgetsTest.php:36-42buildsContentHealthData::from(['missingMetaDescriptionCount' => 0, 'duplicateTitleCount' => 0, 'staleContentCount' => 0, 'emptyContentCount' => 0]), butContentHealthDataonly hasissues: DataCollection(vendor/capell-app/admin/.../ContentHealthData.php). spatie/laravel-data silently drops the four unknown keys, yielding an object with an emptyissuescollection. The test passes because it only asserts provider identity (->toBe(...)), so the fixture documents a contract that no longer exists. —tests/Feature/Filament/DashboardReportsDashboardWidgetsTest.php:36-42 - Done/Shipped: Actions deny missing actors before site scoping.
BuildDefaultContentHealthActionandBuildPublishingTrendActionboth callSiteScope::applyForCurrentActor(Page::query(), denyWhenMissingActor: true), preventing unauthenticated console/queue/future digest paths from falling back to unscoped all-site counts. —src/Actions/Dashboard/BuildDefaultContentHealthAction.php,src/Actions/Dashboard/BuildPublishingTrendAction.php - Query cost vs declared budget.
performance.adminQueryBudget: 40. A single dashboard render of both widgets currently issues ~8 (content health, doubled — see §2) + ~15 (trend loop) = ~23 page COUNTs againstpages, before any other dashboard widget. Within the 40 budget today, but the budget is per package and this leaves little headroom; the loop scales by buckets, not rows, but each COUNT still scans. No test asserts the query count, so regressions won’t be caught. —capell.json:48,src/Actions/Dashboard/BuildPublishingTrendAction.php:25-42 Computed(persist: true, seconds: 300)cache key. Content-health data is Livewire-persisted for 300s. Livewire computed-property persistence is per-component-instance/session, so cross-actor leakage is unlikely, but the cached payload is derived from site-scoped queries — if the persistence key is ever broadened (e.g. shared cache store), a super-admin’s full-site counts could persist into a scoped actor’s view. Add a test proving the cached counts are actor/site-scoped. —src/Filament/Widgets/ContentHealthWidget.php:37- No test coverage for:
canView()gating (role/settings on/off), the blade view render, the health check class, the stale-day threshold, or query counts. Covered today: both Actions’ counts/boundaries, settings-contributor keys + tagging, widget registration, provider binding/non-override, dataset shape, package config. Gaps are the gating and rendering paths a buyer relies on. —tests/Feature/ - Done/Shipped: i18n group label and chart colour drift. The settings group now resolves to
Dashboard Reports, and the chart datasets use theme tokens rather than literal hex colours. Onlyenis shipped. —resources/lang/en/dashboard.php,src/Filament/Widgets/PublishingTrendChartWidget.php - Public-output safety: N/A but worth a guard. Package is admin-only (no frontend surface), so no anonymous-leak risk today. If a digest/export feature is added (§3), it crosses a new boundary and must not embed admin URLs/model IDs for non-admin recipients.
5. Marketplace & Selling
Section titled “5. Marketplace & Selling”Critique. Marketplace/composer copy now names the shipped content-health and publishing-activity widgets directly, and package-local docs/translations no longer describe the package as “generic CMS reporting widgets”. The README is still heavy on dependency-credit cards and code-map boilerplate. Runtime deployment captures for the three screenshots.json targets are now committed.
Improved summary (1 sentence): At-a-glance content-health and publishing-activity widgets for the Capell admin dashboard — spot scheduled, expired, stale, and URL-less pages without opening a single resource.
Improved description (3–4 sentences): Dashboard Reports surfaces editorial health and publishing momentum directly on the Capell admin dashboard, so owners and editors see what needs attention the moment they log in. The Content Health widget flags scheduled, expired, stale, and URL-less pages with one-click deep-links into the page list, while the Publishing Trend chart tracks published-vs-scheduled activity across any date window. All counts respect each user’s site access, and report visibility is toggleable per dashboard. Built on testable Actions and typed Data objects so teams can extend the reporting layer instead of bolting on a bespoke analytics package.
Media gaps. Marketplace gallery coverage is now reconciled through committed PNG captures for each required capture target. Add a short GIF of changing the date range to make the trend chart feel live.
Pricing / tier / bundle. Tier premium, bundle operations, group Capell Operations. With only two read-only widgets and a stub health check, “premium” is hard to defend versus a “standard” tier today; the export + scheduled-digest features in §3 are what justify premium. Position inside the operations bundle alongside diagnostics, publishing-studio, login-audit.
Cross-sell. Strong, evidence-backed paths: publishing-studio already ships its own richer DashboardReports-namespaced Actions (BuildActivityTrailQueryAction, BuildScheduledPublishingQueryAction, BuildStaleDraftsQueryAction, editorial-calendar events — see packages/publishing-studio/), so this package is the natural entry point that publishing-studio deepens. README already cross-links diagnostics, publishing-studio, login-audit. Extension-suite cross-sell: insights and ga4-reports as report-card contributors once the report registry (§3) exists.
Differentiators / value props / buyer. Value: zero new tables (reads existing page state), site-scoped by default, Action-first testability. Differentiator vs table-stakes: scheduled email digests + extensible report registry. Target buyer: multi-site content operations leads / agency operators who manage editorial calendars and need a glanceable health snapshot.
Keywords/tags (8–12): capell, cms, dashboard, reporting, content-health, publishing-trend, editorial-analytics, filament-widgets, admin-dashboard, site-scoped, operations, content-operations
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section ref |
|---|---|---|---|---|
Shipped 2026-06-04: Eliminate double build() in ContentHealthWidget | Done | S | High | §2, §4 |
| Shipped 2026-06-04: Replace 14-query trend loop with grouped aggregates | Done | M | High | §2, §4 |
Shipped 2026-06-04: Implement a real Diagnostics health check (providers/widgets/settings bound); evidence: provider resolution, widget registration, settings key, and broken-binding coverage in DashboardReportsHealthCheckTest | Done | M | High | §4 |
Shipped 2026-06-05: Fix per-issue filtered deep-links with the dashboard_reports_health Page table filter | Done | M | Med | §2, §3 |
| Shipped 2026-06-06: Commit deployment screenshot captures for the 3 screenshot-contract targets | Done | S | Med | §1, §5 |
| Shipped 2026-06-05: Rewrite marketplace summary + composer description | Done | S | Med | §5 |
Shipped 2026-06-04: Remove stale ContentHealthData::from([...]) fixture + add gating/render tests | Done | S | Med | §4 |
| Done/Shipped: De-duplicate date-range mapping (pass resolved range into Action) | Done | S | Med | §2 — Reconciled 2026-06-06: the widget resolves the dashboard date range and passes typed start/end values into the Action. |
| Done/Shipped: Wire configurable stale-day threshold (+ reconcile docs) | Done | S | Med | §2, §3 — Reconciled 2026-06-06: config, resolver clamp, provider wiring, overview docs, and focused coverage already exist. |
Done/Shipped: totalScheduled period semantics are windowed and long-range buckets use range labels | Done | M | Med | §2 — Reconciled 2026-06-06: totalScheduled sums selected-range scheduled buckets and multi-day labels render as explicit ranges. |
Done/Shipped: Pass denyWhenMissingActor: true (or doc Actions as admin-only) | Done | S | Med | §4 — Reconciled 2026-06-06: both dashboard report Actions deny missing actors before applying site scope. |
| Shipped 2026-06-06: CSV export per widget | Done | M | High | §3 |
| Scheduled email digest (Action + command + mailable) | Later | L | High | §3 |
Report registry for sibling-package report cards (deferred dashboard-widget) | Later | L | High | §3, §5 |
| Done/Shipped: Theme-token chart colours + dark-mode parity + i18n group label fix | Done | S | Low | §2, §4 |