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); } // Получаем параметры $queryParams = $request->getQueryParams(); $startDate = $queryParams['start'] ?? null; $endDate = $queryParams['end'] ?? null; $period = $queryParams['period'] ?? '24h'; $zoom = $queryParams['zoom'] ?? null; // Если даты не указаны, вычисляем по period if (!$startDate || !$endDate) { $endDate = new DateTime(); $startDate = clone $endDate; switch ($period) { case '1h': $startDate->modify('-1 hour'); break; case '6h': $startDate->modify('-6 hours'); break; case '7d': $startDate->modify('-7 days'); break; case '30d': $startDate->modify('-30 days'); break; case '24h': default: $startDate->modify('-24 hours'); break; } } else { $startDate = new DateTime($startDate); $endDate = new DateTime($endDate); } // Применяем zoom — ограничиваем end по zoom-периоду if ($zoom && $zoom !== 'auto') { $zoomEnd = new DateTime(); $zoomStart = clone $zoomEnd; switch ($zoom) { case '1h': $zoomStart->modify('-1 hour'); break; case '6h': $zoomStart->modify('-6 hours'); break; case '24h': $zoomStart->modify('-24 hours'); break; case '7d': $zoomStart->modify('-7 days'); break; case '30d': $zoomStart->modify('-30 days'); break; } // Zoom не может выйти за рамки выбранного периода if ($zoomStart < $startDate) $zoomStart = clone $startDate; if ($zoomEnd > $endDate) $zoomEnd = clone $endDate; $startDate = $zoomStart; $endDate = $zoomEnd; } // Валидация: end > start if ($endDate <= $startDate) { $endDate = clone $startDate; $endDate->modify('+24 hours'); } // Вычисляем длительность периода для агрегации $interval = $startDate->diff($endDate); $totalMinutes = ($interval->days * 24 * 60) + ($interval->h * 60) + $interval->i; // Конфигурация агрегации на основе дат $aggConfig = $this->getAggregationConfigFromDates($startDate, $endDate, $totalMinutes); $groupBy = $aggConfig['groupBy']; $bucketFormat = $aggConfig['format']; // Форматируем даты для SQL $startStr = $startDate->format('Y-m-d H:i:s'); $endStr = $endDate->format('Y-m-d H:i:s'); // Запрос с агрегацией если нужно if ($groupBy) { $sql = " SELECT AVG(sm.value) as value, mn.name, mn.unit, DATE_FORMAT(sm.created_at, '{$bucketFormat}') as time_bucket FROM server_metrics sm JOIN metric_names mn ON sm.metric_name_id = mn.id WHERE sm.server_id = :id AND sm.created_at >= :start_date AND sm.created_at <= :end_date {$groupBy} ORDER BY time_bucket ASC "; $stmt = $this->pdo->prepare($sql); $stmt->execute([':id' => $id, ':start_date' => $startStr, ':end_date' => $endStr]); } else { $sql = " 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 >= :start_date AND sm.created_at <= :end_date ORDER BY sm.created_at ASC "; $stmt = $this->pdo->prepare($sql); $stmt->execute([':id' => $id, ':start_date' => $startStr, ':end_date' => $endStr]); } $metrics = $stmt->fetchAll(); // Группируем метрики $groupedMetrics = []; foreach ($metrics as $metric) { $metricName = $metric['name']; if (!isset($groupedMetrics[$metricName])) { $groupedMetrics[$metricName] = []; } $groupedMetrics[$metricName][] = $metric; } // Сортируем метрики в фиксированном порядке: cpu_load → ram_used → disk_used → остальные $priorityOrder = ['cpu_load', 'ram_used', 'disk_used']; $sortedMetrics = []; // Сначала добавляем приоритетные метрики в нужном порядке foreach ($priorityOrder as $metricName) { if (isset($groupedMetrics[$metricName])) { $sortedMetrics[$metricName] = $groupedMetrics[$metricName]; unset($groupedMetrics[$metricName]); } } // Затем добавляем остальные метрики (например, top_cpu_proc, top_ram_proc) foreach ($groupedMetrics as $metricName => $metricData) { $sortedMetrics[$metricName] = $metricData; } $groupedMetrics = $sortedMetrics; // Пороги $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 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 = :id AND mn.name != 'uptime' AND ( mn.name IN ('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 CASE WHEN mn.name = 'cpu_load' THEN 1 WHEN mn.name = 'ram_used' THEN 2 WHEN mn.name LIKE 'disk_used_%' THEN 3 WHEN mn.name LIKE 'net_in_%' THEN 4 WHEN mn.name LIKE 'net_out_%' THEN 5 WHEN mn.name LIKE 'temp_%' THEN 6 END, mn.name "); $stmt->execute([':id' => $id]); $allMetricTypes = $stmt->fetchAll(); // Сервисы $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, 'monitorServices' => $monitorServices, 'startDate' => $startDate->format('Y-m-d\T H:i'), 'endDate' => $endDate->format('Y-m-d\T H:i'), 'aggregation' => $aggConfig, 'totalMinutes' => $totalMinutes, 'period' => $period, 'zoom' => $zoom ]; return $this->twig->render($response, 'servers/detail.twig', $templateData); } private function getAggregationConfigFromDates(DateTime $startDate, DateTime $endDate, int $totalMinutes): array { // Target: ~400 points on chart for optimal performance // Formula: aggregate_minutes = total_minutes / 400 $targetPoints = 400; $aggregateMinutes = ceil($totalMinutes / $targetPoints); // Определяем формат группировки на основе длительности агрегации if ($aggregateMinutes <= 1) { // Менее 1 минуты — без агрегации return [ 'groupBy' => null, 'format' => '%Y-%m-%d %H:%i:%s', 'aggregate_minutes' => 0 ]; } elseif ($aggregateMinutes < 60) { // Минуты — группировка по минутам return [ 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i')", 'format' => '%Y-%m-%d %H:%i', 'aggregate_minutes' => $aggregateMinutes ]; } elseif ($aggregateMinutes < 1440) { // Часы — группировка по часам return [ 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:00')", 'format' => '%Y-%m-%d %H:00', 'aggregate_minutes' => $aggregateMinutes ]; } else { // Дни — группировка по дням return [ 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d')", 'format' => '%Y-%m-%d', 'aggregate_minutes' => $aggregateMinutes ]; } } public function saveThresholds(Request $request, Response $response, $args) { $id = $args['id']; $params = $request->getParsedBody(); // Получаем только метрики которые есть у сервера и отображаются на графиках $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 = :id AND mn.name != 'uptime' AND ( mn.name IN ('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 CASE WHEN mn.name = 'cpu_load' THEN 1 WHEN mn.name = 'ram_used' THEN 2 WHEN mn.name LIKE 'disk_used_%' THEN 3 WHEN mn.name LIKE 'net_in_%' THEN 4 WHEN mn.name LIKE 'net_out_%' THEN 5 WHEN mn.name LIKE 'temp_%' THEN 6 END, mn.name "); $stmt->execute([':id' => $id]); $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) "); // Дефолтные значения порогов (пока хардкод, потом из настроек) $defaultWarning = 80; $defaultCritical = 90; $defaultDuration = 0; $saved = []; $usedDefaults = []; foreach ($metricTypes as $metricType) { $warning = $params[$metricType['name'] . '_warning'] ?? ''; $critical = $params[$metricType['name'] . '_critical'] ?? ''; $duration = $params[$metricType['name'] . '_duration'] ?? ''; // Сохраняем если хотя бы один порог заполнен if ($warning !== '' || $critical !== '') { // Если не указано - используем дефолт $warningVal = $warning !== '' ? (float)$warning : $defaultWarning; $criticalVal = $critical !== '' ? (float)$critical : $defaultCritical; $durationVal = $duration !== '' ? (int)$duration : $defaultDuration; $insertStmt->execute([ ':server_id' => $id, ':metric_name_id' => $metricType['id'], ':warning_threshold' => $warningVal, ':critical_threshold' => $criticalVal, ':duration' => $durationVal ]); $name = $metricType['name']; $used = []; if ($warning === '') $used[] = 'warning=' . $defaultWarning; if ($critical === '') $used[] = 'critical=' . $defaultCritical; if ($duration === '') $used[] = 'duration=' . $defaultDuration; if (count($used) > 0) { $usedDefaults[] = $name . ' (' . implode(', ', $used) . ')'; } else { $saved[] = $name; } } } // Формируем flash сообщение $messages = []; if (count($saved) > 0) { $messages[] = 'Сохранено: ' . implode(', ', $saved); } if (count($usedDefaults) > 0) { $messages[] = 'Для остальных подставлены значения по умолчанию: ' . implode(', ', $usedDefaults); } if (count($messages) === 0) { $messages[] = 'Все пороги удалены'; } $_SESSION['flash_message'] = implode('. ', $messages); $_SESSION['flash_type'] = count($usedDefaults) > 0 ? 'warning' : 'success'; return $response->withHeader('Location', "/servers/{$id}?tab=thresholds")->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); } }