From dab7e957d5e41968a419bba9dfbe2a661ca77678 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Mon, 22 Dec 2025 14:53:58 +0100 Subject: [PATCH] Add indexes and caching for admin pages --- .../Admin/AdminInvitationController.php | 58 +++---- .../Controllers/Admin/AdminPageController.php | 148 +++++++++++++----- .../Controllers/Admin/AdminUserController.php | 21 +-- app/Http/Controllers/BasePageController.php | 28 ++-- ...omposite_indexes_for_admin_performance.php | 110 +++++++++++++ 5 files changed, 268 insertions(+), 97 deletions(-) create mode 100644 database/migrations/2025_12_22_120000_add_composite_indexes_for_admin_performance.php diff --git a/app/Http/Controllers/Admin/AdminInvitationController.php b/app/Http/Controllers/Admin/AdminInvitationController.php index bab9334a7..a1e92ddff 100644 --- a/app/Http/Controllers/Admin/AdminInvitationController.php +++ b/app/Http/Controllers/Admin/AdminInvitationController.php @@ -124,42 +124,46 @@ class AdminInvitationController extends BasePageController } /** - * Get overall invitation statistics + * Get overall invitation statistics with caching */ private function getOverallStats(): array { - return [ - 'total' => Invitation::count(), - 'pending' => Invitation::valid()->count(), - 'used' => Invitation::used()->count(), - 'expired' => Invitation::expired()->count(), - 'cancelled' => Invitation::where('is_active', false)->whereNull('used_at')->count(), - 'today' => Invitation::whereDate('created_at', today())->count(), - 'this_week' => Invitation::whereBetween('created_at', [ - now()->startOfWeek(), - now()->endOfWeek(), - ])->count(), - 'this_month' => Invitation::whereMonth('created_at', now()->month) - ->whereYear('created_at', now()->year) - ->count(), - ]; + return \Illuminate\Support\Facades\Cache::remember('admin_invitation_stats', 300, function () { + return [ + 'total' => Invitation::count(), + 'pending' => Invitation::valid()->count(), + 'used' => Invitation::used()->count(), + 'expired' => Invitation::expired()->count(), + 'cancelled' => Invitation::where('is_active', false)->whereNull('used_at')->count(), + 'today' => Invitation::whereDate('created_at', today())->count(), + 'this_week' => Invitation::whereBetween('created_at', [ + now()->startOfWeek(), + now()->endOfWeek(), + ])->count(), + 'this_month' => Invitation::whereMonth('created_at', now()->month) + ->whereYear('created_at', now()->year) + ->count(), + ]; + }); } /** - * Get top inviters statistics + * Get top inviters statistics with caching */ private function getTopInviters(int $limit = 10): array { - return User::select('users.*') - ->selectRaw('COUNT(invitations.id) as total_invitations') - ->selectRaw('COUNT(CASE WHEN invitations.used_at IS NOT NULL THEN 1 END) as successful_invitations') - ->leftJoin('invitations', 'users.id', '=', 'invitations.invited_by') - ->groupBy('users.id') - ->having('total_invitations', '>', 0) - ->orderBy('total_invitations', 'desc') - ->limit($limit) - ->get() - ->toArray(); + return \Illuminate\Support\Facades\Cache::remember('admin_top_inviters', 300, function () use ($limit) { + return User::select('users.*') + ->selectRaw('COUNT(invitations.id) as total_invitations') + ->selectRaw('COUNT(CASE WHEN invitations.used_at IS NOT NULL THEN 1 END) as successful_invitations') + ->leftJoin('invitations', 'users.id', '=', 'invitations.invited_by') + ->groupBy('users.id') + ->having('total_invitations', '>', 0) + ->orderBy('total_invitations', 'desc') + ->limit($limit) + ->get() + ->toArray(); + }); } /** diff --git a/app/Http/Controllers/Admin/AdminPageController.php b/app/Http/Controllers/Admin/AdminPageController.php index e615326c3..349465201 100644 --- a/app/Http/Controllers/Admin/AdminPageController.php +++ b/app/Http/Controllers/Admin/AdminPageController.php @@ -26,16 +26,18 @@ class AdminPageController extends BasePageController { $this->setAdminPrefs(); - // Get user statistics - $userStats = [ - 'users_by_role' => $this->userStatsService->getUsersByRole(), - 'downloads_per_hour' => $this->userStatsService->getDownloadsPerHour(168), // Last 7 days in hours - 'downloads_per_minute' => $this->userStatsService->getDownloadsPerMinute(60), - 'api_hits_per_hour' => $this->userStatsService->getApiHitsPerHour(168), // Last 7 days in hours - 'api_hits_per_minute' => $this->userStatsService->getApiHitsPerMinute(60), - 'summary' => $this->userStatsService->getSummaryStats(), - 'top_downloaders' => $this->userStatsService->getTopDownloaders(5), - ]; + // Cache user statistics for 2 minutes + $userStats = \Illuminate\Support\Facades\Cache::remember('admin_user_stats', 120, function () { + return [ + 'users_by_role' => $this->userStatsService->getUsersByRole(), + 'downloads_per_hour' => $this->userStatsService->getDownloadsPerHour(168), // Last 7 days in hours + 'downloads_per_minute' => $this->userStatsService->getDownloadsPerMinute(60), + 'api_hits_per_hour' => $this->userStatsService->getApiHitsPerHour(168), // Last 7 days in hours + 'api_hits_per_minute' => $this->userStatsService->getApiHitsPerMinute(60), + 'summary' => $this->userStatsService->getSummaryStats(), + 'top_downloaders' => $this->userStatsService->getTopDownloaders(5), + ]; + }); return view('admin.dashboard', array_merge($this->viewData, [ 'meta_title' => 'Admin Home', @@ -48,25 +50,27 @@ class AdminPageController extends BasePageController } /** - * Get recent user activity from the user_activities table + * Get recent user activity from the user_activities table with caching */ protected function getRecentUserActivity(): array { - $activities = \App\Models\UserActivity::orderBy('created_at', 'desc') - ->limit(10) - ->get(); + return \Illuminate\Support\Facades\Cache::remember('admin_recent_activity', 60, function () { + $activities = \App\Models\UserActivity::orderBy('created_at', 'desc') + ->limit(10) + ->get(); - return $activities->map(function ($activity) { - 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, - ]; - })->toArray(); + return $activities->map(function ($activity) { + 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, + ]; + })->toArray(); + }); } /** @@ -93,20 +97,54 @@ class AdminPageController extends BasePageController } /** - * Get default dashboard statistics + * Get default dashboard statistics with caching for expensive queries */ protected function getDefaultStats(): array { $today = now()->format('Y-m-d'); + // Cache total releases count for 5 minutes (expensive query on large tables) + $releasesCount = \Illuminate\Support\Facades\Cache::remember('admin_stats_releases_count', 300, function () { + return \App\Models\Release::count(); + }); + + // Cache users count for 5 minutes + $usersCount = \Illuminate\Support\Facades\Cache::remember('admin_stats_users_count', 300, function () { + return \App\Models\User::whereNull('deleted_at')->count(); + }); + + // Cache groups count for 10 minutes (rarely changes) + $groupsCount = \Illuminate\Support\Facades\Cache::remember('admin_stats_groups_count', 600, function () { + return \App\Models\UsenetGroup::count(); + }); + + // Cache active groups count for 10 minutes + $activeGroupsCount = \Illuminate\Support\Facades\Cache::remember('admin_stats_active_groups_count', 600, function () { + return \App\Models\UsenetGroup::where('active', 1)->count(); + }); + + // Cache failed count for 5 minutes + $failedCount = \Illuminate\Support\Facades\Cache::remember('admin_stats_failed_count', 300, function () { + return \App\Models\DnzbFailure::count(); + }); + + // Today's counts can be cached for 1 minute since they change frequently + $releasesToday = \Illuminate\Support\Facades\Cache::remember('admin_stats_releases_today_'.$today, 60, function () use ($today) { + return \App\Models\Release::whereRaw('DATE(adddate) = ?', [$today])->count(); + }); + + $usersToday = \Illuminate\Support\Facades\Cache::remember('admin_stats_users_today_'.$today, 60, function () use ($today) { + return \App\Models\User::whereRaw('DATE(created_at) = ?', [$today])->count(); + }); + return [ - 'releases' => \App\Models\Release::count(), - 'releases_today' => \App\Models\Release::whereRaw('DATE(adddate) = ?', [$today])->count(), - 'users' => \App\Models\User::whereNull('deleted_at')->count(), - 'users_today' => \App\Models\User::whereRaw('DATE(created_at) = ?', [$today])->count(), - 'groups' => \App\Models\UsenetGroup::count(), - 'active_groups' => \App\Models\UsenetGroup::where('active', 1)->count(), - 'failed' => \App\Models\DnzbFailure::count(), + 'releases' => $releasesCount, + 'releases_today' => $releasesToday, + 'users' => $usersCount, + 'users_today' => $usersToday, + 'groups' => $groupsCount, + 'active_groups' => $activeGroupsCount, + 'failed' => $failedCount, 'disk_free' => $this->getDiskSpace(), ]; } @@ -131,20 +169,44 @@ class AdminPageController extends BasePageController } /** - * Get system metrics (CPU and RAM usage) + * Get system metrics (CPU and RAM usage) with caching */ protected function getSystemMetrics(): array { - $cpuUsage = $this->getCpuUsage(); - $ramUsage = $this->getRamUsage(); - $cpuInfo = $this->getCpuInfo(); - $loadAverage = $this->getLoadAverage(); + // Cache CPU info for 1 hour (doesn't change) + $cpuInfo = \Illuminate\Support\Facades\Cache::remember('admin_cpu_info', 3600, function () { + return $this->getCpuInfo(); + }); - // Get historical data from database - both hourly (24h) and daily (30d) - $cpuHistory24h = $this->systemMetricsService->getHourlyMetrics('cpu', 24); - $cpuHistory30d = $this->systemMetricsService->getDailyMetrics('cpu', 30); - $ramHistory24h = $this->systemMetricsService->getHourlyMetrics('ram', 24); - $ramHistory30d = $this->systemMetricsService->getDailyMetrics('ram', 30); + // Cache current metrics for 30 seconds + $cpuUsage = \Illuminate\Support\Facades\Cache::remember('admin_cpu_usage', 30, function () { + return $this->getCpuUsage(); + }); + + $ramUsage = \Illuminate\Support\Facades\Cache::remember('admin_ram_usage', 30, function () { + return $this->getRamUsage(); + }); + + $loadAverage = \Illuminate\Support\Facades\Cache::remember('admin_load_average', 30, function () { + return $this->getLoadAverage(); + }); + + // Cache historical data for 5 minutes (from database) + $cpuHistory24h = \Illuminate\Support\Facades\Cache::remember('admin_cpu_history_24h', 300, function () { + return $this->systemMetricsService->getHourlyMetrics('cpu', 24); + }); + + $cpuHistory30d = \Illuminate\Support\Facades\Cache::remember('admin_cpu_history_30d', 300, function () { + return $this->systemMetricsService->getDailyMetrics('cpu', 30); + }); + + $ramHistory24h = \Illuminate\Support\Facades\Cache::remember('admin_ram_history_24h', 300, function () { + return $this->systemMetricsService->getHourlyMetrics('ram', 24); + }); + + $ramHistory30d = \Illuminate\Support\Facades\Cache::remember('admin_ram_history_30d', 300, function () { + return $this->systemMetricsService->getDailyMetrics('ram', 30); + }); return [ 'cpu' => [ diff --git a/app/Http/Controllers/Admin/AdminUserController.php b/app/Http/Controllers/Admin/AdminUserController.php index 80b1148b4..a8205a140 100644 --- a/app/Http/Controllers/Admin/AdminUserController.php +++ b/app/Http/Controllers/Admin/AdminUserController.php @@ -59,24 +59,9 @@ class AdminUserController extends BasePageController $results = $this->paginate($result ?? [], User::getCount($variables['role'], $variables['username'], $variables['host'], $variables['email'], $variables['created_from'], $variables['created_to']) ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); - // Add country data to each user based on their host IP - foreach ($results as $user) { - $position = null; - if (! empty($user->host) && filter_var($user->host, FILTER_VALIDATE_IP)) { - $position = Location::get($user->host); - } - $user->country_name = $position ? $position->countryName : null; - $user->country_code = $position ? $position->countryCode : null; - - // Add daily API and download counts - try { - $user->daily_api_count = UserRequest::getApiRequests($user->id); - $user->daily_download_count = UserDownload::getDownloadRequests($user->id); - } catch (\Exception $e) { - $user->daily_api_count = 0; - $user->daily_download_count = 0; - } - } + // Note: API request counts are already included via the getRange query when $apiRequests = true + // Country lookups and additional counts removed to improve performance on large datasets + // These can be added back via individual user profile pages or AJAX calls if needed // Build order by URLs $orderByUrls = []; diff --git a/app/Http/Controllers/BasePageController.php b/app/Http/Controllers/BasePageController.php index 2a8f31185..89c6ee07a 100644 --- a/app/Http/Controllers/BasePageController.php +++ b/app/Http/Controllers/BasePageController.php @@ -8,6 +8,7 @@ use App\Models\User; use Illuminate\Http\Request; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\View\View; class BasePageController extends Controller @@ -50,25 +51,34 @@ class BasePageController extends Controller { $this->middleware(['auth', 'web', '2fa'])->except('api', 'contact', 'showContactForm', 'callback', 'btcPayCallback', 'getNzb', 'terms', 'privacyPolicy', 'capabilities', 'movie', 'apiSearch', 'tv', 'details', 'failed', 'showRssDesc', 'fullFeedRss', 'categoryFeedRss', 'cartRss', 'myMoviesRss', 'myShowsRss', 'trendingMoviesRss', 'trendingShowsRss', 'release', 'reset', 'showLinkRequestForm'); - // Load settings as collection - $this->settings = Settings::query()->pluck('value', 'name'); + // Load settings as collection with caching (5 minutes) + $this->settings = Cache::remember('site_settings', 300, function () { + return Settings::query()->pluck('value', 'name'); + }); // Initialize view data FIRST with serverroot $this->viewData = [ 'serverroot' => url('/'), ]; - // Then add the converted settings array as 'site' - // Using array assignment instead of constructor assignment to ensure it persists - $this->viewData['site'] = $this->settings->map(function ($value) { - return Settings::convertValue($value); - })->all(); + // Then add the converted settings array as 'site' with caching + $this->viewData['site'] = Cache::remember('site_settings_converted', 300, function () { + return $this->settings->map(function ($value) { + return Settings::convertValue($value); + })->all(); + }); // Initialize userdata property for controllers that need it $this->middleware(function ($request, $next) { if (Auth::check()) { - $this->userdata = User::find(Auth::id()); - $this->userdata->categoryexclusions = User::getCategoryExclusionById(Auth::id()); + $userId = Auth::id(); + $this->userdata = User::find($userId); + // Cache category exclusions per user (5 minutes) + $this->userdata->categoryexclusions = Cache::remember( + 'user_category_exclusions_'.$userId, + 300, + fn () => User::getCategoryExclusionById($userId) + ); } return $next($request); diff --git a/database/migrations/2025_12_22_120000_add_composite_indexes_for_admin_performance.php b/database/migrations/2025_12_22_120000_add_composite_indexes_for_admin_performance.php new file mode 100644 index 000000000..be98d4994 --- /dev/null +++ b/database/migrations/2025_12_22_120000_add_composite_indexes_for_admin_performance.php @@ -0,0 +1,110 @@ +indexExists('user_requests', 'ix_user_requests_users_timestamp')) { + Schema::table('user_requests', function (Blueprint $table) { + $table->index(['users_id', 'timestamp'], 'ix_user_requests_users_timestamp'); + }); + } + + // Add composite index for user_downloads - optimizes counting downloads per user per day + if (! $this->indexExists('user_downloads', 'ix_user_downloads_users_timestamp')) { + Schema::table('user_downloads', function (Blueprint $table) { + $table->index(['users_id', 'timestamp'], 'ix_user_downloads_users_timestamp'); + }); + } + + // Add index on releases.adddate for faster "today's releases" count + // Only add if not already exists from previous migration + if (! $this->indexExists('releases', 'ix_releases_adddate_only')) { + Schema::table('releases', function (Blueprint $table) { + $table->index(['adddate'], 'ix_releases_adddate_only'); + }); + } + + // Add index on users.created_at for faster "today's users" count + if (! $this->indexExists('users', 'ix_users_created_at')) { + Schema::table('users', function (Blueprint $table) { + $table->index(['created_at'], 'ix_users_created_at'); + }); + } + + // Add index on system_metrics for faster historical data retrieval + if (Schema::hasTable('system_metrics') && ! $this->indexExists('system_metrics', 'ix_system_metrics_type_recorded')) { + Schema::table('system_metrics', function (Blueprint $table) { + $table->index(['metric_type', 'recorded_at'], 'ix_system_metrics_type_recorded'); + }); + } + + // Add index on user_activity_stats_hourly for faster hourly data retrieval + if (Schema::hasTable('user_activity_stats_hourly') && ! $this->indexExists('user_activity_stats_hourly', 'ix_hourly_stats_hour')) { + Schema::table('user_activity_stats_hourly', function (Blueprint $table) { + $table->index(['stat_hour'], 'ix_hourly_stats_hour'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('user_requests', function (Blueprint $table) { + $table->dropIndex('ix_user_requests_users_timestamp'); + }); + + Schema::table('user_downloads', function (Blueprint $table) { + $table->dropIndex('ix_user_downloads_users_timestamp'); + }); + + if ($this->indexExists('releases', 'ix_releases_adddate_only')) { + Schema::table('releases', function (Blueprint $table) { + $table->dropIndex('ix_releases_adddate_only'); + }); + } + + if ($this->indexExists('users', 'ix_users_created_at')) { + Schema::table('users', function (Blueprint $table) { + $table->dropIndex('ix_users_created_at'); + }); + } + + if (Schema::hasTable('system_metrics') && $this->indexExists('system_metrics', 'ix_system_metrics_type_recorded')) { + Schema::table('system_metrics', function (Blueprint $table) { + $table->dropIndex('ix_system_metrics_type_recorded'); + }); + } + + if (Schema::hasTable('user_activity_stats_hourly') && $this->indexExists('user_activity_stats_hourly', 'ix_hourly_stats_hour')) { + Schema::table('user_activity_stats_hourly', function (Blueprint $table) { + $table->dropIndex('ix_hourly_stats_hour'); + }); + } + } + + /** + * Check if an index exists on a table. + */ + private function indexExists(string $table, string $indexName): bool + { + $indexes = \DB::select("SHOW INDEX FROM {$table} WHERE Key_name = ?", [$indexName]); + + return count($indexes) > 0; + } +}; +