원격 로그 상태 진단 추가
This commit is contained in:
+136
@@ -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')),
|
||||
|
||||
@@ -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 @@
|
||||
: `<div class="spike-log-empty">${escapeHtml(t('noNoticeHistory'))}</div>`;
|
||||
}
|
||||
|
||||
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 => `
|
||||
<div class="service-item">
|
||||
<div class="service-head">
|
||||
<div class="service-title">
|
||||
<div class="service-name">${escapeHtml(row.title)}</div>
|
||||
<div class="service-desc">${escapeHtml(row.desc)}</div>
|
||||
</div>
|
||||
<div class="service-state ${row.state.className}">${escapeHtml(row.state.label)}</div>
|
||||
</div>
|
||||
<div class="service-meta">${escapeHtml(row.meta)}</div>
|
||||
</div>
|
||||
`).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);
|
||||
|
||||
|
||||
+9
-2
@@ -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}
|
||||
<details class="details-panel" id="diagnosticDetails">
|
||||
<summary data-i18n="diagnostics">Diagnostics</summary>
|
||||
<div class="details-body">
|
||||
<div class="spike-log-box">
|
||||
<h3 data-i18n="remoteLogs">Remote Logs</h3>
|
||||
<div id="remoteLogList" class="service-list remote-log-list">
|
||||
<div class="spike-log-empty" data-i18n="remoteLogsLoading">Remote log status loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spike-log-box">
|
||||
<h3 data-i18n="customServices">Custom Services</h3>
|
||||
<div id="customServiceList" class="service-list">
|
||||
|
||||
Reference in New Issue
Block a user