Notice 조건과 원격 로그 필터 보강

This commit is contained in:
seo
2026-07-26 01:36:07 +09:00
parent 58a81420d1
commit a42b6d7df7
5 changed files with 191 additions and 10 deletions
+80 -5
View File
@@ -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'));
+93 -4
View File
@@ -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;
};
+10 -1
View File
@@ -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}
<h3 data-i18n="synologyLog">Synology Log</h3>
<button class="btn secondary" id="synologyLogToggle" type="button" data-i18n="show">Show</button>
</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>
<pre class="dmesg-log" id="synologyLogOutput" hidden></pre>
</div>