diff --git a/templates/servers/detail.twig b/templates/servers/detail.twig index 4df1bdc..d78f2b4 100755 --- a/templates/servers/detail.twig +++ b/templates/servers/detail.twig @@ -120,34 +120,10 @@ - -
- {% for metricName, metricData in metrics %} - {% if metricName starts with 'disk_used_' and metricName != 'disk_used' %} -
-
-
-
- {% if metricName == 'disk_used_root' %}/ (корень) - {% elseif metricName == 'disk_used_home' %}/home - {% elseif metricName == 'disk_used_boot' %}/boot - {% elseif metricName == 'disk_used_mnt_data' %}/mnt/data - {% else %}{{ metricName|replace({'disk_used_': '', '_': ' '})|title }} - {% endif %} -
-

{{ metricData[0].value }}{{ metricData[0].unit|default('%') }}

-

{{ metricData[0].created_at|date('d.m.Y H:i') }}

-
-
-
-
- {% endif %} - {% endfor %} -
{% for metricName, metricData in metrics %} - {% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" and not (metricName starts with "disk_used_") and metricName != "disk_used" %} + {% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" and metricName!="disk_used" and not (metricName starts with "disk_used_") and not (metricName starts with "disk_total_gb_") and metricName!="ram_total_gb" and not (metricName starts with "net_in_") and not (metricName starts with "net_out_") %}
@@ -186,6 +162,66 @@
+ + {% set net_interfaces = [] %} + {% for metricName in metrics|keys %} + {% if metricName starts with 'net_in_' %} + {% set iface = metricName|replace({'net_in_': ''}) %} + {% set net_interfaces = net_interfaces|merge([iface]) %} + {% endif %} + {% endfor %} + + {% for iface in net_interfaces %} + {% if metrics['net_in_' ~ iface] is defined and metrics['net_out_' ~ iface] is defined %} +
+
+
+
+
Сеть: {{ iface }}
+
+
+ +
+
+
+
+ {% endif %} + {% endfor %} + + +
+ {% for metricName, metricData in metrics %} + {% if metricName starts with 'disk_used_' and metricName != 'disk_used' %} +
+
+
+
+ {% if metricName == 'disk_used_root' %}/ (корень) + {% elseif metricName == 'disk_used_home' %}/home + {% elseif metricName == 'disk_used_boot' %}/boot + {% elseif metricName == 'disk_used_mnt_data' %}/mnt/data + {% else %}{{ metricName|replace({'disk_used_': '', '_': ' '})|title }} + {% endif %} +
+ {% set pct = metricData[0].value|round(1) %} + {% set iface = metricName|replace({'disk_used_': ''}) %} + {% set totalGB = metrics['disk_total_gb_' ~ iface][0].value|default(0) %} + {% set usedGB = (pct / 100 * totalGB)|round(1) %} + {% set freeGB = (totalGB - usedGB)|round(1) %} +
+ Свободно: {{ freeGB }} ГБ + Занято: {{ usedGB }} ГБ +
+

{{ pct }}% из {{ totalGB }} ГБ

+

{{ metricData[0].created_at|date('d.m.Y H:i') }}

