diff --git a/cron/backfill_trends.php b/cron/backfill_trends.php old mode 100755 new mode 100644 index b146c94..1f1e1ec --- a/cron/backfill_trends.php +++ b/cron/backfill_trends.php @@ -1,74 +1,60 @@ #!/usr/bin/env php PDO::ERRMODE_EXCEPTION -]); +use Config\DatabaseConfig; -// Заполняем за последние 48 часов (можно увеличить) -$hours = 48; -$processed = 0; -$errors = 0; +$days = isset($argv[1]) ? max(1, (int)$argv[1]) : 30; +$pdo = DatabaseConfig::getInstance(); -for ($i = 1; $i <= $hours; $i++) { - $hourStart = new DateTime(); - $hourStart->modify("-{$i} hour"); - $hourStart->setTime((int)$hourStart->format('H'), 0, 0); - - $hourEnd = clone $hourStart; - $hourEnd->modify('+59 minutes +59 seconds'); - - $periodStartStr = $hourStart->format('Y-m-d H:00:00'); - $periodEndStr = $hourEnd->format('Y-m-d H:59:59'); - - // Получаем все серверы - $stmt = $pdo->query("SELECT id FROM servers"); - $servers = $stmt->fetchAll(PDO::FETCH_COLUMN); - - foreach ($servers as $serverId) { - // Исключаем метрики с JSON данными (top_cpu_proc, top_ram_proc) - $sql = " - INSERT INTO server_metrics_trends (server_id, metric_name_id, period_start, avg_value, min_value, max_value, count_samples) - SELECT - sm.server_id, - sm.metric_name_id, - :period_start, - AVG(CAST(sm.value AS DECIMAL(20,4))), - MIN(CAST(sm.value AS DECIMAL(20,4))), - MAX(CAST(sm.value AS DECIMAL(20,4))), - COUNT(*) - FROM server_metrics sm - INNER JOIN metric_names mn ON sm.metric_name_id = mn.id - WHERE sm.server_id = :server_id - AND sm.created_at >= :start_date - AND sm.created_at <= :end_date - AND mn.name NOT IN ('top_cpu_proc', 'top_ram_proc') - GROUP BY sm.server_id, sm.metric_name_id - ON DUPLICATE KEY UPDATE - avg_value = VALUES(avg_value), - min_value = VALUES(min_value), - max_value = VALUES(max_value), - count_samples = VALUES(count_samples), - created_at = NOW() - "; - - try { - $stmt = $pdo->prepare($sql); - $stmt->execute([ - ':server_id' => $serverId, - ':period_start' => $periodStartStr, - ':start_date' => $periodStartStr, - ':end_date' => $periodEndStr - ]); - $processed++; - } catch (Exception $e) { - $errors++; - } - } - - echo "Processed hour: $periodStartStr ($processed servers)\n"; -} +$startedAt = new DateTime(); +$rangeStart = (clone $startedAt)->modify("-{$days} days")->format('Y-m-d H:i:s'); -echo "Done! Processed: $processed, Errors: $errors\n"; \ No newline at end of file +echo "Backfilling server_metrics_trends for the last {$days} days...\n"; +echo "Range start: {$rangeStart}\n"; + +$sql = " + INSERT INTO server_metrics_trends ( + server_id, + metric_name_id, + period_start, + avg_value, + min_value, + max_value, + count_samples + ) + SELECT + sm.server_id, + sm.metric_name_id, + DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:00:00') AS period_start, + AVG(CAST(sm.value AS DECIMAL(20,4))) AS avg_value, + MIN(CAST(sm.value AS DECIMAL(20,4))) AS min_value, + MAX(CAST(sm.value AS DECIMAL(20,4))) AS max_value, + COUNT(*) AS count_samples + FROM server_metrics sm + INNER JOIN metric_names mn ON mn.id = sm.metric_name_id + WHERE sm.created_at >= :range_start + AND mn.name NOT IN ('top_cpu_proc', 'top_ram_proc') + GROUP BY + sm.server_id, + sm.metric_name_id, + DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:00:00') + ON DUPLICATE KEY UPDATE + avg_value = VALUES(avg_value), + min_value = VALUES(min_value), + max_value = VALUES(max_value), + count_samples = VALUES(count_samples), + created_at = NOW() +"; + +$stmt = $pdo->prepare($sql); +$stmt->execute([':range_start' => $rangeStart]); + +$duration = (new DateTime())->getTimestamp() - $startedAt->getTimestamp(); +echo "Done in {$duration} sec.\n"; diff --git a/public/debug-login.php b/public/debug-login.php deleted file mode 100755 index d57220c..0000000 --- a/public/debug-login.php +++ /dev/null @@ -1,57 +0,0 @@ -authenticate($username, $password); - - if ($user) { - echo "DEBUG: Auth successful!\n"; - - $_SESSION['user_id'] = $user['id']; - $_SESSION['username'] = $user['username']; - $_SESSION['role'] = $user['role']; - - header('Location: /'); - exit; - } else { - echo "DEBUG: Auth failed!\n"; - header('Location: /login'); - exit; - } -} -?> - -
-Check /tmp/login_debug.log for details
- - diff --git a/public/index.php.broken b/public/index.php.broken deleted file mode 100755 index 63a1c50..0000000 --- a/public/index.php.broken +++ /dev/null @@ -1,202 +0,0 @@ -getResponseFactory()); -$csrf->setPersistentTokenMode(true); - -// Create Twig view -$twig = Twig::create(__DIR__ . '/../templates', ['cache' => false]); - -// Add CSRF middleware FIRST -$app->add($csrf); - -// Add Twig middleware -$twigMiddleware = TwigMiddleware::create($app, $twig); -$app->add($twigMiddleware); -$sessionMiddleware = new AppMiddlewaresSessionMiddleware($twig); -$app->add($sessionMiddleware); -// Add session data to Twig$sessionMiddleware = new SessionMiddleware($twig);$app->add($sessionMiddleware); - -// Add a route to get CSRF tokens via AJAX -$app->get('/csrf-token', function (Request $request, Response $response, $args) use ($csrf) { - $data = [ - 'name_key' => $csrf->getTokenNameKey(), - 'value_key' => $csrf->getTokenValueKey(), - 'name' => $csrf->getTokenName(), - 'value' => $csrf->getTokenValue() - ]; - - $response->getBody()->write(json_encode($data)); - return $response->withHeader('Content-Type', 'application/json'); -}); - -// Define /test route -$app->get('/test', function (Request $request, Response $response, $args) use ($twig) { - $templateData = [ - 'title' => 'Тест системы', - 'message' => 'Система мониторинга запущена' - ]; - - return $twig->render($response, 'test.twig', $templateData); -}); - -// Login routes (without auth middleware, but with CSRF) -$app->get('/login', function (Request $request, Response $response, $args) use ($twig, $csrf) { - $templateData = [ - 'title' => 'Вход в систему', - 'csrf' => [ - 'name_key' => $csrf->getTokenNameKey(), - 'value_key' => $csrf->getTokenValueKey(), - 'name' => $csrf->getTokenName(), - 'value' => $csrf->getTokenValue() - ] - ]; - - return $twig->render($response, 'login.twig', $templateData); -}); - -$app->post('/login', function (Request $request, Response $response, $args) use ($csrf) { - $params = $request->getParsedBody(); - - // Validate CSRF token - $nameKey = $csrf->getTokenNameKey(); - $valueKey = $csrf->getTokenValueKey(); - - if (!isset($params[$nameKey]) || !isset($params[$valueKey]) || !$csrf->validateToken($params[$nameKey], $params[$valueKey])) { - error_log('CSRF validation failed for /login'); - return $response->withHeader('Location', '/login')->withStatus(302); - } - - $username = $params['username'] ?? ''; - $password = $params['password'] ?? ''; - - $userModel = new User(); - $user = $userModel->authenticate($username, $password); - - if ($user) { - $_SESSION['user_id'] = $user['id']; - $_SESSION['username'] = $user['username']; - $_SESSION['role'] = $user['role']; - - return $response->withHeader('Location', '/')->withStatus(302); - } else { - return $response->withHeader('Location', '/login')->withStatus(302); - } -}); - -// Logout route (without auth middleware) -$app->get('/logout', function (Request $request, Response $response, $args) { - session_destroy(); - return $response->withHeader('Location', '/login')->withStatus(302); -}); - -// Dashboard route (protected with auth middleware) -$app->get('/', function (Request $request, Response $response, $args) use ($twig) { - $serverModel = new ServerModel(); - - // Get statistics - $stats = $serverModel->getStats(); - - // Get servers with latest metrics - $servers = $serverModel->getAll(); - - $templateData = [ - 'title' => 'Дашборд мониторинга', - 'stats' => $stats, - 'servers' => $servers - ]; - - return $twig->render($response, 'dashboard.twig', $templateData); -})->add(AuthMiddleware::class); - -// Routes for groups (protected with auth middleware) -$groupController = new GroupController($twig); - -$app->get('/groups', [$groupController, 'index'])->add(AuthMiddleware::class); -$app->get('/groups/create', [$groupController, 'create'])->add(AuthMiddleware::class); -$app->post('/groups', [$groupController, 'store'])->add(AuthMiddleware::class); -$app->get('/groups/{id}/edit', [$groupController, 'edit'])->add(AuthMiddleware::class); -$app->post('/groups/{id}', [$groupController, 'update'])->add(AuthMiddleware::class); -$app->delete('/groups/{id}', [$groupController, 'delete'])->add(AuthMiddleware::class); - -// Routes for servers (protected with auth middleware) -$serverController = new ServerController($twig); - -$app->get('/servers', [$serverController, 'index'])->add(AuthMiddleware::class); -$app->get('/servers/create', [$serverController, 'create'])->add(AuthMiddleware::class); -$app->post('/servers', [$serverController, 'store'])->add(AuthMiddleware::class); -$app->get('/servers/{id}/edit', [$serverController, 'edit'])->add(AuthMiddleware::class); -$app->post('/servers/{id}', [$serverController, 'update'])->add(AuthMiddleware::class); -$app->delete('/servers/{id}', [$serverController, 'delete'])->add(AuthMiddleware::class); -$app->get('/servers/{id}/regenerate-token', [$serverController, 'regenerateToken'])->add(AuthMiddleware::class); - -// Server detail route (protected with auth middleware) -$serverDetailController = new ServerDetailController($twig); - -$app->get('/servers/{id}', [$serverDetailController, 'show'])->add(AuthMiddleware::class); - -// Alerts routes (protected with auth middleware) -$alertController = new AlertController($twig); - -$app->get('/alerts', [$alertController, 'index'])->add(AuthMiddleware::class); -$app->get('/alerts/{id}/resolve', [$alertController, 'markAsResolved'])->add(AuthMiddleware::class); - -// Admin routes (protected with auth middleware) -$adminController = new AdminController($twig); - -$app->get('/admin/users', [$adminController, 'usersList'])->add(AuthMiddleware::class); -$app->get('/admin/notifications', [$adminController, 'notificationSettings'])->add(AuthMiddleware::class); - -// API route for agents (public, no auth middleware) -$metricsController = new MetricsController(); - -$app->post('/api/v1/metrics', [$metricsController, 'collectMetrics']); - -// API status endpoint (public, no auth middleware) -$app->get('/api/status', function (Request $request, Response $response, $args) { - $data = [ - 'status' => 'ok', - 'timestamp' => date('Y-m-d H:i:s'), - 'version' => '1.0.0' - ]; - - $response->getBody()->write(json_encode($data)); - return $response - ->withHeader('Content-Type', 'application/json'); -}); - -// Agent installation script route (public, no auth middleware) -$agentController = new AgentController(); - -$app->get('/agent/install.sh', [$agentController, 'generateInstallScript']); - -// Run app -$app->run(); diff --git a/public/login-direct.php b/public/login-direct.php deleted file mode 100755 index 29726ad..0000000 --- a/public/login-direct.php +++ /dev/null @@ -1,46 +0,0 @@ - - - - -Тестовые креды: admin / admin_test_2026
- - diff --git a/public/session_check.php b/public/session_check.php deleted file mode 100755 index 135a837..0000000 --- a/public/session_check.php +++ /dev/null @@ -1,12 +0,0 @@ - session_id(), - 'user_id' => $_SESSION['user_id'] ?? null, - 'username' => $_SESSION['username'] ?? null, - 'role' => $_SESSION['role'] ?? null, - 'session_data' => $_SESSION -], JSON_PRETTY_PRINT); diff --git a/public/session_test.php b/public/session_test.php deleted file mode 100755 index a7c1dc5..0000000 --- a/public/session_test.php +++ /dev/null @@ -1,23 +0,0 @@ -fetch(); $decryptedToken = $tokenRow ? \App\Utils\EncryptionHelper::decrypt($tokenRow['encrypted_token']) : null; - // Получаем все метрики которые есть у серве��а + // Получаем только метрики, которые можно показывать на странице сервера $stmt = $this->pdo->prepare(" SELECT DISTINCT mn.id, mn.name, mn.unit FROM metric_names mn JOIN server_metrics sm ON sm.metric_name_id = mn.id WHERE sm.server_id = :server_id + AND mn.name NOT LIKE '%_proc' + AND ( + mn.name IN ('uptime', 'cpu_load', 'ram_used') + OR mn.name LIKE 'disk_used_%' + OR mn.name LIKE 'net_in_%' + OR mn.name LIKE 'net_out_%' + OR mn.name LIKE 'temp_%' + ) ORDER BY mn.name "); $stmt->execute([':server_id' => $id]); @@ -181,7 +189,8 @@ class ServerController extends Model // Собираем выбранные метрики $displayMetrics = $params['display_metrics'] ?? []; - $displayMetricsJson = json_encode(array_values($displayMetrics)); + $displayMetrics = array_values(array_unique(array_filter($displayMetrics, fn ($metric) => is_string($metric) && $metric !== ''))); + $displayMetricsJson = json_encode($displayMetrics); $stmt = $this->pdo->prepare(" UPDATE servers diff --git a/src/Controllers/ServerDetailController.php b/src/Controllers/ServerDetailController.php index 9c2626b..7ec7487 100755 --- a/src/Controllers/ServerDetailController.php +++ b/src/Controllers/ServerDetailController.php @@ -131,15 +131,17 @@ class ServerDetailController extends Model $startStr = $startDate->format('Y-m-d H:i:s'); $endStr = $endDate->format('Y-m-d H:i:s'); - // Формируем фильтр метрик из настроек сервера + // Формируем фильтр метрик из настроек сервера. + // Дополнительные метрики подгружаем автоматически, если они нужны для отображения. $metricsFilter = ''; $metricParams = []; - if ($displayMetrics) { + $queryMetricNames = $displayMetrics ? $this->expandDisplayMetrics($displayMetrics) : []; + if ($queryMetricNames) { $placeholders = []; - foreach ($displayMetrics as $i => $m) { + foreach (array_values($queryMetricNames) as $i => $metricName) { $key = ':metric_' . $i; $placeholders[] = $key; - $metricParams[$key] = $m; + $metricParams[$key] = $metricName; } $metricsFilter = 'AND mn.name IN (' . implode(', ', $placeholders) . ')'; } @@ -203,19 +205,6 @@ class ServerDetailController extends Model AND 1=1 {$metricsFilter} ORDER BY t.period_start ASC "; - - // Конвертируем имена метрик в ID для filters - $metricIds = []; - if ($displayMetrics) { - $placeholders = []; - foreach ($displayMetrics as $i => $m) { - $key = ':metric_' . $i; - $placeholders[] = $key; - $metricParams[$key] = $m; - } - $metricsFilter = 'AND mn.name IN (' . implode(', ', $placeholders) . ')'; - } - $stmt = $this->pdo->prepare($sql); $executeParams = array_merge([':id' => $id, ':period_start' => $periodStartStr, ':period_end' => $periodEndStr], $metricParams); $stmt->execute($executeParams); @@ -328,31 +317,28 @@ class ServerDetailController extends Model $stmt->execute([':id' => $id]); $latestUptime = $stmt->fetch(); + $simpleMetricCharts = $this->buildSimpleMetricCharts($groupedMetrics, $displayMetrics); + $networkCharts = $this->buildNetworkCharts($groupedMetrics, $displayMetrics); + $temperatureChart = $this->buildTemperatureChart($groupedMetrics, $displayMetrics); + $diskCharts = $this->buildDiskCharts($groupedMetrics, $displayMetrics); + $templateData = [ 'title' => 'Сервер: ' . $server['name'], 'server' => $server, 'metrics' => $groupedMetrics, 'displayMetrics' => $displayMetrics, - // Группировка метрик по категориям для отдельных секций - 'diskMetrics' => array_filter($groupedMetrics, function($key) { - return str_starts_with($key, 'disk_used_'); - }, ARRAY_FILTER_USE_KEY), - 'tempMetrics' => array_filter($groupedMetrics, function($key) { - return str_starts_with($key, 'temp_'); - }, ARRAY_FILTER_USE_KEY), - 'netInMetrics' => array_filter($groupedMetrics, function($key) { - return str_starts_with($key, 'net_in_'); - }, ARRAY_FILTER_USE_KEY), - 'netOutMetrics' => array_filter($groupedMetrics, function($key) { - return str_starts_with($key, 'net_out_'); - }, ARRAY_FILTER_USE_KEY), + 'simpleMetricCharts' => $simpleMetricCharts, + 'networkCharts' => $networkCharts, + 'temperatureChart' => $temperatureChart, + 'diskCharts' => $diskCharts, 'allMetricTypes' => $allMetricTypes, 'existingThresholds' => $existingThresholds, 'allServices' => $allServices, 'monitorServices' => $monitorServices, 'latestUptime' => $latestUptime, - 'startDate' => $startDate->format('Y-m-d\T H:i'), - 'endDate' => $endDate->format('Y-m-d\T H:i'), + 'uptimeText' => $this->formatUptime($latestUptime['value'] ?? null), + 'startDate' => $startDate->format('Y-m-d\TH:i'), + 'endDate' => $endDate->format('Y-m-d\TH:i'), 'aggregation' => $aggConfig, 'totalMinutes' => $totalMinutes, 'period' => $period, @@ -402,6 +388,283 @@ class ServerDetailController extends Model } } + private function expandDisplayMetrics(array $displayMetrics): array + { + $expanded = []; + + foreach ($displayMetrics as $metricName) { + if ($metricName === 'uptime') { + continue; + } + + $expanded[$metricName] = $metricName; + + if (str_starts_with($metricName, 'disk_used_')) { + $suffix = substr($metricName, strlen('disk_used_')); + $expanded['disk_total_gb_' . $suffix] = 'disk_total_gb_' . $suffix; + } + } + + return array_values($expanded); + } + + private function buildSimpleMetricCharts(array $groupedMetrics, ?array $displayMetrics): array + { + $charts = []; + $config = [ + 'cpu_load' => ['title' => 'Загрузка CPU', 'color' => '#0d6efd'], + 'ram_used' => ['title' => 'Использование RAM', 'color' => '#198754'], + ]; + + foreach ($config as $metricName => $meta) { + if (!$this->isMetricSelected($metricName, $displayMetrics) || empty($groupedMetrics[$metricName])) { + continue; + } + + $charts[] = [ + 'id' => $metricName, + 'title' => $meta['title'], + 'unit' => $groupedMetrics[$metricName][0]['unit'] ?? '', + 'color' => $meta['color'], + 'labels' => $this->extractLabels($groupedMetrics[$metricName]), + 'timestamps' => $this->extractTimestamps($groupedMetrics[$metricName]), + 'values' => $this->extractValues($groupedMetrics[$metricName]), + 'lastValue' => round((float)($groupedMetrics[$metricName][0]['value'] ?? 0), 2), + 'lastTime' => $this->formatPointTime($groupedMetrics[$metricName][0] ?? []), + ]; + } + + return $charts; + } + + private function buildNetworkCharts(array $groupedMetrics, ?array $displayMetrics): array + { + $interfaces = []; + + foreach (array_keys($groupedMetrics) as $metricName) { + if (str_starts_with($metricName, 'net_in_')) { + $interfaces[substr($metricName, strlen('net_in_'))] = true; + } + if (str_starts_with($metricName, 'net_out_')) { + $interfaces[substr($metricName, strlen('net_out_'))] = true; + } + } + + $charts = []; + foreach (array_keys($interfaces) as $iface) { + $inMetric = 'net_in_' . $iface; + $outMetric = 'net_out_' . $iface; + $showIn = $this->isMetricSelected($inMetric, $displayMetrics) && !empty($groupedMetrics[$inMetric]); + $showOut = $this->isMetricSelected($outMetric, $displayMetrics) && !empty($groupedMetrics[$outMetric]); + + if (!$showIn && !$showOut) { + continue; + } + + $baseSeries = $showIn ? $groupedMetrics[$inMetric] : $groupedMetrics[$outMetric]; + $datasets = []; + + if ($showIn) { + $datasets[] = [ + 'label' => 'Входящий трафик', + 'color' => '#198754', + 'values' => $this->extractValues($groupedMetrics[$inMetric]), + ]; + } + + if ($showOut) { + $datasets[] = [ + 'label' => 'Исходящий трафик', + 'color' => '#dc3545', + 'values' => $this->extractValues($groupedMetrics[$outMetric]), + ]; + } + + $charts[] = [ + 'id' => $iface, + 'title' => 'Сеть: ' . $iface, + 'unit' => $baseSeries[0]['unit'] ?? '', + 'labels' => $this->extractLabels($baseSeries), + 'timestamps' => $this->extractTimestamps($baseSeries), + 'datasets' => $datasets, + ]; + } + + return $charts; + } + + private function buildTemperatureChart(array $groupedMetrics, ?array $displayMetrics): array + { + $datasets = []; + $labels = []; + $colors = ['#dc3545', '#fd7e14', '#0dcaf0', '#6f42c1', '#20c997', '#ffc107', '#6610f2', '#198754']; + $colorIndex = 0; + + foreach ($groupedMetrics as $metricName => $points) { + if (!str_starts_with($metricName, 'temp_') || !$this->isMetricSelected($metricName, $displayMetrics) || empty($points)) { + continue; + } + + if (!$labels) { + $labels = $this->extractLabels($points); + } + + $datasets[] = [ + 'label' => $this->formatMetricLabel($metricName), + 'color' => $colors[$colorIndex % count($colors)], + 'values' => $this->extractValues($points), + ]; + $colorIndex++; + } + + return [ + 'unit' => '°C', + 'labels' => $labels, + 'timestamps' => $labels ? $this->extractTimestamps($points) : [], + 'datasets' => $datasets, + ]; + } + + private function buildDiskCharts(array $groupedMetrics, ?array $displayMetrics): array + { + $charts = []; + + foreach ($groupedMetrics as $metricName => $points) { + if ( + !str_starts_with($metricName, 'disk_used_') + || $metricName === 'disk_used' + || !$this->isMetricSelected($metricName, $displayMetrics) + || empty($points) + ) { + continue; + } + + $suffix = substr($metricName, strlen('disk_used_')); + $totalMetric = 'disk_total_gb_' . $suffix; + $percent = (float)($points[0]['value'] ?? 0); + $totalGb = isset($groupedMetrics[$totalMetric][0]['value']) ? (float)$groupedMetrics[$totalMetric][0]['value'] : 0.0; + $usedGb = $totalGb > 0 ? round(($percent / 100) * $totalGb, 1) : null; + $freeGb = $totalGb > 0 ? round($totalGb - $usedGb, 1) : null; + + $charts[] = [ + 'id' => $suffix, + 'title' => $this->formatDiskTitle($suffix), + 'percent' => round($percent, 1), + 'totalGb' => $totalGb > 0 ? round($totalGb, 1) : null, + 'usedGb' => $usedGb, + 'freeGb' => $freeGb, + 'updatedAt' => $this->formatPointTime($points[0]), + ]; + } + + return $charts; + } + + private function isMetricSelected(string $metricName, ?array $displayMetrics): bool + { + return is_array($displayMetrics) && in_array($metricName, $displayMetrics, true); + } + + private function extractLabels(array $points): array + { + $labels = []; + + foreach ($points as $point) { + $labels[] = $this->formatPointTime($point, 'd.m H:i'); + } + + return $labels; + } + + private function extractValues(array $points): array + { + $values = []; + + foreach ($points as $point) { + $values[] = round((float)($point['value'] ?? 0), 2); + } + + return $values; + } + + private function extractTimestamps(array $points): array + { + $timestamps = []; + + foreach ($points as $point) { + $timestamps[] = $point['time_bucket'] ?? $point['created_at'] ?? null; + } + + return $timestamps; + } + + private function formatPointTime(array $point, string $format = 'd.m.Y H:i:s'): string + { + $raw = $point['time_bucket'] ?? $point['created_at'] ?? null; + if (!$raw) { + return ''; + } + + return (new DateTime($raw))->format($format); + } + + private function formatMetricLabel(string $metricName): string + { + if ($metricName === 'cpu_load') { + return 'Загрузка CPU'; + } + + if ($metricName === 'ram_used') { + return 'Использование RAM'; + } + + if (str_starts_with($metricName, 'temp_')) { + return 'Температура ' . str_replace('_', ' ', substr($metricName, strlen('temp_'))); + } + + return str_replace('_', ' ', $metricName); + } + + private function formatDiskTitle(string $suffix): string + { + return match ($suffix) { + 'root' => '/ (корень)', + 'home' => '/home', + 'boot' => '/boot', + 'mnt_data' => '/mnt/data', + default => '/' . str_replace('_', '/', $suffix), + }; + } + + private function formatUptime($value): ?string + { + if ($value === null || $value === '') { + return null; + } + + $seconds = (int)round((float)$value); + if ($seconds < 0) { + return null; + } + + $days = intdiv($seconds, 86400); + $seconds %= 86400; + $hours = intdiv($seconds, 3600); + $seconds %= 3600; + $minutes = intdiv($seconds, 60); + + $parts = []; + if ($days > 0) { + $parts[] = $days . ' д'; + } + if ($hours > 0 || $days > 0) { + $parts[] = $hours . ' ч'; + } + $parts[] = $minutes . ' мин'; + + return implode(' ', $parts); + } + public function saveThresholds(Request $request, Response $response, $args) { $id = $args['id']; diff --git a/src/Controllers/ServerDetailController.php.broken b/src/Controllers/ServerDetailController.php.broken deleted file mode 100755 index a3b3278..0000000 --- a/src/Controllers/ServerDetailController.php.broken +++ /dev/null @@ -1,198 +0,0 @@ -twig = $twig; - } - - public function show(Request $request, Response $response, $args) - { - $id = $args['id']; - - // Получаем информацию о сервере - $stmt = $this->pdo->prepare(" - SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color - FROM servers s - LEFT JOIN server_groups sg ON s.group_id = sg.id - WHERE s.id = :id - "); - $stmt->execute([':id' => $id]); - $server = $stmt->fetch(); - - if (!$server) { - return $response->withHeader('Location', '/servers')->withStatus(302); - } - - // Получаем период для выборки метрик - $period = $request->getQueryParams()['period'] ?? '24h'; - - // Определяем интервал времени в зависимости от периода - $interval = match($period) { - '7d' => 'INTERVAL 7 DAY', - '30d' => 'INTERVAL 30 DAY', - default => 'INTERVAL 24 HOUR' - }; - - // Получаем последние метрики для этого сервера - $stmt = $this->pdo->prepare(" - SELECT sm.value, mn.name, mn.unit, sm.created_at - FROM server_metrics sm - JOIN metric_names mn ON sm.metric_name_id = mn.id - WHERE sm.server_id = :id - AND sm.created_at >= DATE_SUB(NOW(), {$interval}) - ORDER BY sm.created_at DESC - "); - $stmt->execute([':id' => $id]); - $metrics = $stmt->fetchAll(); - - // Группируем метрики по типу - $groupedMetrics = []; - foreach ($metrics as $metric) { - $name = $metric['name']; - if (!isset($groupedMetrics[$name])) { - $groupedMetrics[$name] = []; - } - $groupedMetrics[$name][] = $metric; - } - - // Получаем все доступные типы метрик для настройки порогов - $stmt = $this->pdo->query("SELECT name, unit FROM metric_names ORDER BY name"); - $allMetricTypes = $stmt->fetchAll(); - - // Получаем текущие пороговые значения для сервера - $stmt = $this->pdo->prepare(" - SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name - FROM metric_thresholds mt - JOIN metric_names mn ON mt.metric_name_id = mn.id - WHERE mt.server_id = :id - "); - $stmt->execute([':id' => $id]); - $existingThresholds = []; - foreach ($stmt->fetchAll() as $threshold) { - $existingThresholds[$threshold['name']] = [ - 'warning' => $threshold['warning_threshold'], - 'critical' => $threshold['critical_threshold'], - 'duration' => $threshold['duration'] - ]; - } - - // Получаем список сервисов - $stmt = $this->pdo->prepare(" - SELECT service_name, status, load_state, active_state, sub_state - FROM service_status - WHERE server_id = :server_id - ORDER BY service_name - "); - $stmt->execute([':server_id' => $id]); - $allServices = $stmt->fetchAll(); - - // Получаем список сервисов для мониторинга из конфигурации агента - $stmt = $this->pdo->prepare(" - SELECT monitor_services FROM agent_configs WHERE server_id = :server_id - "); - $stmt->execute([':server_id' => $id]); - $agentConfig = $stmt->fetch(); - - $monitorServices = []; - if ($agentConfig && !empty($agentConfig['monitor_services'])) { - $monitorServices = json_decode($agentConfig['monitor_services'], true) ?? []; - } - - $templateData = [ - 'title' => 'Сервер: ' . $server['name'], - 'server' => $server, - 'metrics' => $groupedMetrics, - 'allMetricTypes' => $allMetricTypes, - 'existingThresholds' => $existingThresholds, - 'allServices' => $allServices, - 'period' => $period, - 'request' => $request->getQueryParams() - 'monitorServices' => $monitorServices - ]; - - return $this->twig->render($response, 'servers/detail.twig', $templateData); - } - - public function saveThresholds(Request $request, Response $response, $args) - { - $id = $args['id']; - $params = $request->getParsedBody(); - - // Получаем все типы метрик - $stmt = $this->pdo->query("SELECT id, name FROM metric_names ORDER BY name"); - $metricTypes = $stmt->fetchAll(); - - // Удаляем старые пороги для этого сервера - $stmt = $this->pdo->prepare("DELETE FROM metric_thresholds WHERE server_id = :server_id"); - $stmt->execute([':server_id' => $id]); - - // Добавляем новые пороги - $insertStmt = $this->pdo->prepare(" - INSERT INTO metric_thresholds (server_id, metric_name_id, warning_threshold, critical_threshold, duration) - VALUES (:server_id, :metric_name_id, :warning_threshold, :critical_threshold, :duration) - "); - - foreach ($metricTypes as $metricType) { - $warning = $params[$metricType['name'] . '_warning'] ?? null; - $critical = $params[$metricType['name'] . '_critical'] ?? null; - $duration = $params[$metricType['name'] . '_duration'] ?? 0; - - // Сохраняем только если указан хотя бы один порог - if ($warning !== null || $critical !== null) { - $insertStmt->execute([ - ':server_id' => $id, - ':metric_name_id' => $metricType['id'], - ':warning_threshold' => $warning, - ':critical_threshold' => $critical, - ':duration' => (int)$duration - ]); - } - } - - // Возвращаемся на страницу сервера - return $response->withHeader('Location', "/servers/{$id}")->withStatus(302); - } - - public function saveServices(Request $request, Response $response, $args) - { - $id = $args['id']; - $params = $request->getParsedBody(); - - // Получаем список сервисов для мониторинга - $services = $params['services'] ?? []; - - if (is_string($services)) { - $services = json_decode($services, true) ?? []; - } - - // Обновляем конфигурацию агента - $stmt = $this->pdo->prepare(" - INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled) - VALUES (:server_id, 60, :services, TRUE) - ON DUPLICATE KEY UPDATE - monitor_services = VALUES(monitor_services), - updated_at = CURRENT_TIMESTAMP - "); - - $stmt->execute([ - ':server_id' => $id, - ':services' => json_encode($services) - ]); - - // Возвращаемся на страницу сервера - return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302); - } -} diff --git a/templates/servers/detail.twig b/templates/servers/detail.twig old mode 100755 new mode 100644 index 7e793d9..2eefc80 --- a/templates/servers/detail.twig +++ b/templates/servers/detail.twig @@ -4,8 +4,8 @@Нет данных за этот период
+ Нет данных {% endif %}{{ pct }}% из {{ totalGB }} ГБ
-{{ metricData[0].created_at|date('d.m.Y H:i') }}
- -0 = алерт сразу при превышении, >0 = алерт только если превышено дольше указанного времени. Оставьте поле пустым для отключения порога. @@ -357,83 +354,61 @@