*/ public function get(): array { return Cache::flexible(self::CACHE_KEY, self::CACHE_TTL, fn (): array => $this->build()); } /** * Forcibly rebuild and re-cache the snapshot. Used by the warmer command * and by admin actions that want immediate freshness. * * @return array */ public function warm(): array { $payload = $this->build(); // Re-prime both the value and the SWR control key used by Cache::flexible(). Cache::forget(self::CACHE_KEY); return Cache::flexible(self::CACHE_KEY, self::CACHE_TTL, static fn (): array => $payload); } public function forget(): void { Cache::forget(self::CACHE_KEY); } /** * @return array */ private function build(): array { $now = now(); $registrationStatus = $this->registrationStatusService->resolve($now); $nextRegistrationPeriod = $registrationStatus['active_period'] === null ? $this->registrationStatusService->getNextUpcomingPeriod($now) : null; return [ 'generated_at' => $now->toIso8601String(), 'stats' => $this->buildStats(), 'userStats' => $this->buildUserStats(), 'systemMetrics' => $this->buildSystemMetrics(), 'recent_activity' => $this->buildRecentActivity(), 'recent_payments' => $this->buildRecentPayments(), 'serviceStatuses' => $this->siteStatusService->getEnabledServices(), 'activeIncidents' => $this->siteStatusService->getActiveIncidents(), 'registrationStatus' => $registrationStatus, 'nextRegistrationPeriod' => $nextRegistrationPeriod, ]; } /** * @return array */ private function buildStats(): array { $today = now()->format('Y-m-d'); // Approximate counts on huge tables — InnoDB metadata estimates are // good enough for the admin overview tiles. $releasesCount = ApproximateRowCount::for('releases'); $usersApproximateCount = ApproximateRowCount::for('users'); $groupsCount = ApproximateRowCount::for('usenet_groups'); $failedCount = ApproximateRowCount::for('dnzb_failures'); // Small / index-friendly aggregates kept exact. $activeGroupsCount = UsenetGroup::where('active', 1)->count(); $reportedCount = ReleaseReport::where('status', 'pending')->count(); $softDeletedCount = User::onlyTrashed()->count(); $activeUsersCount = max($usersApproximateCount - $softDeletedCount, 0); // Replaces previous whereJsonContains() count which couldn't use an index. $permanentlyDeletedCount = UserActivity::query() ->where('activity_type', 'deleted') ->where('is_permanent', true) ->count(); $releasesToday = Release::whereDate('adddate', $today)->count(); $usersToday = User::whereNull('deleted_at')->whereDate('created_at', $today)->count(); return [ 'releases' => $releasesCount, 'releases_today' => $releasesToday, 'users' => $activeUsersCount, 'users_today' => $usersToday, 'groups' => $groupsCount, 'active_groups' => $activeGroupsCount, 'failed' => $failedCount, 'reported' => $reportedCount, 'soft_deleted_users' => $softDeletedCount, 'permanently_deleted_users' => $permanentlyDeletedCount, 'disk_free' => $this->systemMetricsService->getDiskSpace(), ]; } /** * @return array */ private function buildUserStats(): array { return [ 'users_by_role' => $this->userStatsService->getUsersByRole(), 'downloads_per_hour' => $this->userStatsService->getDownloadsPerHour(168), 'downloads_per_minute' => $this->userStatsService->getDownloadsPerMinute(60), 'api_hits_per_hour' => $this->userStatsService->getApiHitsPerHour(168), 'api_hits_per_minute' => $this->userStatsService->getApiHitsPerMinute(60), 'summary' => $this->userStatsService->getSummaryStats(), 'top_downloaders' => $this->userStatsService->getTopDownloaders(5), ]; } /** * @return array */ private function buildSystemMetrics(): array { $cpuInfo = $this->systemMetricsService->getCpuInfo(); $cpuUsage = $this->systemMetricsService->getCpuUsage(); $ramUsage = $this->systemMetricsService->getRamUsage(); $loadAverage = $this->systemMetricsService->getLoadAverage(); return [ 'cpu' => [ 'current' => $cpuUsage, 'label' => 'CPU Usage', 'history_24h' => $this->systemMetricsService->getHourlyMetrics('cpu', 24), 'history_30d' => $this->systemMetricsService->getDailyMetrics('cpu', 30), 'cores' => $cpuInfo['cores'], 'threads' => $cpuInfo['threads'], 'model' => $cpuInfo['model'], 'load_average' => $loadAverage, ], 'ram' => [ 'used' => $ramUsage['used'], 'total' => $ramUsage['total'], 'percentage' => $ramUsage['percentage'], 'label' => 'RAM Usage', 'history_24h' => $this->systemMetricsService->getHourlyMetrics('ram', 24), 'history_30d' => $this->systemMetricsService->getDailyMetrics('ram', 30), ], ]; } /** * @return array */ private function buildRecentActivity(): array { $activities = UserActivity::orderBy('created_at', 'desc') ->limit(10) ->get(); return $activities->map(static function ($activity): object { return (object) [ 'type' => $activity->activity_type, 'message' => $activity->description, 'icon' => $activity->icon, 'icon_bg' => 'bg-'.$activity->color_class.'-100 dark:bg-'.$activity->color_class.'-900', 'icon_color' => 'text-'.$activity->color_class.'-600 dark:text-'.$activity->color_class.'-400', 'created_at' => $activity->created_at, 'username' => $activity->username, 'metadata' => $activity->metadata, ]; })->all(); } /** * Build the "Last 5 payments" widget payload (most recent records by id desc). * * @return array> */ private function buildRecentPayments(): array { return Payment::query() ->orderByDesc('id') ->limit(5) ->get([ 'id', 'created_at', 'username', 'email', 'item_description', 'invoice_amount', 'payment_method', 'payment_status', 'invoice_status', 'order_id', ]) ->map(static function (Payment $payment): array { $createdAt = $payment->created_at; return [ 'id' => (int) $payment->id, 'username' => (string) $payment->username, 'email' => (string) $payment->email, 'item_description' => (string) $payment->item_description, 'invoice_amount' => (string) $payment->invoice_amount, 'payment_method' => (string) $payment->payment_method, 'payment_status' => (string) $payment->payment_status, 'invoice_status' => (string) ($payment->invoice_status ?? Payment::INVOICE_STATUS_PENDING), 'order_id' => (string) $payment->order_id, 'created_at' => $createdAt?->toIso8601String(), 'created_at_human' => $createdAt?->diffForHumans(), 'created_at_formatted' => $createdAt?->timezone(config('app.timezone'))->format('Y-m-d H:i'), ]; }) ->all(); } }