# Dashboard Reports — Improvement & Growth Plan

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

## 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)

- **Stop building content-health twice per render** — `ContentHealthWidget::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 aggregates** — `BuildPublishingTrendAction::handle()` issues 2 `COUNT`s 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

## 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/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)_

## 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 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.

## 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

| 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                                                                                                                             |