+
+
+
+
+ {% endif %} + {% endfor %} +
+
@@ -505,9 +541,19 @@ document.addEventListener('DOMContentLoaded', function() { } }); +// Параметры системы +var ramTotalGB = {{ metrics['ram_total_gb'] is defined ? metrics['ram_total_gb'][0].value : 0 }}; +var diskTotalGB = { +{% for m, _data in metrics %} +{% if m starts with 'disk_total_gb_' %} + '{{ m|replace({'disk_total_gb_': ''}) }}': {{ metrics[m][0].value|default(0) }}, +{% endif %} +{% endfor %} +}; + // Графики метрик {% for metricName, metricData in metrics %} -{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" and not (metricName starts with "disk_used_") and metricName != "disk_used" %} +{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" and metricName!="disk_used" and not (metricName starts with "disk_used_") and not (metricName starts with "disk_total_gb_") and metricName!="ram_total_gb" and not (metricName starts with "net_in_") and not (metricName starts with "net_out_") %} const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementById('chart-{{ metricName }}').getContext('2d'); // Подготовка данных для графика @@ -528,8 +574,9 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr datasets: [{ label: '{{ metricName|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}', data: data{{ metricName }}, - borderColor: 'rgb(75, 192, 192)', - backgroundColor: 'rgba(75, 192, 192, 0.2)', +{% if metricName == 'cpu_load' %}{% set lineColor = 'rgba(54, 162, 235, 1)' %}{% set fillColor = 'rgba(54, 162, 235, 0.15)' %}{% elseif metricName == 'ram_used' %}{% set lineColor = 'rgba(153, 102, 255, 1)' %}{% set fillColor = 'rgba(153, 102, 255, 0.15)' %}{% elseif metricName starts with 'disk_used_' %}{% set lineColor = 'rgba(255, 159, 64, 1)' %}{% set fillColor = 'rgba(255, 159, 64, 0.15)' %}{% else %}{% set lineColor = 'rgba(75, 192, 192, 1)' %}{% set fillColor = 'rgba(75, 192, 192, 0.15)' %}{% endif %} + borderColor: '{{ lineColor }}', + backgroundColor: '{{ fillColor }}', tension: 0.1 }] }, @@ -591,25 +638,40 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr .then(data => { var lines = []; lines.push('Время: ' + time); +{% if metricName == 'ram_used' %} + var ramPct = data{{ metricName }}[dataIndex]; + var ramUsed = (ramPct / 100 * ramTotalGB).toFixed(1); + var ramFree = (ramTotalGB - ramUsed).toFixed(1); + lines.push('Всего: ' + ramTotalGB.toFixed(1) + ' ГБ'); + lines.push('Занято: ' + ramUsed + ' ГБ'); + lines.push('Свободно: ' + ramFree + ' ГБ'); + lines.push(''); + if (data.top_ram && data.top_ram.length > 0) { + lines.push('TOP RAM:'); + data.top_ram.forEach(function(proc) { + lines.push(' ' + ((proc.cmdline || '').trim() || proc.name) + ': ' + proc.value + '%'); + }); + } +{% elseif metricName starts with 'disk_used_' %} + var diskPct = data{{ metricName }}[dataIndex]; + var iface = '{{ metricName }}'.replace('disk_used_', ''); + var diskTotal = diskTotalGB[iface] || 0; + var diskUsed = (diskPct / 100 * diskTotal).toFixed(1); + var diskFree = (diskTotal - diskUsed).toFixed(1); + lines.push('Всего: ' + diskTotal.toFixed(1) + ' ГБ'); + lines.push('Занято: ' + diskUsed + ' ГБ'); + lines.push('Свободно: ' + diskFree + ' ГБ'); +{% else %} lines.push('Значение: ' + data{{ metricName }}[dataIndex]); {% if metricName == 'cpu_load' %} - // Показываем только top_cpu if (data.top_cpu && data.top_cpu.length > 0) { lines.push(''); lines.push('TOP CPU:'); data.top_cpu.forEach(function(proc) { - lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%'); - }); - } -{% elseif metricName == 'ram_used' %} - // Показываем только top_ram - if (data.top_ram && data.top_ram.length > 0) { - lines.push(''); - lines.push('TOP RAM:'); - data.top_ram.forEach(function(proc) { - lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%'); + lines.push(' ' + ((proc.cmdline || '').trim() || proc.name) + ': ' + proc.value + '%'); }); } +{% endif %} {% endif %} // Show tooltip @@ -665,7 +727,7 @@ chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas.addEventListener('mou // Глобальный обработчик для скрытия тултипов при уходе курсора за пределы canvas document.addEventListener('mousemove', function(e) { {% for metricName, metricData in metrics %} -{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" and not (metricName starts with "disk_used_") and metricName != "disk_used" %} +{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" and metricName!="disk_used" and not (metricName starts with "disk_used_") and not (metricName starts with "disk_total_gb_") and metricName!="ram_total_gb" and not (metricName starts with "net_in_") and not (metricName starts with "net_out_") %} (function() { var canvas = chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas; var rect = canvas.getBoundingClientRect(); @@ -686,6 +748,83 @@ document.addEventListener('mousemove', function(e) { + +// Графики сетевых интерфейсов (две линии: In зелёная, Out красная) +{% set net_interfaces = [] %} +{% for metricName in metrics|keys %} + {% if metricName starts with 'net_in_' %} + {% set net_interfaces = net_interfaces|merge([metricName|replace({'net_in_': ''})]) %} + {% endif %} +{% endfor %} + +{% for iface in net_interfaces %} +{% if metrics['net_in_' ~ iface] is defined and metrics['net_out_' ~ iface] is defined %} +(function() { + var ctx = document.getElementById('chart-net-{{ iface }}'); + if (!ctx) return; + + var labels = []; + var inData = []; + var outData = []; + +{% for m in metrics['net_in_' ~ iface]|slice(0, 500)|reverse %} + labels.push('{{ m.created_at|date("d.m H:i") }}'); + inData.push({{ m.value }}); +{% endfor %} + +{% set outMetrics = metrics['net_out_' ~ iface]|slice(0, 500)|reverse %} +{% for m in outMetrics %} + outData.push({{ m.value }}); +{% endfor %} + + new Chart(ctx.getContext('2d'), { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Входящий (In)', + data: inData, + borderColor: 'rgba(25, 135, 84, 1)', + backgroundColor: 'rgba(25, 135, 84, 0.1)', + fill: true, + tension: 0.1, + pointRadius: 0, + borderWidth: 1.5 + }, + { + label: 'Исходящий (Out)', + data: outData, + borderColor: 'rgba(220, 53, 69, 1)', + backgroundColor: 'rgba(220, 53, 69, 0.1)', + fill: true, + tension: 0.1, + pointRadius: 0, + borderWidth: 1.5 + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'index', intersect: false }, + plugins: { + legend: { display: true, position: 'top' }, + tooltip: { enabled: true, mode: 'index', intersect: false }, + zoom: { + zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: 'x' }, + pan: { enabled: true, mode: 'x' } + } + }, + scales: { + y: { beginAtZero: true, ticks: { callback: function(v) { return v + '%'; } } } + } + } + }); +})(); +{% endif %} +{% endfor %} + // Doughnut графики для разделов дисков {% for metricName, metricData in metrics %} {% if metricName starts with 'disk_used_' and metricName != 'disk_used' %} @@ -731,7 +870,7 @@ document.addEventListener('mousemove', function(e) { // Сбросить зум на всех графиках function resetAllZoom() { {% for metricName, metricData in metrics %} -{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" and not (metricName starts with "disk_used_") and metricName != "disk_used" %} +{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" and metricName!="disk_used" and not (metricName starts with "disk_used_") and not (metricName starts with "disk_total_gb_") and metricName!="ram_total_gb" and not (metricName starts with "net_in_") and not (metricName starts with "net_out_") %} if (typeof chart{{ metricName|replace({'-': '_', '.': '_'}) }} !== 'undefined') { chart{{ metricName|replace({'-': '_', '.': '_'}) }}.resetZoom(); }