네트워크 이력 차트 원본 선 표시
This commit is contained in:
+199
-13
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user