네트워크 이력 차트 원본 선 표시

This commit is contained in:
seo
2026-07-25 22:32:54 +09:00
parent 30c0cb4663
commit 9cd5160f03
4 changed files with 230 additions and 24 deletions
+25 -5
View File
@@ -1022,13 +1022,26 @@ function primary_network_interface(array $network): ?array
return $rows[0] ?? null;
}
function add_network_usage_log(array $network): void
function network_log_interfaces(array $network): array
{
$iface = primary_network_interface($network);
if (!$iface) {
return;
$targets = [];
$primary = primary_network_interface($network);
if ($primary) {
$targets[(string)($primary['name'] ?? '')] = $primary;
}
foreach ((array)($network['interfaces'] ?? []) as $row) {
if (($row['name'] ?? '') === 'br0') {
$targets['br0'] = $row;
}
}
unset($targets['']);
return array_values($targets);
}
function add_network_usage_log_for_interface(array $iface): void
{
$name = (string)($iface['name'] ?? '');
$rxBytes = isset($iface['rx_bytes']) ? (int)$iface['rx_bytes'] : -1;
$txBytes = isset($iface['tx_bytes']) ? (int)$iface['tx_bytes'] : -1;
@@ -1119,6 +1132,13 @@ function add_network_usage_log(array $network): void
]);
}
function add_network_usage_log(array $network): void
{
foreach (network_log_interfaces($network) as $iface) {
add_network_usage_log_for_interface($iface);
}
}
function network_usage_history(int $limit = 240): array
{
$limit = max(1, min(1500, $limit));
@@ -4530,7 +4550,7 @@ function collect_snapshot(bool $applyFan = true): array
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes);
$network = network_info();
add_network_usage_log($network);
$networkHistory = network_usage_history(240);
$networkHistory = network_usage_history(480);
$shouldLogNotice = !empty($fanSpike['alert_started'])
|| (!empty($fanSpike['active']) && (bool)setting_value('notice.log_during_alert'));
+199 -13
View File
@@ -161,10 +161,14 @@
rp1Temp: 'RP1 Temp',
cpuWatt: 'CPU Watt',
networkHistory: 'Network History',
downloadPerSecond: 'Download / s',
uploadPerSecond: 'Upload / s',
rxPacketsPerSecond: 'RX Packets / s',
txPacketsPerSecond: 'TX Packets / s',
wanDownloadPackets: 'WAN Download / RX Packets',
wanUploadPackets: 'WAN Upload / TX Packets',
bridgeDownloadPackets: 'Bridge Download / RX Packets',
bridgeUploadPackets: 'Bridge Upload / TX Packets',
downloadLine: 'Download',
uploadLine: 'Upload',
rxPacketsLine: 'RX Packets',
txPacketsLine: 'TX Packets',
remaining: 'Remaining',
batteryVoltage: 'Battery Voltage',
wifiClients: 'DHCP Clients',
@@ -361,10 +365,14 @@
rp1Temp: 'RP1 온도',
cpuWatt: 'CPU 전력',
networkHistory: '네트워크 이력',
downloadPerSecond: '초당 다운로드 속도',
uploadPerSecond: '초당 업로드 속도',
rxPacketsPerSecond: '초당 RX 패킷',
txPacketsPerSecond: '초당 TX 패킷',
wanDownloadPackets: 'WAN 다운로드 / RX 패킷',
wanUploadPackets: 'WAN 업로드 / TX 패킷',
bridgeDownloadPackets: '브릿지 다운로드 / RX 패킷',
bridgeUploadPackets: '브릿지 업로드 / TX 패킷',
downloadLine: '다운로드',
uploadLine: '업로드',
rxPacketsLine: 'RX 패킷',
txPacketsLine: 'TX 패킷',
remaining: '잔여 시간',
batteryVoltage: '배터리 전압',
wifiClients: 'DHCP 클라이언트',
@@ -2439,6 +2447,181 @@
c.update('none');
}
function timeAxis(rows) {
const labels = rows.map(row => String(row.time || '').slice(11, 19));
const timestamps = rows.map(row => {
const time = String(row.time || '').replace(' ', 'T');
const parsed = Date.parse(time);
return Number.isFinite(parsed) ? parsed : null;
});
const latestTimestamp = [...timestamps].reverse().find(value => value !== null) ?? null;
const tickAgeSeconds = index => {
if (latestTimestamp === null || timestamps[index] === null) return null;
return Math.max(0, Math.round((latestTimestamp - timestamps[index]) / 1000));
};
const xTickMarkers = new Map();
[60, 120, 180].forEach(targetAge => {
let bestIndex = -1;
let bestDistance = Number.POSITIVE_INFINITY;
timestamps.forEach((_timestamp, index) => {
const age = tickAgeSeconds(index);
if (age === null) return;
const distance = Math.abs(age - targetAge);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = index;
}
});
if (bestIndex >= 0 && bestDistance <= 15) {
xTickMarkers.set(bestIndex, t('minuteAgo', { minutes: targetAge / 60 }));
}
});
const xTickCallback = (_value, index) => xTickMarkers.get(index) || '';
const xGridColor = context => {
const index = Number(context.index ?? context.tick?.value ?? -1);
return xTickMarkers.has(index) ? 'rgba(203,213,225,.18)' : 'rgba(203,213,225,0)';
};
return { labels, xTickCallback, xGridColor };
}
function formatChartNumber(value, suffix = '') {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return '-';
return `${numeric.toLocaleString(undefined, { maximumFractionDigits: 2 })}${suffix}`;
}
function networkRowsForIface(rows, iface) {
return rows.filter(row => String(row.iface || '') === iface).slice(-180);
}
function networkChart(canvasId, rows, speedKey, speedLabel, packetKey, packetLabel, speedColor = '#0ea5e9') {
const canvas = $('#' + canvasId);
if (!canvas || typeof Chart === 'undefined') return;
const { labels, xTickCallback, xGridColor } = timeAxis(rows);
const speedValues = rows.map(row => {
const value = Number(row[speedKey]);
return Number.isFinite(value) ? value : null;
});
const packetValues = rows.map(row => {
const value = Number(row[packetKey]);
return Number.isFinite(value) ? value : null;
});
const speedScale = dynamicScaleForValues(speedValues, { minSpan: 1, padding: 0.3 });
const packetScale = dynamicScaleForValues(packetValues, { minSpan: 10, padding: 5 });
const datasets = [
{
label: speedLabel,
data: speedValues,
yAxisID: 'speed',
borderColor: speedColor,
backgroundColor: 'transparent',
borderWidth: 2.2,
pointRadius: 0,
tension: 0.22,
fill: false,
},
{
label: packetLabel,
data: packetValues,
yAxisID: 'packets',
borderColor: '#f97316',
backgroundColor: 'transparent',
borderWidth: 2,
borderDash: [5, 4],
pointRadius: 0,
tension: 0.22,
fill: false,
},
];
const options = {
responsive: true,
maintainAspectRatio: false,
animation: false,
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
legend: {
display: true,
position: 'bottom',
labels: {
boxWidth: 18,
boxHeight: 2,
color: '#8d98aa',
padding: 10,
},
},
tooltip: {
displayColors: true,
padding: 8,
callbacks: {
label: item => {
const suffix = item.dataset.yAxisID === 'speed' ? ' Mbit/s' : ' pps';
return `${item.dataset.label}: ${formatChartNumber(item.parsed.y, suffix)}`;
},
},
},
},
scales: {
x: {
ticks: {
color: '#64748b',
maxRotation: 0,
autoSkip: false,
callback: xTickCallback,
},
grid: {
color: xGridColor,
drawTicks: false,
},
border: { display: false },
},
speed: {
min: speedScale.min,
max: speedScale.max,
position: 'left',
ticks: {
color: speedColor,
maxTicksLimit: 4,
callback: value => formatChartNumber(value, ' Mbit/s'),
},
grid: { color: 'rgba(255,255,255,.045)', drawTicks: false },
border: { display: false },
},
packets: {
min: packetScale.min,
max: packetScale.max,
position: 'right',
ticks: {
color: '#f97316',
maxTicksLimit: 4,
callback: value => formatChartNumber(value, ' pps'),
},
grid: { drawOnChartArea: false, drawTicks: false },
border: { display: false },
},
},
};
if (!state.charts[canvasId]) {
state.charts[canvasId] = new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options,
});
return;
}
const c = state.charts[canvasId];
c.data.labels = labels;
c.data.datasets = datasets;
c.options = options;
c.update('none');
}
function dynamicScaleForValues(values, options = {}) {
const floor = Number(options.floor ?? 0);
const ceiling = Number.isFinite(Number(options.ceiling)) ? Number(options.ceiling) : null;
@@ -2495,11 +2678,14 @@
chart('batteryVoltageChart', 'BATTERYV', rows, 'battery_voltage', '#14b8a6', 'V', dynamicScale(rows, 'battery_voltage', { minSpan: 0.2, padding: 0.06 }));
}
const networkRows = (data.network_history || []).slice(-180);
chart('netRxChart', 'NETRX', networkRows, 'rx_mbps', '#0ea5e9', ' Mbit/s', dynamicScale(networkRows, 'rx_mbps', { minSpan: 1, padding: 0.3 }));
chart('netTxChart', 'NETTX', networkRows, 'tx_mbps', '#22c55e', ' Mbit/s', dynamicScale(networkRows, 'tx_mbps', { minSpan: 1, padding: 0.3 }));
chart('netRxPacketsChart', 'NETRXPKT', networkRows, 'rx_pps', '#f97316', ' pps', dynamicScale(networkRows, 'rx_pps', { minSpan: 10, padding: 5 }));
chart('netTxPacketsChart', 'NETTXPKT', networkRows, 'tx_pps', '#a855f7', ' pps', dynamicScale(networkRows, 'tx_pps', { minSpan: 10, padding: 5 }));
const networkRows = data.network_history || [];
const wanIface = String(data.network?.default_interface || 'eth0');
const wanRows = networkRowsForIface(networkRows, wanIface);
const bridgeRows = networkRowsForIface(networkRows, 'br0');
networkChart('netWanRxChart', wanRows, 'rx_mbps', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9');
networkChart('netWanTxChart', wanRows, 'tx_mbps', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e');
networkChart('netBridgeRxChart', bridgeRows, 'rx_mbps', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9');
networkChart('netBridgeTxChart', bridgeRows, 'tx_mbps', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e');
}
function resizeChartsSoon() {
+4 -4
View File
@@ -287,10 +287,10 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
<div class="card">
<h2 data-i18n="networkHistory">Network History</h2>
<div class="chart-grid">
<div class="chart-box"><h3 data-i18n="downloadPerSecond">Download / s</h3><div class="chart-canvas"><canvas id="netRxChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="uploadPerSecond">Upload / s</h3><div class="chart-canvas"><canvas id="netTxChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="rxPacketsPerSecond">RX Packets / s</h3><div class="chart-canvas"><canvas id="netRxPacketsChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="txPacketsPerSecond">TX Packets / s</h3><div class="chart-canvas"><canvas id="netTxPacketsChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="wanDownloadPackets">WAN Download / RX Packets</h3><div class="chart-canvas"><canvas id="netWanRxChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="wanUploadPackets">WAN Upload / TX Packets</h3><div class="chart-canvas"><canvas id="netWanTxChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="bridgeDownloadPackets">Bridge Download / RX Packets</h3><div class="chart-canvas"><canvas id="netBridgeRxChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="bridgeUploadPackets">Bridge Upload / TX Packets</h3><div class="chart-canvas"><canvas id="netBridgeTxChart"></canvas></div></div>
</div>
</div>