diff --git a/README.md b/README.md index 736bd23..6f921b7 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - System Notice 최근 이력 표시 - process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시 - `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시 +- 사용자 서비스 로그는 서비스별 최근 500줄까지 조회하고, 사용자가 서비스 목록을 스크롤하는 동안에는 자동 갱신이 화면 위치를 되돌리지 않습니다. - dmesg 로그는 진단 접힘 영역에서 필요할 때 열람 - WakeLock 버튼으로 대시보드 화면 꺼짐 방지 - Reboot 버튼으로 2단계 확인 후 시스템 재부팅 요청 @@ -230,8 +231,11 @@ control-wifi-observe.service - 센서 수집 주기와 DB 증가량을 확인합니다. - UPS/배터리를 다시 붙일 때는 `/home/seo/secret/control.php`의 `battery.enabled`와 용량값을 먼저 확인합니다. - WiFi client 표는 갱신 중에도 가로 스크롤 위치를 보존합니다. +- 사용자 서비스 목록은 갱신 중에도 스크롤 위치를 보존하고, 스크롤/터치/휠 조작 직후에는 DOM 교체를 잠시 미룹니다. - 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다. +- AP 국가는 `PA`로 두고, `router-performance.service`가 `wlan0`, `wlan1` power save를 끄고 출력 요청값을 `3600`으로 넣습니다. 실제 출력은 무선 모듈/드라이버가 허용하는 값에서 멈추며 현재 두 AP 모두 `31.00 dBm`으로 표시됩니다. - 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다. +- `hostapd-24g.service`, `hostapd-5g.service`는 종료 후 해당 `wlan` 인터페이스를 `br0`에서 떼도록 두어, 모듈이 빠졌다가 돌아온 뒤 hostapd가 다시 붙이는 흐름으로 정리합니다. - 5G WiFi 연결 시간은 외부 모듈이 값을 제공하지 않을 때 서버가 처음 감지한 시각 기준으로 계산합니다. 현재 5G 목록에서 MAC이 사라지면 관측 세션을 즉시 종료하므로 재연결 시 0부터 다시 누적됩니다. - `control-wifi-observe.timer`는 10초마다 `/var/www/control/bin/wifi_observe.php`를 실행해 사용자가 대시보드에 접속하지 않아도 5G 연결 시간 카운터를 시작하고 유지합니다. - WebSocket은 장기 실행 프로세스이므로 `public/api.php`, `config/config.php`, `bin/control_ws.php` 변경을 감지하면 15초 안에 종료되고 `control-websocket.service`가 새 프로세스로 재시작합니다. diff --git a/config/config.php b/config/config.php index 2015cf8..27f92e3 100644 --- a/config/config.php +++ b/config/config.php @@ -170,7 +170,7 @@ function setting_definitions(): array 'notice.log_interval_seconds' => ['group' => 'notice', 'label' => 'Alert 반복 기록 간격', 'type' => 'number', 'default' => 300, 'min' => 30, 'max' => 3600, 'step' => 30, 'unit' => '초'], 'notice.downward_only_process_hide' => ['group' => 'notice', 'label' => '하강 변화 원인 후보 숨김', 'type' => 'boolean', 'default' => 1], 'display.process_limit' => ['group' => 'display', 'label' => '프로세스 후보 표시 수', 'type' => 'number', 'default' => 6, 'min' => 1, 'max' => 30, 'step' => 1, 'unit' => '개'], - 'display.custom_service_log_lines' => ['group' => 'display', 'label' => '사용자 서비스 로그 줄 수', 'type' => 'number', 'default' => 12, 'min' => 1, 'max' => 50, 'step' => 1, 'unit' => '줄'], + 'display.custom_service_log_lines' => ['group' => 'display', 'label' => '사용자 서비스 로그 줄 수', 'type' => 'number', 'default' => 500, 'min' => 1, 'max' => 500, 'step' => 10, 'unit' => '줄'], 'display.custom_service_cache_seconds' => ['group' => 'display', 'label' => '사용자 서비스 캐시 시간', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 60, 'step' => 1, 'unit' => '초'], 'display.fan_notice_history_limit' => ['group' => 'display', 'label' => '팬 이상 이력 표시 수', 'type' => 'number', 'default' => 100, 'min' => 10, 'max' => 500, 'step' => 10, 'unit' => '개'], 'battery.trend_hours' => ['group' => 'battery', 'label' => '단기 배터리 학습 범위', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 48, 'step' => 1, 'unit' => '시간'], diff --git a/public/api.php b/public/api.php index f57cb29..af03664 100644 --- a/public/api.php +++ b/public/api.php @@ -3065,7 +3065,7 @@ function parse_systemctl_show_records(string $out): array function systemd_service_logs(string $unit, int $limit = 12): array { - $limit = max(1, min(50, $limit)); + $limit = max(1, min(500, $limit)); $cmd = [ '/usr/bin/journalctl', '-u', diff --git a/public/assets/app.js b/public/assets/app.js index aa03b02..cf4a754 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -92,6 +92,7 @@ settingsPayload: null, settingsOpen: false, settingsDirty: false, + customServiceScrollHoldUntil: 0, }; const storageKeys = { @@ -1621,6 +1622,9 @@ function renderCustomServices(rows = []) { if (!els.customServiceList) return; + if (Date.now() < state.customServiceScrollHoldUntil) return; + + const scrollTop = els.customServiceList.scrollTop; els.customServiceList.innerHTML = rows.length ? rows.map(row => { @@ -1653,6 +1657,8 @@ `; }).join('') : `