From 7f9a5d1a8106d123104c4a8ed6c0d21750b48823 Mon Sep 17 00:00:00 2001 From: seo Date: Sat, 25 Jul 2026 23:15:18 +0900 Subject: [PATCH] =?UTF-8?q?=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=93=9C=EB=A1=AD=20=EC=9D=B4=EB=A0=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- config/config.php | 16 ++++++ public/api.php | 52 +++++++++++++++++- public/assets/app.js | 124 +++++++++++++++++++++++++++++++++++++++++++ public/index.php | 8 +++ 5 files changed, 201 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dda1181..ec77103 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지 파라미터 조정 - 설정 모달에서 라즈베리파이 정품 7인치 DSI 터치 디스플레이 상태 확인, 밝기/백라이트 즉시 적용, 콘솔 회전, 터치 좌표 보정, LCD/터치 비활성화 부팅 설정 조정 - CPU 온도, RP1 온도, CPU 전력, 팬 RPM 센서 이력 차트 -- DHCP client 목록 아래에서 WAN, LAN, 2.4G WiFi, 5G WiFi 기준 다운로드/업로드 속도와 RX/TX 패킷 이력 차트 표시 +- DHCP client 목록 아래에서 WAN, LAN, 2.4G WiFi, 5G WiFi 기준 다운로드/업로드 속도, RX/TX 패킷, 오류/드롭 이력 차트 표시 - UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음 - 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시 - WiFi/LAN client 목록과 2.4G/5G/LAN client 수 표시 @@ -76,7 +76,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - `control_state`: 팬 모드와 PWM 상태 - `presence_fan_policy_state`: 외출/귀가 기반 팬 정책의 마지막 확인/적용 상태 - `sensor_logs`: 온도, RPM, PWM, load, memory, disk, uptime. 과거 배터리 컬럼은 호환을 위해 유지하지만 UPS 제거 상태에서는 새 배터리값을 기록하지 않습니다. -- `network_usage_logs`: WAN, LAN, 2.4G WiFi, 5G WiFi 기준 누적 byte/packet 카운터와 초당 다운로드/업로드 속도, RX/TX 패킷 이력 +- `network_usage_logs`: WAN, LAN, 2.4G WiFi, 5G WiFi 기준 누적 byte/packet/error/drop 카운터와 초당 다운로드/업로드 속도, RX/TX 패킷, 오류/드롭 이력 - `power_event_states`: 저전압/스로틀링 episode 상태, 최근 지속시간, 최근 이벤트 이력 - `battery_profile_cache`: 배터리 잔여시간 계산용 장기 프로파일 캐시 - `systemd_service_cache`: 사용자 생성 systemd 서비스 목록과 최근 journal 로그의 짧은 TTL 캐시 diff --git a/config/config.php b/config/config.php index fc1604b..a084fc7 100644 --- a/config/config.php +++ b/config/config.php @@ -599,11 +599,19 @@ function bootstrap_db(): void tx_bytes BIGINT UNSIGNED NOT NULL, rx_packets BIGINT UNSIGNED NOT NULL, tx_packets BIGINT UNSIGNED NOT NULL, + rx_errors BIGINT UNSIGNED NOT NULL DEFAULT 0, + tx_errors BIGINT UNSIGNED NOT NULL DEFAULT 0, + rx_dropped BIGINT UNSIGNED NOT NULL DEFAULT 0, + tx_dropped BIGINT UNSIGNED NOT NULL DEFAULT 0, rx_mbps DECIMAL(12,4) NULL, tx_mbps DECIMAL(12,4) NULL, rx_pps DECIMAL(12,2) NULL, tx_pps DECIMAL(12,2) NULL, + rx_errors_per_sec DECIMAL(12,2) NULL, + tx_errors_per_sec DECIMAL(12,2) NULL, + rx_dropped_per_sec DECIMAL(12,2) NULL, + tx_dropped_per_sec DECIMAL(12,2) NULL, create_ip VARCHAR(64) NULL, @@ -688,6 +696,14 @@ function bootstrap_db(): void "ALTER TABLE system_notice_state ADD COLUMN active_temp_delta DECIMAL(8,2) NULL AFTER active_reason", "ALTER TABLE system_notice_state ADD COLUMN active_rpm_delta DECIMAL(12,2) NULL AFTER active_temp_delta", "ALTER TABLE system_notice_state ADD COLUMN process_signature VARCHAR(255) NULL AFTER active_rpm_delta", + "ALTER TABLE network_usage_logs ADD COLUMN rx_errors BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER tx_packets", + "ALTER TABLE network_usage_logs ADD COLUMN tx_errors BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER rx_errors", + "ALTER TABLE network_usage_logs ADD COLUMN rx_dropped BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER tx_errors", + "ALTER TABLE network_usage_logs ADD COLUMN tx_dropped BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER rx_dropped", + "ALTER TABLE network_usage_logs ADD COLUMN rx_errors_per_sec DECIMAL(12,2) NULL AFTER tx_pps", + "ALTER TABLE network_usage_logs ADD COLUMN tx_errors_per_sec DECIMAL(12,2) NULL AFTER rx_errors_per_sec", + "ALTER TABLE network_usage_logs ADD COLUMN rx_dropped_per_sec DECIMAL(12,2) NULL AFTER tx_errors_per_sec", + "ALTER TABLE network_usage_logs ADD COLUMN tx_dropped_per_sec DECIMAL(12,2) NULL AFTER rx_dropped_per_sec", "ALTER TABLE sensor_logs DROP COLUMN disk_total_gb", "ALTER TABLE sensor_logs DROP COLUMN disk_used_gb", "ALTER TABLE sensor_logs DROP COLUMN disk_free_gb", diff --git a/public/api.php b/public/api.php index fa42ea9..97db0da 100644 --- a/public/api.php +++ b/public/api.php @@ -1048,6 +1048,10 @@ function add_network_usage_log_for_interface(array $iface): void $txBytes = isset($iface['tx_bytes']) ? (int)$iface['tx_bytes'] : -1; $rxPackets = isset($iface['rx_packets']) ? (int)$iface['rx_packets'] : -1; $txPackets = isset($iface['tx_packets']) ? (int)$iface['tx_packets'] : -1; + $rxErrors = isset($iface['rx_errors']) ? (int)$iface['rx_errors'] : 0; + $txErrors = isset($iface['tx_errors']) ? (int)$iface['tx_errors'] : 0; + $rxDropped = isset($iface['rx_dropped']) ? (int)$iface['rx_dropped'] : 0; + $txDropped = isset($iface['tx_dropped']) ? (int)$iface['tx_dropped'] : 0; if ($name === '' || $rxBytes < 0 || $txBytes < 0 || $rxPackets < 0 || $txPackets < 0) { return; } @@ -1060,6 +1064,10 @@ function add_network_usage_log_for_interface(array $iface): void tx_bytes, rx_packets, tx_packets, + rx_errors, + tx_errors, + rx_dropped, + tx_dropped, TIMESTAMPDIFF(MICROSECOND, recorded_at, NOW(3)) AS elapsed_us FROM network_usage_logs WHERE iface = :iface @@ -1073,6 +1081,10 @@ function add_network_usage_log_for_interface(array $iface): void $txMbps = null; $rxPps = null; $txPps = null; + $rxErrorsPerSec = null; + $txErrorsPerSec = null; + $rxDroppedPerSec = null; + $txDroppedPerSec = null; if ($previous) { $seconds = max(0.0, (float)($previous['elapsed_us'] ?? 0) / 1000000); if ($seconds < 0.8) { @@ -1083,12 +1095,22 @@ function add_network_usage_log_for_interface(array $iface): void $txDelta = $txBytes - (int)$previous['tx_bytes']; $rxPacketDelta = $rxPackets - (int)$previous['rx_packets']; $txPacketDelta = $txPackets - (int)$previous['tx_packets']; + $rxErrorDelta = $rxErrors - (int)$previous['rx_errors']; + $txErrorDelta = $txErrors - (int)$previous['tx_errors']; + $rxDroppedDelta = $rxDropped - (int)$previous['rx_dropped']; + $txDroppedDelta = $txDropped - (int)$previous['tx_dropped']; if ($seconds > 0 && $seconds <= 60 && $rxDelta >= 0 && $txDelta >= 0 && $rxPacketDelta >= 0 && $txPacketDelta >= 0) { $rxMbps = round(($rxDelta * 8) / $seconds / 1000000, 4); $txMbps = round(($txDelta * 8) / $seconds / 1000000, 4); $rxPps = round($rxPacketDelta / $seconds, 2); $txPps = round($txPacketDelta / $seconds, 2); } + if ($seconds > 0 && $seconds <= 60 && $rxErrorDelta >= 0 && $txErrorDelta >= 0 && $rxDroppedDelta >= 0 && $txDroppedDelta >= 0) { + $rxErrorsPerSec = round($rxErrorDelta / $seconds, 2); + $txErrorsPerSec = round($txErrorDelta / $seconds, 2); + $rxDroppedPerSec = round($rxDroppedDelta / $seconds, 2); + $txDroppedPerSec = round($txDroppedDelta / $seconds, 2); + } } $stmt = $pdo->prepare(" @@ -1099,10 +1121,18 @@ function add_network_usage_log_for_interface(array $iface): void tx_bytes, rx_packets, tx_packets, + rx_errors, + tx_errors, + rx_dropped, + tx_dropped, rx_mbps, tx_mbps, rx_pps, tx_pps, + rx_errors_per_sec, + tx_errors_per_sec, + rx_dropped_per_sec, + tx_dropped_per_sec, create_ip ) VALUES @@ -1112,10 +1142,18 @@ function add_network_usage_log_for_interface(array $iface): void :tx_bytes, :rx_packets, :tx_packets, + :rx_errors, + :tx_errors, + :rx_dropped, + :tx_dropped, :rx_mbps, :tx_mbps, :rx_pps, :tx_pps, + :rx_errors_per_sec, + :tx_errors_per_sec, + :rx_dropped_per_sec, + :tx_dropped_per_sec, :create_ip ) "); @@ -1125,10 +1163,18 @@ function add_network_usage_log_for_interface(array $iface): void ':tx_bytes' => $txBytes, ':rx_packets' => $rxPackets, ':tx_packets' => $txPackets, + ':rx_errors' => $rxErrors, + ':tx_errors' => $txErrors, + ':rx_dropped' => $rxDropped, + ':tx_dropped' => $txDropped, ':rx_mbps' => $rxMbps, ':tx_mbps' => $txMbps, ':rx_pps' => $rxPps, ':tx_pps' => $txPps, + ':rx_errors_per_sec' => $rxErrorsPerSec, + ':tx_errors_per_sec' => $txErrorsPerSec, + ':rx_dropped_per_sec' => $rxDroppedPerSec, + ':tx_dropped_per_sec' => $txDroppedPerSec, ':create_ip' => $_SERVER['REMOTE_ADDR'] ?? 'cli', ]); } @@ -1152,7 +1198,11 @@ function network_usage_history(int $limit = 240): array ROUND(rx_mbps / 8, 4) AS rx_mbytes, ROUND(tx_mbps / 8, 4) AS tx_mbytes, rx_pps, - tx_pps + tx_pps, + rx_errors_per_sec, + tx_errors_per_sec, + rx_dropped_per_sec, + tx_dropped_per_sec FROM network_usage_logs WHERE rx_mbps IS NOT NULL AND tx_mbps IS NOT NULL diff --git a/public/assets/app.js b/public/assets/app.js index ab1ae8b..d7b3aa3 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -167,10 +167,14 @@ wifi5History: '5G WiFi History', downloadRxPackets: 'Download / RX Packets', uploadTxPackets: 'Upload / TX Packets', + rxErrorsDrops: 'RX Errors / Drops', + txErrorsDrops: 'TX Errors / Drops', downloadLine: 'Download', uploadLine: 'Upload', rxPacketsLine: 'RX Packets', txPacketsLine: 'TX Packets', + errorsLine: 'Errors', + dropsLine: 'Drops', remaining: 'Remaining', batteryVoltage: 'Battery Voltage', wifiClients: 'DHCP Clients', @@ -373,10 +377,14 @@ wifi5History: '5G WiFi 이력', downloadRxPackets: '다운로드 / RX 패킷', uploadTxPackets: '업로드 / TX 패킷', + rxErrorsDrops: 'RX 오류 / 드롭', + txErrorsDrops: 'TX 오류 / 드롭', downloadLine: '다운로드', uploadLine: '업로드', rxPacketsLine: 'RX 패킷', txPacketsLine: 'TX 패킷', + errorsLine: '오류', + dropsLine: '드롭', remaining: '잔여 시간', batteryVoltage: '배터리 전압', wifiClients: 'DHCP 클라이언트', @@ -2626,6 +2634,114 @@ c.update('none'); } + function networkQualityChart(canvasId, rows, errorKey, dropKey) { + const canvas = $('#' + canvasId); + if (!canvas || typeof Chart === 'undefined') return; + + const { labels, xTickCallback, xGridColor } = timeAxis(rows); + const errorValues = rows.map(row => { + const value = Number(row[errorKey]); + return Number.isFinite(value) ? value : null; + }); + const dropValues = rows.map(row => { + const value = Number(row[dropKey]); + return Number.isFinite(value) ? value : null; + }); + const scale = dynamicScaleForValues([...errorValues, ...dropValues], { minSpan: 1, padding: 0.2 }); + const datasets = [ + { + label: t('errorsLine'), + data: errorValues, + borderColor: '#ef4444', + backgroundColor: 'transparent', + borderWidth: 2.1, + pointRadius: 0, + tension: 0.2, + fill: false, + }, + { + label: t('dropsLine'), + data: dropValues, + borderColor: '#f59e0b', + backgroundColor: 'transparent', + borderWidth: 2, + borderDash: [5, 4], + pointRadius: 0, + tension: 0.2, + 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 => `${item.dataset.label}: ${formatChartNumber(item.parsed.y, '/s')}`, + }, + }, + }, + scales: { + x: { + ticks: { + color: '#64748b', + maxRotation: 0, + autoSkip: false, + callback: xTickCallback, + }, + grid: { + color: xGridColor, + drawTicks: false, + }, + border: { display: false }, + }, + y: { + min: scale.min, + max: scale.max, + ticks: { + color: '#8d98aa', + maxTicksLimit: 4, + callback: value => formatChartNumber(value, '/s'), + }, + grid: { color: 'rgba(255,255,255,.045)', 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; @@ -2690,12 +2806,20 @@ const wifi5Rows = networkRowsForIface(networkRows, 'wlan1'); networkChart('netWanRxChart', wanRows, 'rx_mbytes', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9'); networkChart('netWanTxChart', wanRows, 'tx_mbytes', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e'); + networkQualityChart('netWanRxQualityChart', wanRows, 'rx_errors_per_sec', 'rx_dropped_per_sec'); + networkQualityChart('netWanTxQualityChart', wanRows, 'tx_errors_per_sec', 'tx_dropped_per_sec'); networkChart('netLanRxChart', lanRows, 'rx_mbytes', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9'); networkChart('netLanTxChart', lanRows, 'tx_mbytes', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e'); + networkQualityChart('netLanRxQualityChart', lanRows, 'rx_errors_per_sec', 'rx_dropped_per_sec'); + networkQualityChart('netLanTxQualityChart', lanRows, 'tx_errors_per_sec', 'tx_dropped_per_sec'); networkChart('netWifi24RxChart', wifi24Rows, 'rx_mbytes', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9'); networkChart('netWifi24TxChart', wifi24Rows, 'tx_mbytes', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e'); + networkQualityChart('netWifi24RxQualityChart', wifi24Rows, 'rx_errors_per_sec', 'rx_dropped_per_sec'); + networkQualityChart('netWifi24TxQualityChart', wifi24Rows, 'tx_errors_per_sec', 'tx_dropped_per_sec'); networkChart('netWifi5RxChart', wifi5Rows, 'rx_mbytes', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9'); networkChart('netWifi5TxChart', wifi5Rows, 'tx_mbytes', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e'); + networkQualityChart('netWifi5RxQualityChart', wifi5Rows, 'rx_errors_per_sec', 'rx_dropped_per_sec'); + networkQualityChart('netWifi5TxQualityChart', wifi5Rows, 'tx_errors_per_sec', 'tx_dropped_per_sec'); } function resizeChartsSoon() { diff --git a/public/index.php b/public/index.php index 8981289..15afb2f 100644 --- a/public/index.php +++ b/public/index.php @@ -289,6 +289,8 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}

Download / RX Packets

Upload / TX Packets

+

RX Errors / Drops

+

TX Errors / Drops

@@ -297,6 +299,8 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}

Download / RX Packets

Upload / TX Packets

+

RX Errors / Drops

+

TX Errors / Drops

@@ -305,6 +309,8 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}

Download / RX Packets

Upload / TX Packets

+

RX Errors / Drops

+

TX Errors / Drops

@@ -313,6 +319,8 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}

Download / RX Packets

Upload / TX Packets

+

RX Errors / Drops

+

TX Errors / Drops