Add indexes and caching for admin pages

This commit is contained in:
DariusIII
2025-12-22 14:53:58 +01:00
parent 99250460f6
commit dab7e957d5
5 changed files with 268 additions and 97 deletions
@@ -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();
});
}
/**
@@ -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' => [
@@ -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 = [];
+19 -9
View File
@@ -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);
@@ -0,0 +1,110 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Add composite indexes to improve admin page query performance.
* These indexes optimize queries used on admin dashboard and user list pages.
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Add composite index for user_requests - optimizes counting API requests per user per day
if (! $this->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;
}
};