mirvmon/templates/servers/detail.twig

761 lines
45 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "layout.twig" %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h3 class="mb-0">
<i class="fas fa-server"></i>
{{ server.name }}
{% if server.group_name %}
<span class="badge ms-2" {% if server.group_color %}style="background-color: {{ server.group_color }}"{% endif %}>
<i class="fas {{ server.group_icon|default('fa-box') }} me-1"></i>{{ server.group_name }}
</span>
{% endif %}
</h3>
<div>
<a href="/servers/{{ server.id }}/edit" class="btn btn-outline-primary me-2">
<i class="fas fa-edit"></i> <span class="d-none d-sm-inline">Редактировать</span>
</a>
<a href="/servers" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> <span class="d-none d-sm-inline">Назад к списку</span>
</a>
</div>
</div>
<div class="card-body">
<ul class="nav nav-tabs mb-4" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="metrics-tab" data-bs-toggle="tab" data-bs-target="#metrics" type="button" role="tab" aria-controls="metrics" aria-selected="true">
<i class="fas fa-chart-line me-1"></i> Метрики
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="services-tab" data-bs-toggle="tab" data-bs-target="#services" type="button" role="tab" aria-controls="services" aria-selected="false">
<i class="fas fa-cogs me-1"></i> Сервисы
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="thresholds-tab" data-bs-toggle="tab" data-bs-target="#thresholds" type="button" role="tab" aria-controls="thresholds" aria-selected="false">
<i class="fas fa-bell me-1"></i> Пороги
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="metrics" role="tabpanel" aria-labelledby="metrics-tab">
<div class="card mb-4">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-end">
<div>
<div class="small text-muted mb-2">Период отображения</div>
<div class="btn-group flex-wrap" role="group" aria-label="Период">
<a href="?period=1h&tab=metrics" class="btn btn-sm {% if period == '1h' %}btn-primary{% else %}btn-outline-primary{% endif %}">1 час</a>
<a href="?period=6h&tab=metrics" class="btn btn-sm {% if period == '6h' %}btn-primary{% else %}btn-outline-primary{% endif %}">6 часов</a>
<a href="?period=24h&tab=metrics" class="btn btn-sm {% if period == '24h' %}btn-primary{% else %}btn-outline-primary{% endif %}">24 часа</a>
<a href="?period=7d&tab=metrics" class="btn btn-sm {% if period == '7d' %}btn-primary{% else %}btn-outline-primary{% endif %}">7 дней</a>
<a href="?period=30d&tab=metrics" class="btn btn-sm {% if period == '30d' %}btn-primary{% else %}btn-outline-primary{% endif %}">30 дней</a>
</div>
</div>
<form method="get" class="row g-2 align-items-end">
<input type="hidden" name="tab" value="metrics">
<div class="col-auto">
<label for="start" class="form-label small text-muted mb-1">Начало</label>
<input type="datetime-local" class="form-control form-control-sm" id="start" name="start" value="{{ startDate }}">
</div>
<div class="col-auto">
<label for="end" class="form-label small text-muted mb-1">Конец</label>
<input type="datetime-local" class="form-control form-control-sm" id="end" name="end" value="{{ endDate }}">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-outline-secondary">Применить</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-sm btn-outline-dark" onclick="resetAllZoom()">Сбросить зум</button>
</div>
</form>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small mb-1">Последняя метрика</div>
<div class="fw-semibold">
{% if server.last_seen %}
{{ server.last_seen|date('d.m.Y H:i:s') }}
{% else %}
Нет данных
{% endif %}
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small mb-1">Аптайм</div>
<div class="fw-semibold">
{% if uptimeText %}
{{ uptimeText }}
{% else %}
Нет данных
{% endif %}
</div>
{% if latestUptime and latestUptime.created_at %}
<div class="small text-muted mt-1">{{ latestUptime.created_at|date('d.m.Y H:i:s') }}</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<div class="text-muted small mb-1">Выбранный период</div>
<div class="fw-semibold">{{ totalMinutes }} мин</div>
<div class="small text-muted mt-1">{{ period }}{% if zoom %}, zoom: {{ zoom }}{% endif %}</div>
</div>
</div>
</div>
</div>
{% if summaryCards is not empty %}
<div class="row g-3 mb-4">
{% for card in summaryCards %}
<div class="col-md-6 col-xl">
<div class="card h-100">
<div class="card-body">
<div class="small text-muted mb-1">{{ card.title }}</div>
<div class="fw-semibold">{{ card.value }}</div>
<div class="small text-muted mt-1">{{ card.subtitle }}</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if not displayMetrics or displayMetrics is empty %}
<div class="alert alert-warning">
<i class="fas fa-info-circle"></i>
Метрики для отображения не выбраны. Настройте их в <a href="/servers/{{ server.id }}/edit">редактировании сервера</a>.
</div>
{% else %}
{% if simpleMetricCharts is empty and networkCharts is empty and temperatureChart.datasets is empty and diskCharts is empty %}
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> Нет данных для выбранных метрик за указанный период.
</div>
{% endif %}
{% for chart in simpleMetricCharts %}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{{ chart.title }}</h6>
<div class="text-end">
<div class="fw-semibold">{{ chart.lastValue }}{{ chart.unit }}</div>
{% if chart.id == 'ram_used' and chart.details %}
<div class="small text-muted">
Всего: {{ chart.details.totalGb }} ГБ | Занято: {{ chart.details.usedGb }} ГБ
</div>
<div class="small text-muted">
Свободно: {{ chart.details.freeGb }} ГБ
</div>
{% endif %}
<div class="small text-muted">{{ chart.lastTime }}</div>
</div>
</div>
<div class="card-body">
<div style="height: 260px;">
<canvas id="chart-{{ chart.id }}"></canvas>
</div>
</div>
</div>
{% endfor %}
{% for chart in networkCharts %}
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-network-wired me-1"></i>{{ chart.title }}</h6>
</div>
<div class="card-body">
<div style="height: 260px;">
<canvas id="chart-net-{{ chart.id }}"></canvas>
</div>
</div>
</div>
{% endfor %}
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-thermometer-half me-1"></i>Температуры</h6>
</div>
<div class="card-body">
{% if temperatureChart.datasets is not empty %}
<div style="height: 320px;">
<canvas id="chart-temperatures"></canvas>
</div>
{% else %}
<div class="alert alert-info mb-0">
Температурные датчики не выбраны или по ним нет данных.
</div>
{% endif %}
</div>
</div>
<div class="row">
{% if diskCharts is not empty %}
{% for disk in diskCharts %}
<div class="col-md-6 col-xl-4 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<h6 class="card-title">{{ disk.title }}</h6>
<div class="mb-3">
<span class="badge bg-primary">{{ disk.percent }}%</span>
</div>
{% if disk.totalGb %}
<div class="small text-muted mb-2">Всего: {{ disk.totalGb }} ГБ</div>
<div class="small mb-3">
<span class="text-danger">Занято: {{ disk.usedGb }} ГБ</span>
<span class="mx-1">|</span>
<span class="text-success">Свободно: {{ disk.freeGb }} ГБ</span>
</div>
{% endif %}
<div style="max-width: 180px; margin: 0 auto;">
<canvas id="disk-chart-{{ disk.id }}"></canvas>
</div>
<div class="small text-muted mt-3">{{ disk.updatedAt }}</div>
</div>
</div>
</div>
{% endfor %}
{% elseif displayMetrics|filter(v => v starts with 'disk_used_')|length > 0 %}
<div class="col-12">
<div class="alert alert-info">
Данные по дискам выбраны, но за этот период не найдены.
</div>
</div>
{% endif %}
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="services" role="tabpanel" aria-labelledby="services-tab">
<div class="row mb-3">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h4 class="mb-0">
<i class="fas fa-cogs"></i> Сервисы сервера
{% if allServices is defined %}
<small class="text-muted">(найдено: {{ allServices|length }})</small>
{% endif %}
</h4>
</div>
<div>
<a href="?tab=services" class="btn btn-outline-primary">
<i class="fas fa-sync-alt"></i> <span class="d-none d-sm-inline">Обновить список</span>
</a>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<form method="post" action="/servers/{{ server.id }}/services">
<div class="card">
<div class="card-body">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="selectAllServices">
<label class="form-check-label" for="selectAllServices">Выбрать все</label>
</div>
<hr>
<div class="row" id="servicesList">
{% if allServices is defined and allServices|length > 0 %}
{% set runningServices = allServices|filter(s => s.status == 'running')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
{% set stoppedServices = allServices|filter(s => s.status == 'stopped')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
{% set unknownServices = allServices|filter(s => s.status != 'running' and s.status != 'stopped')|sort((a, b) => a.service_name|lower > b.service_name|lower) %}
{% for service in runningServices %}
<div class="col-md-4 col-lg-3 mb-2">
<div class="card border-success">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-start">
<div class="form-check mb-0">
<input class="form-check-input service-checkbox" type="checkbox" id="service_{{ service.service_name }}" name="services[]" value="{{ service.service_name }}" {% if service.service_name in monitorServices %}checked{% endif %}>
<label class="form-check-label" for="service_{{ service.service_name }}">
<i class="fas fa-check-circle text-success me-1"></i>{{ service.service_name }}
</label>
</div>
<span class="badge bg-success">running</span>
</div>
<div class="small text-muted mt-1">
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
</div>
</div>
</div>
</div>
{% endfor %}
{% for service in stoppedServices %}
<div class="col-md-4 col-lg-3 mb-2">
<div class="card border-danger">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-start">
<div class="form-check mb-0">
<input class="form-check-input service-checkbox" type="checkbox" id="service_{{ service.service_name }}" name="services[]" value="{{ service.service_name }}" {% if service.service_name in monitorServices %}checked{% endif %}>
<label class="form-check-label" for="service_{{ service.service_name }}">
<i class="fas fa-times-circle text-danger me-1"></i>{{ service.service_name }}
</label>
</div>
<span class="badge bg-danger">stopped</span>
</div>
<div class="small text-muted mt-1">
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
</div>
</div>
</div>
</div>
{% endfor %}
{% for service in unknownServices %}
<div class="col-md-4 col-lg-3 mb-2">
<div class="card border-warning">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-start">
<div class="form-check mb-0">
<input class="form-check-input service-checkbox" type="checkbox" id="service_{{ service.service_name }}" name="services[]" value="{{ service.service_name }}" {% if service.service_name in monitorServices %}checked{% endif %}>
<label class="form-check-label" for="service_{{ service.service_name }}">
<i class="fas fa-question-circle text-warning me-1"></i>{{ service.service_name }}
</label>
</div>
<span class="badge bg-warning">unknown</span>
</div>
<div class="small text-muted mt-1">
<small>Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }}</small>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<div class="alert alert-warning text-center">
<i class="fas fa-exclamation-triangle"></i> Агент не отправил список сервисов или не установлен
</div>
</div>
<div class="col-12 text-center mt-3">
<button type="button" class="btn btn-outline-primary" onclick="requestServices()">
<i class="fas fa-download"></i> Запросить список сервисов
</button>
</div>
{% endif %}
</div>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Сохранить конфигурацию
</button>
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane fade" id="thresholds" role="tabpanel" aria-labelledby="thresholds-tab">
<h4>Настройка порогов</h4>
<p class="text-muted mb-3">
<i class="fas fa-info-circle"></i> 0 = алерт сразу при превышении, >0 = алерт только если превышено дольше указанного времени. Оставьте поле пустым для отключения порога.
</p>
<div class="row">
<div class="col-12">
<form method="post" action="/servers/{{ server.id }}/thresholds">
{% for metricType in allMetricTypes %}
{% set metricUnit = '%' %}
{% set metricLabel = metricType.name %}
{% if metricType.name starts with 'temp_' %}
{% set metricUnit = '°C' %}
{% set metricLabel = 'Температура ' ~ (metricType.name|replace({'temp_': '', '_': ' '}))|title %}
{% elseif metricType.name == 'cpu_load' %}
{% set metricLabel = 'Загрузка CPU' %}
{% elseif metricType.name == 'ram_used' %}
{% set metricLabel = 'Использование RAM' %}
{% elseif metricType.name starts with 'disk_used_' %}
{% set iface = metricType.name|replace({'disk_used_': ''}) %}
{% if iface == 'root' %}{% set metricLabel = 'Диск (корень /)' %}
{% elseif iface == 'home' %}{% set metricLabel = 'Диск (/home)' %}
{% elseif iface == 'boot' %}{% set metricLabel = 'Диск (/boot)' %}
{% elseif iface == 'mnt_data' %}{% set metricLabel = 'Диск (/mnt/data)' %}
{% else %}{% set metricLabel = 'Диск (/' ~ (iface|replace({'_': '/'})) ~ ')' %}
{% endif %}
{% elseif metricType.name starts with 'net_in_' %}
{% set iface = metricType.name|replace({'net_in_': ''}) %}
{% set metricLabel = 'Сеть входящая (' ~ iface ~ ')' %}
{% elseif metricType.name starts with 'net_out_' %}
{% set iface = metricType.name|replace({'net_out_': ''}) %}
{% set metricLabel = 'Сеть исходящая (' ~ iface ~ ')' %}
{% endif %}
<div class="card mb-2">
<div class="card-body py-2">
<div class="row align-items-center">
<div class="col-md-3 mb-2 mb-md-0">
<strong>{{ metricLabel }}</strong>
<small class="text-muted">({{ metricUnit }})</small>
</div>
<div class="col-md-3 mb-2 mb-md-0">
<div class="input-group input-group-sm" title="Порог предупреждения">
<span class="input-group-text"><i class="fas fa-exclamation-triangle text-warning"></i></span>
<input type="number" class="form-control" style="max-width: 90px;" name="{{ metricType.name }}_warning" step="0.01" placeholder="80" {% if existingThresholds[metricType.name].warning is defined %}value="{{ existingThresholds[metricType.name].warning }}"{% endif %}>
<span class="input-group-text">{{ metricUnit }}</span>
</div>
</div>
<div class="col-md-3 mb-2 mb-md-0">
<div class="input-group input-group-sm" title="Порог критический">
<span class="input-group-text"><i class="fas fa-exclamation-circle text-danger"></i></span>
<input type="number" class="form-control" style="max-width: 90px;" name="{{ metricType.name }}_critical" step="0.01" placeholder="90" {% if existingThresholds[metricType.name].critical is defined %}value="{{ existingThresholds[metricType.name].critical }}"{% endif %}>
<span class="input-group-text">{{ metricUnit }}</span>
</div>
</div>
<div class="col-md-3">
<div class="input-group input-group-sm" title="Длительность превышения (минуты)">
<span class="input-group-text"><i class="fas fa-clock"></i></span>
<input type="number" class="form-control" style="max-width: 90px;" name="{{ metricType.name }}_duration" min="0" step="1" placeholder="0" {% if existingThresholds[metricType.name].duration is defined %}value="{{ existingThresholds[metricType.name].duration }}"{% endif %}>
<span class="input-group-text">мин</span>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Сохранить пороги
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.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>
const managedCharts = [];
function requestServices() {
fetch('/api/v1/agent/{{ server.id }}/services')
.then(response => response.json())
.then(data => {
if (data.services) {
location.reload();
}
})
.catch(error => {
alert('Ошибка получения списка сервисов: ' + error);
});
}
function fetchProcesses(serverId, time) {
return fetch('/api/v1/agent/' + serverId + '/processes?time=' + encodeURIComponent(time))
.then(response => response.json())
.catch(() => ({ top_cpu: [], top_ram: [] }));
}
function renderProcessTooltip(context, chartMeta) {
let tooltipEl = document.getElementById('chartjs-tooltip-' + chartMeta.id);
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = 'chartjs-tooltip-' + chartMeta.id;
tooltipEl.style.opacity = 0;
tooltipEl.style.position = 'absolute';
tooltipEl.style.background = 'rgba(33, 37, 41, 0.92)';
tooltipEl.style.color = '#fff';
tooltipEl.style.borderRadius = '6px';
tooltipEl.style.padding = '10px 12px';
tooltipEl.style.fontSize = '12px';
tooltipEl.style.pointerEvents = 'none';
tooltipEl.style.whiteSpace = 'pre-line';
tooltipEl.style.zIndex = 1080;
document.body.appendChild(tooltipEl);
}
const tooltip = context.tooltip;
if (!tooltip || tooltip.opacity === 0 || !tooltip.dataPoints || !tooltip.dataPoints.length) {
tooltipEl.style.opacity = 0;
return;
}
const point = tooltip.dataPoints[0];
const dataIndex = point.dataIndex;
const timestamp = chartMeta.timestamps[dataIndex] || chartMeta.labels[dataIndex];
const value = point.raw;
const valueText = `${point.dataset.label}: ${value}${chartMeta.unit || ''}`;
tooltipEl.innerHTML = `Время: ${chartMeta.labels[dataIndex]}<br>${valueText}<br><span class="text-muted">Загрузка...</span>`;
tooltipEl.style.opacity = 1;
const position = context.chart.canvas.getBoundingClientRect();
tooltipEl.style.left = position.left + window.pageXOffset + tooltip.caretX + 12 + 'px';
tooltipEl.style.top = position.top + window.pageYOffset + tooltip.caretY + 'px';
fetchProcesses({{ server.id }}, timestamp).then(function(data) {
const lines = [`Время: ${chartMeta.labels[dataIndex]}`, valueText];
if (chartMeta.id === 'ram_used' && chartMeta.details && chartMeta.details.totalGb) {
const totalGb = Number(chartMeta.details.totalGb);
const usedGb = Number(((Number(value) / 100) * totalGb).toFixed(1));
const freeGb = Number((totalGb - usedGb).toFixed(1));
lines.push(`Всего: ${totalGb.toFixed(1)} ГБ`);
lines.push(`Занято: ${usedGb.toFixed(1)} ГБ`);
lines.push(`Свободно: ${freeGb.toFixed(1)} ГБ`);
}
const processList = chartMeta.id === 'cpu_load' ? (data.top_cpu || []) : (data.top_ram || []);
if (processList.length > 0) {
lines.push('');
lines.push(chartMeta.id === 'cpu_load' ? 'Топ CPU:' : 'Топ RAM:');
processList.slice(0, 5).forEach(function(proc) {
lines.push(`${proc.cmdline || proc.name}: ${proc.value}%`);
});
}
tooltipEl.innerHTML = lines.join('<br>');
});
}
function createLineChart(canvasId, labels, datasets, unit, chartMeta = null) {
const canvas = document.getElementById(canvasId);
if (!canvas) {
return;
}
const chart = new Chart(canvas.getContext('2d'), {
type: 'line',
data: { labels, datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: datasets.length > 1, position: 'top' },
tooltip: chartMeta ? {
enabled: false,
external: function(context) {
renderProcessTooltip(context, chartMeta);
}
} : {
enabled: true
},
zoom: {
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
drag: { enabled: true },
mode: 'x'
},
pan: {
enabled: true,
mode: 'x'
}
}
},
scales: {
y: {
beginAtZero: false,
ticks: {
callback: function(value) {
return unit ? value + unit : value;
}
}
}
}
}
});
managedCharts.push(chart);
return chart;
}
function createDoughnutChart(canvasId, percent) {
const canvas = document.getElementById(canvasId);
if (!canvas) {
return;
}
const color = percent >= 85 ? '#dc3545' : (percent >= 70 ? '#fd7e14' : '#198754');
new Chart(canvas.getContext('2d'), {
type: 'doughnut',
data: {
labels: ['Использовано', 'Свободно'],
datasets: [{
data: [percent, Math.max(0, 100 - percent)],
backgroundColor: [color, '#e9ecef'],
borderWidth: 0
}]
},
options: {
responsive: true,
cutout: '68%',
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return context.label + ': ' + context.parsed.toFixed(1) + '%';
}
}
}
}
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
const activeTab = urlParams.get('tab') || 'metrics';
const tabButton = document.getElementById(activeTab + '-tab');
if (tabButton) {
tabButton.click();
}
const selectAllServices = document.getElementById('selectAllServices');
if (selectAllServices) {
selectAllServices.addEventListener('change', function() {
document.querySelectorAll('.service-checkbox').forEach(function(checkbox) {
checkbox.checked = selectAllServices.checked;
});
});
}
const simpleMetricCharts = {{ simpleMetricCharts|json_encode|raw }};
simpleMetricCharts.forEach(function(chart) {
const datasets = [{
label: chart.title,
data: chart.values,
borderColor: chart.color,
backgroundColor: chart.color + '22',
fill: true,
tension: 0.2,
pointRadius: 0,
borderWidth: 2
}];
if (chart.thresholds && chart.thresholds.warning !== null) {
datasets.push({
label: 'Warning',
data: chart.labels.map(() => Number(chart.thresholds.warning)),
borderColor: '#fd7e14',
backgroundColor: 'transparent',
fill: false,
tension: 0,
pointRadius: 0,
borderWidth: 1.5,
borderDash: [6, 4]
});
}
if (chart.thresholds && chart.thresholds.critical !== null) {
datasets.push({
label: 'Critical',
data: chart.labels.map(() => Number(chart.thresholds.critical)),
borderColor: '#dc3545',
backgroundColor: 'transparent',
fill: false,
tension: 0,
pointRadius: 0,
borderWidth: 1.5,
borderDash: [3, 3]
});
}
createLineChart(
'chart-' + chart.id,
chart.labels,
datasets,
chart.unit,
{
id: chart.id,
labels: chart.labels,
timestamps: chart.timestamps || [],
unit: chart.unit,
details: chart.details || null
}
);
});
const networkCharts = {{ networkCharts|json_encode|raw }};
networkCharts.forEach(function(chart) {
createLineChart(
'chart-net-' + chart.id,
chart.labels,
chart.datasets.map(function(dataset) {
return {
label: dataset.label,
data: dataset.values,
borderColor: dataset.color,
backgroundColor: dataset.color + '22',
fill: false,
tension: 0.2,
pointRadius: 0,
borderWidth: 2
};
}),
chart.unit,
null
);
});
const temperatureChart = {{ temperatureChart|json_encode|raw }};
if (temperatureChart.datasets && temperatureChart.datasets.length > 0) {
createLineChart(
'chart-temperatures',
temperatureChart.labels,
temperatureChart.datasets.map(function(dataset) {
return {
label: dataset.label,
data: dataset.values,
borderColor: dataset.color,
backgroundColor: dataset.color + '22',
fill: false,
tension: 0.2,
pointRadius: 0,
borderWidth: 2
};
}),
temperatureChart.unit || '',
null
);
}
const diskCharts = {{ diskCharts|json_encode|raw }};
diskCharts.forEach(function(disk) {
createDoughnutChart('disk-chart-' + disk.id, disk.percent);
});
});
function resetAllZoom() {
managedCharts.forEach(function(chart) {
if (chart && typeof chart.resetZoom === 'function') {
chart.resetZoom();
}
});
}
</script>
{% endblock %}