Passed
Push — master ( ccfa31...315237 )
by Darko
34:55
created

AdminPageController::getUserActivityMinutes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 8
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace App\Http\Controllers\Admin;
4
5
use App\Http\Controllers\BasePageController;
6
use App\Services\SystemMetricsService;
7
use App\Services\UserStatsService;
8
9
class AdminPageController extends BasePageController
10
{
11
    protected UserStatsService $userStatsService;
12
13
    protected SystemMetricsService $systemMetricsService;
14
15
    public function __construct(UserStatsService $userStatsService, SystemMetricsService $systemMetricsService)
16
    {
17
        parent::__construct();
18
        $this->userStatsService = $userStatsService;
19
        $this->systemMetricsService = $systemMetricsService;
20
    }
21
22
    /**
23
     * @throws \Exception
24
     */
25
    public function index()
26
    {
27
        $this->setAdminPrefs();
28
29
        // Get user statistics
30
        $userStats = [
31
            'users_by_role' => $this->userStatsService->getUsersByRole(),
32
            'downloads_per_hour' => $this->userStatsService->getDownloadsPerHour(168), // Last 7 days in hours
33
            'downloads_per_minute' => $this->userStatsService->getDownloadsPerMinute(60),
34
            'api_hits_per_hour' => $this->userStatsService->getApiHitsPerHour(168), // Last 7 days in hours
35
            'api_hits_per_minute' => $this->userStatsService->getApiHitsPerMinute(60),
36
            'summary' => $this->userStatsService->getSummaryStats(),
37
            'top_downloaders' => $this->userStatsService->getTopDownloaders(5),
38
        ];
39
40
        return view('admin.dashboard', array_merge($this->viewData, [
41
            'meta_title' => 'Admin Home',
42
            'meta_description' => 'Admin home page',
43
            'userStats' => $userStats,
44
            'stats' => $this->getDefaultStats(),
45
            'systemMetrics' => $this->getSystemMetrics(),
46
            'recent_activity' => $this->getRecentUserActivity(),
47
        ]));
48
    }
49
50
    /**
51
     * Get recent user activity (registrations, deletions, activations, role changes)
52
     */
53
    protected function getRecentUserActivity(): array
54
    {
55
        $activities = [];
56
57
        // Get newly registered users (last 7 days)
58
        $newUsers = \App\Models\User::select('id', 'username', 'created_at')
59
            ->where('created_at', '>=', now()->subDays(7))
60
            ->orderBy('created_at', 'desc')
61
            ->limit(10)
62
            ->get();
63
64
        foreach ($newUsers as $user) {
65
            $activities[] = (object) [
66
                'type' => 'registration',
67
                'message' => "New user registered: {$user->username}",
68
                'icon' => 'user-plus',
69
                'icon_bg' => 'bg-green-100 dark:bg-green-900',
70
                'icon_color' => 'text-green-600 dark:text-green-400',
71
                'created_at' => $user->created_at,
72
                'username' => $user->username,
73
            ];
74
        }
75
76
        // Get recently deleted users (last 7 days) - only soft deleted
77
        $deletedUsers = \App\Models\User::onlyTrashed()
78
            ->select('id', 'username', 'deleted_at')
79
            ->where('deleted_at', '>=', now()->subDays(7))
80
            ->orderBy('deleted_at', 'desc')
0 ignored issues
show
Bug introduced by
'deleted_at' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

80
            ->orderBy(/** @scrutinizer ignore-type */ 'deleted_at', 'desc')
Loading history...
81
            ->limit(10)
82
            ->get();
83
84
        foreach ($deletedUsers as $user) {
85
            $activities[] = (object) [
86
                'type' => 'deletion',
87
                'message' => "User deleted: {$user->username}",
88
                'icon' => 'user-minus',
89
                'icon_bg' => 'bg-red-100 dark:bg-red-900',
90
                'icon_color' => 'text-red-600 dark:text-red-400',
91
                'created_at' => $user->deleted_at,
92
                'username' => $user->username,
93
            ];
94
        }
95
96
        // Get recently activated users (last 7 days)
97
        $activatedUsers = \App\Models\User::select('id', 'username', 'email_verified_at')
98
            ->whereNotNull('email_verified_at')
99
            ->where('email_verified_at', '>=', now()->subDays(7))
100
            ->orderBy('email_verified_at', 'desc')
101
            ->limit(10)
102
            ->get();
103
104
        foreach ($activatedUsers as $user) {
105
            $activities[] = (object) [
106
                'type' => 'activation',
107
                'message' => "User activated: {$user->username}",
108
                'icon' => 'user-check',
109
                'icon_bg' => 'bg-blue-100 dark:bg-blue-900',
110
                'icon_color' => 'text-blue-600 dark:text-blue-400',
111
                'created_at' => $user->email_verified_at,
112
                'username' => $user->username,
113
            ];
114
        }
115
116
        // Get users with recent role changes (last 7 days)
117
        $roleChanges = \App\Models\User::select('id', 'username', 'rolechangedate', 'roles_id')
118
            ->whereNotNull('rolechangedate')
119
            ->where('rolechangedate', '>=', now()->subDays(7))
120
            ->orderBy('rolechangedate', 'desc')
121
            ->limit(10)
122
            ->get();
123
124
        foreach ($roleChanges as $user) {
125
            // Get role name safely
126
            $roleName = 'Unknown';
127
            if ($user->roles_id) {
128
                try {
129
                    $role = \Spatie\Permission\Models\Role::find($user->roles_id);
130
                    $roleName = $role ? $role->name : 'Unknown';
131
                } catch (\Exception $e) {
132
                    $roleName = 'Role #'.$user->roles_id;
133
                }
134
            }
135
136
            $activities[] = (object) [
137
                'type' => 'role_change',
138
                'message' => "User role changed: {$user->username} → {$roleName}",
139
                'icon' => 'user-tag',
140
                'icon_bg' => 'bg-purple-100 dark:bg-purple-900',
141
                'icon_color' => 'text-purple-600 dark:text-purple-400',
142
                'created_at' => \Carbon\Carbon::parse($user->rolechangedate),
143
                'username' => $user->username,
144
            ];
145
        }
146
147
        // Sort all activities by date (most recent first)
148
        usort($activities, function ($a, $b) {
149
            return $b->created_at <=> $a->created_at;
150
        });
151
152
        // Return only the 10 most recent
153
        return array_slice($activities, 0, 10);
154
    }
155
156
    /**
157
     * Get default dashboard statistics
158
     */
159
    protected function getDefaultStats(): array
160
    {
161
        $today = now()->format('Y-m-d');
162
163
        return [
164
            'releases' => \App\Models\Release::count(),
165
            'releases_today' => \App\Models\Release::whereRaw('DATE(adddate) = ?', [$today])->count(),
166
            'users' => \App\Models\User::whereNull('deleted_at')->count(),
167
            'users_today' => \App\Models\User::whereRaw('DATE(created_at) = ?', [$today])->count(),
168
            'groups' => \App\Models\UsenetGroup::count(),
169
            'active_groups' => \App\Models\UsenetGroup::where('active', 1)->count(),
170
            'failed' => \App\Models\DnzbFailure::count(),
171
            'disk_free' => $this->getDiskSpace(),
172
        ];
173
    }
174
175
    /**
176
     * Get disk space information
177
     */
178
    protected function getDiskSpace(): string
179
    {
180
        try {
181
            $bytes = disk_free_space('/');
182
            $units = ['B', 'KB', 'MB', 'GB', 'TB'];
183
184
            for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
185
                $bytes /= 1024;
186
            }
187
188
            return round($bytes, 2).' '.$units[$i];
189
        } catch (\Exception $e) {
190
            return 'N/A';
191
        }
192
    }
