From a42b6d7df780938ac5395fedebb23ca2381cea2a Mon Sep 17 00:00:00 2001 From: seo Date: Sun, 26 Jul 2026 01:36:07 +0900 Subject: [PATCH] =?UTF-8?q?Notice=20=EC=A1=B0=EA=B1=B4=EA=B3=BC=20?= =?UTF-8?q?=EC=9B=90=EA=B2=A9=20=EB=A1=9C=EA=B7=B8=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + config/config.php | 6 +++ public/api.php | 85 +++++++++++++++++++++++++++++++++++--- public/assets/app.js | 97 ++++++++++++++++++++++++++++++++++++++++++-- public/index.php | 11 ++++- 5 files changed, 191 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bcde38b..006ab0b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정 - WiFi client 신호는 7단계 한글 등급으로 표시하고, hover/focus/touch 시 커스텀 툴팁에서 실제 dBm을 표시 - System Notice 최근 이력 표시 +- 저전압/스로틀링 반복, 디스크 사용률, 최근 네트워크 오류/드롭 기준 System Notice 조건 - process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시 - `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시 - 사용자 서비스 로그는 서비스별 최근 500줄까지 조회하고, 사용자가 서비스 목록을 스크롤하는 동안에는 자동 갱신이 화면 위치를 되돌리지 않습니다. @@ -149,6 +150,7 @@ System Notice는 다음 조건으로 동작합니다. - System Notice 복구: 고정 기준선 또는 최신 rolling 기준선 대비 RPM/온도 차이가 각각 복구 차이 이하로 내려가면 normal로 돌아갑니다. 팬 모드가 `off`이면 fan off 기준선을 별도로 고정하고 System Notice alert를 만들지 않습니다. - System Notice 기록: alert 신규 진입 시 `system_notice_logs`에 기록합니다. `Alert 유지 중 반복 기록`을 켜면 유지 중에도 설정한 반복 기록 간격마다 다시 기록할 수 있습니다. - 저전압/스로틀링 표시: `vcgencmd get_throttled`의 현재 비트와 부팅 후 이력 비트를 읽어 현재 상태, 최근 감지, 지속시간, 최근 10분 통계를 표시합니다. 이 값은 System Notice와 별개이며, 팬/온도 alert 조건에는 직접 섞지 않습니다. +- 추가 Notice 조건: 설정값에 따라 저전압/스로틀링 반복, 디스크 사용률, 최근 네트워크 오류/드롭 증가량도 System Notice 이력에 함께 기록합니다. ## WiFi 관측 자동화 diff --git a/config/config.php b/config/config.php index 5f1b720..65ed608 100644 --- a/config/config.php +++ b/config/config.php @@ -172,6 +172,12 @@ function setting_definitions(): array '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], + '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.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' => '초'], diff --git a/public/api.php b/public/api.php index b5e2201..b54a147 100644 --- a/public/api.php +++ b/public/api.php @@ -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); $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'); $recoverTempThreshold = (float)setting_value('notice.recover_temp_delta'); - if ($rpmExpected && abs($rpmDelta) >= $rpmThreshold) $reasons[] = 'RPM ' . signed_delta_text($rpmDelta); - if (abs($tempDelta) >= $tempThreshold) $reasons[] = 'TEMP ' . signed_delta_text($tempDelta, 'C'); + $fanReasons = []; + 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; @@ -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']); } } else { - $recovered = (abs($rpmDelta) <= $recoverRpmThreshold && abs($tempDelta) <= $recoverTempThreshold) + $fanRecovered = (abs($rpmDelta) <= $recoverRpmThreshold && abs($tempDelta) <= $recoverTempThreshold) || (abs($rollingRpmDelta) <= $recoverRpmThreshold && abs($rollingTempDelta) <= $recoverTempThreshold); + $recovered = $reasons === [] || ($extraReasons === [] && $fanRecovered); if ($recovered || $reasons === []) { $currentState = 'normal'; 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')); - $fanSpike = fan_spike_analysis($history, $fan, $system, $processes); $network = network_info(); add_network_usage_log($network); $networkHistory = network_usage_history(1000); $networkQualityTotals = network_quality_totals(); + $fanSpike = fan_spike_analysis($history, $fan, $system, $processes, $networkHistory); $shouldLogNotice = !empty($fanSpike['alert_started']) || (!empty($fanSpike['active']) && (bool)setting_value('notice.log_during_alert')); diff --git a/public/assets/app.js b/public/assets/app.js index b818df9..b4abff9 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -61,6 +61,7 @@ dmesgOutput: $('#dmesgOutput'), dmesgMeta: $('#dmesgMeta'), synologyLogToggle: $('#synologyLogToggle'), + synologyLogFilters: $('#synologyLogFilters'), synologyLogOutput: $('#synologyLogOutput'), synologyLogMeta: $('#synologyLogMeta'), processDetails: $('#processDetails'), @@ -86,6 +87,8 @@ synologyLogTimer: null, synologyLogLatestKey: null, synologyLogScrollHoldUntil: 0, + synologyLogFilter: 'all', + synologyLogData: null, ws: null, wsConnected: false, wsFallbackTimer: null, @@ -225,6 +228,14 @@ remoteFiles: 'Files', remoteUsage: 'Usage', 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', remotePending: 'Pending', serviceLogs: 'Logs', @@ -448,6 +459,14 @@ remoteFiles: '파일', remoteUsage: '사용량', remoteBoots: '부팅 이력', + logFilterAll: '전체', + logFilterImportant: '중요', + logFilterError: '오류', + logFilterFailed: '실패', + logFilterBlocked: '차단', + logFilterConnection: '연결', + logFilterSystem: '시스템', + logFilterNoLines: '필터에 맞는 로그가 없습니다.', remoteSaved: '저장', remotePending: '대기', serviceLogs: '로그', @@ -599,6 +618,7 @@ if (els.synologyLogToggle) { els.synologyLogToggle.textContent = state.synologyLogOpen ? t('hide') : t('show'); } + updateSynologyLogFilters(); } function initPreferences() { @@ -2048,7 +2068,15 @@ 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')}`; + 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) { @@ -2878,11 +2906,56 @@ 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) { if (!els.synologyLogOutput || !els.synologyLogMeta) return; if (Date.now() < state.synologyLogScrollHoldUntil) return; const scrollTop = els.synologyLogOutput.scrollTop; + state.synologyLogData = data || null; if (!data?.available) { els.synologyLogOutput.textContent = data?.message || t('synologyLogUnavailable'); @@ -2890,12 +2963,13 @@ return; } - const lines = data.lines || []; + const rawLines = data.lines || []; + const lines = filteredSynologyLogLines(rawLines); const latestKey = `${data.line_count || 0}\n${lines[0] || ''}`; const hasNewLine = state.synologyLogLatestKey !== null && latestKey !== state.synologyLogLatestKey; - els.synologyLogOutput.textContent = lines.join('\n'); - els.synologyLogMeta.textContent = `${data.path || '/var/log/remote/synology/chaegeon.log'} · ${data.line_count || 0} lines · updated ${data.updated_at || '-'}`; + els.synologyLogOutput.textContent = lines.length ? lines.join('\n') : t('logFilterNoLines'); + 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; els.synologyLogOutput.scrollTop = Date.now() < state.synologyLogScrollHoldUntil ? scrollTop : 0; @@ -2934,9 +3008,13 @@ if (els.synologyLogOutput) { els.synologyLogOutput.hidden = !open; } + if (els.synologyLogFilters) { + els.synologyLogFilters.hidden = !open; + } if (els.synologyLogToggle) { els.synologyLogToggle.textContent = open ? t('hide') : t('show'); } + updateSynologyLogFilters(); stopSynologyLogFallback(); if (open) { @@ -3094,6 +3172,17 @@ els.synologyLogToggle?.addEventListener('click', () => { 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 = () => { state.synologyLogScrollHoldUntil = Date.now() + 4000; }; diff --git a/public/index.php b/public/index.php index 46265ed..aa8dddc 100644 --- a/public/index.php +++ b/public/index.php @@ -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} .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} -.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} .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)} @@ -389,6 +389,15 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}

Synology Log

+
/var/log/remote/synology/chaegeon.log