From ce577c5d51fcb3a37e2a564a23cb5de658fd4b6a Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 14 Apr 2026 03:13:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20=D0=B2=D1=81=D0=B5=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=D0=BF=D0=B5=D1=80=D0=B0=D1=82=D1=83=D1=80=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D0=BA=D0=B8=20(temp=5F*?= =?UTF-8?q?)=20=D0=B2=20=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=20=D1=81=20=D1=80=D0=B0=D0=B7=D0=BD=D1=8B?= =?UTF-8?q?=D0=BC=D0=B8=20=D1=86=D0=B2=D0=B5=D1=82=D0=B0=D0=BC=D0=B8,=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BD=D1=83=D1=82=D1=8C=20=D1=81=D0=B5=D1=82?= =?UTF-8?q?=D0=B5=D0=B2=D1=8B=D0=B5=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Qwen-Coder --- templates/servers/detail.twig | 209 +++++++++++++++++++++++++++------- 1 file changed, 171 insertions(+), 38 deletions(-) diff --git a/templates/servers/detail.twig b/templates/servers/detail.twig index 12c71b3..3a5a712 100755 --- a/templates/servers/detail.twig +++ b/templates/servers/detail.twig @@ -123,7 +123,7 @@
{% for metricName, metricData in metrics %} - {% 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_") and metricName!="network_rx" and metricName!="network_tx" %} + {% 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_") and metricName!="network_rx" and metricName!="network_tx" and not (metricName starts with "temp_") %}
@@ -160,10 +160,36 @@
{% endif %}
- - {% set has_temps = false %} - {% for m in metrics %}{% if m starts with 'temp_' %}{% set has_temps = true %}{% endif %}{% endfor %} - {% if has_temps %} + + {% 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 %} + + + + +
@@ -171,12 +197,11 @@
Температуры
- +
- {% endif %}
@@ -552,7 +577,7 @@ var diskTotalGB = { // Графики метрик {% for metricName, metricData in metrics %} -{% 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_") and metricName!="network_rx" and metricName!="network_tx" %} +{% 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_") and metricName!="network_rx" and metricName!="network_tx" and not (metricName starts with "temp_") %} const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementById('chart-{{ metricName }}').getContext('2d'); // Подготовка данных для графика @@ -726,7 +751,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 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_") and metricName!="network_rx" and metricName!="network_tx" %} +{% 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_") and metricName!="network_rx" and metricName!="network_tx" and not (metricName starts with "temp_") %} (function() { var canvas = chart{{ metricName|replace({'-': '_', '.': '_'}) }}.canvas; var rect = canvas.getBoundingClientRect(); @@ -748,40 +773,61 @@ document.addEventListener('mousemove', function(e) { -// График температур -{% set temp_metrics = [] %} -{% for m in metrics %}{% if m starts with 'temp_' %}{% set temp_metrics = temp_metrics|merge([m]) %}{% endif %}{% endfor %} -{% if temp_metrics %} +// Графики сетевых интерфейсов (две линии: 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-temperatures'); + var ctx = document.getElementById('chart-net-{{ iface }}'); if (!ctx) return; var labels = []; - {% if metrics[temp_metrics[0]] is defined %} - {% for p in metrics[temp_metrics[0]]|slice(-100) %} - labels.push('{{ p.time_bucket|default(p.created_at)|date("d.m H:i") }}'); - {% endfor %} - {% endif %} + var inData = []; + var outData = []; - var datasets = []; - {% for m in temp_metrics %} - var data_{{ m }} = { - label: '{{ m|replace({'temp_': '', '_': ' '})|title }}', - data: [], - fill: false, - tension: 0.1, - pointRadius: 1, - borderWidth: 1 - }; - {% for p in metrics[m]|slice(-100) %} - data_{{ m }}.data.push({{ p.value }}); - {% endfor %} - datasets.push(data_{{ m }}); - {% endfor %} +{% 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: datasets }, + 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, @@ -789,13 +835,97 @@ document.addEventListener('mousemove', function(e) { 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' } } + zoom: { + zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: 'x' }, + pan: { enabled: true, mode: 'x' } + } }, - scales: { y: { beginAtZero: false, ticks: { callback: v => v + '°C' } } } + scales: { + y: { beginAtZero: true, ticks: { callback: function(v) { return v + '%'; } } } + } } }); })(); {% endif %} +{% endfor %} + + +// График температур — ВСЕ temp_* на одном графике +(function() { + var ctx = document.getElementById('chart-temperatures'); + if (!ctx) return; + + var tempColors = [ + { border: 'rgba(255, 99, 132, 1)', bg: 'rgba(255, 99, 132, 0.1)' }, + { border: 'rgba(75, 192, 192, 1)', bg: 'rgba(75, 192, 192, 0.1)' }, + { border: 'rgba(255, 159, 64, 1)', bg: 'rgba(255, 159, 64, 0.1)' }, + { border: 'rgba(255, 205, 86, 1)', bg: 'rgba(255, 205, 86, 0.1)' }, + { border: 'rgba(201, 203, 207, 1)', bg: 'rgba(201, 203, 207, 0.1)' }, + { border: 'rgba(54, 162, 235, 1)', bg: 'rgba(54, 162, 235, 0.1)' }, + { border: 'rgba(153, 102, 255, 1)', bg: 'rgba(153, 102, 255, 0.1)' }, + { border: 'rgba(255, 99, 255, 1)', bg: 'rgba(255, 99, 255, 0.1)' }, + { border: 'rgba(100, 200, 100, 1)', bg: 'rgba(100, 200, 100, 0.1)' }, + { border: 'rgba(200, 150, 50, 1)', bg: 'rgba(200, 150, 50, 0.1)' } + ]; + + var labels = []; + var labelsFilled = false; + var datasets = []; + var colorIdx = 0; + + {% for mn, md in metrics %} + {% if mn starts with 'temp_' %} + if (!labelsFilled) { + {% for p in md|slice(-1000) %} + labels.push('{{ p.time_bucket|default(p.created_at)|date("d.m H:i") }}'); + {% endfor %} + labelsFilled = true; + } + + (function() { + var data = []; + {% for p in md|slice(-1000) %} + data.push({{ p.value }}); + {% endfor %} + + var color = tempColors[colorIdx % tempColors.length]; + colorIdx++; + + datasets.push({ + label: '{{ mn|replace({'temp_': '', '_': ' '})|title }}', + data: data, + borderColor: color.border, + backgroundColor: color.bg, + fill: false, + tension: 0.1, + pointRadius: 1, + borderWidth: 2 + }); + })(); + {% endif %} + {% endfor %} + + var chart = new Chart(ctx.getContext('2d'), { + type: 'line', + data: { labels: labels, datasets: datasets }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'index', intersect: false }, + plugins: { + legend: { display: true, position: 'top', labels: { boxWidth: 12, font: { size: 11 } } }, + 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: false, ticks: { callback: function(v) { return v + '°C'; } } } } + } + }); + + window.chartTemperatures = chart; +})(); // Doughnut графики для разделов дисков {% for metricName, metricData in metrics %} @@ -842,12 +972,15 @@ document.addEventListener('mousemove', function(e) { // Сбросить зум на всех графиках function resetAllZoom() { {% for metricName, metricData in metrics %} -{% 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_") and metricName!="network_rx" and metricName!="network_tx" %} +{% 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_") and metricName!="network_rx" and metricName!="network_tx" and not (metricName starts with "temp_") %} if (typeof chart{{ metricName|replace({'-': '_', '.': '_'}) }} !== 'undefined') { chart{{ metricName|replace({'-': '_', '.': '_'}) }}.resetZoom(); } {% endif %} {% endfor %} + if (typeof window.chartTemperatures !== 'undefined') { + window.chartTemperatures.resetZoom(); + } }