feat: масштабирование, дашборд, алерты и тултипы
- Исправлена ось времени: старые данные слева, новые справа - Подключён chartjs-plugin-zoom (колёсико, drag, pan) - Переделаны кнопки периода: 1ч/6ч/24ч/7д/30д (по умолчанию 6ч) - Добавлен cmdline в процессы тултипа (показывает полный путь) - Улучшена логика алертов: нет спама, resolved уведомления - Исправлено сохранение порогов (приведение типов) - Исправлена страница алертов (Twig syntax: ends_with -> matches) - Дашборд: цвета прогресс-баров по реальным порогам сервера Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
b875e57e4c
commit
0219fda95f
File diff suppressed because one or more lines are too long
|
|
@ -176,25 +176,106 @@ class MetricsController extends Model
|
|||
}
|
||||
|
||||
if ($severity) {
|
||||
// Создаем алерт
|
||||
// Проверяем есть ли уже неразрешённый алерт для этой метрики
|
||||
$stmt = $this->pdo->prepare("
|
||||
INSERT INTO alerts (server_id, metric_name, value, severity)
|
||||
VALUES (:server_id, :metric_name, :value, :severity)
|
||||
SELECT id, severity FROM alerts
|
||||
WHERE server_id = :server_id AND metric_name = :metric_name AND resolved = FALSE
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':server_id' => $serverId,
|
||||
':metric_name' => $metricName,
|
||||
':value' => $value,
|
||||
':severity' => $severity
|
||||
':metric_name' => $metricName
|
||||
]);
|
||||
$existingAlert = $stmt->fetch();
|
||||
|
||||
if ($existingAlert) {
|
||||
// Алерт уже есть — обновляем значение но НЕ отправляем уведомление
|
||||
$stmt = $this->pdo->prepare("
|
||||
UPDATE alerts SET value = :value WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
':value' => $value,
|
||||
':id' => $existingAlert['id']
|
||||
]);
|
||||
|
||||
// Если серьёзность повысилась (warning -> critical) — отправляем
|
||||
if ($severity === 'critical' && $existingAlert['severity'] === 'warning') {
|
||||
$stmt = $this->pdo->prepare("
|
||||
UPDATE alerts SET severity = :severity WHERE id = :id
|
||||
");
|
||||
$stmt->execute([
|
||||
':severity' => $severity,
|
||||
':id' => $existingAlert['id']
|
||||
]);
|
||||
$this->notificationService->sendAlertNotification(
|
||||
$serverName,
|
||||
$metricName,
|
||||
$value,
|
||||
$severity,
|
||||
$threshold
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Нового алерта нет — создаём и отправляем уведомление
|
||||
$stmt = $this->pdo->prepare("
|
||||
INSERT INTO alerts (server_id, metric_name, value, severity)
|
||||
VALUES (:server_id, :metric_name, :value, :severity)
|
||||
");
|
||||
$stmt->execute([
|
||||
':server_id' => $serverId,
|
||||
':metric_name' => $metricName,
|
||||
':value' => $value,
|
||||
':severity' => $severity
|
||||
]);
|
||||
|
||||
$this->notificationService->sendAlertNotification(
|
||||
$serverName,
|
||||
$metricName,
|
||||
$value,
|
||||
$severity,
|
||||
$threshold
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Всегда проверяем resolved — даже если пороги не настроены или удалены
|
||||
// Если есть неразрешённый алерт а значение сейчас в норме — разрешаем
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT id FROM alerts
|
||||
WHERE server_id = :server_id AND metric_name = :metric_name AND resolved = FALSE
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':server_id' => $serverId,
|
||||
':metric_name' => $metricName
|
||||
]);
|
||||
$existingAlert = $stmt->fetch();
|
||||
|
||||
if ($existingAlert) {
|
||||
// Проверяем действительно ли значение в норме
|
||||
// (если пороги есть — проверяем по ним, если нет — считаем что в норме)
|
||||
$isNormal = true;
|
||||
if ($thresholds) {
|
||||
$w = $thresholds['warning_threshold'];
|
||||
$c = $thresholds['critical_threshold'];
|
||||
if (($c && $value >= $c) || ($w && $value >= $w)) {
|
||||
$isNormal = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isNormal) {
|
||||
$stmt = $this->pdo->prepare("
|
||||
UPDATE alerts SET resolved = TRUE, resolved_at = NOW() WHERE id = :id
|
||||
");
|
||||
$stmt->execute([':id' => $existingAlert['id']]);
|
||||
|
||||
// Отправляем уведомление
|
||||
$this->notificationService->sendAlertNotification(
|
||||
$serverName,
|
||||
$metricName,
|
||||
$value,
|
||||
$severity,
|
||||
$threshold
|
||||
'resolved',
|
||||
'Порог более не превышен'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,14 @@ class DashboardController
|
|||
|
||||
// Получаем список серверов со статусами для цветных карточек
|
||||
$servers = $this->serverModel->getServersWithStatus();
|
||||
|
||||
|
||||
// Загружаем пороги для каждого сервера
|
||||
foreach ($servers as &$server) {
|
||||
$t = $this->serverModel->getThresholds($server['id']);
|
||||
$server['thresholds'] = $t;
|
||||
file_put_contents('/tmp/thresholds_debug.log', "Server {$server['id']}: " . json_encode($t) . "\n", FILE_APPEND);
|
||||
}
|
||||
unset($server);
|
||||
|
||||
$templateData = [
|
||||
'title' => 'Дашборд мониторинга',
|
||||
|
|
@ -33,7 +40,6 @@ class DashboardController
|
|||
'servers' => $servers
|
||||
];
|
||||
|
||||
file_put_contents("/tmp/dashboard_debug.log", json_encode($servers) . "\n", FILE_APPEND);
|
||||
return $this->twig->render($response, 'dashboard.twig', $templateData);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,21 +37,69 @@ class ServerDetailController extends Model
|
|||
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;
|
||||
|
||||
// Если даты не указаны, используем последние 24 часа по умолчанию
|
||||
// Если даты не указаны, вычисляем по period
|
||||
if (!$startDate || !$endDate) {
|
||||
$endDate = new DateTime();
|
||||
$startDate = clone $endDate;
|
||||
$startDate->modify('-24 hours');
|
||||
|
||||
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;
|
||||
|
|
@ -185,7 +233,9 @@ class ServerDetailController extends Model
|
|||
'startDate' => $startDate->format('Y-m-d\T H:i'),
|
||||
'endDate' => $endDate->format('Y-m-d\T H:i'),
|
||||
'aggregation' => $aggConfig,
|
||||
'totalMinutes' => $totalMinutes
|
||||
'totalMinutes' => $totalMinutes,
|
||||
'period' => $period,
|
||||
'zoom' => $zoom
|
||||
];
|
||||
|
||||
return $this->twig->render($response, 'servers/detail.twig', $templateData);
|
||||
|
|
|
|||
|
|
@ -124,6 +124,44 @@ class Server
|
|||
$server['active_alerts'] = (int)$activeAlerts;
|
||||
}
|
||||
|
||||
// Загружаем пороги для каждого сервера
|
||||
foreach ($servers as &$server) {
|
||||
$stmt = $this->db->prepare("SELECT mn.name, mt.warning_threshold, mt.critical_threshold FROM metric_thresholds mt JOIN metric_names mn ON mt.metric_name_id = mn.id WHERE mt.server_id = :server_id");
|
||||
$stmt->execute([':server_id' => $server['id']]);
|
||||
$thresholds = $stmt->fetchAll();
|
||||
$server['thresholds'] = [];
|
||||
foreach ($thresholds as $t) {
|
||||
$server['thresholds'][$t['name']] = [
|
||||
'warning' => (float)$t['warning_threshold'],
|
||||
'critical' => (float)$t['critical_threshold']
|
||||
];
|
||||
}
|
||||
}
|
||||
unset($server);
|
||||
|
||||
return $servers;
|
||||
}
|
||||
|
||||
public function getThresholds($serverId)
|
||||
{
|
||||
error_log("getThresholds called for server $serverId");
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT mn.name, mt.warning_threshold, mt.critical_threshold
|
||||
FROM metric_thresholds mt
|
||||
JOIN metric_names mn ON mt.metric_name_id = mn.id
|
||||
WHERE mt.server_id = :server_id
|
||||
");
|
||||
$stmt->execute([':server_id' => $serverId]);
|
||||
$thresholds = $stmt->fetchAll();
|
||||
error_log("getThresholds result for server $serverId: " . json_encode($thresholds));
|
||||
$result = [];
|
||||
foreach ($thresholds as $t) {
|
||||
$result[$t['name']] = [
|
||||
'warning' => (float)$t['warning_threshold'],
|
||||
'critical' => (float)$t['critical_threshold']
|
||||
];
|
||||
}
|
||||
error_log("getThresholds returning for server $serverId: " . json_encode($result));
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,14 +28,26 @@ class NotificationService
|
|||
*/
|
||||
public function sendAlertNotification($serverName, $metricName, $value, $severity, $threshold)
|
||||
{
|
||||
$severityText = $severity === 'critical' ? 'КРИТИЧЕСКИЙ' : 'ПРЕДУПРЕЖДЕНИЕ';
|
||||
$subject = "🚨 {$severityText}: Превышение порога {$metricName}";
|
||||
$message = "Сервер: {$serverName}\n";
|
||||
$message .= "Метрика: {$metricName}\n";
|
||||
$message .= "Значение: {$value}\n";
|
||||
$message .= "Порог: {$threshold}\n";
|
||||
$message .= "Время: " . date('d.m.Y H:i:s') . "\n";
|
||||
$message .= "Серьёзность: {$severityText}";
|
||||
if ($severity === 'resolved') {
|
||||
$severityText = 'ВОССТАНОВЛЕНИЕ';
|
||||
$emoji = '✅';
|
||||
$subject = "{$emoji} {$severityText}: {$metricName} в норме";
|
||||
$message = "Сервер: {$serverName}\n";
|
||||
$message .= "Метрика: {$metricName}\n";
|
||||
$message .= "Текущее значение: {$value}\n";
|
||||
$message .= "Статус: Порог более не превышен\n";
|
||||
$message .= "Время: " . date('d.m.Y H:i:s');
|
||||
} else {
|
||||
$severityText = $severity === 'critical' ? 'КРИТИЧЕСКИЙ' : 'ПРЕДУПРЕЖДЕНИЕ';
|
||||
$emoji = '🚨';
|
||||
$subject = "{$emoji} {$severityText}: Превышение порога {$metricName}";
|
||||
$message = "Сервер: {$serverName}\n";
|
||||
$message .= "Метрика: {$metricName}\n";
|
||||
$message .= "Значение: {$value}\n";
|
||||
$message .= "Порог: {$threshold}\n";
|
||||
$message .= "Время: " . date('d.m.Y H:i:s') . "\n";
|
||||
$message .= "Серьёзность: {$severityText}";
|
||||
}
|
||||
|
||||
// Отправка Email
|
||||
if (!empty($this->settings['email_enabled']) && !empty($this->settings['smtp_host'])) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<tr class="{% if alert.severity == 'critical' %}table-danger{% else %}table-warning{% endif %}">
|
||||
<td>{{ alert.server_name }}</td>
|
||||
<td>{{ alert.metric_name|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}</td>
|
||||
<td>{{ alert.value }}{% if alert.metric_name ends_with '_load' or alert.metric_name ends_with '_used' %}%{% endif %}</td>
|
||||
<td>{{ alert.value }}{% if alert.metric_name matches '/_load$/' or alert.metric_name matches '/_used$/' %}%{% endif %}</td>
|
||||
<td>
|
||||
{% if alert.severity == 'critical' %}
|
||||
<span class="badge bg-danger"><i class="fas fa-exclamation-triangle"></i> Критично</span>
|
||||
|
|
|
|||
|
|
@ -54,8 +54,11 @@
|
|||
</div>
|
||||
|
||||
<!-- Карточки серверов -->
|
||||
<!-- DEBUG: {{ servers|json_encode }} -->
|
||||
{% for s in servers %}<!-- DBG: id={{ s.id }} t={{ s.thresholds|json_encode }} -->{% endfor %}
|
||||
<div class="row">
|
||||
{% for server in servers %}
|
||||
<!-- DEBUG: server_id={{ server.id }} thresholds={{ server.thresholds|json_encode }} ram_metric={{ server.latest_metrics["ram_used"].value }} -->
|
||||
|
||||
<div class="col-xl-4 col-lg-6 col-md-6 mb-4">
|
||||
<div class="card h-100 server-card" data-server-id="{{ server.id }}" data-status="{{ server.status }}">
|
||||
|
|
@ -98,6 +101,7 @@
|
|||
<div class="mb-2"><span class="badge bg-info">Статус: {{ server.status }}</span></div>
|
||||
|
||||
<!-- Метрики -->
|
||||
{% for s in servers %}<!-- DBG: id={{ s.id }} t={{ s.thresholds|json_encode }} -->{% endfor %}
|
||||
<div class="row">
|
||||
{% if server.latest_metrics['cpu_load'] is defined %}
|
||||
<div class="col-6 mb-2">
|
||||
|
|
@ -105,8 +109,20 @@
|
|||
<small class="text-muted"><i class="fas fa-microchip"></i> CPU</small>
|
||||
<strong>{{ server.latest_metrics['cpu_load'].value }}{{ server.latest_metrics['cpu_load'].unit }}</strong>
|
||||
</div>
|
||||
{% set cpu_t = server.thresholds['cpu_load']|default(null) %}
|
||||
{% if cpu_t and server.latest_metrics['cpu_load'].value >= cpu_t.critical %}
|
||||
{% set cpu_color = 'bg-danger' %}
|
||||
{% elseif cpu_t and server.latest_metrics['cpu_load'].value >= cpu_t.warning %}
|
||||
{% set cpu_color = 'bg-warning' %}
|
||||
{% elseif server.latest_metrics['cpu_load'].value > 80 %}
|
||||
{% set cpu_color = 'bg-danger' %}
|
||||
{% elseif server.latest_metrics['cpu_load'].value > 60 %}
|
||||
{% set cpu_color = 'bg-warning' %}
|
||||
{% else %}
|
||||
{% set cpu_color = 'bg-success' %}
|
||||
{% endif %}
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar {% if server.latest_metrics['cpu_load'].value > 80 %}bg-danger{% elseif server.latest_metrics['cpu_load'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
<div class="progress-bar {{ cpu_color }}"
|
||||
role="progressbar"
|
||||
style="width: {{ server.latest_metrics['cpu_load'].value }}%"></div>
|
||||
</div>
|
||||
|
|
@ -119,8 +135,20 @@
|
|||
<small class="text-muted"><i class="fas fa-memory"></i> RAM</small>
|
||||
<strong>{{ server.latest_metrics['ram_used'].value }}{{ server.latest_metrics['ram_used'].unit }}</strong>
|
||||
</div>
|
||||
{% set ram_t = server.thresholds['ram_used']|default(null) %}
|
||||
{% if ram_t and server.latest_metrics['ram_used'].value >= ram_t.critical %}
|
||||
{% set ram_color = 'bg-danger' %}
|
||||
{% elseif ram_t and server.latest_metrics['ram_used'].value >= ram_t.warning %}
|
||||
{% set ram_color = 'bg-warning' %}
|
||||
{% elseif server.latest_metrics['ram_used'].value > 80 %}
|
||||
{% set ram_color = 'bg-danger' %}
|
||||
{% elseif server.latest_metrics['ram_used'].value > 60 %}
|
||||
{% set ram_color = 'bg-warning' %}
|
||||
{% else %}
|
||||
{% set ram_color = 'bg-success' %}
|
||||
{% endif %}
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar {% if server.latest_metrics['ram_used'].value > 80 %}bg-danger{% elseif server.latest_metrics['ram_used'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
<div class="progress-bar {{ ram_color }}"
|
||||
role="progressbar"
|
||||
style="width: {{ server.latest_metrics['ram_used'].value }}%"></div>
|
||||
</div>
|
||||
|
|
@ -133,8 +161,20 @@
|
|||
<small class="text-muted"><i class="fas fa-hdd"></i> Диск</small>
|
||||
<strong>{{ server.latest_metrics['disk_used'].value }}{{ server.latest_metrics['disk_used'].unit }}</strong>
|
||||
</div>
|
||||
{% set disk_t = server.thresholds['disk_used']|default(null) %}
|
||||
{% if disk_t and server.latest_metrics['disk_used'].value >= disk_t.critical %}
|
||||
{% set disk_color = 'bg-danger' %}
|
||||
{% elseif disk_t and server.latest_metrics['disk_used'].value >= disk_t.warning %}
|
||||
{% set disk_color = 'bg-warning' %}
|
||||
{% elseif server.latest_metrics['disk_used'].value > 80 %}
|
||||
{% set disk_color = 'bg-danger' %}
|
||||
{% elseif server.latest_metrics['disk_used'].value > 60 %}
|
||||
{% set disk_color = 'bg-warning' %}
|
||||
{% else %}
|
||||
{% set disk_color = 'bg-success' %}
|
||||
{% endif %}
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar {% if server.latest_metrics['disk_used'].value > 80 %}bg-danger{% elseif server.latest_metrics['disk_used'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
<div class="progress-bar {{ disk_color }}"
|
||||
role="progressbar"
|
||||
style="width: {{ server.latest_metrics['disk_used'].value }}%"></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -88,46 +88,34 @@
|
|||
<div class="tab-content mt-3">
|
||||
<!-- Вкладка "Метрики" -->
|
||||
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
|
||||
<div class="row mb-3">
|
||||
<div class="row mt-2 mb-2">
|
||||
<div class="col-md-12">
|
||||
<!-- Отладка: period = {{ request.query.period }} -->
|
||||
<!-- Отладка: period = {{ period }}, request = {{ request.period }} -->
|
||||
<div class="btn-group d-flex" role="group">
|
||||
<a href="?tab=metrics&period=24h" class="btn btn-outline-primary w-100 {% if period == '24h' or period is empty %}active{% endif %}">
|
||||
24 часа
|
||||
</a>
|
||||
<a href="?tab=metrics&period=7d" class="btn btn-outline-primary w-100 {% if period == '7d' %}active{% endif %}">
|
||||
7 дней
|
||||
</a>
|
||||
<a href="?tab=metrics&period=30d" class="btn btn-outline-primary w-100 {% if period == '30d' %}active{% endif %}">
|
||||
30 дней
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted">Период:</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-12">
|
||||
<small class="text-muted">Масштаб:</small>
|
||||
<div class="btn-group ms-2" role="group">
|
||||
<a href="?tab=metrics&period={{ period }}&zoom=auto" class="btn btn-sm {% if not zoom or zoom == 'auto' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||
авто
|
||||
</a>
|
||||
<a href="?tab=metrics&period={{ period }}&zoom=1h" class="btn btn-sm {% if zoom == '1h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||
<div class="col-md-12 mt-1">
|
||||
<div class="btn-group d-flex" role="group">
|
||||
<a href="?tab=metrics&period=1h" class="btn btn-outline-primary w-100 {% if period == '1h' %}active{% endif %}">
|
||||
1ч
|
||||
</a>
|
||||
<a href="?tab=metrics&period={{ period }}&zoom=6h" class="btn btn-sm {% if zoom == '6h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||
<a href="?tab=metrics&period=6h" class="btn btn-outline-primary w-100 {% if period == '6h' or period is empty %}active{% endif %}">
|
||||
6ч
|
||||
</a>
|
||||
<a href="?tab=metrics&period={{ period }}&zoom=24h" class="btn btn-sm {% if zoom == '24h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||
<a href="?tab=metrics&period=24h" class="btn btn-outline-primary w-100 {% if period == '24h' %}active{% endif %}">
|
||||
24ч
|
||||
</a>
|
||||
<a href="?tab=metrics&period={{ period }}&zoom=7d" class="btn btn-sm {% if zoom == '7d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||
<a href="?tab=metrics&period=7d" class="btn btn-outline-primary w-100 {% if period == '7d' %}active{% endif %}">
|
||||
7д
|
||||
</a>
|
||||
<a href="?tab=metrics&period={{ period }}&zoom=30d" class="btn btn-sm {% if zoom == '30d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||
<a href="?tab=metrics&period=30d" class="btn btn-outline-primary w-100 {% if period == '30d' %}active{% endif %}">
|
||||
30д
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group d-flex mt-1" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary w-100" onclick="resetAllZoom()" title="Сбросить интерактивный зум">
|
||||
<i class="fas fa-search-minus"></i> Сбросить зум
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">💡 Колёсико мыши = зум, перетаскивание = выделение области, Shift+колёсико = панорама</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -411,6 +399,8 @@
|
|||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="/chartjs-plugin-crosshair.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js"></script>
|
||||
<script src="/chartjs-plugin-zoom.min.js"></script>
|
||||
<script>
|
||||
|
||||
// Функция для получения топ-процессов для указанного времени
|
||||
|
|
@ -434,7 +424,7 @@ function fetchProcesses(serverId, time) {
|
|||
lines.push('');
|
||||
lines.push('🏆 Топ CPU:');
|
||||
data.top_cpu.forEach(function(proc) {
|
||||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
||||
lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -443,7 +433,7 @@ function fetchProcesses(serverId, time) {
|
|||
lines.push('');
|
||||
lines.push('💾 Топ RAM:');
|
||||
data.top_ram.forEach(function(proc) {
|
||||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
||||
lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -499,7 +489,7 @@ const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementByI
|
|||
var labels{{ metricName }} = [];
|
||||
var data{{ metricName }} = [];
|
||||
|
||||
{% for metric in metricData|slice(0, 50000)|reverse %}
|
||||
{% for metric in metricData|slice(0, 50000) %}
|
||||
{% set time_val = metric.time_bucket|default(metric.created_at) %}
|
||||
{% set time_format = metric.time_bucket ? 'd.m H:i' : 'H:i' %}
|
||||
labels{{ metricName }}.push('{{ time_val|date(time_format) }}');
|
||||
|
|
@ -596,7 +586,7 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
|||
lines.push('');
|
||||
lines.push('TOP CPU:');
|
||||
data.top_cpu.forEach(function(proc) {
|
||||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
||||
lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%');
|
||||
});
|
||||
}
|
||||
{% elseif metricName == 'ram_used' %}
|
||||
|
|
@ -605,7 +595,7 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
|||
lines.push('');
|
||||
lines.push('TOP RAM:');
|
||||
data.top_ram.forEach(function(proc) {
|
||||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
||||
lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%');
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
|
@ -623,6 +613,24 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
|||
// }, 3000);
|
||||
});
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
zoom: {
|
||||
drag: {
|
||||
enabled: true
|
||||
},
|
||||
pinch: {
|
||||
enabled: true
|
||||
},
|
||||
wheel: {
|
||||
enabled: true
|
||||
},
|
||||
mode: 'x'
|
||||
},
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: 'x'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -664,5 +672,17 @@ document.addEventListener('mousemove', function(e) {
|
|||
{% endfor %}
|
||||
});
|
||||
|
||||
|
||||
// Сбросить зум на всех графиках
|
||||
function resetAllZoom() {
|
||||
{% for metricName, metricData in metrics %}
|
||||
{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" %}
|
||||
if (typeof chart{{ metricName|replace({'-': '_', '.': '_'}) }} !== 'undefined') {
|
||||
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.resetZoom();
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in New Issue