Improve metrics view summaries and RAM details

This commit is contained in:
mirivlad 2026-04-26 20:34:30 +08:00
parent a809b55b86
commit 3edf138e43
2 changed files with 211 additions and 15 deletions

View File

@ -317,10 +317,11 @@ class ServerDetailController extends Model
$stmt->execute([':id' => $id]);
$latestUptime = $stmt->fetch();
$simpleMetricCharts = $this->buildSimpleMetricCharts($groupedMetrics, $displayMetrics);
$simpleMetricCharts = $this->buildSimpleMetricCharts($groupedMetrics, $displayMetrics, $existingThresholds);
$networkCharts = $this->buildNetworkCharts($groupedMetrics, $displayMetrics);
$temperatureChart = $this->buildTemperatureChart($groupedMetrics, $displayMetrics);
$diskCharts = $this->buildDiskCharts($groupedMetrics, $displayMetrics);
$summaryCards = $this->buildSummaryCards($groupedMetrics, $displayMetrics, $temperatureChart, $diskCharts, $networkCharts);
$templateData = [
'title' => 'Сервер: ' . $server['name'],
@ -331,6 +332,7 @@ class ServerDetailController extends Model
'networkCharts' => $networkCharts,
'temperatureChart' => $temperatureChart,
'diskCharts' => $diskCharts,
'summaryCards' => $summaryCards,
'allMetricTypes' => $allMetricTypes,
'existingThresholds' => $existingThresholds,
'allServices' => $allServices,
@ -399,6 +401,10 @@ class ServerDetailController extends Model
$expanded[$metricName] = $metricName;
if ($metricName === 'ram_used') {
$expanded['ram_total_gb'] = 'ram_total_gb';
}
if (str_starts_with($metricName, 'disk_used_')) {
$suffix = substr($metricName, strlen('disk_used_'));
$expanded['disk_total_gb_' . $suffix] = 'disk_total_gb_' . $suffix;
@ -408,7 +414,7 @@ class ServerDetailController extends Model
return array_values($expanded);
}
private function buildSimpleMetricCharts(array $groupedMetrics, ?array $displayMetrics): array
private function buildSimpleMetricCharts(array $groupedMetrics, ?array $displayMetrics, array $existingThresholds): array
{
$charts = [];
$config = [
@ -431,9 +437,13 @@ class ServerDetailController extends Model
'values' => $this->extractValues($groupedMetrics[$metricName]),
'lastValue' => round((float)($groupedMetrics[$metricName][0]['value'] ?? 0), 2),
'lastTime' => $this->formatPointTime($groupedMetrics[$metricName][0] ?? []),
'thresholds' => $existingThresholds[$metricName] ?? null,
'details' => $metricName === 'ram_used' ? $this->buildRamDetails($groupedMetrics) : null,
];
}
usort($charts, fn ($a, $b) => strnatcasecmp($a['title'], $b['title']));
return $charts;
}
@ -490,6 +500,8 @@ class ServerDetailController extends Model
];
}
usort($charts, fn ($a, $b) => strnatcasecmp($a['id'], $b['id']));
return $charts;
}
@ -500,6 +512,8 @@ class ServerDetailController extends Model
$colors = ['#dc3545', '#fd7e14', '#0dcaf0', '#6f42c1', '#20c997', '#ffc107', '#6610f2', '#198754'];
$colorIndex = 0;
$tempSeries = [];
foreach ($groupedMetrics as $metricName => $points) {
if (!str_starts_with($metricName, 'temp_') || !$this->isMetricSelected($metricName, $displayMetrics) || empty($points)) {
continue;
@ -509,11 +523,22 @@ class ServerDetailController extends Model
$labels = $this->extractLabels($points);
}
$datasets[] = [
$tempSeries[] = [
'label' => $this->formatMetricLabel($metricName),
'color' => $colors[$colorIndex % count($colors)],
'metricName' => $metricName,
'values' => $this->extractValues($points),
];
}
usort($tempSeries, fn ($a, $b) => strnatcasecmp($a['label'], $b['label']));
foreach ($tempSeries as $series) {
$datasets[] = [
'label' => $series['label'],
'metricName' => $series['metricName'],
'color' => $colors[$colorIndex % count($colors)],
'values' => $series['values'],
];
$colorIndex++;
}
@ -557,9 +582,115 @@ class ServerDetailController extends Model
];
}
usort($charts, fn ($a, $b) => strnatcasecmp($a['title'], $b['title']));
return $charts;
}
private function buildSummaryCards(array $groupedMetrics, ?array $displayMetrics, array $temperatureChart, array $diskCharts, array $networkCharts): array
{
$cards = [];
if ($this->isMetricSelected('cpu_load', $displayMetrics) && isset($groupedMetrics['cpu_load'][0]['value'])) {
$cards[] = [
'title' => 'CPU сейчас',
'value' => round((float)$groupedMetrics['cpu_load'][0]['value'], 2) . '%',
'subtitle' => $this->formatPointTime($groupedMetrics['cpu_load'][0]),
];
}
if ($this->isMetricSelected('ram_used', $displayMetrics) && isset($groupedMetrics['ram_used'][0]['value'])) {
$ramDetails = $this->buildRamDetails($groupedMetrics);
$cards[] = [
'title' => 'RAM сейчас',
'value' => round((float)$groupedMetrics['ram_used'][0]['value'], 2) . '%',
'subtitle' => $ramDetails
? sprintf('Всего: %.1f ГБ | Занято: %.1f ГБ | Свободно: %.1f ГБ', $ramDetails['totalGb'], $ramDetails['usedGb'], $ramDetails['freeGb'])
: $this->formatPointTime($groupedMetrics['ram_used'][0]),
];
}
if (!empty($temperatureChart['datasets'])) {
$hottest = null;
foreach ($temperatureChart['datasets'] as $dataset) {
$current = $dataset['values'][count($dataset['values']) - 1] ?? null;
if ($current === null) {
continue;
}
if ($hottest === null || $current > $hottest['value']) {
$hottest = ['label' => $dataset['label'], 'value' => $current];
}
}
if ($hottest) {
$cards[] = [
'title' => 'Самый горячий датчик',
'value' => $hottest['value'] . '°C',
'subtitle' => $hottest['label'],
];
}
}
if (!empty($diskCharts)) {
$topDisk = $diskCharts[0];
foreach ($diskCharts as $disk) {
if ($disk['percent'] > $topDisk['percent']) {
$topDisk = $disk;
}
}
$cards[] = [
'title' => 'Самый занятый диск',
'value' => $topDisk['percent'] . '%',
'subtitle' => $topDisk['title'],
];
}
if (!empty($networkCharts)) {
$topNetwork = null;
foreach ($networkCharts as $chart) {
$peak = 0.0;
foreach ($chart['datasets'] as $dataset) {
foreach ($dataset['values'] as $value) {
$peak = max($peak, (float)$value);
}
}
if ($topNetwork === null || $peak > $topNetwork['value']) {
$topNetwork = ['label' => $chart['title'], 'value' => round($peak, 2), 'unit' => $chart['unit'] ?? ''];
}
}
if ($topNetwork) {
$cards[] = [
'title' => 'Самый активный интерфейс',
'value' => $topNetwork['value'] . $topNetwork['unit'],
'subtitle' => $topNetwork['label'],
];
}
}
return $cards;
}
private function buildRamDetails(array $groupedMetrics): ?array
{
if (!isset($groupedMetrics['ram_used'][0]['value'], $groupedMetrics['ram_total_gb'][0]['value'])) {
return null;
}
$percentUsed = (float)$groupedMetrics['ram_used'][0]['value'];
$totalGb = (float)$groupedMetrics['ram_total_gb'][0]['value'];
if ($totalGb <= 0) {
return null;
}
$usedGb = round(($percentUsed / 100) * $totalGb, 1);
$freeGb = round($totalGb - $usedGb, 1);
return [
'totalGb' => round($totalGb, 1),
'usedGb' => $usedGb,
'freeGb' => $freeGb,
];
}
private function isMetricSelected(string $metricName, ?array $displayMetrics): bool
{
return is_array($displayMetrics) && in_array($metricName, $displayMetrics, true);

View File

@ -122,6 +122,22 @@
</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>
@ -140,6 +156,14 @@
<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>
@ -490,6 +514,16 @@ function renderProcessTooltip(context, chartMeta) {
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) {
@ -611,25 +645,56 @@ document.addEventListener('DOMContentLoaded', function() {
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,
[{
label: chart.title,
data: chart.values,
borderColor: chart.color,
backgroundColor: chart.color + '22',
fill: true,
tension: 0.2,
pointRadius: 0,
borderWidth: 2
}],
datasets,
chart.unit,
{
id: chart.id,
labels: chart.labels,
timestamps: chart.timestamps || [],
unit: chart.unit
unit: chart.unit,
details: chart.details || null
}
);
});