usersStat(), $this->searchesStat(), $this->stationsStat(), $this->weeklyForecastStat(), $this->apiErrorsStat(), ]; } private function usersStat(): Stat { return Stat::make('Total users', User::count()) ->icon('heroicon-o-users') ->color('primary'); } private function searchesStat(): Stat { return Stat::make('Total searches', Search::count()) ->icon('heroicon-o-magnifying-glass') ->url(route('filament.admin.resources.searches.index')) ->color('primary'); } private function stationsStat(): Stat { $count = Station::count(); $lastSeen = Station::max('last_seen_at'); $description = $lastSeen ? 'Last seen '.Carbon::parse($lastSeen)->diffForHumans() : 'No stations yet'; return Stat::make('Stations in DB', number_format($count)) ->description($description) ->url(route('filament.admin.resources.stations.index')) ->icon('heroicon-o-map-pin') ->color('success'); } private function weeklyForecastStat(): Stat { $forecast = WeeklyForecast::query()->latest('generated_at')->first(); if ($forecast === null) { return Stat::make('Latest weekly forecast', 'None') ->icon('heroicon-o-beaker') ->color('gray'); } $ageHours = $forecast->generated_at->diffInHours(now()); $color = $ageHours > 168 ? 'warning' : 'success'; // weekly forecast → stale after a week $directionLabel = ucfirst($forecast->direction); $value = $directionLabel.' · '.$forecast->ridge_confidence.'%'; return Stat::make('Latest weekly forecast', $value) ->description('For week of '.$forecast->forecast_for->toDateString()) ->icon('heroicon-o-beaker') ->color($color); } private function apiErrorsStat(): Stat { $errors = ApiLog::where('created_at', '>=', now()->subDay()) ->where(fn ($q) => $q->where('status_code', '>=', 400) ->orWhereNull('status_code') ->orWhereNotNull('error')) ->count(); $color = $errors > 0 ? 'danger' : 'success'; return Stat::make('API errors (24h)', $errors) ->icon('heroicon-o-exclamation-triangle') ->url(route('filament.admin.resources.api-logs.index')) ->color($color); } }