Skip to content

Dashboard Reports — Improvement & Growth Plan

Package: capell-app/dashboard-reports · Kind: package · Tier: premium · Product group: Capell Operations · Bundle: operations · Status: Draft

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.

  • Stop building content-health twice per renderContentHealthWidget::canView() calls hasContentHealthData() which runs ->build() (4 COUNT queries), then data() 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 cached data() result. — src/Filament/Widgets/ContentHealthWidget.php:33,38,45 — S
  • Collapse the 14-query publishing-trend loop into grouped aggregatesBuildPublishingTrendAction::handle() issues 2 COUNTs per bucket inside a 7-iteration loop, plus 1 for totalScheduled (15 round-trips). Replace with two grouped queries (one published, one scheduled) bucketed in SQL, or a single selectRaw with conditional sums, then map rows to buckets. Material on large pages tables. — src/Actions/Dashboard/BuildPublishingTrendAction.php:25-42,58-78 — M
  • Done/Shipped: date-range mapping is owned by the widget trait. PublishingTrendChartWidget uses HasDashboardDateRange::getDashboardDateRange() and passes resolved CarbonImmutable start/end values into BuildPublishingTrendAction, 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: totalScheduled follows the selected period. BuildPublishingTrendAction calculates scheduled counts through the same bounded buckets used by the chart and sets totalScheduled to array_sum($scheduledCounts), matching totalPublished window 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. BuildPublishingTrendAction keeps the existing 7 bounded buckets but now labels multi-day buckets as ranges (for example, Jan 1 - Feb 22) while preserving single-day M j labels. 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:export now exports either content-health or publishing-trend widget data as CSV, with --path, publishing-trend --from/--to, and content-health --stale-days options. 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. PublishingTrendChartWidget now 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 to Dashboard 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_health table filter preselected for scheduled_pages, expired_pages, pages_without_urls, or stale_pages. Evidence: BuildDefaultContentHealthAction::PAGE_TABLE_FILTER_KEY, pageIndexUrl(), DashboardReportsPageTableExtender, overview docs, and existing coverage in BuildDefaultContentHealthActionTest. — M
  • Done/Shipped: configurable stale-day threshold is wired. DashboardReportsContentHealthDataProvider resolves DashboardReportsSettingsResolver::settings() and passes stalePageThresholdDays into BuildDefaultContentHealthAction. The config key capell-dashboard-reports.stale_page_threshold_days is 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

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/doctor all null), no queued job. A weekly “content health + publishing trend” email to editors/owners is the highest-value differentiator and fits the operations bundle. 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 so insights, ga4-reports, login-audit can 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)
  • Health check is a stub but advertised as critical. DashboardReportsHealthCheck implements ChecksExtensionHealth, and that contract (vendor/capell-app/core/.../ExtensionContribution.php) only requires compatibleCapellApiVersion(): string. The class returns '^4.0' and asserts nothing else. Yet capell.json labels 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-42 builds ContentHealthData::from(['missingMetaDescriptionCount' => 0, 'duplicateTitleCount' => 0, 'staleContentCount' => 0, 'emptyContentCount' => 0]), but ContentHealthData only has issues: DataCollection (vendor/capell-app/admin/.../ContentHealthData.php). spatie/laravel-data silently drops the four unknown keys, yielding an object with an empty issues collection. 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. BuildDefaultContentHealthAction and BuildPublishingTrendAction both call SiteScope::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 against pages, 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. Only en is 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.

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

ItemBucketEffortImpactSection ref
Shipped 2026-06-04: Eliminate double build() in ContentHealthWidgetDoneSHigh§2, §4
Shipped 2026-06-04: Replace 14-query trend loop with grouped aggregatesDoneMHigh§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 DashboardReportsHealthCheckTestDoneMHigh§4
Shipped 2026-06-05: Fix per-issue filtered deep-links with the dashboard_reports_health Page table filterDoneMMed§2, §3
Shipped 2026-06-06: Commit deployment screenshot captures for the 3 screenshot-contract targetsDoneSMed§1, §5
Shipped 2026-06-05: Rewrite marketplace summary + composer descriptionDoneSMed§5
Shipped 2026-06-04: Remove stale ContentHealthData::from([...]) fixture + add gating/render testsDoneSMed§4
Done/Shipped: De-duplicate date-range mapping (pass resolved range into Action)DoneSMed§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)DoneSMed§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 labelsDoneMMed§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)DoneSMed§4 — Reconciled 2026-06-06: both dashboard report Actions deny missing actors before applying site scope.
Shipped 2026-06-06: CSV export per widgetDoneMHigh§3
Scheduled email digest (Action + command + mailable)LaterLHigh§3
Report registry for sibling-package report cards (deferred dashboard-widget)LaterLHigh§3, §5
Done/Shipped: Theme-token chart colours + dark-mode parity + i18n group label fixDoneSLow§2, §4