193
194
    /**
195
     * Get system metrics (CPU and RAM usage)
196
     */
197
    protected function getSystemMetrics(): array
198
    {
199
        $cpuUsage = $this->getCpuUsage();
200
        $ramUsage = $this->getRamUsage();
201
        $cpuInfo = $this->getCpuInfo();
202
        $loadAverage = $this->getLoadAverage();
203
204
        // Get historical data from database - both hourly (24h) and daily (30d)
205
        $cpuHistory24h = $this->systemMetricsService->getHourlyMetrics('cpu', 24);
206
        $cpuHistory30d = $this->systemMetricsService->getDailyMetrics('cpu', 30);
207
        $ramHistory24h = $this->systemMetricsService->getHourlyMetrics('ram', 24);
208
        $ramHistory30d = $this->systemMetricsService->getDailyMetrics('ram', 30);
209
210
        return [
211
            'cpu' => [
212
                'current' => $cpuUsage,
213
                'label' => 'CPU Usage',
214
                'history_24h' => $cpuHistory24h,
215
                'history_30d' => $cpuHistory30d,
216
                'cores' => $cpuInfo['cores'],
217
                'threads' => $cpuInfo['threads'],
218
                'model' => $cpuInfo['model'],
219
                'load_average' => $loadAverage,
220
            ],
221
            'ram' => [
222
                'used' => $ramUsage['used'],
223
                'total' => $ramUsage['total'],
224
                'percentage' => $ramUsage['percentage'],
225
                'label' => 'RAM Usage',
226
                'history_24h' => $ramHistory24h,
227
                'history_30d' => $ramHistory30d,
228
            ],
229
        ];
230
    }
