Notice 조건과 원격 로그 필터 보강
This commit is contained in:
@@ -25,6 +25,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정
|
- WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정
|
||||||
- WiFi client 신호는 7단계 한글 등급으로 표시하고, hover/focus/touch 시 커스텀 툴팁에서 실제 dBm을 표시
|
- WiFi client 신호는 7단계 한글 등급으로 표시하고, hover/focus/touch 시 커스텀 툴팁에서 실제 dBm을 표시
|
||||||
- System Notice 최근 이력 표시
|
- System Notice 최근 이력 표시
|
||||||
|
- 저전압/스로틀링 반복, 디스크 사용률, 최근 네트워크 오류/드롭 기준 System Notice 조건
|
||||||
- process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시
|
- process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시
|
||||||
- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시
|
- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시
|
||||||
- 사용자 서비스 로그는 서비스별 최근 500줄까지 조회하고, 사용자가 서비스 목록을 스크롤하는 동안에는 자동 갱신이 화면 위치를 되돌리지 않습니다.
|
- 사용자 서비스 로그는 서비스별 최근 500줄까지 조회하고, 사용자가 서비스 목록을 스크롤하는 동안에는 자동 갱신이 화면 위치를 되돌리지 않습니다.
|
||||||
@@ -149,6 +150,7 @@ System Notice는 다음 조건으로 동작합니다.
|
|||||||
- System Notice 복구: 고정 기준선 또는 최신 rolling 기준선 대비 RPM/온도 차이가 각각 복구 차이 이하로 내려가면 normal로 돌아갑니다. 팬 모드가 `off`이면 fan off 기준선을 별도로 고정하고 System Notice alert를 만들지 않습니다.
|
- System Notice 복구: 고정 기준선 또는 최신 rolling 기준선 대비 RPM/온도 차이가 각각 복구 차이 이하로 내려가면 normal로 돌아갑니다. 팬 모드가 `off`이면 fan off 기준선을 별도로 고정하고 System Notice alert를 만들지 않습니다.
|
||||||
- System Notice 기록: alert 신규 진입 시 `system_notice_logs`에 기록합니다. `Alert 유지 중 반복 기록`을 켜면 유지 중에도 설정한 반복 기록 간격마다 다시 기록할 수 있습니다.
|
- System Notice 기록: alert 신규 진입 시 `system_notice_logs`에 기록합니다. `Alert 유지 중 반복 기록`을 켜면 유지 중에도 설정한 반복 기록 간격마다 다시 기록할 수 있습니다.
|
||||||
- 저전압/스로틀링 표시: `vcgencmd get_throttled`의 현재 비트와 부팅 후 이력 비트를 읽어 현재 상태, 최근 감지, 지속시간, 최근 10분 통계를 표시합니다. 이 값은 System Notice와 별개이며, 팬/온도 alert 조건에는 직접 섞지 않습니다.
|
- 저전압/스로틀링 표시: `vcgencmd get_throttled`의 현재 비트와 부팅 후 이력 비트를 읽어 현재 상태, 최근 감지, 지속시간, 최근 10분 통계를 표시합니다. 이 값은 System Notice와 별개이며, 팬/온도 alert 조건에는 직접 섞지 않습니다.
|
||||||
|
- 추가 Notice 조건: 설정값에 따라 저전압/스로틀링 반복, 디스크 사용률, 최근 네트워크 오류/드롭 증가량도 System Notice 이력에 함께 기록합니다.
|
||||||
|
|
||||||
## WiFi 관측 자동화
|
## WiFi 관측 자동화
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,12 @@ function setting_definitions(): array
|
|||||||
'notice.log_during_alert' => ['group' => 'notice', 'label' => 'Alert 유지 중 반복 기록', 'type' => 'boolean', 'default' => 0],
|
'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.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],
|
'notice.downward_only_process_hide' => ['group' => 'notice', 'label' => '하강 변화 원인 후보 숨김', 'type' => 'boolean', 'default' => 1],
|
||||||
|
'notice.low_voltage_repeated' => ['group' => 'notice', 'label' => '저전압 반복 알림', 'type' => 'boolean', 'default' => 1],
|
||||||
|
'notice.throttling_repeated' => ['group' => 'notice', 'label' => '스로틀링 반복 알림', 'type' => 'boolean', 'default' => 1],
|
||||||
|
'notice.disk_percent_threshold' => ['group' => 'notice', 'label' => '디스크 사용률 알림 기준', 'type' => 'number', 'default' => 90, 'min' => 50, 'max' => 100, 'step' => 1, 'unit' => '%'],
|
||||||
|
'notice.network_quality_window_seconds' => ['group' => 'notice', 'label' => '네트워크 품질 알림 판정창', 'type' => 'number', 'default' => 600, 'min' => 60, 'max' => 7200, 'step' => 30, 'unit' => '초'],
|
||||||
|
'notice.network_error_threshold' => ['group' => 'notice', 'label' => '네트워크 오류 알림 기준', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 1000, 'step' => 1, 'unit' => '개'],
|
||||||
|
'notice.network_drop_threshold' => ['group' => 'notice', 'label' => '네트워크 드롭 알림 기준', 'type' => 'number', 'default' => 5000, 'min' => 0, 'max' => 100000, 'step' => 10, 'unit' => '개'],
|
||||||
'display.process_limit' => ['group' => 'display', 'label' => '프로세스 후보 표시 수', 'type' => 'number', 'default' => 6, 'min' => 1, 'max' => 30, 'step' => 1, 'unit' => '개'],
|
'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' => 500, 'min' => 1, 'max' => 500, 'step' => 10, '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.custom_service_cache_seconds' => ['group' => 'display', 'label' => '사용자 서비스 캐시 시간', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 60, 'step' => 1, 'unit' => '초'],
|
||||||
|
|||||||
+80
-5
@@ -2712,7 +2712,78 @@ function notice_rolling_baseline(array $history, float $temp, float $rpm, float
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function fan_spike_analysis(array $history, array $fan, array $system, array $processes = []): array
|
function system_notice_extra_reasons(array $system, array $networkHistory): array
|
||||||
|
{
|
||||||
|
$reasons = [];
|
||||||
|
|
||||||
|
if ((bool)setting_value('notice.low_voltage_repeated') && !empty($system['low_voltage']['repeated'])) {
|
||||||
|
$count = (int)($system['low_voltage']['recent_event_count'] ?? 0);
|
||||||
|
$reasons[] = 'LOW VOLTAGE repeated ' . $count . 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool)setting_value('notice.throttling_repeated') && !empty($system['throttling']['repeated'])) {
|
||||||
|
$count = (int)($system['throttling']['recent_event_count'] ?? 0);
|
||||||
|
$reasons[] = 'THROTTLING repeated ' . $count . 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
$diskThreshold = (float)setting_value('notice.disk_percent_threshold');
|
||||||
|
$diskPercent = (float)($system['disk']['percent'] ?? 0);
|
||||||
|
if ($diskThreshold > 0 && $diskPercent >= $diskThreshold) {
|
||||||
|
$reasons[] = 'DISK ' . round($diskPercent, 1) . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$networkWindow = max(60, (int)setting_value('notice.network_quality_window_seconds'));
|
||||||
|
$errorThreshold = (float)setting_value('notice.network_error_threshold');
|
||||||
|
$dropThreshold = (float)setting_value('notice.network_drop_threshold');
|
||||||
|
if (($errorThreshold > 0 || $dropThreshold > 0) && $networkHistory !== []) {
|
||||||
|
$cutoff = time() - $networkWindow;
|
||||||
|
$totals = [];
|
||||||
|
$lastSeenAt = [];
|
||||||
|
foreach ($networkHistory as $row) {
|
||||||
|
$recordedAt = strtotime((string)($row['time'] ?? '')) ?: 0;
|
||||||
|
if ($recordedAt < $cutoff) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$iface = (string)($row['iface'] ?? '');
|
||||||
|
if ($iface === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totals[$iface] ??= [
|
||||||
|
'rx_errors' => 0.0,
|
||||||
|
'tx_errors' => 0.0,
|
||||||
|
'rx_dropped' => 0.0,
|
||||||
|
'tx_dropped' => 0.0,
|
||||||
|
];
|
||||||
|
$seconds = 1.0;
|
||||||
|
if (isset($lastSeenAt[$iface])) {
|
||||||
|
$seconds = (float)max(1, min(60, $recordedAt - $lastSeenAt[$iface]));
|
||||||
|
}
|
||||||
|
$lastSeenAt[$iface] = $recordedAt;
|
||||||
|
|
||||||
|
$totals[$iface]['rx_errors'] += max(0.0, (float)($row['rx_errors_per_sec'] ?? 0)) * $seconds;
|
||||||
|
$totals[$iface]['tx_errors'] += max(0.0, (float)($row['tx_errors_per_sec'] ?? 0)) * $seconds;
|
||||||
|
$totals[$iface]['rx_dropped'] += max(0.0, (float)($row['rx_dropped_per_sec'] ?? 0)) * $seconds;
|
||||||
|
$totals[$iface]['tx_dropped'] += max(0.0, (float)($row['tx_dropped_per_sec'] ?? 0)) * $seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($totals as $iface => $total) {
|
||||||
|
$errors = (int)round($total['rx_errors'] + $total['tx_errors']);
|
||||||
|
$drops = (int)round($total['rx_dropped'] + $total['tx_dropped']);
|
||||||
|
if ($errorThreshold > 0 && $errors >= $errorThreshold) {
|
||||||
|
$reasons[] = 'NET ' . $iface . ' errors +' . $errors;
|
||||||
|
}
|
||||||
|
if ($dropThreshold > 0 && $drops >= $dropThreshold) {
|
||||||
|
$reasons[] = 'NET ' . $iface . ' drops +' . $drops;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fan_spike_analysis(array $history, array $fan, array $system, array $processes = [], array $networkHistory = []): array
|
||||||
{
|
{
|
||||||
$rpm = (float)($fan['rpm'] ?? 0);
|
$rpm = (float)($fan['rpm'] ?? 0);
|
||||||
$pwm = (float)($fan['pwm'] ?? 0);
|
$pwm = (float)($fan['pwm'] ?? 0);
|
||||||
@@ -2788,8 +2859,11 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr
|
|||||||
$recoverRpmThreshold = (float)setting_value('notice.recover_rpm_delta');
|
$recoverRpmThreshold = (float)setting_value('notice.recover_rpm_delta');
|
||||||
$recoverTempThreshold = (float)setting_value('notice.recover_temp_delta');
|
$recoverTempThreshold = (float)setting_value('notice.recover_temp_delta');
|
||||||
|
|
||||||
if ($rpmExpected && abs($rpmDelta) >= $rpmThreshold) $reasons[] = 'RPM ' . signed_delta_text($rpmDelta);
|
$fanReasons = [];
|
||||||
if (abs($tempDelta) >= $tempThreshold) $reasons[] = 'TEMP ' . signed_delta_text($tempDelta, 'C');
|
if ($rpmExpected && abs($rpmDelta) >= $rpmThreshold) $fanReasons[] = 'RPM ' . signed_delta_text($rpmDelta);
|
||||||
|
if (abs($tempDelta) >= $tempThreshold) $fanReasons[] = 'TEMP ' . signed_delta_text($tempDelta, 'C');
|
||||||
|
$extraReasons = system_notice_extra_reasons($system, $networkHistory);
|
||||||
|
$reasons = array_values(array_merge($fanReasons, $extraReasons));
|
||||||
|
|
||||||
$alertStarted = false;
|
$alertStarted = false;
|
||||||
|
|
||||||
@@ -2812,8 +2886,9 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr
|
|||||||
save_system_notice_state('normal', $rollingBaseline['temp'], $rollingBaseline['rpm'], $rollingBaseline['pwm']);
|
save_system_notice_state('normal', $rollingBaseline['temp'], $rollingBaseline['rpm'], $rollingBaseline['pwm']);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$recovered = (abs($rpmDelta) <= $recoverRpmThreshold && abs($tempDelta) <= $recoverTempThreshold)
|
$fanRecovered = (abs($rpmDelta) <= $recoverRpmThreshold && abs($tempDelta) <= $recoverTempThreshold)
|
||||||
|| (abs($rollingRpmDelta) <= $recoverRpmThreshold && abs($rollingTempDelta) <= $recoverTempThreshold);
|
|| (abs($rollingRpmDelta) <= $recoverRpmThreshold && abs($rollingTempDelta) <= $recoverTempThreshold);
|
||||||
|
$recovered = $reasons === [] || ($extraReasons === [] && $fanRecovered);
|
||||||
if ($recovered || $reasons === []) {
|
if ($recovered || $reasons === []) {
|
||||||
$currentState = 'normal';
|
$currentState = 'normal';
|
||||||
save_system_notice_state('normal', $rollingBaseline['temp'], $rollingBaseline['rpm'], $rollingBaseline['pwm']);
|
save_system_notice_state('normal', $rollingBaseline['temp'], $rollingBaseline['rpm'], $rollingBaseline['pwm']);
|
||||||
@@ -4741,11 +4816,11 @@ function collect_snapshot(bool $applyFan = true): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
$processes = process_resource_data((int)setting_value('display.process_limit'));
|
$processes = process_resource_data((int)setting_value('display.process_limit'));
|
||||||
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes);
|
|
||||||
$network = network_info();
|
$network = network_info();
|
||||||
add_network_usage_log($network);
|
add_network_usage_log($network);
|
||||||
$networkHistory = network_usage_history(1000);
|
$networkHistory = network_usage_history(1000);
|
||||||
$networkQualityTotals = network_quality_totals();
|
$networkQualityTotals = network_quality_totals();
|
||||||
|
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes, $networkHistory);
|
||||||
|
|
||||||
$shouldLogNotice = !empty($fanSpike['alert_started'])
|
$shouldLogNotice = !empty($fanSpike['alert_started'])
|
||||||
|| (!empty($fanSpike['active']) && (bool)setting_value('notice.log_during_alert'));
|
|| (!empty($fanSpike['active']) && (bool)setting_value('notice.log_during_alert'));
|
||||||
|
|||||||
+93
-4
@@ -61,6 +61,7 @@
|
|||||||
dmesgOutput: $('#dmesgOutput'),
|
dmesgOutput: $('#dmesgOutput'),
|
||||||
dmesgMeta: $('#dmesgMeta'),
|
dmesgMeta: $('#dmesgMeta'),
|
||||||
synologyLogToggle: $('#synologyLogToggle'),
|
synologyLogToggle: $('#synologyLogToggle'),
|
||||||
|
synologyLogFilters: $('#synologyLogFilters'),
|
||||||
synologyLogOutput: $('#synologyLogOutput'),
|
synologyLogOutput: $('#synologyLogOutput'),
|
||||||
synologyLogMeta: $('#synologyLogMeta'),
|
synologyLogMeta: $('#synologyLogMeta'),
|
||||||
processDetails: $('#processDetails'),
|
processDetails: $('#processDetails'),
|
||||||
@@ -86,6 +87,8 @@
|
|||||||
synologyLogTimer: null,
|
synologyLogTimer: null,
|
||||||
synologyLogLatestKey: null,
|
synologyLogLatestKey: null,
|
||||||
synologyLogScrollHoldUntil: 0,
|
synologyLogScrollHoldUntil: 0,
|
||||||
|
synologyLogFilter: 'all',
|
||||||
|
synologyLogData: null,
|
||||||
ws: null,
|
ws: null,
|
||||||
wsConnected: false,
|
wsConnected: false,
|
||||||
wsFallbackTimer: null,
|
wsFallbackTimer: null,
|
||||||
@@ -225,6 +228,14 @@
|
|||||||
remoteFiles: 'Files',
|
remoteFiles: 'Files',
|
||||||
remoteUsage: 'Usage',
|
remoteUsage: 'Usage',
|
||||||
remoteBoots: 'Boots',
|
remoteBoots: 'Boots',
|
||||||
|
logFilterAll: 'All',
|
||||||
|
logFilterImportant: 'Important',
|
||||||
|
logFilterError: 'Error',
|
||||||
|
logFilterFailed: 'Failed',
|
||||||
|
logFilterBlocked: 'Blocked',
|
||||||
|
logFilterConnection: 'Connection',
|
||||||
|
logFilterSystem: 'System',
|
||||||
|
logFilterNoLines: 'No log lines match this filter.',
|
||||||
remoteSaved: 'Saved',
|
remoteSaved: 'Saved',
|
||||||
remotePending: 'Pending',
|
remotePending: 'Pending',
|
||||||
serviceLogs: 'Logs',
|
serviceLogs: 'Logs',
|
||||||
@@ -448,6 +459,14 @@
|
|||||||
remoteFiles: '파일',
|
remoteFiles: '파일',
|
||||||
remoteUsage: '사용량',
|
remoteUsage: '사용량',
|
||||||
remoteBoots: '부팅 이력',
|
remoteBoots: '부팅 이력',
|
||||||
|
logFilterAll: '전체',
|
||||||
|
logFilterImportant: '중요',
|
||||||
|
logFilterError: '오류',
|
||||||
|
logFilterFailed: '실패',
|
||||||
|
logFilterBlocked: '차단',
|
||||||
|
logFilterConnection: '연결',
|
||||||
|
logFilterSystem: '시스템',
|
||||||
|
logFilterNoLines: '필터에 맞는 로그가 없습니다.',
|
||||||
remoteSaved: '저장',
|
remoteSaved: '저장',
|
||||||
remotePending: '대기',
|
remotePending: '대기',
|
||||||
serviceLogs: '로그',
|
serviceLogs: '로그',
|
||||||
@@ -599,6 +618,7 @@
|
|||||||
if (els.synologyLogToggle) {
|
if (els.synologyLogToggle) {
|
||||||
els.synologyLogToggle.textContent = state.synologyLogOpen ? t('hide') : t('show');
|
els.synologyLogToggle.textContent = state.synologyLogOpen ? t('hide') : t('show');
|
||||||
}
|
}
|
||||||
|
updateSynologyLogFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initPreferences() {
|
function initPreferences() {
|
||||||
@@ -2048,7 +2068,15 @@
|
|||||||
if (Math.abs(m.tempDelta) >= tempThreshold) reasons.push(t(m.tempDelta >= 0 ? 'tempHigher' : 'tempLower'));
|
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'));
|
if (Math.abs(m.rpmDelta) >= rpmThreshold) reasons.push(t(m.rpmDelta >= 0 ? 'rpmHigher' : 'rpmLower'));
|
||||||
|
|
||||||
return `${t('recordedReason')}: ${reasons.length ? reasons.join(', ') : t('instantChange')}`;
|
if (reasons.length) {
|
||||||
|
return `${t('recordedReason')}: ${reasons.join(', ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = String(row.summary || '')
|
||||||
|
.replace(/^System notice(?: active)?:\s*/i, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return `${t('recordedReason')}: ${summary || t('instantChange')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noticePreviousText(row) {
|
function noticePreviousText(row) {
|
||||||
@@ -2878,11 +2906,56 @@
|
|||||||
els.synologyLogOutput.scrollTop = 0;
|
els.synologyLogOutput.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function synologyLogFilterLabel(filter = state.synologyLogFilter) {
|
||||||
|
const keys = {
|
||||||
|
all: 'logFilterAll',
|
||||||
|
important: 'logFilterImportant',
|
||||||
|
error: 'logFilterError',
|
||||||
|
failed: 'logFilterFailed',
|
||||||
|
blocked: 'logFilterBlocked',
|
||||||
|
connection: 'logFilterConnection',
|
||||||
|
system: 'logFilterSystem',
|
||||||
|
};
|
||||||
|
|
||||||
|
return t(keys[filter] || keys.important);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSynologyLogFilters() {
|
||||||
|
if (!els.synologyLogFilters) return;
|
||||||
|
|
||||||
|
els.synologyLogFilters.querySelectorAll('[data-log-filter]').forEach(button => {
|
||||||
|
const key = button.dataset.logFilter || 'all';
|
||||||
|
button.textContent = synologyLogFilterLabel(key);
|
||||||
|
button.dataset.active = key === state.synologyLogFilter ? '1' : '0';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function synologyLogLineMatches(line, filter) {
|
||||||
|
if (filter === 'all') return true;
|
||||||
|
|
||||||
|
const value = String(line || '').toLowerCase();
|
||||||
|
const patterns = {
|
||||||
|
important: /(error|err|fail|failed|failure|denied|blocked|drop|reject|disconnect|timeout|oom|panic|critical|warn|warning|throttl|undervoltage)/i,
|
||||||
|
error: /(error|err|exception|critical|panic|oom)/i,
|
||||||
|
failed: /(fail|failed|failure|timeout|unreachable|cannot|could not)/i,
|
||||||
|
blocked: /(blocked|denied|reject|drop|ban|iptables|nft|firewall)/i,
|
||||||
|
connection: /(connect|disconnect|connection|ssh|tls|handshake|listen|socket)/i,
|
||||||
|
system: /(systemd|kernel|docker|nginx|mariadb|rsyslog|syslog|journal)/i,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (patterns[filter] || patterns.important).test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filteredSynologyLogLines(lines) {
|
||||||
|
return (lines || []).filter(line => synologyLogLineMatches(line, state.synologyLogFilter));
|
||||||
|
}
|
||||||
|
|
||||||
function renderSynologyLog(data) {
|
function renderSynologyLog(data) {
|
||||||
if (!els.synologyLogOutput || !els.synologyLogMeta) return;
|
if (!els.synologyLogOutput || !els.synologyLogMeta) return;
|
||||||
if (Date.now() < state.synologyLogScrollHoldUntil) return;
|
if (Date.now() < state.synologyLogScrollHoldUntil) return;
|
||||||
|
|
||||||
const scrollTop = els.synologyLogOutput.scrollTop;
|
const scrollTop = els.synologyLogOutput.scrollTop;
|
||||||
|
state.synologyLogData = data || null;
|
||||||
|
|
||||||
if (!data?.available) {
|
if (!data?.available) {
|
||||||
els.synologyLogOutput.textContent = data?.message || t('synologyLogUnavailable');
|
els.synologyLogOutput.textContent = data?.message || t('synologyLogUnavailable');
|
||||||
@@ -2890,12 +2963,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = data.lines || [];
|
const rawLines = data.lines || [];
|
||||||
|
const lines = filteredSynologyLogLines(rawLines);
|
||||||
const latestKey = `${data.line_count || 0}\n${lines[0] || ''}`;
|
const latestKey = `${data.line_count || 0}\n${lines[0] || ''}`;
|
||||||
const hasNewLine = state.synologyLogLatestKey !== null && latestKey !== state.synologyLogLatestKey;
|
const hasNewLine = state.synologyLogLatestKey !== null && latestKey !== state.synologyLogLatestKey;
|
||||||
|
|
||||||
els.synologyLogOutput.textContent = lines.join('\n');
|
els.synologyLogOutput.textContent = lines.length ? lines.join('\n') : t('logFilterNoLines');
|
||||||
els.synologyLogMeta.textContent = `${data.path || '/var/log/remote/synology/chaegeon.log'} · ${data.line_count || 0} lines · updated ${data.updated_at || '-'}`;
|
els.synologyLogMeta.textContent = `${data.path || '/var/log/remote/synology/chaegeon.log'} · ${lines.length} / ${data.line_count || 0} lines · ${synologyLogFilterLabel()} · updated ${data.updated_at || '-'}`;
|
||||||
state.synologyLogLatestKey = latestKey;
|
state.synologyLogLatestKey = latestKey;
|
||||||
els.synologyLogOutput.scrollTop = Date.now() < state.synologyLogScrollHoldUntil ? scrollTop : 0;
|
els.synologyLogOutput.scrollTop = Date.now() < state.synologyLogScrollHoldUntil ? scrollTop : 0;
|
||||||
|
|
||||||
@@ -2934,9 +3008,13 @@
|
|||||||
if (els.synologyLogOutput) {
|
if (els.synologyLogOutput) {
|
||||||
els.synologyLogOutput.hidden = !open;
|
els.synologyLogOutput.hidden = !open;
|
||||||
}
|
}
|
||||||
|
if (els.synologyLogFilters) {
|
||||||
|
els.synologyLogFilters.hidden = !open;
|
||||||
|
}
|
||||||
if (els.synologyLogToggle) {
|
if (els.synologyLogToggle) {
|
||||||
els.synologyLogToggle.textContent = open ? t('hide') : t('show');
|
els.synologyLogToggle.textContent = open ? t('hide') : t('show');
|
||||||
}
|
}
|
||||||
|
updateSynologyLogFilters();
|
||||||
|
|
||||||
stopSynologyLogFallback();
|
stopSynologyLogFallback();
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -3094,6 +3172,17 @@
|
|||||||
els.synologyLogToggle?.addEventListener('click', () => {
|
els.synologyLogToggle?.addEventListener('click', () => {
|
||||||
setSynologyLogOpen(!state.synologyLogOpen);
|
setSynologyLogOpen(!state.synologyLogOpen);
|
||||||
});
|
});
|
||||||
|
els.synologyLogFilters?.addEventListener('click', event => {
|
||||||
|
const button = event.target?.closest?.('[data-log-filter]');
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
state.synologyLogFilter = button.dataset.logFilter || 'all';
|
||||||
|
state.synologyLogScrollHoldUntil = 0;
|
||||||
|
updateSynologyLogFilters();
|
||||||
|
if (state.synologyLogData) {
|
||||||
|
renderSynologyLog(state.synologyLogData);
|
||||||
|
}
|
||||||
|
});
|
||||||
const holdSynologyLogScroll = () => {
|
const holdSynologyLogScroll = () => {
|
||||||
state.synologyLogScrollHoldUntil = Date.now() + 4000;
|
state.synologyLogScrollHoldUntil = Date.now() + 4000;
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-1
@@ -152,7 +152,7 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
|
|||||||
.wifi-signal{font-weight:700;font-variant-numeric:tabular-nums;cursor:help;text-decoration:underline;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px}.wifi-signal.best{color:#16a34a}.wifi-signal.excellent{color:#22c55e}.wifi-signal.good{color:#38bdf8}.wifi-signal.fine{color:#0ea5e9}.wifi-signal.normal{color:#f59e0b}.wifi-signal.weak{color:#fb7185}.wifi-signal.bad{color:#ef4444}.wifi-signal.unknown{color:var(--sub);text-decoration:none;cursor:default}
|
.wifi-signal{font-weight:700;font-variant-numeric:tabular-nums;cursor:help;text-decoration:underline;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px}.wifi-signal.best{color:#16a34a}.wifi-signal.excellent{color:#22c55e}.wifi-signal.good{color:#38bdf8}.wifi-signal.fine{color:#0ea5e9}.wifi-signal.normal{color:#f59e0b}.wifi-signal.weak{color:#fb7185}.wifi-signal.bad{color:#ef4444}.wifi-signal.unknown{color:var(--sub);text-decoration:none;cursor:default}
|
||||||
.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.wide{grid-column:span 2}.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}.quality-summary{height:48px;padding:0 12px;display:flex;align-items:center}.quality-pill{display:grid;grid-template-columns:auto 1fr;gap:8px 18px;width:100%;font-size:13px;color:var(--sub);font-variant-numeric:tabular-nums}.quality-pill strong{color:var(--text);font-size:15px;font-weight:500}.quality-pill span:last-child{justify-self:end}
|
.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.wide{grid-column:span 2}.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}.quality-summary{height:48px;padding:0 12px;display:flex;align-items:center}.quality-pill{display:grid;grid-template-columns:auto 1fr;gap:8px 18px;width:100%;font-size:13px;color:var(--sub);font-variant-numeric:tabular-nums}.quality-pill strong{color:var(--text);font-size:15px;font-weight:500}.quality-pill span:last-child{justify-self:end}
|
||||||
.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}
|
.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-list.remote-log-list{grid-template-columns:repeat(auto-fit,minmax(230px,1fr));max-height:none}.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.warning{color:#fde68a;border-color:rgba(245,158,11,.42);background:rgba(180,83,9,.16)}.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-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-list.remote-log-list{grid-template-columns:repeat(auto-fit,minmax(230px,1fr));max-height:none}.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.warning{color:#fde68a;border-color:rgba(245,158,11,.42);background:rgba(180,83,9,.16)}.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}.log-filter-bar{display:flex;gap:6px;flex-wrap:wrap;margin:10px 0}.log-filter{min-width:0;height:32px;padding:0 10px;border-radius:10px;font-size:12px}.log-filter[data-active="1"]{background:var(--blue);color:#fff}.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}
|
||||||
html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}html[data-theme="light"] .service-item{background:#e8f0f8;border-color:#b7c8dd}html[data-theme="light"] .service-name{color:#0f1f35}html[data-theme="light"] .service-desc,html[data-theme="light"] .service-meta{color:#42526a}html[data-theme="light"] .service-state{background:#f8fbff;border-color:#aebed2;color:#304057}html[data-theme="light"] .service-state.active{background:#d8f3e3;border-color:#7fc99c;color:#146c39}html[data-theme="light"] .service-state.warning{background:#fff2c7;border-color:#dca642;color:#8a4d08}html[data-theme="light"] .service-state.failed{background:#ffe2e2;border-color:#e38b8b;color:#9f1d1d}html[data-theme="light"] .service-log,html[data-theme="light"] .dmesg-log{background:#f7fafc;border-color:#b9c8da;color:#0f2742}
|
html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}html[data-theme="light"] .service-item{background:#e8f0f8;border-color:#b7c8dd}html[data-theme="light"] .service-name{color:#0f1f35}html[data-theme="light"] .service-desc,html[data-theme="light"] .service-meta{color:#42526a}html[data-theme="light"] .service-state{background:#f8fbff;border-color:#aebed2;color:#304057}html[data-theme="light"] .service-state.active{background:#d8f3e3;border-color:#7fc99c;color:#146c39}html[data-theme="light"] .service-state.warning{background:#fff2c7;border-color:#dca642;color:#8a4d08}html[data-theme="light"] .service-state.failed{background:#ffe2e2;border-color:#e38b8b;color:#9f1d1d}html[data-theme="light"] .service-log,html[data-theme="light"] .dmesg-log{background:#f7fafc;border-color:#b9c8da;color:#0f2742}
|
||||||
.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}
|
.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)}
|
.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)}
|
||||||
@@ -389,6 +389,15 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
|||||||
<h3 data-i18n="synologyLog">Synology Log</h3>
|
<h3 data-i18n="synologyLog">Synology Log</h3>
|
||||||
<button class="btn secondary" id="synologyLogToggle" type="button" data-i18n="show">Show</button>
|
<button class="btn secondary" id="synologyLogToggle" type="button" data-i18n="show">Show</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="log-filter-bar" id="synologyLogFilters" hidden>
|
||||||
|
<button class="btn secondary log-filter" type="button" data-log-filter="all" data-active="1">All</button>
|
||||||
|
<button class="btn secondary log-filter" type="button" data-log-filter="important">Important</button>
|
||||||
|
<button class="btn secondary log-filter" type="button" data-log-filter="error">Error</button>
|
||||||
|
<button class="btn secondary log-filter" type="button" data-log-filter="failed">Failed</button>
|
||||||
|
<button class="btn secondary log-filter" type="button" data-log-filter="blocked">Blocked</button>
|
||||||
|
<button class="btn secondary log-filter" type="button" data-log-filter="connection">Connection</button>
|
||||||
|
<button class="btn secondary log-filter" type="button" data-log-filter="system">System</button>
|
||||||
|
</div>
|
||||||
<div class="dmesg-meta" id="synologyLogMeta">/var/log/remote/synology/chaegeon.log</div>
|
<div class="dmesg-meta" id="synologyLogMeta">/var/log/remote/synology/chaegeon.log</div>
|
||||||
<pre class="dmesg-log" id="synologyLogOutput" hidden></pre>
|
<pre class="dmesg-log" id="synologyLogOutput" hidden></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user