henryavila/filament-health-dashboard
Filament Health Dashboard adds a health-check dashboard to your Filament admin panel, giving you a quick overview of your Laravel app's key services and dependencies so you can spot problems before they affect users.
Prerequisites — this package is a view layer only. It assumes spatie/laravel-health is already installed, configured, and has checks registered (via Health::checks([...]) in a service provider or the config/health.php file). It also assumes Filament v4 — v3 panels are not supported.
Install and verify checks first, UI second:
composer require <vendor>/filament-health-dashboard
php artisan health:check # confirm checks pass from CLI before blaming the UI
If the CLI output is empty, the dashboard will be empty too — the two read the same registry.
First use case — pick the lightest surface and confirm data flows:
public function panel(Panel $panel): Panel
{
return $panel
// ...
->pages([
Dashboard::class,
\Vendor\FilamentHealthDashboard\Pages\HealthDashboard::class,
]);
}
Load it. You should see the status grid (color + icon + message per check) and the failing-count badge in the nav.
Only then wire the widget or the embed — the page is the fastest way to validate that Result->meta from your custom checks renders sanely.
Where to look first in the source: the CheckIntegration contract and the modal-rendering component. Those two determine 90% of what you'll want to customise; everything else is presentation you'll likely leave alone.
Choosing a surface — three options, different trade-offs:
| Surface | Use when |
|---|---|
| Standalone page | Ops/support staff need a dedicated "is it healthy?" screen, deep-linked from Slack alerts |
| Widget | You want the failing count visible ambiently on an existing dashboard without a nav trip |
<livewire:filament-health-dashboard /> |
Embedding inside a custom Resource page, a settings page, or a tenant-scoped admin screen |
Widget registration is the usual pattern:
public function panel(Panel $panel): Panel
{
return $panel->widgets([
\Vendor\FilamentHealthDashboard\Widgets\HealthOverview::class,
]);
}
Set protected int|string|array $columnSpan = 'full' on your dashboard page's getWidgets() ordering if the grid crowds — the status grid is dense and reads poorly in a 1/3 column.
Embedding without a panel page — the Livewire component is the integration point for anything conditional:
@if (auth()->user()->can('viewHealth'))
<livewire:filament-health-dashboard />
@endif
Because styles are inlined, this works in a plain Blade view too — but see the payload gotcha below.
Rich tables per check via CheckIntegration — this is where the package earns its keep. Instead of dumping raw meta, map a check to a shaped table:
public function integrations(): array
{
return [
DatabaseConnectionCheck::class => MyDbIntegration::class,
];
}
The integration returns a list of records (assoc arrays); the package renders it as a table and widens the drill-down modal to fit the widest table across all checks. Keep column keys stable across rows — the table renderer assumes homogeneity.
Remediation actions — attach them per check so a failing check offers a fix rather than just a red square. Actions surface success and failure through Filament notifications, so a remediation that throws will not silently show green. Make remediation idempotent: the re-run button is debounced but not deduplicated server-side, and a double-click on a slow action can still land twice.
Auto-refresh — enable it for wall-mounted/ops displays; disable it on shared admin dashboards. Polling re-executes checks, which can mean repeated HTTP/DNS/DB round-trips per open tab. If your checks are slow, prefer a scheduled health:check plus a static read rather than live polling.
Metadata discipline — the generic renderer means you get useful output for free if your checks return well-shaped meta:
So make custom checks return ['rows' => [['key' => ..., 'value' => ...], ...]] rather than a flat associative array of unrelated values, and you get a table without writing an integration.
Filament v4 is a hard floor. Do not add this to a v3 panel expecting graceful degradation — the page/widget base classes differ. If you're mid-migration, keep the package out of the dependency graph until the panel is on v4, or you'll get opaque "class not found" errors during panel boot.
No filament:assets step, but that's not free. The inlined stylesheet means zero build friction on deploy — genuinely nice — but it also means the CSS ships in every render of the component. Embedding the same component several times on one page duplicates it. If you're rendering it in a loop (per-tenant blocks, for example), render once and scope with your own wrapper.
Inlined CSS vs. a custom Vite theme. Because the stylesheet is inlined rather than registered as a Filament asset, your panel's compiled theme takes a different cascade position than you'd expect. Test dark mode and any panel that uses ->viteTheme() explicitly — overrides may need !important or a wrapper class rather than the usual Filament CSS-variable approach.
Health data is sensitive. Result->meta routinely contains hostnames, DSN fragments, exception messages, and config values. The dashboard exposes all of it to anyone who can reach the page. Authorize the page and the re-run action separately — the re-run button is authorized, but that doesn't mean the page is. Add a policy check in canAccessPanel() or gate the page class.
Pretty-printed JSON can leak. Nested meta renders as JSON verbatim; if a check puts a caught exception in meta, stack traces and connection strings land in the UI. Audit what your custom checks stuff into meta before shipping this to non-engineers.
Modal width scales with the widest table. Great on desktop, painful on mobile — a 10-column table will produce a horizontally scrolling modal that's hard to dismiss on a phone. Keep integration tables under ~6 columns, or push detail into an expandable row rather than more columns.
Don't treat green as "resolved". Remediation notifications reflect the action's own success/failure, not the check's re-evaluated state. After a remediation, trigger a re-run and confirm the grid actually flipped; a remediation that returns true while the check still fails is a false-green you have to catch yourself.
Debounce ≠ concurrency control. The re-run button is debounced client-side. Two admins clicking simultaneously still fire two runs, and slow checks (external HTTP) will block the Livewire request. Set explicit timeouts on any check that does network I/O, otherwise the dashboard appears hung.
Badge counts go stale. The failing-count nav badge is derived from the last render. After fixing something via CLI or a scheduler, reload the page rather than trusting a badge you saw before the fix.
Extension point summary — CheckIntegration is the seam for rich tables and remediation; the metadata renderer handles everything else generically. Reach for an integration only when the generic table shape genuinely doesn't fit, since each one is per-app code you now maintain against the package's API.
Testing — assert the page renders with a stubbed check registry (Health::checks([...]) with a fake check) rather than hitting real infrastructure in CI. Health checks that touch the network are the most common source of flaky dashboard tests.
How can I help you explore Laravel packages today?