Features

Dashboard

The dashboard is the Fortify home page: DashboardController renders Inertia dashboard with cached activity, storage, upload-health, Insights MVP widgets, and a Health tab fed by DashboardHealthMetrics. Access requires can-show-dashboard.

Related

Access

ItemDetail
ControllerApp\Http\Controllers\DashboardController
Methodindex
RouteGET /dashboard (dashboard)
Middlewareauth, verified, permission:can-show-dashboard
Permissioncan-show-dashboard (PermissionEnum::CanShowDashboard)
Frontendresources/js/pages/dashboard.tsx
Defined inroutes/web.php

Unauthenticated visitors never reach this page; Fortify’s home path is /dashboard.

Tabs: Overview | Health

Compact inline tabs (underline, same row as the description) switch content. There are no primary “Open Pulse” / “Open Horizon” CTAs on the Health tab — metrics render as native Externa cards/charts. Livewire UIs at /pulse and /horizon remain for super-admin deep dives only (bookmark / type URL).

TabContents
OverviewInsights / activity / storage / upload-health widgets (below)
HealthNative cards from DashboardHealthMetrics (system + Pulse aggregates + Horizon/Redis queue)

Deep-link: /dashboard?tab=health opens the Health tab. Switching tabs updates the query via history.replaceState.

Authenticated Inertia pages still share healthBadge (ok / warn / fail + label) from DashboardHealthMetrics::badge() (cached ~60s) for clients that need it; there is no sidebar chrome entry — open Health from the Dashboard tab. Fail when Redis is down or Horizon masters are stopped; warn when failed jobs / exceptions / Horizon failed counts are non-zero.

See also Pulse & Health tab.

Overview widgets

DashboardController builds Inertia props from Eloquent / Spatie Activity queries wrapped in Cache::remember. There is no dedicated dashboard service class for Overview. There is no Insights panel builder — fixed MVP panels only.

Insights MVP (~60s cache)

PropCache keyContents
collectionCountsdashboard:collection-countsEach collection’s items_count via withCount('items')
activityOverTimedashboard:activity-over-time:30dDaily activity counts for the last 30 days
contentEventBreakdowndashboard:content-event-breakdown:30dcreated / updated / deleted counts for CollectionItem subjects (30d)

Charts use Recharts (last 7 days of activityOverTime + event breakdown bars).

~60 second cache (ops widgets)

PropCache keyContents
latestActivitydashboard:latest-activityLast 10 Spatie\Activitylog\Models\Activity rows with causer/subject
fileStatsdashboard:file-statsfiles_count, folders_count, files_size_sum from App\Models\File by FileTypeEnum
uploadHealthdashboard:upload-healthin_progress_count, stale_count from App\Models\FileUpload
mostActiveUsersdashboard:most-active-users:24hTop 5 activity causers in the last 24 hours
suspiciousEventsdashboard:suspicious-events:1hLast 25 activities with event = 'failed' in the last hour
stuckOrphanUploadsdashboard:stuck-orphan-uploadsUp to 10 orphan / expired / stale uploads
largestFilesdashboard:largest-filesTop 10 files by size
fileStatsByDiskdashboard:file-stats-by-diskPer-disk stats, loaded with Inertia::defer (group storage)

TTL for these keys is 60 seconds.

Longer cache

| Prop | Cache key | TTL | Contents | | --- | --- | --- | | storageTrend | dashboard:storage-trend:14d | 1 hour | 14-day series of bytes added |

Cache nuance

Product copy often says “~60s widgets.” That matches activity, Insights, file counts/size, and upload health. The storage trend series is intentionally cached for one hour. This is not the notifications unread poll — that poll runs only when realtime broadcasting is off (Reverb & Echo).

Health tab (native metrics)

Prop health comes from App\Services\Dashboard\DashboardHealthMetrics::summary() (not Livewire). Empty / unavailable states show when Pulse tables or Horizon Redis APIs are missing.

System strip

FieldMeaning
queue_connectionconfig('queue.default')
broadcast_connectionconfig('broadcasting.default')
redis_okRedis::connection()->ping()
pending_jobsDatabase jobs count or Redis queues:{name} length
failed_jobsfailed_jobs table count

Pulse card + chart (24h aggregates)

Requires PULSE_ENABLED and pulse_aggregates table (pulse_available).

FieldPulse type
exceptions_24hexception
slow_jobs_24hslow_job
slow_queries_24hslow_query

Also surfaces pulse_enabled / pulse_ingest for the card description. Quiet empty state when all three counts are zero.

Horizon card + workloads table

FieldMeaning
horizon.availableHorizon contracts resolvable
horizon.statusrunning if master supervisors exist, else stopped
horizon.pending / failed / processesFrom workload + job repos
horizon.jobs_per_minute / throughputMetrics repository
horizon.workloads[]Per-queue name, length, wait, processes

What each area answers

  • Insights — collection size; activity volume; create/update/delete mix on content items.
  • Activity — recent events and causers; failed auth under suspicious events.
  • File counts / size — files, folders, bytes, largest files, optional per-disk breakdown.
  • Upload health — in-progress and stale chunked uploads; stuck/orphan rows.
  • Health — queue/broadcast/Redis status; Pulse exception/slow-job/slow-query volume; Horizon pending/failed/throughput/workloads.

Processes needed for Health data

ProcessWhy
php artisan horizonRedis queue worker + Horizon metrics
php artisan pulse:workDrain Pulse redis ingest into aggregates
Herd Reverb (or reverb:start)Not required for Health metrics, but needed for Echo notifications/presence

See Minimal vs full stack.

Source map

ConcernLocation
Controllerapp/Http/Controllers/DashboardController.php
Health metricsapp/Services/Dashboard/DashboardHealthMetrics.php
Pageresources/js/pages/dashboard.tsx
Permission enumapp/Enums/PermissionEnum.phpCanShowDashboard
File modelapp/Models/File.php
Upload modelapp/Models/FileUpload.php
Previous
AI assistant model