diff --git a/README.md b/README.md index f3beb1e..e4678dc 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - process CPU/MEM 후보는 프로세스 상세 접힘 영역에서 표시 - `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시 - 사용자 서비스 로그는 서비스별 최근 500줄까지 조회하고, 사용자가 서비스 목록을 스크롤하는 동안에는 자동 갱신이 화면 위치를 되돌리지 않습니다. +- 라즈베리파이와 시놀로지 사이 원격 syslog 송수신 상태, 영구 journal, 커널 크래시 보존 상태를 진단 접힘 영역에서 표시 - dmesg 로그는 진단 접힘 영역에서 필요할 때 열람 - WakeLock 버튼으로 대시보드 화면 꺼짐 방지 - Reboot 버튼으로 2단계 확인 후 시스템 재부팅 요청 @@ -85,6 +86,14 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, 선택적 배터리 설정, WOL 전용 API 토큰 - UPS/배터리 기능은 `/home/seo/secret/control.php`의 `battery.enabled`가 명시적으로 true일 때만 동작합니다. 현재 운영 기준은 UPS 제거 상태이므로 기본값은 false입니다. +## 원격 로그 + +라즈베리파이는 `rsyslog`와 `rsyslog-gnutls`로 로컬 journal/syslog를 시놀로지 Log Center에 TCP/TLS 6514로 보냅니다. 전송 대상은 `chaegeon.com:6514`이며, 네트워크가 끊기면 `/var/spool/rsyslog/control_synology_fwd*` 큐에 남았다가 재시도합니다. + +시놀로지는 DSM `syslog-ng`에서 라즈베리파이 `121.157.202.229:6514`로 TCP/TLS syslog를 보냅니다. 라즈베리파이 수신 로그는 `/var/log/remote/synology/*.log`에 저장하고 `/etc/logrotate.d/control-remote-syslog`가 매일 회전합니다. + +커널 패닉이나 강제 재부팅 분석을 위해 `systemd-journald`는 `/var/log/journal`에 영구 저장합니다. `dtoverlay=ramoops` 기반 pstore는 부팅 직후 `control-pstore-collect.service`가 `/var/log/pstore`로 복사합니다. 현재 pstore에 남은 크래시 조각이 없으면 이 서비스는 조건 불충족으로 `inactive`일 수 있으며 정상입니다. + ## UPS/배터리 상태 UPS가 과부하 상황에서 전압 하강과 강제 종료를 반복해 현재 운영 구성에서는 제거되어 있습니다. 이에 따라 Control은 기본적으로 MAX17048 배터리 센서를 읽지 않고, 배터리 SOC/전압 차트와 잔여 시간 UI도 표시하지 않습니다. diff --git a/public/api.php b/public/api.php index 5faf0c2..61d7710 100644 --- a/public/api.php +++ b/public/api.php @@ -121,6 +121,141 @@ function dmesg_log(): array ]; } +function command_first_line(array $cmd, bool $root = false, int $timeout = 3): string +{ + $result = sh($cmd, $root, $timeout); + $lines = array_values(array_filter( + preg_split('/\R/', trim((string)$result['out'])) ?: [], + static fn($line): bool => trim((string)$line) !== '' + )); + + return (int)$result['code'] === 0 && $lines ? (string)$lines[0] : ''; +} + +function remote_log_file_status(string $dir): array +{ + $files = []; + $totalBytes = 0; + $newest = null; + + foreach (glob(rtrim($dir, '/') . '/*.log') ?: [] as $path) { + $size = filesize($path) ?: 0; + $mtime = filemtime($path) ?: 0; + $totalBytes += $size; + + $row = [ + 'name' => basename($path), + 'path' => $path, + 'size_bytes' => $size, + 'size' => bytes_human($size), + 'updated_at' => $mtime > 0 ? date('Y-m-d H:i:s', $mtime) : null, + 'age_seconds' => $mtime > 0 ? max(0, time() - $mtime) : null, + ]; + $files[] = $row; + + if ($newest === null || (int)$row['age_seconds'] < (int)($newest['age_seconds'] ?? PHP_INT_MAX)) { + $newest = $row; + } + } + + usort($files, static fn($a, $b): int => (int)($a['age_seconds'] ?? PHP_INT_MAX) <=> (int)($b['age_seconds'] ?? PHP_INT_MAX)); + + return [ + 'path' => $dir, + 'available' => is_dir($dir), + 'file_count' => count($files), + 'total_bytes' => $totalBytes, + 'total_size' => bytes_human($totalBytes), + 'newest' => $newest, + 'files' => array_slice($files, 0, 5), + ]; +} + +function remote_log_queue_status(): array +{ + $result = sh([ + '/usr/bin/find', + '/var/spool/rsyslog', + '-maxdepth', + '1', + '-name', + 'control_synology_fwd*', + '-printf', + '%s\n', + ], true, 3); + + if ((int)$result['code'] !== 0) { + return [ + 'available' => false, + 'file_count' => null, + 'total_bytes' => null, + 'total_size' => '-', + ]; + } + + $sizes = array_values(array_filter( + array_map('trim', preg_split('/\R/', trim((string)$result['out'])) ?: []), + static fn($line): bool => $line !== '' + )); + $total = array_sum(array_map('intval', $sizes)); + + return [ + 'available' => true, + 'file_count' => count($sizes), + 'total_bytes' => $total, + 'total_size' => bytes_human($total), + ]; +} + +function remote_log_status(): array +{ + $journalDir = '/var/log/journal'; + $journalDisk = command_first_line(['/usr/bin/journalctl', '--disk-usage', '--no-pager'], false, 3); + $journalBoots = command_first_line(['/usr/bin/bash', '-lc', 'journalctl --list-boots --no-pager 2>/dev/null | wc -l'], false, 3); + $rsyslogActive = command_first_line(['/usr/bin/systemctl', 'is-active', 'rsyslog'], false, 3) ?: 'unknown'; + $rsyslogEnabled = command_first_line(['/usr/bin/systemctl', 'is-enabled', 'rsyslog'], false, 3) ?: 'unknown'; + $pstoreActive = command_first_line(['/usr/bin/systemctl', 'is-active', 'control-pstore-collect.service'], false, 3) ?: 'inactive'; + $listener = sh(['/usr/bin/ss', '-lnt', 'sport', '=', ':6514'], false, 3); + $listenerOk = (int)$listener['code'] === 0 && str_contains((string)$listener['out'], ':6514'); + $remoteFiles = remote_log_file_status('/var/log/remote/synology'); + $queue = remote_log_queue_status(); + $savedCrashFiles = glob('/var/log/pstore/*') ?: []; + $pstoreFiles = glob('/sys/fs/pstore/*') ?: []; + + return [ + 'raspberry_to_synology' => [ + 'target' => 'chaegeon.com:6514', + 'transport' => 'TCP/TLS', + 'rsyslog_active' => $rsyslogActive, + 'rsyslog_enabled' => $rsyslogEnabled, + 'queue' => $queue, + 'ok' => $rsyslogActive === 'active', + ], + 'synology_to_raspberry' => [ + 'listen_port' => 6514, + 'transport' => 'TCP/TLS', + 'listener' => $listenerOk, + 'logs' => $remoteFiles, + 'ok' => $listenerOk && $remoteFiles['file_count'] > 0, + ], + 'journal' => [ + 'path' => $journalDir, + 'persistent' => is_dir($journalDir), + 'disk_usage' => $journalDisk ?: '-', + 'boot_count' => is_numeric(trim($journalBoots)) ? (int)trim($journalBoots) : null, + 'ok' => is_dir($journalDir), + ], + 'pstore' => [ + 'mounted' => is_dir('/sys/fs/pstore'), + 'collector_active' => $pstoreActive, + 'pending_files' => count($pstoreFiles), + 'saved_files' => count($savedCrashFiles), + 'saved_path' => '/var/log/pstore', + 'ok' => is_dir('/sys/fs/pstore'), + ], + ]; +} + function power_event_state_read(string $eventKey): array { $state = [ @@ -4123,6 +4258,7 @@ function collect_snapshot(bool $applyFan = true): array 'history' => $history, 'processes' => $processes, 'custom_services' => custom_systemd_services(), + 'remote_logs' => remote_log_status(), 'touch_display' => touch_display_status(), 'fan_spike' => $fanSpike, 'fan_spike_history' => fan_spike_history((int)setting_value('display.fan_notice_history_limit')), diff --git a/public/assets/app.js b/public/assets/app.js index e1ad980..2bde63a 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -37,6 +37,7 @@ statusBatteryRemaining: $('#statusBatteryRemaining'), batteryRuntimeTooltip: $('#batteryRuntimeTooltip'), spikeLogList: $('#spikeLogList'), + remoteLogList: $('#remoteLogList'), customServiceList: $('#customServiceList'), noticeBaseline: $('#noticeBaseline'), rebootBtn: $('#rebootBtn'), @@ -179,6 +180,24 @@ noNoticeHistory: 'No system notice history.', customServices: 'Custom Services', noCustomServices: 'No custom services.', + remoteLogs: 'Remote Logs', + remoteLogsLoading: 'Remote log status loading...', + remotePiToSynology: 'Raspberry -> Synology', + remoteSynologyToPi: 'Synology -> Raspberry', + remotePersistentJournal: 'Persistent Journal', + remoteCrashLogs: 'Crash Logs', + remoteOk: 'OK', + remoteWarn: 'Check', + remoteTarget: 'Target', + remoteTransport: 'Transport', + remoteQueue: 'Queue', + remoteListener: 'Listener', + remoteLatest: 'Latest', + remoteFiles: 'Files', + remoteUsage: 'Usage', + remoteBoots: 'Boots', + remoteSaved: 'Saved', + remotePending: 'Pending', serviceLogs: 'Logs', serviceEnabled: 'enabled', servicePid: 'pid', @@ -353,6 +372,24 @@ noNoticeHistory: '시스템 알림 이력이 없습니다.', customServices: '사용자 서비스', noCustomServices: '사용자 생성 서비스가 없습니다.', + remoteLogs: '원격 로그', + remoteLogsLoading: '원격 로그 상태 확인 중...', + remotePiToSynology: '라즈베리파이 -> 시놀로지', + remoteSynologyToPi: '시놀로지 -> 라즈베리파이', + remotePersistentJournal: '영구 저널', + remoteCrashLogs: '커널 크래시', + remoteOk: '정상', + remoteWarn: '확인', + remoteTarget: '대상', + remoteTransport: '전송', + remoteQueue: '대기열', + remoteListener: '수신', + remoteLatest: '최근', + remoteFiles: '파일', + remoteUsage: '사용량', + remoteBoots: '부팅 이력', + remoteSaved: '저장', + remotePending: '대기', serviceLogs: '로그', serviceEnabled: '활성화', servicePid: 'PID', @@ -1957,6 +1994,106 @@ : `
${escapeHtml(t('noNoticeHistory'))}
`; } + function remoteState(ok, warn = false) { + if (ok) { + return { + className: 'active', + label: t('remoteOk'), + }; + } + + return { + className: warn ? 'warning' : 'failed', + label: t('remoteWarn'), + }; + } + + function remoteMeta(items) { + return items + .filter(item => item.value !== null && item.value !== undefined && item.value !== '') + .map(item => `${item.label}: ${item.value}`) + .join(' · ') || '-'; + } + + function remoteAge(row) { + if (!row || row.age_seconds === null || row.age_seconds === undefined) { + return '-'; + } + + const formatted = formatDhms(Number(row.age_seconds)); + return formatted ? `${formatted} ${t('ago')}` : '-'; + } + + function renderRemoteLogs(data = {}) { + if (!els.remoteLogList) return; + + const piToSynology = data.raspberry_to_synology || {}; + const synologyToPi = data.synology_to_raspberry || {}; + const journal = data.journal || {}; + const pstore = data.pstore || {}; + const newest = synologyToPi.logs?.newest || null; + const queue = piToSynology.queue || {}; + const rows = [ + { + title: t('remotePiToSynology'), + desc: remoteMeta([ + { label: t('remoteTarget'), value: piToSynology.target || '-' }, + { label: t('remoteTransport'), value: piToSynology.transport || '-' }, + ]), + meta: remoteMeta([ + { label: 'rsyslog', value: `${piToSynology.rsyslog_active || '-'} / ${piToSynology.rsyslog_enabled || '-'}` }, + { label: t('remoteQueue'), value: queue.available === false ? '-' : `${Number(queue.file_count || 0).toLocaleString()} / ${queue.total_size || '-'}` }, + ]), + state: remoteState(Boolean(piToSynology.ok), true), + }, + { + title: t('remoteSynologyToPi'), + desc: remoteMeta([ + { label: t('remoteListener'), value: synologyToPi.listener ? `:${synologyToPi.listen_port || 6514}` : '-' }, + { label: t('remoteTransport'), value: synologyToPi.transport || '-' }, + ]), + meta: remoteMeta([ + { label: t('remoteLatest'), value: newest ? `${newest.name || '-'} / ${remoteAge(newest)}` : '-' }, + { label: t('remoteFiles'), value: `${Number(synologyToPi.logs?.file_count || 0).toLocaleString()} / ${synologyToPi.logs?.total_size || '-'}` }, + ]), + state: remoteState(Boolean(synologyToPi.ok), true), + }, + { + title: t('remotePersistentJournal'), + desc: journal.path || '-', + meta: remoteMeta([ + { label: t('remoteUsage'), value: journal.disk_usage || '-' }, + { label: t('remoteBoots'), value: journal.boot_count === null || journal.boot_count === undefined ? '-' : Number(journal.boot_count).toLocaleString() }, + ]), + state: remoteState(Boolean(journal.ok), true), + }, + { + title: t('remoteCrashLogs'), + desc: pstore.saved_path || '-', + meta: remoteMeta([ + { label: 'pstore', value: pstore.mounted ? 'mounted' : '-' }, + { label: 'collector', value: pstore.collector_active || '-' }, + { label: t('remoteSaved'), value: Number(pstore.saved_files || 0).toLocaleString() }, + { label: t('remotePending'), value: Number(pstore.pending_files || 0).toLocaleString() }, + ]), + state: remoteState(Boolean(pstore.ok), true), + }, + ]; + + els.remoteLogList.innerHTML = rows.map(row => ` +
+
+
+
${escapeHtml(row.title)}
+
${escapeHtml(row.desc)}
+
+
${escapeHtml(row.state.label)}
+
+
${escapeHtml(row.meta)}
+
+ `).join(''); + } + function renderCustomServices(rows = []) { if (!els.customServiceList) return; if (Date.now() < state.customServiceScrollHoldUntil) return; @@ -2011,6 +2148,7 @@ ? `${baselineTemp.toFixed(1)}°C / ${Math.round(baselineRpm).toLocaleString()} RPM · ${stateText}` : stateText; renderSpikeHistory(data.fan_spike_history || []); + renderRemoteLogs(data.remote_logs || {}); renderCustomServices(data.custom_services || []); setText(els.noticeBaseline, baselineText); diff --git a/public/index.php b/public/index.php index 4afdf10..0e09033 100644 --- a/public/index.php +++ b/public/index.php @@ -138,8 +138,8 @@ 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 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} .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-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.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} -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.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-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} +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)} .status-value.interactive{cursor:help;text-decoration:underline;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:4px}.runtime-tooltip{position:fixed;left:0;top:0;z-index:110;display:none;width:min(520px,calc(100vw - 28px));max-height:calc(100vh - 28px);overflow:hidden;padding:14px 16px;border:1px solid rgba(84,101,128,.78);border-radius:16px;background:var(--card);color:var(--text);box-shadow:0 18px 48px rgba(0,0,0,.38);font-size:13px;line-height:1.55;white-space:pre-line;word-break:keep-all;overflow-wrap:anywhere;pointer-events:none}.runtime-tooltip.signal-tooltip{width:auto;min-width:118px;max-width:220px;padding:9px 12px;border-radius:12px;font-size:12px;line-height:1.45;box-shadow:0 12px 30px rgba(0,0,0,.28)}.runtime-tooltip[data-open="1"]{display:block}.runtime-tooltip::before{content:"";position:absolute;left:18px;top:-7px;width:12px;height:12px;border-left:1px solid rgba(84,101,128,.78);border-top:1px solid rgba(84,101,128,.78);background:var(--card);transform:rotate(45deg)}html[data-theme="light"] .runtime-tooltip{background:#f8fbff;border-color:#aebed2;color:#172033;box-shadow:0 18px 44px rgba(28,43,64,.18)}html[data-theme="light"] .runtime-tooltip.signal-tooltip{box-shadow:0 12px 28px rgba(28,43,64,.16)}html[data-theme="light"] .runtime-tooltip::before{background:#f8fbff;border-color:#aebed2} @@ -316,6 +316,13 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
Diagnostics
+
+

Remote Logs

+
+
Remote log status loading...
+
+
+

Custom Services