From 39388e5eb2649adc855189d24c89c8240e6e7b83 Mon Sep 17 00:00:00 2001 From: seo Date: Mon, 22 Jun 2026 17:26:52 +0900 Subject: [PATCH] Declutter control diagnostics sections --- README.md | 17 +++--- public/assets/app.js | 28 +++++++++- public/index.php | 122 +++++++++++++++++++++++++------------------ 3 files changed, 106 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 4a924d9..71fb3a7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 프로젝트 성격 -Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 웹에서 관리하기 위한 내부 운영 도구입니다. 로그인 후 대시보드에서 온도, 팬 RPM, PWM, WiFi client, 배터리, notice, dmesg, process 후보를 확인하고 필요한 제어 명령을 실행합니다. +Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 웹에서 관리하기 위한 내부 운영 도구입니다. 로그인 후 대시보드에서 온도, 팬 RPM, PWM, WiFi client, 배터리, notice를 확인하고 필요한 제어 명령을 실행합니다. 첫 화면은 운영 판단에 필요한 핵심 정보 위주로 구성하고, 상세 차트와 진단성 데이터는 접힘 영역에 둡니다. 상태 갱신은 WebSocket을 우선 사용하고, 연결 실패 시 HTTP fallback으로 전환합니다. 팬 정책 적용과 센서 수집은 백그라운드 작업과 API 호출을 통해 DB에 기록됩니다. @@ -12,18 +12,20 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - 팬 모드 `auto`, `manual`, `off` 제어 - PWM slider 기반 수동 팬 제어 -- CPU/RP1 온도, 팬 RPM, 팬 효율, CPU 전력, 배터리 상태 차트 +- CPU 온도, 팬 RPM, 배터리 SOC, 배터리 전압 핵심 차트 +- RP1 온도, 팬 효율, CPU 전력, 잔여 시간 상세 차트는 접힘 영역에서 확인 - 배터리 잔여 시간은 1S8P 병렬팩 `2200mAh x 8 = 17600mAh`, nominal `3.7V`, 총 `65.12Wh` 기준으로 계산 - 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시 - WiFi client 목록과 2.4G/5G client 수 표시 - 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정 -- System Notice와 process CPU/MEM 후보 표시 -- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그 표시 -- dmesg 로그 열람 +- System Notice 최근 이력 표시 +- process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시 +- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시 +- dmesg 로그는 진단 접힘 영역에서 필요할 때 열람 - Home Assistant webhook 기반 Android 모바일 알림 - 배터리 SOC 20% 이하 경고를 20/15/10/5% 단계로 구분하고 동일 `tag`로 갱신해 알림이 과도하게 쌓이지 않도록 처리 - 배터리 SOC가 20%를 초과해 복구되면 같은 `tag`의 Android 알림을 `clear_notification`으로 제거 -- 최근 HA 알림 발송 성공/실패 이력을 대시보드에 표시 +- 최근 HA 알림 발송 성공/실패 이력은 진단 접힘 영역에 표시 - WakeLock 버튼으로 대시보드 화면 꺼짐 방지 - Reboot 버튼으로 2단계 확인 후 시스템 재부팅 요청 - 기본 브라우저 alert/confirm/prompt 대신 대시보드 디자인에 맞춘 custom dialog 사용 @@ -119,7 +121,8 @@ control-wifi-observe.service 9. 시스템 상태는 `vcgencmd get_throttled`를 읽어 현재 저전압/스로틀링 여부와 부팅 후 이력, 최근 감지 시각, 현재/최근 지속시간을 표시합니다. 10. 저전압/스로틀링 상태 파일에는 최근 episode 시작/종료를 보관하고, 최근 10분 발생 횟수, 감지 누적시간, 감지 비율을 계산합니다. 11. 배터리 SOC 또는 System Notice 조건이 맞으면 Control이 HA webhook으로 알림을 발송하고 `ha_notify_logs`에 결과를 저장합니다. -12. 대시보드는 최근 HA 알림 성공/실패 이력을 `ha_notify_logs`에서 읽어 표시합니다. +12. 대시보드는 최근 HA 알림 성공/실패 이력을 `ha_notify_logs`에서 읽어 진단 접힘 영역에 표시합니다. +13. 대시보드 첫 화면에는 팬/전원/스로틀링/WiFi/핵심 차트를 우선 배치하고, 상세 차트, 프로세스 후보, 사용자 서비스, HA 알림 이력, dmesg는 접힘 영역으로 분리합니다. ## 주요 함수/모듈 diff --git a/public/assets/app.js b/public/assets/app.js index 4586e41..f7fe28a 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -50,6 +50,9 @@ dmesgToggle: $('#dmesgToggle'), dmesgOutput: $('#dmesgOutput'), dmesgMeta: $('#dmesgMeta'), + secondaryChartDetails: $('#secondaryChartDetails'), + processDetails: $('#processDetails'), + diagnosticDetails: $('#diagnosticDetails'), }; const state = { @@ -122,6 +125,7 @@ batteryV: 'Battery V', batterySoc: 'Battery SOC', sensorHistory: 'Sensor History', + detailCharts: 'Detail Charts', temperature: 'Temperature', rp1Temp: 'RP1 Temp', fanEfficiency: 'Fan Efficiency', @@ -137,6 +141,8 @@ connected: 'Connected', inactive: 'Inactive', systemNotice: 'System Notice', + processDetails: 'Process Details', + diagnostics: 'Diagnostics', noticeHistory: 'Notice History', noNoticeHistory: 'No system notice history.', haNotifyHistory: 'HA Notify History', @@ -250,6 +256,7 @@ batteryV: '배터리 전압', batterySoc: '배터리 SOC', sensorHistory: '센서 이력', + detailCharts: '상세 차트', temperature: '온도', rp1Temp: 'RP1 온도', fanEfficiency: '팬 효율', @@ -265,6 +272,8 @@ connected: '연결 시간', inactive: '비활성', systemNotice: '시스템 알림', + processDetails: '프로세스 상세', + diagnostics: '진단', noticeHistory: '알림 이력', noNoticeHistory: '시스템 알림 이력이 없습니다.', haNotifyHistory: 'HA 알림 이력', @@ -1002,8 +1011,9 @@ function renderSpikeHistory(rows = []) { if (!els.spikeLogList) return; - els.spikeLogList.innerHTML = rows.length - ? rows.map((row, index) => { + const visibleRows = rows.slice(0, 3); + els.spikeLogList.innerHTML = visibleRows.length + ? visibleRows.map((row, index) => { const processText = spikeProcessText(row); return ` @@ -1388,6 +1398,17 @@ chart('batteryVoltageChart', 'BATTERYV', rows, 'battery_voltage', '#14b8a6', 'V', dynamicScale(rows, 'battery_voltage', { minSpan: 0.2, padding: 0.06 })); } + function resizeChartsSoon() { + window.setTimeout(() => { + Object.values(state.charts).forEach(chartInstance => { + if (chartInstance && typeof chartInstance.resize === 'function') { + chartInstance.resize(); + chartInstance.update('none'); + } + }); + }, 80); + } + function render(data) { renderTop(data); renderSystemStatus(data); @@ -1616,6 +1637,9 @@ els.dmesgToggle?.addEventListener('click', () => { setDmesgOpen(!state.dmesgOpen); }); + [els.secondaryChartDetails, els.processDetails, els.diagnosticDetails].forEach(node => { + node?.addEventListener('toggle', resizeChartsSoon); + }); els.rebootBtn?.addEventListener('click', () => { requestReboot(); }); diff --git a/public/index.php b/public/index.php index a6df198..af6c566 100644 --- a/public/index.php +++ b/public/index.php @@ -104,8 +104,9 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da .btn:disabled{opacity:.55;cursor:not-allowed} .status-list{display:grid;gap:10px}.status-row{display:grid;grid-template-columns:112px minmax(0,1fr);gap:12px;align-items:baseline;padding:10px 12px;background:var(--card2);border-radius:14px}.status-key{color:var(--sub);font-size:13px}.status-value{overflow:visible;white-space:normal;word-break:break-word;font-variant-numeric:tabular-nums} .table-wrap{max-width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;border-radius:16px;border:1px solid var(--line)}.wifi-table{width:100%;min-width:1080px;table-layout:fixed;border-collapse:collapse}.wifi-table th,.wifi-table td{padding:11px 13px;border-bottom:1px solid rgba(128,145,170,.18);text-align:left;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wifi-table th{position:sticky;top:0;background:var(--table-head);z-index:1;font-weight:500}.wifi-table tr:hover{background:rgba(128,145,170,.09)}.wifi-table .col-band{width:72px}.wifi-table .col-host{width:240px}.wifi-table .col-ip{width:132px}.wifi-table .col-mac{width:170px}.wifi-table .col-signal{width:100px}.wifi-table .col-rate{width:120px}.wifi-table .col-time{width:130px} -.chart-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.chart-box{height:280px;background:var(--input);border:1px solid rgba(84,101,128,.58);border-radius:16px;padding:12px 12px 18px}.chart-box h3{margin:0 0 8px;color:var(--sub);font-size:13px}.chart-canvas{position:relative;height:calc(100% - 27px);min-height:0}.chart-canvas canvas{width:100%!important;height:100%!important} -.resource-card{margin-top:18px}.resource-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:16px}.resource-head h2{margin:0}.baseline-pill{color:var(--sub);font-size:12px;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}.spike-log-box{margin-top:16px}.spike-log-box h3{margin:0 0 9px;color:var(--text);font-size:14px;font-weight:500}.spike-log-list{display:grid;gap:8px;max-height:260px;overflow:auto;padding-right:4px}.spike-log-item{display:grid;gap:4px;padding:10px 12px;border:1px solid var(--notice-border);background:var(--notice-bg);border-radius:12px}.spike-log-item.latest{border:1px solid var(--notice-border-strong);background:var(--notice-bg-strong);box-shadow:0 0 0 1px var(--notice-border) inset;}.spike-log-item.alert{border-color:rgba(255,95,87,.58);background:rgba(150,38,38,.18)}.spike-log-item.alert strong{color:#ffb4ad}.spike-log-item strong{font-size:13px;font-weight:500;color:var(--notice-title)}.spike-log-item span{font-size:12px;color:var(--notice-copy);font-variant-numeric:tabular-nums}.spike-log-empty{padding:10px 12px;border-radius:12px;background:var(--card2);color:var(--sub);font-size:13px}.service-list{display:grid;gap:10px;max-height:520px;overflow:auto;padding-right:4px}.service-item{border:1px solid var(--line);border-radius:12px;background:var(--card2);padding:12px;display:grid;gap:9px}.service-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.service-title{display:grid;gap:3px;min-width:0}.service-name{font-size:14px;color:var(--text);font-weight:600;word-break:break-word}.service-desc,.service-meta{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums;word-break:break-word}.service-state{border:1px solid var(--line);border-radius:999px;padding:4px 9px;font-size:12px;color:var(--sub);white-space:nowrap}.service-state.active{color:#bbf7d0;border-color:rgba(34,197,94,.35);background:rgba(22,163,74,.14)}.service-state.failed{color:#fecaca;border-color:rgba(239,68,68,.4);background:rgba(185,28,28,.16)}.service-log{margin:0;max-height:160px;overflow:auto;border:1px solid var(--line);border-radius:10px;background:var(--code-bg);color:var(--mono-text);padding:10px;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.dmesg-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.dmesg-meta{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums}.dmesg-log{margin:10px 0 0;max-height:420px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--code-bg);color:var(--mono-text);padding:12px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.dmesg-log[hidden]{display:none} +.chart-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.chart-box{height:280px;background:var(--input);border:1px solid rgba(84,101,128,.58);border-radius:16px;padding:12px 12px 18px}.chart-box h3{margin:0 0 8px;color:var(--sub);font-size:13px}.chart-canvas{position:relative;height:calc(100% - 27px);min-height:0}.chart-canvas canvas{width:100%!important;height:100%!important}.chart-secondary{margin-top:14px} +.details-panel{border:1px solid rgba(84,101,128,.52);border-radius:16px;background:rgba(128,145,170,.06);padding:0;margin-top:14px;overflow:hidden}.details-panel>summary{cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:48px;padding:0 14px;color:var(--text);font-weight:500}.details-panel>summary::-webkit-details-marker{display:none}.details-panel>summary::after{content:"+";display:grid;place-items:center;width:24px;height:24px;border:1px solid var(--line);border-radius:999px;color:var(--sub);font-weight:700}.details-panel[open]>summary::after{content:"-"}.details-body{padding:0 14px 14px}.section-note{margin:0;color:var(--sub);font-size:12px} +.resource-card{margin-top:18px}.resource-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px}.resource-head h2{margin:0}.baseline-pill{color:var(--sub);font-size:12px;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}.spike-log-box{margin-top:16px}.spike-log-box h3{margin:0 0 9px;color:var(--text);font-size:14px;font-weight:500}.spike-log-list{display:grid;gap:8px;max-height:260px;overflow:auto;padding-right:4px}.spike-log-list.compact{max-height:180px}.spike-log-item{display:grid;gap:4px;padding:10px 12px;border:1px solid var(--notice-border);background:var(--notice-bg);border-radius:12px}.spike-log-item.latest{border:1px solid var(--notice-border-strong);background:var(--notice-bg-strong);box-shadow:0 0 0 1px var(--notice-border) inset;}.spike-log-item.alert{border-color:rgba(255,95,87,.58);background:rgba(150,38,38,.18)}.spike-log-item.alert strong{color:#ffb4ad}.spike-log-item strong{font-size:13px;font-weight:500;color:var(--notice-title)}.spike-log-item span{font-size:12px;color:var(--notice-copy);font-variant-numeric:tabular-nums}.spike-log-empty{padding:10px 12px;border-radius:12px;background:var(--card2);color:var(--sub);font-size:13px}.service-list{display:grid;gap:10px;max-height:520px;overflow:auto;padding-right:4px}.service-item{border:1px solid var(--line);border-radius:12px;background:var(--card2);padding:12px;display:grid;gap:9px}.service-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.service-title{display:grid;gap:3px;min-width:0}.service-name{font-size:14px;color:var(--text);font-weight:600;word-break:break-word}.service-desc,.service-meta{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums;word-break:break-word}.service-state{border:1px solid var(--line);border-radius:999px;padding:4px 9px;font-size:12px;color:var(--sub);white-space:nowrap}.service-state.active{color:#bbf7d0;border-color:rgba(34,197,94,.35);background:rgba(22,163,74,.14)}.service-state.failed{color:#fecaca;border-color:rgba(239,68,68,.4);background:rgba(185,28,28,.16)}.service-log{margin:0;max-height:160px;overflow:auto;border:1px solid var(--line);border-radius:10px;background:var(--code-bg);color:var(--mono-text);padding:10px;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.dmesg-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.dmesg-meta{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums}.dmesg-log{margin:10px 0 0;max-height:420px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--code-bg);color:var(--mono-text);padding:12px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.dmesg-log[hidden]{display:none} .resource-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.resource-box{min-width:0}.resource-box h3{margin:0 0 9px;color:var(--sub);font-size:14px;font-weight:500}.resource-table{width:100%;min-width:620px;table-layout:fixed;border-collapse:collapse;border:1px solid var(--line);border-radius:14px;overflow:hidden}.resource-table th,.resource-table td{padding:9px 10px;border-bottom:1px solid rgba(128,145,170,.18);background:var(--input);text-align:left;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.resource-table th{background:var(--table-head);color:var(--sub);font-weight:500}.resource-table .pid{width:72px}.resource-table .metric{width:82px}.resource-table .service{width:150px}.resource-table .cmd{width:auto} .notice{position:fixed;right:20px;bottom:20px;min-width:220px;max-width:420px;padding:14px 16px;border-radius:14px;font-weight:500;background:var(--table-head);color:var(--text);border:1px solid var(--line);box-shadow:var(--shadow);z-index:99}.notice:empty{display:none}.notice[data-type=success]{border-color:rgba(53,196,107,.5)}.notice[data-type=error]{border-color:rgba(255,95,87,.5)} .dialog-layer{position:fixed;inset:0;display:none;align-items:center;justify-content:center;padding:22px;background:rgba(4,7,12,.68);backdrop-filter:blur(8px);z-index:120}.dialog-layer[data-open="1"]{display:flex}.dialog-box{width:min(430px,100%);border:1px solid rgba(84,101,128,.78);border-radius:20px;background:var(--card);box-shadow:0 24px 70px rgba(0,0,0,.42);padding:20px}.dialog-box h3{margin:0 0 8px;font-size:19px;font-weight:500}.dialog-message{margin:0 0 16px;color:var(--sub);line-height:1.5;white-space:pre-wrap}.dialog-input{width:100%;height:48px;margin-bottom:14px;border-radius:14px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 14px;outline:none}.dialog-input:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.dialog-actions{display:flex;justify-content:flex-end;gap:10px}.dialog-actions .btn{min-width:84px}.dialog-actions .btn.red{background:#c62828} @@ -201,16 +202,23 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da