231
232
    /**
233
     * Get current CPU usage percentage
234
     */
235
    protected function getCpuUsage(): float
236
    {
237
        try {
238
            if (PHP_OS_FAMILY === 'Windows') {
239
                // Windows command
240
                $output = shell_exec('wmic cpu get loadpercentage');
241
                if ($output) {
242
                    preg_match('/\d+/', $output, $matches);
243
244
                    return $matches[0] ?? 0;
245
                }
246
            } else {
247
                // Linux command - get load average and convert to percentage
248
                $load = sys_getloadavg();
249
                if ($load !== false) {
250
                    $cpuCount = $this->getCpuCount();
251
252
                    return round(($load[0] / $cpuCount) * 100, 2);
253
                }
254
            }
255
        } catch (\Exception $e) {
256
            \Log::warning('Could not get CPU usage: '.$e->getMessage());
257
        }
258
259
        return 0;
260
    }
261
262
    /**
263
     * Get number of CPU cores
264
     */
265
    protected function getCpuCount(): int
266
    {
267
        try {
268
            if (PHP_OS_FAMILY === 'Windows') {
269
                $output = shell_exec('wmic cpu get NumberOfLogicalProcessors');
270
                if ($output) {
271
                    preg_match('/\d+/', $output, $matches);
272
273
                    return (int) ($matches[0] ?? 1);
274
                }
275
            } else {
276
                $cpuinfo = file_get_contents('/proc/cpuinfo');
277
                preg_match_all('/^processor/m', $cpuinfo, $matches);
278
279
                return count($matches[0]) ?: 1;
280
            }
281
        } catch (\Exception $e) {
282
            return 1;
283
        }
284
285
        return 1;
286
    }
287
288
    /**
289
     * Get detailed CPU information (cores, threads, model)
290
     */
291
    protected function getCpuInfo(): array
292
    {
293
        $info = [
294
            'cores' => 0,
295
            'threads' => 0,
296
            'model' => 'Unknown',
297
        ];
298
299
        try {
300
            if (PHP_OS_FAMILY === 'Windows') {
301
                // Get number of cores
302
                $coresOutput = shell_exec('wmic cpu get NumberOfCores');
303
                if ($coresOutput) {
304
                    preg_match('/\d+/', $coresOutput, $matches);
305
                    $info['cores'] = (int) ($matches[0] ?? 0);
306
                }
307
308
                // Get number of logical processors (threads)
309
                $threadsOutput = shell_exec('wmic cpu get NumberOfLogicalProcessors');
310
                if ($threadsOutput) {
311
                    preg_match('/\d+/', $threadsOutput, $matches);
312
                    $info['threads'] = (int) ($matches[0] ?? 0);
313
                }
314
315
                // Get CPU model
316
                $modelOutput = shell_exec('wmic cpu get Name');
317
                if ($modelOutput) {
318
                    $lines = explode("\n", trim($modelOutput));
319
                    if (isset($lines[1])) {
320
                        $info['model'] = trim($lines[1]);
321
                    }
322
                }
323
            } else {
324
                // Linux
325
                $cpuinfo = file_get_contents('/proc/cpuinfo');
326
327
                // Get number of physical cores
328
                preg_match_all('/^cpu cores\s*:\s*(\d+)/m', $cpuinfo, $coresMatches);
329
                if (! empty($coresMatches[1])) {
330
                    $info['cores'] = (int) $coresMatches[1][0];
331
                }
332
333
                // Get number of logical processors (threads)
334
                preg_match_all('/^processor/m', $cpuinfo, $processorMatches);
335
                $info['threads'] = count($processorMatches[0]) ?: 0;
336
337
                // Get CPU model
338
                preg_match('/^model name\s*:\s*(.+)$/m', $cpuinfo, $modelMatches);
339
                if (! empty($modelMatches[1])) {
340
                    $info['model'] = trim($modelMatches[1]);
341
                }
342
343
                // If cores is 0, try to get from physical id count
344
                if ($info['cores'] === 0) {
345
                    preg_match_all('/^physical id\s*:\s*(\d+)/m', $cpuinfo, $physicalMatches);
346
                    $uniquePhysical = ! empty($physicalMatches[1]) ? count(array_unique($physicalMatches[1])) : 1;
347
                    $info['cores'] = (int) ($info['threads'] / $uniquePhysical);
348
                }
349
            }
350
        } catch (\Exception $e) {
351
            \Log::warning('Could not get CPU info: '.$e->getMessage());
352
        }
353
354
        return $info;
355
    }
