square1/nova-metrics
Square1 Nova Metrics adds customizable metric cards to Laravel Nova, helping you display key application statistics and trends directly in your Nova dashboard with simple, configurable value and trend metrics.
Architecture fit
Value, Trend, Partition, Progress) resolve their own query inside calculate(NovaRequest $request). Filter state is not threaded through automatically, so a dashboard card and the table directly beneath it can legitimately disagree — the "numbers don't match" complaint is a product defect, not a user misunderstanding.Integration feasibility
composer require: declared Nova major, Laravel, and PHP constraints in composer.json; whether a CI matrix proves the declared Nova version; whether a pre-built JS asset ships (a PHP-only package is materially safer than an unversioned front-end bundle you inherit at upgrade time); and total LOC — under ~500 lines makes fork-and-own a 1–2 day job and changes the entire risk calculus.Filter::apply($request, $query) server-side per request, or accept opaque filter state from the client? Server-side is auditable; client-supplied state is a tampering surface and a potential cross-tenant leak.Technical risk
canSee() and user-dependent options() behave differently in a metric request context than on an index request and must be verified with two users of different scopes returning different numbers.where clauses. Divergence reproduces exactly the bug the package was installed to fix, with extra steps.DateFilter ranges computed against a different request context can be off by a day at boundaries.Key questions to answer before committing
Nova.booting; card packages historically break across that line.options() queries now run once per card on every dashboard load, and what does that cost?Trend card with an expensive whereHas filter (one query per bucket)?Stack fit
Migration path
Compatibility
composer require square1/nova-metrics:x.y.z. No caret ranges on a zero-dependent package.App\Nova\Metrics\FilterAwareMetric base class. Every concrete metric extends that. Swapping to a fork or first-party code then touches one file, not every dashboard.Sequencing
composer.json, confirm pre-built assets, count LOC, identify the filter-application mechanism. Cancel here if the mechanism is client-supplied state.Value card, one resource, one simple filter (status or owner) that users already apply on that index page.Define success before rollout. Installation is not success. Pick the signals now: fewer "numbers don't match" tickets, actual usage of filtered cards, reduced time-to-answer in ops/sales reviews, whether stakeholders stop exporting to spreadsheets.
Maintenance
TicketResource::filters() so metric and index cannot drift apart over time.Support
Scaling
Trend metrics issue one query per bucket. That is ~30 queries for a cheap column predicate — fine. With a whereHas filter it becomes 30 correlated subqueries and a slow dashboard. If a filter is expensive, use a Value card or precompute.options() runs on every metric request, once per card. Heavy options() queries now execute on every dashboard load. Measure this before adding cards beyond the pilot.Failure modes and mitigations
| Failure | Impact | Mitigation |
|---|---|---|
| Filter ignored in metric request | Silent wrong number; looks trustworthy | Parity harness green before second card |
| Authorization not honoured server-side | Cross-tenant data exposure | Two-user test with different scopes asserting different numbers |
| Cache key omits user | Cross-tenant number leak | Include filter payload + user ID; security review |
| Client-supplied filter state | Tampering surface | Audit on Day 0; reject if not server-side |
| Nova 5 upgrade breaks the card | Dashboard blank during upgrade | Pinned version, internal base class, fork budgeted |
options() cost per card |
Slow dashboard under load | Measure during pilot; cap card count |
Expensive filter in Trend |
30 correlated subqueries | Use Value card or precompute |
Ramp-up
DB::listen() or ->dd() on the builder during a spike; diff the metric's query with bindings against the index query with bindings. That divergence explains most wrong numbers faster than reading package source.How can I help you explore Laravel packages today?