Sensor History

-
-

Temperature

-

RP1 Temp

+
+

Temperature

Fan RPM

-

Fan Efficiency

-

CPU Watt

Battery SOC

-

Remaining

Battery Voltage

+
+ Detail Charts +
+
+

RP1 Temp

+

Fan Efficiency

+

CPU Watt

+

Remaining

+
+
+
@@ -241,58 +249,68 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da

System Notice

Baseline: -
-
-
-

CPU

-
- - - - -
PIDCPUServiceCommand
loading...
-
-
-
-

Memory

-
- - - - -
PIDMEMServiceCommand
loading...
-
-
-
-
-

Notice History

-
+

Notice History

+
No system notice history.
-
-

Custom Services

-
-
No custom services.
+
+ Process Details +
+
+
+

CPU

+
+ + + + +
PIDCPUServiceCommand
loading...
+
+
+
+

Memory

+
+ + + + +
PIDMEMServiceCommand
loading...
+
+
+
-
+ -
-

HA Notify History

-
-
No HA notify history.
-
-
+
+ Diagnostics +
+
+

Custom Services

+
+
No custom services.
+
+
-
-
-

dmesg

- +
+

HA Notify History

+
+
No HA notify history.
+
+
+ +
+
+

dmesg

+ +
+
/tmp/dmesg.log
+ +
-
/tmp/dmesg.log
- -
+
@@ -307,7 +325,7 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da - +