356
357
    /**
358
     * Get system load average
359
     */
360
    protected function getLoadAverage(): array
361
    {
362
        $loadAvg = [
363
            '1min' => 0,
364
            '5min' => 0,
365
            '15min' => 0,
366
        ];
367
368
        try {
369
            if (PHP_OS_FAMILY === 'Windows') {
370
                // Windows doesn't have load average, use CPU queue length instead
371
                $output = shell_exec('wmic path Win32_PerfFormattedData_PerfOS_System get ProcessorQueueLength');
372
                if ($output) {
373
                    preg_match('/\d+/', $output, $matches);
374
                    $queueLength = (int) ($matches[0] ?? 0);
375
                    // Approximate load average
376
                    $loadAvg['1min'] = round($queueLength / 2, 2);
377
                    $loadAvg['5min'] = round($queueLength / 2, 2);
378
                    $loadAvg['15min'] = round($queueLength / 2, 2);
379
                }
380
            } else {
381
                // Linux has native load average
382
                $load = sys_getloadavg();
383
                if ($load !== false) {
384
                    $loadAvg['1min'] = round($load[0], 2);
385
                    $loadAvg['5min'] = round($load[1], 2);
386
                    $loadAvg['15min'] = round($load[2], 2);
387
                }
388
            }
389
        } catch (\Exception $e) {
390
            \Log::warning('Could not get load average: '.$e->getMessage());
391
        }
392
393
        return $loadAvg;
394
    }
395
396
    /**
397
     * Get RAM usage information
398
     */
399
    protected function getRamUsage(): array
400
    {
401
        try {
402
            if (PHP_OS_FAMILY === 'Windows') {
403
                // Windows command
404
                $output = shell_exec('wmic OS get FreePhysicalMemory,TotalVisibleMemorySize /Value');
405
                if ($output) {
406
                    preg_match('/FreePhysicalMemory=(\d+)/', $output, $free);
407
                    preg_match('/TotalVisibleMemorySize=(\d+)/', $output, $total);
408
409
                    if (isset($free[1]) && isset($total[1])) {
410
                        $freeKb = (float) $free[1];
411
                        $totalKb = (float) $total[1];
412
                        $usedKb = $totalKb - $freeKb;
413
414
                        return [
415
                            'used' => round($usedKb / 1024 / 1024, 2),
416
                            'total' => round($totalKb / 1024 / 1024, 2),
417
                            'percentage' => round(($usedKb / $totalKb) * 100, 2),
418
                        ];
419
                    }
420
                }
421
            } else {
422
                // Linux command
423
                $meminfo = file_get_contents('/proc/meminfo');
424
                preg_match('/MemTotal:\s+(\d+)/', $meminfo, $total);
425
                preg_match('/MemAvailable:\s+(\d+)/', $meminfo, $available);
426
427
                if (isset($total[1]) && isset($available[1])) {
428
                    $totalKb = (float) $total[1];
429
                    $availableKb = (float) $available[1];
430
                    $usedKb = $totalKb - $availableKb;
431
432
                    return [
433
                        'used' => round($usedKb / 1024 / 1024, 2),
434
                        'total' => round($totalKb / 1024 / 1024, 2),
435
                        'percentage' => round(($usedKb / $totalKb) * 100, 2),
436
                    ];
437
                }
438
            }
439
        } catch (\Exception $e) {
440
            \Log::warning('Could not get RAM usage: '.$e->getMessage());
441
        }
442
443
        return [
444
            'used' => 0,
445
            'total' => 0,
446
            'percentage' => 0,
447
        ];
448
    }
449
450
    /**
451
     * Get minute-to-minute user activity data (API endpoint)
452
     */
453
    public function getUserActivityMinutes()
454
    {
455
        $downloadsPerMinute = $this->userStatsService->getDownloadsPerMinute(60);
456
        $apiHitsPerMinute = $this->userStatsService->getApiHitsPerMinute(60);
457
458
        return response()->json([
459
            'downloads' => $downloadsPerMinute,
460
            'api_hits' => $apiHitsPerMinute,
461
        ]);
462
    }
463
}
464