appstract/nova-horizon
Displays Laravel Horizon queue metrics inside Laravel Nova, so you can monitor workload, jobs per minute, wait times, and failed jobs from your Nova dashboard without switching tools.
Confirm the preconditions before you type composer require. This package is a Nova tool/card that reads Horizon's metrics. It is worthless — and potentially misleading — if you don't have Laravel Nova (licensed, installed) and Laravel Horizon running on Redis. Database/SQS queue users: stop. Teams without a Nova license: stop. The whole value proposition is "queue health where the non-SSH crowd already lives."
Install and publish:
composer require appstract/nova-horizon
php artisan vendor:publish --provider="Appstract\NovaHorizon\ToolServiceProvider"
Pin the version in composer.json ("appstract/nova-horizon": "x.y.z", no caret) the same day you install it. This package has 77 stars and 0 dependents; you are effectively the maintainer.
Register it in app/Nova/NovaServiceProvider.php:
use Appstract\NovaHorizon\NovaHorizon;
public function tools()
{
return [
new NovaHorizon,
];
}
Note the discrepancy: the README/description calls it "a simple Nova card," but the registration path in the docs is
tools(). Read the published source before registering — confirm whether it ships aTool(sidebar entry) or aCard(dashboard panel) for your Nova major, and register it in the corresponding method (tools()vscards()). Getting this wrong yields a silent no-show, not an error.
Build Nova assets so the entry actually renders:
php artisan nova:publish
If you compile Nova assets yourself, also run npm run prod in the app directory and check the browser console afterwards (see Gotchas).
First use case — validate before you trust it. Open Nova, click the Horizon entry, and put it side-by-side with /horizon on the same environment:
If any of these disagree, do not ship it. Wrong queue metrics are worse than no queue metrics because someone will page on them or reassure a customer with them.
Where to look first in the source (this is the ten-minute review that decides adoption):
src/NovaHorizon.php and the card/tool classes — does it consume Laravel\Horizon\Contracts\MetricsRepository, JobRepository, QueueRepository, etc.? That's the stable surface. If it reads raw Redis keys like horizon:queue:* or horizon:failed:* directly, it will break on Horizon minor-ish internal changes — treat that as a fork-or-rebuild signal.canSee() / authorizedToSee() return? Nova's default viewNova gate is a UI gate, not a policy for exposing job payloads.Mental model: /horizon is authoritative, this is the convenience layer. Frame it internally as "a read-only glance for people who don't have /horizon access." Every workflow below assumes you still open /horizon to act on a problem.
Gate it to an ops/support role, not all of Nova. Nova tools inherit the default Nova gate, which is coarse. Define a gate and check it:
// AuthServiceProvider
Gate::define('viewQueueHealth', fn (User $user) => $user->hasAnyRole(['ops', 'support-lead', 'engineer']));
Then register the tool only inside a serving context and let the tool's canSee()/policy enforce visibility. If the shipped tool has no policy hook, subclass it or wrap registration:
public function tools()
{
return Nova::whenServing(function () {
return [
new NovaHorizon,
];
});
}
Day-to-day on-call triage loop — the workflow this package actually optimizes:
Pager/Slack alert
→ open Nova (already logged in, no VPN)
→ Horizon tool: which queue is backed up? is anything failing?
→ escalate to /horizon (ops gate) to inspect, retry, or kill the job
→ if it's payload-level debugging, SSH or logs as usual
The card answers "what's happening" in ~10 seconds. It does not answer "why." Don't let the team start treating it as the pager — alerting stays on Horizon's native notification path (Horizon::routeMailNotificationsTo, long-wait thresholds, failed-job listeners) or your existing Sentry/Pulse wiring.
Support-lead pattern (the highest-value non-engineer use case). Support staff with Nova access but no /horizon access can answer "is this customer's job stuck / failing?" without a ticket to engineering. This is the pattern that justifies adoption. Write a one-page internal runbook for it: where to click, how to read the numbers, what to escalate.
Multi-connection / multi-environment topologies need explicit verification. If config/horizon.php has multiple environments, multiple Redis connections, or cluster/sentinel setups, check what the card actually renders. Two outcomes:
config('horizon.environments').Treat it as a card-first read model on the dashboard. If your team lives in the Nova dashboard, put it there and check it at the start of the day / after a deploy. That's the low-friction habit. If your team lives in /horizon, don't bother.
Upgrade runbook entry (do this on day one, not later). Add to your Nova and Horizon upgrade checklist:
/horizon — throughput, failed count, wait time, queue list.Fork-first policy. The moment you need anything the package doesn't do — per-connection filtering, custom gating, different metrics, a Nova 6 compatibility patch — fork it into your namespace (internal/nova-horizon or a VCS path repo). This package has no plugin API; extension means editing source, which means owning a fork. Don't open PRs for features; open them only for compat fixes.
Custom card fallback is roughly a day of work and is worth it if you need policy control over payloads:
class QueueHealth extends Card
{
public function component() { return 'queue-health'; }
}
// Controller / card data — public contracts only
public function metrics(MetricsRepository $metrics): array
{
return [
'throughput' => $metrics->throughputForQueue('default'),
'wait' => $metrics->waitTimeForQueue('default'),
];
}
Rollback is two lines. Remove new NovaHorizon from tools() and composer remove appstract/nova-horizon. No migrations, no schema, no job changes. That reversibility is the argument for a 1–2 day staging spike rather than an extended evaluation.
Version coupling is the primary risk, not functionality. Both Nova's tool/card APIs and Horizon's contracts have moved across majors. With zero dependents, assume the package is stale until a compatibility spike proves otherwise against your pinned Nova and Horizon versions. Pin exact versions and assign a named internal owner — no vendor, no SLA, no predictable responsiveness.
Silent wrong metrics is the failure mode to fear. A broken JS asset gives you a blank page — obvious. A changed Horizon repository contract can render plausible-looking numbers that are simply wrong — invisible. Every Nova/Horizon major, re-validate against /horizon in a real environment before shipping. This single habit is the highest-ROI thing you can do.
Test with Redis unreachable. Open the Nova dashboard with Redis down (or Horizon stopped). An isolated card error is fine; a 500 that takes out the whole admin panel is not. If it takes Nova down with it, isolate it:
public function tools()
{
return RescueTool::make(fn () => [new NovaHorizon]);
}
or gate registration behind a gate callback that itself checks Horizon health.
Authorization is not included beyond viewNova. Failed-job and exception views typically include serialized job payloads — request bodies, customer IDs, occasionally tokens. If the tool surfaces failed jobs, this is PII in your admin panel, and a Nova user who isn't on the ops team shouldn't see it. Nova's default gate won't stop them. Either enforce a canSee() policy on a fork, or build the custom card — policy control is not something you can retrofit cleanly onto a card you don't own.
Audit the failed-jobs panel in your security review if Nova is accessible more broadly than engineering (support, sales, contractors). Treat it the same as any other surface exposing raw queue payloads.
Blank page after install = asset problem, almost always. Check the browser console first: a 404 or parse error on js/nova-horizon.js means either nova:publish didn't run, ran against the wrong Nova major, or your npm run prod bundle collided with the published asset. Second check: whether it registers as a tool vs. a card (see Getting Started).
Sidebar entry appears, page is empty → the Vue component name doesn't match what the registered tool/card declares. Re-read the class's component() and match it to the compiled JS.
Don't fragment ownership if Pulse is in play. If your platform team is standardizing on Laravel Pulse, Datadog, Grafana, or Sentry, adopting this alongside them creates a second, weaker source of truth. Pulse covers a lot of the same ground. Decide on one owner and one dashboard for queue observability.
Sizing the maintenance budget honestly. Steady state is ~zero. Expect ~0.5–1 day per Nova major or Horizon major upgrade to re-validate and patch. If you can't name that owner, don't adopt — an unowned, silently-stale queue dashboard is a trap.
Small-package diligence checklist before you commit (an hour, once):
Laravel\Horizon\Contracts\*, not raw Redis keyscanSee() / policy behavior understood; failed-payload exposure assessedHow can I help you explore Laravel packages today?