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

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
+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() {