From da8c4e863de6df5d176e487d990fe835941f111f Mon Sep 17 00:00:00 2001 From: seo Date: Fri, 26 Jun 2026 03:21:01 +0900 Subject: [PATCH] =?UTF-8?q?Control=20=EC=95=8C=EB=A6=BC=EA=B3=BC=20notice?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=84=B8=EB=B6=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++++++------ config/config.php | 40 +++++++++++++++++++++++++++++------- public/api.php | 48 ++++++++++++++++++++++++++++++-------------- public/assets/app.js | 15 ++++++++++---- 4 files changed, 84 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index d75d494..b5c0845 100644 --- a/README.md +++ b/README.md @@ -111,10 +111,11 @@ WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과 설정 가능한 항목은 다음 범위입니다. - 팬 자동 제어: 팬 시작 온도, 최대 온도, 즉시 최대 PWM 온도, 자동 상승/하강 PWM step -- 보안 정책: 자동 로그인 유지 기간, remember 쿠키 Secure/SameSite, CSRF 길이, remember 토큰 길이, User-Agent/IP 접두사 검증, 로그인 실패 잠금, 재부팅 확인 문구, 명령 timeout, WiFi 조작 허용 여부 +- 보안 정책: 자동 로그인 유지 기간, remember 쿠키 Secure/SameSite, CSRF 길이, remember 토큰 길이, User-Agent/IP 접두사 검증, 로그인 실패 잠금, 재부팅 허용 여부, 재부팅 확인 문구, 명령 timeout, WiFi 조작 허용 여부 +- 알림 정책: 배터리 긴급/위험/경고/복구 기준, 배터리 낮음/복구 알림 쿨다운, 시스템 유의사항 알림 쿨다운 - 저전압/스로틀링: 최근 판정창, 복구 중 유지 시간, 반복 발생 기준 횟수 -- 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이 -- 배터리 예측: 단기/장기 학습 범위, 후보 필터 비율, 부하 보정 강도, 전압 하한, 용량 모델 가중치 +- 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이, RPM 감시 시작 PWM, alert 유지 중 반복 기록, 반복 기록 간격, 하강 변화 원인 후보 표시 여부 +- 배터리 예측: 단기/장기 학습 범위, 후보 필터 비율, 부하 보정 강도, 전압 하한, 용량 모델 가중치, 상태/잔여시간 차트 표본 수 ## 갱신 주기 @@ -139,9 +140,9 @@ Control은 서버에서 Home Assistant webhook으로 알림 payload를 보내고 - 사용 속성: `tag`, `group`, `channel`, `importance`, `priority`, `ttl`, `sticky`, `persistent`, `renotify`, `color`, `notification_icon`, `vibrationPattern`, `ledColor`, `visibility`, `timeout`, `clickAction`, `url`, `actions` - 발송 결과는 `ha_notify_logs`에 저장 -배터리 SOC 20% 이하 경고는 `control-battery-low` tag를 사용합니다. SOC 값이 조금씩 달라져도 Android 알림은 같은 tag로 갱신되어 알림 목록에 여러 장이 계속 쌓이지 않습니다. 20% 이하에서는 `주의`, 15% 이하에서는 `경고`, 10% 이하에서는 `위험`, 5% 이하에서는 `긴급` 단계로 채널, 중요도, 아이콘, 진동 패턴을 조정합니다. SOC가 20%를 초과하면 `clear_notification`으로 해당 tag 알림을 제거합니다. +배터리 SOC 낮음 경고는 `control-battery-low` tag를 사용합니다. SOC 값이 조금씩 달라져도 Android 알림은 같은 tag로 갱신되어 알림 목록에 여러 장이 계속 쌓이지 않습니다. 주의/경고/위험/긴급/복구 기준과 알림 쿨다운은 설정 모달의 알림 정책에서 조정합니다. -System Notice는 `control-system-notice` tag를 사용하며, 기본 10분 쿨다운으로 급격한 팬/온도 변화 알림을 제한합니다. 쿨다운은 `/home/seo/secret/control.php`의 `ha_notify.cooldowns`에서 조정할 수 있습니다. +System Notice는 `control-system-notice` tag를 사용하며, 설정된 쿨다운으로 급격한 팬/온도 변화 알림을 제한합니다. alert 상태는 신규 진입과 유지 상태를 분리해 관리하며, alert 유지 중 반복 기록 여부와 기록 간격도 설정 모달에서 조정합니다. Android 알림 채널은 생성 후 휴대폰 설정에 의해 중요도와 진동 패턴이 고정될 수 있습니다. 채널을 다시 만들 필요가 있을 때는 운영 CLI로 제거 명령을 보낸 뒤 다음 알림에서 재생성합니다. @@ -209,7 +210,7 @@ control-wifi-observe.service - `ha_notify_due()`: 동일 tag 기준 쿨다운 판정 - `clear_ha_notification()`: 같은 tag의 Android 알림 제거 명령 발송 - `battery_low_notify_profile()`: 배터리 SOC 단계별 채널, 중요도, 아이콘, 진동 정책 선택 -- `send_battery_low_notify_if_needed()`: 배터리 SOC 20% 이하 알림 발송 +- `send_battery_low_notify_if_needed()`: 설정된 배터리 SOC 기준에 따라 낮음/복구 알림 발송 - `ha_notify_log_rows()`: 대시보드 HA 알림 이력 조회 - `battery_trend_history()`: 최근 24시간 배터리 기록을 1분 단위로 집계 - `battery_profile_history()`: 최대 45일 배터리 기록을 약 5분 간격으로 샘플링하고 파일 캐시로 보관 diff --git a/config/config.php b/config/config.php index 56c8aab..fb0d9c9 100644 --- a/config/config.php +++ b/config/config.php @@ -147,6 +147,7 @@ function setting_definitions(): array 'security.login_window_seconds' => ['group' => 'security', 'label' => '로그인 실패 집계 시간', 'type' => 'number', 'default' => 600, 'min' => 60, 'max' => 86400, 'step' => 60, 'unit' => '초'], 'security.login_lock_seconds' => ['group' => 'security', 'label' => '로그인 잠금 시간', 'type' => 'number', 'default' => 300, 'min' => 10, 'max' => 86400, 'step' => 10, 'unit' => '초'], 'security.reboot_phrase' => ['group' => 'security', 'label' => '재부팅 확인 문구', 'type' => 'text', 'default' => '재부팅', 'min_length' => 2, 'max_length' => 32], + 'security.allow_reboot' => ['group' => 'security', 'label' => '재부팅 실행 허용', 'type' => 'boolean', 'default' => 1], 'security.reboot_timeout_seconds' => ['group' => 'security', 'label' => '재부팅 명령 timeout', 'type' => 'number', 'default' => 5, 'min' => 2, 'max' => 30, 'step' => 1, 'unit' => '초'], 'security.wifi_command_timeout_seconds' => ['group' => 'security', 'label' => 'WiFi 명령 timeout', 'type' => 'number', 'default' => 25, 'min' => 5, 'max' => 90, 'step' => 1, 'unit' => '초'], 'security.shell_default_timeout_seconds' => ['group' => 'security', 'label' => '기본 명령 timeout', 'type' => 'number', 'default' => 8, 'min' => 2, 'max' => 60, 'step' => 1, 'unit' => '초'], @@ -167,6 +168,17 @@ function setting_definitions(): array 'notice.temp_delta_threshold' => ['group' => 'notice', 'label' => '온도 이상 감지 차이', 'type' => 'number', 'default' => 3.0, 'min' => 0.5, 'max' => 15, 'step' => 0.1, 'unit' => 'C'], 'notice.recover_rpm_delta' => ['group' => 'notice', 'label' => '팬 RPM 복구 차이', 'type' => 'number', 'default' => 500, 'min' => 50, 'max' => 3000, 'step' => 50, 'unit' => 'RPM'], 'notice.recover_temp_delta' => ['group' => 'notice', 'label' => '온도 복구 차이', 'type' => 'number', 'default' => 1.5, 'min' => 0.2, 'max' => 10, 'step' => 0.1, 'unit' => 'C'], + 'notice.rpm_expected_pwm' => ['group' => 'notice', 'label' => 'RPM 감시 시작 PWM', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 255, 'step' => 1, 'unit' => 'PWM'], + 'notice.log_during_alert' => ['group' => 'notice', 'label' => 'Alert 유지 중 반복 기록', 'type' => 'boolean', 'default' => 0], + '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], + 'notify.battery_emergency_percent' => ['group' => 'notify', 'label' => '배터리 긴급 기준', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 30, 'step' => 0.5, 'unit' => '%'], + 'notify.battery_critical_percent' => ['group' => 'notify', 'label' => '배터리 위험 기준', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 40, 'step' => 0.5, 'unit' => '%'], + 'notify.battery_warning_percent' => ['group' => 'notify', 'label' => '배터리 경고 기준', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 50, 'step' => 0.5, 'unit' => '%'], + 'notify.battery_clear_percent' => ['group' => 'notify', 'label' => '배터리 복구 기준', 'type' => 'number', 'default' => 20, 'min' => 2, 'max' => 80, 'step' => 0.5, 'unit' => '%'], + 'notify.battery_low_cooldown_seconds' => ['group' => 'notify', 'label' => '배터리 낮음 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_BATTERY_LOW_COOLDOWN, 'min' => 1, 'max' => 86400, 'step' => 1, 'unit' => '초'], + 'notify.battery_clear_cooldown_seconds' => ['group' => 'notify', 'label' => '배터리 복구 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_BATTERY_CLEAR_COOLDOWN, 'min' => 1, 'max' => 86400, 'step' => 1, 'unit' => '초'], + 'notify.system_notice_cooldown_seconds' => ['group' => 'notify', 'label' => '시스템 유의사항 알림 쿨다운', 'type' => 'number', 'default' => HA_NOTIFY_SYSTEM_NOTICE_COOLDOWN, 'min' => 10, 'max' => 86400, 'step' => 10, 'unit' => '초'], 'battery.trend_hours' => ['group' => 'battery', 'label' => '단기 배터리 학습 범위', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 48, 'step' => 1, 'unit' => '시간'], 'battery.profile_days' => ['group' => 'battery', 'label' => '장기 배터리 학습 범위', 'type' => 'number', 'default' => 45, 'min' => 3, 'max' => 90, 'step' => 1, 'unit' => '일'], 'battery.load_adjust_recent' => ['group' => 'battery', 'label' => '최근 추세 부하 보정 강도', 'type' => 'number', 'default' => 0.45, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'], @@ -178,6 +190,8 @@ function setting_definitions(): array 'battery.candidate_min_ratio' => ['group' => 'battery', 'label' => '후보 최소 속도 비율', 'type' => 'number', 'default' => 0.38, 'min' => 0.05, 'max' => 1, 'step' => 0.01, 'unit' => 'x'], 'battery.candidate_max_ratio' => ['group' => 'battery', 'label' => '후보 최대 속도 비율', 'type' => 'number', 'default' => 2.65, 'min' => 1, 'max' => 8, 'step' => 0.05, 'unit' => 'x'], 'battery.capacity_model_weight' => ['group' => 'battery', 'label' => '용량 모델 가중치', 'type' => 'number', 'default' => 0.18, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'], + 'battery.history_chart_limit' => ['group' => 'battery', 'label' => '상태 차트 표본 수', 'type' => 'number', 'default' => 240, 'min' => 60, 'max' => 1500, 'step' => 10, 'unit' => '개'], + 'battery.history_sync_limit' => ['group' => 'battery', 'label' => '잔여시간 차트 표본 수', 'type' => 'number', 'default' => 240, 'min' => 60, 'max' => 1500, 'step' => 10, 'unit' => '개'], ]; } @@ -246,6 +260,7 @@ function settings_payload(): array $values = setting_rows(); $groups = [ 'security' => '보안 정책', + 'notify' => '알림 정책', 'fan' => '팬 자동 제어', 'battery' => '배터리 예측', 'event' => '저전압/스로틀링', @@ -501,7 +516,7 @@ function bootstrap_db(): void $pdo->exec(" CREATE TABLE IF NOT EXISTS remember_tokens ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, - selector CHAR(24) NOT NULL, + selector VARCHAR(64) NOT NULL, validator_hash CHAR(64) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, @@ -583,6 +598,7 @@ function bootstrap_db(): void "ALTER TABLE sensor_logs DROP COLUMN created_ip", "ALTER TABLE fan_spike_logs ADD COLUMN spike_key VARCHAR(32) NULL AFTER created_at", "ALTER TABLE fan_spike_logs ADD UNIQUE KEY uniq_spike_key (spike_key)", + "ALTER TABLE remember_tokens MODIFY selector VARCHAR(64) NOT NULL", "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", @@ -949,7 +965,11 @@ function clear_ha_notification(string $tag, string $title = 'Control 알림 해 function battery_low_notify_profile(float $percent): array { - if ($percent <= 5.0) { + $emergency = (float)setting_value('notify.battery_emergency_percent'); + $critical = max($emergency, (float)setting_value('notify.battery_critical_percent')); + $warning = max($critical, (float)setting_value('notify.battery_warning_percent')); + + if ($percent <= $emergency) { return [ 'label' => '긴급', 'level' => 'critical', @@ -964,7 +984,7 @@ function battery_low_notify_profile(float $percent): array ]; } - if ($percent <= 10.0) { + if ($percent <= $critical) { return [ 'label' => '위험', 'level' => 'critical', @@ -979,7 +999,7 @@ function battery_low_notify_profile(float $percent): array ]; } - if ($percent <= 15.0) { + if ($percent <= $warning) { return [ 'label' => '경고', 'level' => 'warning', @@ -1025,13 +1045,19 @@ function send_battery_low_notify_if_needed(array $battery): array : null; $tag = 'control-battery-low'; - if ($percent > 20.0) { + $warning = max( + (float)setting_value('notify.battery_emergency_percent'), + (float)setting_value('notify.battery_critical_percent'), + (float)setting_value('notify.battery_warning_percent') + ); + $clearPercent = max($warning + 0.5, (float)setting_value('notify.battery_clear_percent')); + if ($percent > $clearPercent) { $latestLow = latest_ha_notify_epoch_by_tag($tag); $latestClear = latest_ha_notify_epoch_by_tag($tag . '-clear'); if ( $latestLow > 0 && $latestLow > $latestClear - && ha_notify_due($tag . '-clear', HA_NOTIFY_BATTERY_CLEAR_COOLDOWN) + && ha_notify_due($tag . '-clear', (int)setting_value('notify.battery_clear_cooldown_seconds')) ) { return clear_ha_notification($tag, '배터리 SOC 복구'); } @@ -1043,7 +1069,7 @@ function send_battery_low_notify_if_needed(array $battery): array ]; } - if (!ha_notify_due($tag, HA_NOTIFY_BATTERY_LOW_COOLDOWN)) { + if (!ha_notify_due($tag, (int)setting_value('notify.battery_low_cooldown_seconds'))) { return [ 'sent' => 0, 'failed' => 0, diff --git a/public/api.php b/public/api.php index 2ca7c87..27e20e5 100644 --- a/public/api.php +++ b/public/api.php @@ -1720,7 +1720,7 @@ function sync_current_battery_remaining_history(array $history, array $currentRo } $history[] = $currentRow; - return array_slice($history, -240); + return array_slice($history, -(int)setting_value('battery.history_sync_limit')); } function process_service_name(int $pid): string @@ -1973,7 +1973,8 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr ]; } - $rpmExpected = $mode !== 'off' && ($pwm >= 20 || $pwmAvg >= 20); + $rpmExpectedPwm = (float)setting_value('notice.rpm_expected_pwm'); + $rpmExpected = $mode !== 'off' && ($pwm >= $rpmExpectedPwm || $pwmAvg >= $rpmExpectedPwm); $reasons = []; $rpmThreshold = (float)setting_value('notice.rpm_delta_threshold'); @@ -1984,11 +1985,12 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr if ($rpmExpected && abs($rpmDelta) >= $rpmThreshold) $reasons[] = 'RPM ' . signed_delta_text($rpmDelta); if (abs($tempDelta) >= $tempThreshold) $reasons[] = 'TEMP ' . signed_delta_text($tempDelta, 'C'); - $spiking = false; + $alertStarted = false; if ($currentState === 'normal') { - $spiking = $reasons !== []; - if ($spiking) { + $alertStarted = $reasons !== []; + if ($alertStarted) { + $currentState = 'alert'; save_system_notice_state( 'alert', $tempAvg, @@ -2031,14 +2033,17 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr } $summary = 'No system notice detected in recent samples.'; - if ($spiking) { + if ($alertStarted) { $summary = 'System notice: ' . implode(', ', $reasons); } elseif ($currentState === 'alert' && $reasons !== []) { $summary = 'System notice active: ' . implode(', ', $reasons); } + $alertActive = $currentState === 'alert'; + return [ - 'active' => $spiking, + 'active' => $alertActive, + 'alert_started' => $alertStarted, 'summary' => $summary, 'rpm_delta' => round($rpmDelta, 1), 'pwm_delta' => round($pwmDelta, 1), @@ -2046,7 +2051,7 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr 'rpm_avg' => round($rpmAvg, 1), 'pwm_avg' => round($pwmAvg, 1), 'temp_avg' => round($tempAvg, 1), - 'notice_state' => $spiking ? 'alert' : $currentState, + 'notice_state' => $currentState, 'baseline_source' => $currentState === 'alert' ? 'frozen_alert' : 'rolling', 'baseline_samples' => (int)$rollingBaseline['samples'], ]; @@ -2148,8 +2153,9 @@ function add_fan_spike_log(array $spike, array $fan, array $system, array $proce $rpmDelta = round((float)($spike['rpm_delta'] ?? 0), 1); $pwmDelta = round((float)($spike['pwm_delta'] ?? 0), 1); $tempDelta = round((float)($spike['temp_delta'] ?? 0), 1); - $spikeKey = 'fan:' . date('YmdHi'); - $loggedProcesses = notice_downward_only($tempDelta, $rpmDelta) + $interval = max(60, (int)setting_value('notice.log_interval_seconds')); + $spikeKey = 'fan:' . (int)floor(time() / $interval); + $loggedProcesses = ((bool)setting_value('notice.downward_only_process_hide') && notice_downward_only($tempDelta, $rpmDelta)) ? ['cpu' => [], 'memory' => []] : $processes; @@ -2412,7 +2418,7 @@ function send_fan_spike_notify(array $spike, array $fan, array $system, array $p . number_format((float)($fan['rpm'] ?? 0), 0) . ' RPM / PWM ' . number_format((float)($fan['pwm'] ?? 0), 0) - . (notice_downward_only($tempDelta, $rpmDelta) ? '' : "\n원인 후보: " . expected_process_detail_text($processes)); + . (((bool)setting_value('notice.downward_only_process_hide') && notice_downward_only($tempDelta, $rpmDelta)) ? '' : "\n원인 후보: " . expected_process_detail_text($processes)); return send_ha_notify([ 'title' => '시스템 유의사항', @@ -3224,7 +3230,7 @@ function collect_snapshot(bool $applyFan = true): array 'throttling' => $throttledStatuses['throttling'], ]); - $history = add_battery_remaining_history(sensor_history(240)); + $history = add_battery_remaining_history(sensor_history((int)setting_value('battery.history_chart_limit'))); $batteryTrend = battery_trend_history((int)setting_value('battery.trend_hours')); $batteryProfile = battery_profile_history((int)setting_value('battery.profile_days')); $battery['remaining'] = battery_remaining_estimate($battery, $history, $batteryTrend, $batteryProfile); @@ -3251,23 +3257,27 @@ function collect_snapshot(bool $applyFan = true): array 'mem_used_mb' => $mem['used_mb'], 'mem_percent' => $mem['total_mb'] > 0 ? round($mem['used_mb'] / $mem['total_mb'] * 100, 1) : 0, ], $battery['remaining']['seconds'] ?? null); + $history = array_slice($history, -(int)setting_value('battery.history_sync_limit')); $processes = process_resource_data(6); $fanSpike = fan_spike_analysis($history, $fan, $system, $processes); - if (!empty($fanSpike['active'])) { + $shouldLogNotice = !empty($fanSpike['alert_started']) + || (!empty($fanSpike['active']) && (bool)setting_value('notice.log_during_alert')); + if ($shouldLogNotice) { $latestNoticeNotify = latest_system_notice_notify_epoch(); $spikeLogId = add_fan_spike_log($fanSpike, $fan, $system, $processes); if ($spikeLogId > 0) { $fanSpike['log_id'] = $spikeLogId; - if ($latestNoticeNotify <= 0 || time() - $latestNoticeNotify >= HA_NOTIFY_SYSTEM_NOTICE_COOLDOWN) { + $noticeCooldown = (int)setting_value('notify.system_notice_cooldown_seconds'); + if ($latestNoticeNotify <= 0 || time() - $latestNoticeNotify >= $noticeCooldown) { $fanSpike['notify'] = send_fan_spike_notify($fanSpike, $fan, $system, $processes); } else { $fanSpike['notify'] = [ 'sent' => 0, 'failed' => 0, 'skipped' => 'system_notice_cooldown', - 'cooldown_seconds' => HA_NOTIFY_SYSTEM_NOTICE_COOLDOWN, + 'cooldown_seconds' => $noticeCooldown, ]; } } @@ -3383,6 +3393,14 @@ function control_api_dispatch(): void $password = (string)($_POST['password'] ?? ''); $expectedPhrase = (string)setting_value('security.reboot_phrase'); + if (!(bool)setting_value('security.allow_reboot')) { + json_out([ + 'ok' => false, + 'error' => 'reboot_disabled', + 'message' => '재부팅 실행이 설정에서 비활성화되어 있습니다.', + ], 403); + } + if ($phrase !== $expectedPhrase) { json_out([ 'ok' => false, diff --git a/public/assets/app.js b/public/assets/app.js index 58cacdf..750757a 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -625,7 +625,7 @@ return ` - ${checked ? 'ON' : 'OFF'} + ${checked ? '켜기' : '끄기'} `; } @@ -662,6 +662,11 @@ if (item.type === 'select' && item.options && item.options[item.default]) { return item.options[item.default]; } + if (item.type === 'boolean') { + return item.default === true || item.default === 1 || item.default === '1' || item.default === 'true' + ? '켜기' + : '끄기'; + } return item.default ?? ''; }; @@ -687,7 +692,7 @@ els.settingsBody.querySelectorAll('.settings-toggle input').forEach(input => { input.addEventListener('change', () => { const label = input.closest('.settings-toggle')?.querySelector('span'); - if (label) label.textContent = input.checked ? 'ON' : 'OFF'; + if (label) label.textContent = input.checked ? '켜기' : '끄기'; }); input.closest('.settings-toggle')?.addEventListener('click', () => { input.checked = !input.checked; @@ -1382,8 +1387,10 @@ const m = noticeMetrics(row); const reasons = []; - if (Math.abs(m.tempDelta) >= 3) reasons.push(t(m.tempDelta >= 0 ? 'tempHigher' : 'tempLower')); - if (Math.abs(m.rpmDelta) >= 1000) reasons.push(t(m.rpmDelta >= 0 ? 'rpmHigher' : 'rpmLower')); + const tempThreshold = Number(settingByKey('notice.temp_delta_threshold', 3)); + const rpmThreshold = Number(settingByKey('notice.rpm_delta_threshold', 1000)); + if (Math.abs(m.tempDelta) >= tempThreshold) reasons.push(t(m.tempDelta >= 0 ? 'tempHigher' : 'tempLower')); + if (Math.abs(m.rpmDelta) >= rpmThreshold) reasons.push(t(m.rpmDelta >= 0 ? 'rpmHigher' : 'rpmLower')); return `${t('recordedReason')}: ${reasons.length ? reasons.join(', ') : t('instantChange')}`; }