시놀로지 원격 로그 뷰어 추가
This commit is contained in:
@@ -121,6 +121,58 @@ function dmesg_log(): array
|
||||
];
|
||||
}
|
||||
|
||||
function text_log_tail(string $path, string $label, int $limit = 500): array
|
||||
{
|
||||
$limit = max(1, min(1000, $limit));
|
||||
$stat = sh(['/usr/bin/stat', '-c', '%s|%Y', $path], true, 3);
|
||||
if ((int)$stat['code'] !== 0) {
|
||||
return [
|
||||
'path' => $path,
|
||||
'available' => false,
|
||||
'lines' => [],
|
||||
'line_count' => 0,
|
||||
'size_bytes' => 0,
|
||||
'updated_at' => null,
|
||||
'message' => $label . ' log is not available yet.',
|
||||
];
|
||||
}
|
||||
|
||||
$parts = explode('|', trim((string)$stat['out']), 2);
|
||||
$size = (int)($parts[0] ?? 0);
|
||||
$mtime = (int)($parts[1] ?? 0);
|
||||
$tail = sh(['/usr/bin/tail', '-n', (string)$limit, $path], true, 4);
|
||||
if ((int)$tail['code'] !== 0) {
|
||||
return [
|
||||
'path' => $path,
|
||||
'available' => false,
|
||||
'lines' => [],
|
||||
'line_count' => 0,
|
||||
'size_bytes' => $size,
|
||||
'updated_at' => $mtime > 0 ? date('Y-m-d H:i:s', $mtime) : null,
|
||||
'message' => 'failed to open ' . $label . ' log.',
|
||||
];
|
||||
}
|
||||
|
||||
$lines = array_values(array_filter(
|
||||
preg_split('/\R/', trim((string)$tail['out'])) ?: [],
|
||||
static fn($line): bool => trim((string)$line) !== ''
|
||||
));
|
||||
|
||||
return [
|
||||
'path' => $path,
|
||||
'available' => true,
|
||||
'lines' => array_reverse($lines),
|
||||
'line_count' => count($lines),
|
||||
'size_bytes' => $size,
|
||||
'updated_at' => $mtime > 0 ? date('Y-m-d H:i:s', $mtime) : null,
|
||||
];
|
||||
}
|
||||
|
||||
function synology_remote_log(): array
|
||||
{
|
||||
return text_log_tail('/var/log/remote/synology/chaegeon.log', 'synology remote');
|
||||
}
|
||||
|
||||
function command_first_line(array $cmd, bool $root = false, int $timeout = 3): string
|
||||
{
|
||||
$result = sh($cmd, $root, $timeout);
|
||||
@@ -4288,6 +4340,13 @@ function control_api_dispatch(): void
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'synology_log') {
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'data' => synology_remote_log(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'status') {
|
||||
json_out([
|
||||
'ok' => true,
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
dmesgToggle: $('#dmesgToggle'),
|
||||
dmesgOutput: $('#dmesgOutput'),
|
||||
dmesgMeta: $('#dmesgMeta'),
|
||||
synologyLogToggle: $('#synologyLogToggle'),
|
||||
synologyLogOutput: $('#synologyLogOutput'),
|
||||
synologyLogMeta: $('#synologyLogMeta'),
|
||||
processDetails: $('#processDetails'),
|
||||
diagnosticDetails: $('#diagnosticDetails'),
|
||||
};
|
||||
@@ -79,6 +82,10 @@
|
||||
dmesgOpen: false,
|
||||
dmesgTimer: null,
|
||||
dmesgLatestKey: null,
|
||||
synologyLogOpen: false,
|
||||
synologyLogTimer: null,
|
||||
synologyLogLatestKey: null,
|
||||
synologyLogScrollHoldUntil: 0,
|
||||
ws: null,
|
||||
wsConnected: false,
|
||||
wsFallbackTimer: null,
|
||||
@@ -281,6 +288,9 @@
|
||||
fanUpdateFailed: 'fan update failed',
|
||||
dmesgUnavailable: 'dmesg log is not available yet.',
|
||||
dmesgRefreshFailed: 'dmesg refresh failed',
|
||||
synologyLog: 'Synology Log',
|
||||
synologyLogUnavailable: 'Synology log is not available yet.',
|
||||
synologyLogRefreshFailed: 'Synology log refresh failed',
|
||||
rebootPrompt: 'Type reboot below to continue.',
|
||||
rebootPhrase: 'reboot',
|
||||
rebootTitle: 'System Reboot',
|
||||
@@ -473,6 +483,9 @@
|
||||
fanUpdateFailed: '팬 갱신 실패',
|
||||
dmesgUnavailable: 'dmesg 로그를 아직 사용할 수 없습니다.',
|
||||
dmesgRefreshFailed: 'dmesg 갱신 실패',
|
||||
synologyLog: '시놀로지 로그',
|
||||
synologyLogUnavailable: '시놀로지 로그를 아직 사용할 수 없습니다.',
|
||||
synologyLogRefreshFailed: '시놀로지 로그 갱신 실패',
|
||||
rebootPrompt: '재부팅을 계속하려면 아래에 재부팅을 입력하세요.',
|
||||
rebootPhrase: '재부팅',
|
||||
rebootTitle: '시스템 재부팅',
|
||||
@@ -527,6 +540,9 @@
|
||||
if (els.dmesgToggle) {
|
||||
els.dmesgToggle.textContent = state.dmesgOpen ? t('hide') : t('show');
|
||||
}
|
||||
if (els.synologyLogToggle) {
|
||||
els.synologyLogToggle.textContent = state.synologyLogOpen ? t('hide') : t('show');
|
||||
}
|
||||
}
|
||||
|
||||
function initPreferences() {
|
||||
@@ -1163,6 +1179,10 @@
|
||||
stopDmesgFallback();
|
||||
sendWs({ type: 'dmesg', open: true });
|
||||
}
|
||||
if (state.synologyLogOpen) {
|
||||
stopSynologyLogFallback();
|
||||
sendWs({ type: 'synology_log', open: true });
|
||||
}
|
||||
});
|
||||
|
||||
state.ws.addEventListener('message', event => {
|
||||
@@ -1184,6 +1204,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (message?.type === 'synology_log') {
|
||||
renderSynologyLog(message.data || {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message?.type === 'error') {
|
||||
console.error(message.message || 'websocket error');
|
||||
}
|
||||
@@ -1195,6 +1220,9 @@
|
||||
if (state.dmesgOpen) {
|
||||
startDmesgFallback();
|
||||
}
|
||||
if (state.synologyLogOpen) {
|
||||
startSynologyLogFallback();
|
||||
}
|
||||
const delay = state.wsReconnectDelay;
|
||||
state.wsReconnectDelay = Math.min(15000, state.wsReconnectDelay * 1.7);
|
||||
state.wsReconnectTimer = setTimeout(connectControlSocket, delay);
|
||||
@@ -2554,6 +2582,84 @@
|
||||
}
|
||||
}
|
||||
|
||||
function scrollSynologyLogToTop() {
|
||||
if (!els.synologyLogOutput) return;
|
||||
if (els.synologyLogOutput.hidden) return;
|
||||
if (Date.now() < state.synologyLogScrollHoldUntil) return;
|
||||
|
||||
els.synologyLogOutput.scrollTop = 0;
|
||||
}
|
||||
|
||||
function renderSynologyLog(data) {
|
||||
if (!els.synologyLogOutput || !els.synologyLogMeta) return;
|
||||
if (Date.now() < state.synologyLogScrollHoldUntil) return;
|
||||
|
||||
const scrollTop = els.synologyLogOutput.scrollTop;
|
||||
|
||||
if (!data?.available) {
|
||||
els.synologyLogOutput.textContent = data?.message || t('synologyLogUnavailable');
|
||||
els.synologyLogMeta.textContent = '/var/log/remote/synology/chaegeon.log unavailable';
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = data.lines || [];
|
||||
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 || '-'}`;
|
||||
state.synologyLogLatestKey = latestKey;
|
||||
els.synologyLogOutput.scrollTop = Date.now() < state.synologyLogScrollHoldUntil ? scrollTop : 0;
|
||||
|
||||
if (hasNewLine) {
|
||||
requestAnimationFrame(scrollSynologyLogToTop);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSynologyLog() {
|
||||
if (!state.synologyLogOpen) return;
|
||||
|
||||
try {
|
||||
renderSynologyLog(await api('synology_log'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (els.synologyLogOutput) {
|
||||
els.synologyLogOutput.textContent = e.message || t('synologyLogRefreshFailed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stopSynologyLogFallback() {
|
||||
clearInterval(state.synologyLogTimer);
|
||||
state.synologyLogTimer = null;
|
||||
}
|
||||
|
||||
function startSynologyLogFallback() {
|
||||
if (state.synologyLogTimer) return;
|
||||
|
||||
refreshSynologyLog();
|
||||
state.synologyLogTimer = setInterval(refreshSynologyLog, 1000);
|
||||
}
|
||||
|
||||
function setSynologyLogOpen(open) {
|
||||
state.synologyLogOpen = open;
|
||||
if (els.synologyLogOutput) {
|
||||
els.synologyLogOutput.hidden = !open;
|
||||
}
|
||||
if (els.synologyLogToggle) {
|
||||
els.synologyLogToggle.textContent = open ? t('hide') : t('show');
|
||||
}
|
||||
|
||||
stopSynologyLogFallback();
|
||||
if (open) {
|
||||
if (!sendWs({ type: 'synology_log', open: true })) {
|
||||
startSynologyLogFallback();
|
||||
}
|
||||
} else {
|
||||
sendWs({ type: 'synology_log', open: false });
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
if (state.loading) return;
|
||||
state.loading = true;
|
||||
@@ -2697,6 +2803,15 @@
|
||||
els.dmesgToggle?.addEventListener('click', () => {
|
||||
setDmesgOpen(!state.dmesgOpen);
|
||||
});
|
||||
els.synologyLogToggle?.addEventListener('click', () => {
|
||||
setSynologyLogOpen(!state.synologyLogOpen);
|
||||
});
|
||||
const holdSynologyLogScroll = () => {
|
||||
state.synologyLogScrollHoldUntil = Date.now() + 4000;
|
||||
};
|
||||
['scroll', 'wheel', 'touchstart', 'touchmove', 'pointerdown'].forEach(eventName => {
|
||||
els.synologyLogOutput?.addEventListener(eventName, holdSynologyLogScroll, { passive: true });
|
||||
});
|
||||
const holdCustomServiceScroll = () => {
|
||||
state.customServiceScrollHoldUntil = Date.now() + 4000;
|
||||
};
|
||||
@@ -2776,6 +2891,9 @@
|
||||
if (state.dmesgOpen && !sendWs({ type: 'dmesg', open: true })) {
|
||||
refreshDmesg();
|
||||
}
|
||||
if (state.synologyLogOpen && !sendWs({ type: 'synology_log', open: true })) {
|
||||
refreshSynologyLog();
|
||||
}
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', event => {
|
||||
|
||||
@@ -344,6 +344,15 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spike-log-box">
|
||||
<div class="dmesg-head">
|
||||
<h3 data-i18n="synologyLog">Synology Log</h3>
|
||||
<button class="btn secondary" id="synologyLogToggle" type="button" data-i18n="show">Show</button>
|
||||
</div>
|
||||
<div class="dmesg-meta" id="synologyLogMeta">/var/log/remote/synology/chaegeon.log</div>
|
||||
<pre class="dmesg-log" id="synologyLogOutput" hidden></pre>
|
||||
</div>
|
||||
|
||||
<div class="spike-log-box">
|
||||
<div class="dmesg-head">
|
||||
<h3>dmesg</h3>
|
||||
|
||||
Reference in New Issue
Block a user