시놀로지 원격 로그 뷰어 추가
This commit is contained in:
@@ -29,7 +29,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시
|
- `/etc/systemd/system/*.service` 기준 사용자 생성 서비스의 서비스명, 현재 상태, enable 상태, PID, restart 횟수, 최근 journal 로그를 진단 접힘 영역에서 표시
|
||||||
- 사용자 서비스 로그는 서비스별 최근 500줄까지 조회하고, 사용자가 서비스 목록을 스크롤하는 동안에는 자동 갱신이 화면 위치를 되돌리지 않습니다.
|
- 사용자 서비스 로그는 서비스별 최근 500줄까지 조회하고, 사용자가 서비스 목록을 스크롤하는 동안에는 자동 갱신이 화면 위치를 되돌리지 않습니다.
|
||||||
- 라즈베리파이와 시놀로지 사이 원격 syslog 송수신 상태, 영구 journal, 커널 크래시 보존 상태를 진단 접힘 영역에서 표시
|
- 라즈베리파이와 시놀로지 사이 원격 syslog 송수신 상태, 영구 journal, 커널 크래시 보존 상태를 진단 접힘 영역에서 표시
|
||||||
- dmesg 로그는 진단 접힘 영역에서 필요할 때 열람
|
- 시놀로지에서 라즈베리파이로 들어온 원격 로그와 dmesg 로그는 진단 접힘 영역에서 필요할 때 열람
|
||||||
- WakeLock 버튼으로 대시보드 화면 꺼짐 방지
|
- WakeLock 버튼으로 대시보드 화면 꺼짐 방지
|
||||||
- Reboot 버튼으로 2단계 확인 후 시스템 재부팅 요청
|
- Reboot 버튼으로 2단계 확인 후 시스템 재부팅 요청
|
||||||
- 기본 브라우저 alert/confirm/prompt 대신 대시보드 디자인에 맞춘 custom dialog 사용
|
- 기본 브라우저 alert/confirm/prompt 대신 대시보드 디자인에 맞춘 custom dialog 사용
|
||||||
@@ -51,6 +51,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- `public/api.php?action=settings_reset`: 설정 모달 값을 기본값으로 초기화
|
- `public/api.php?action=settings_reset`: 설정 모달 값을 기본값으로 초기화
|
||||||
- `public/api.php?action=touch_display_save`: `/boot/firmware/config.txt`, `/boot/firmware/cmdline.txt`에 DSI 터치 디스플레이 부팅 설정 저장
|
- `public/api.php?action=touch_display_save`: `/boot/firmware/config.txt`, `/boot/firmware/cmdline.txt`에 DSI 터치 디스플레이 부팅 설정 저장
|
||||||
- `public/api.php?action=touch_display_runtime_save`: DSI 터치 디스플레이 백라이트 밝기와 백라이트 전원 즉시 적용
|
- `public/api.php?action=touch_display_runtime_save`: DSI 터치 디스플레이 백라이트 밝기와 백라이트 전원 즉시 적용
|
||||||
|
- `public/api.php?action=synology_log`: 라즈베리파이에 저장된 시놀로지 원격 로그 최근 500줄 조회
|
||||||
- `public/api.php?action=dmesg`: dmesg 로그 조회
|
- `public/api.php?action=dmesg`: dmesg 로그 조회
|
||||||
- `public/api.php?action=reboot`: 확인 단어와 관리자 암호 재검증 후 sudo reboot 실행
|
- `public/api.php?action=reboot`: 확인 단어와 관리자 암호 재검증 후 sudo reboot 실행
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
|
|
||||||
라즈베리파이는 `rsyslog`와 `rsyslog-gnutls`로 로컬 journal/syslog를 시놀로지 Log Center에 TCP/TLS 6514로 보냅니다. 전송 대상은 `chaegeon.com:6514`이며, 네트워크가 끊기면 `/var/spool/rsyslog/control_synology_fwd*` 큐에 남았다가 재시도합니다.
|
라즈베리파이는 `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`가 매일 회전합니다.
|
시놀로지는 DSM `syslog-ng`에서 라즈베리파이 `121.157.202.229:6514`로 TCP/TLS syslog를 보냅니다. 라즈베리파이 수신 로그는 `/var/log/remote/synology/*.log`에 저장하고 `/etc/logrotate.d/control-remote-syslog`가 매일 회전합니다. Control 진단 패널의 시놀로지 로그 뷰어는 `/var/log/remote/synology/chaegeon.log` 최근 500줄을 최신순으로 보여주며, WebSocket 또는 HTTP fallback으로 1초마다 갱신합니다. 사용자가 로그 영역을 스크롤하는 동안에는 자동 갱신을 잠깐 멈춰 화면 위치가 튀지 않게 합니다.
|
||||||
|
|
||||||
커널 패닉이나 강제 재부팅 분석을 위해 `systemd-journald`는 `/var/log/journal`에 영구 저장합니다. `dtoverlay=ramoops` 기반 pstore는 부팅 직후 `control-pstore-collect.service`가 `/var/log/pstore`로 복사합니다. 현재 pstore에 남은 크래시 조각이 없으면 이 서비스는 조건 불충족으로 `inactive`일 수 있으며 정상입니다.
|
커널 패닉이나 강제 재부팅 분석을 위해 `systemd-journald`는 `/var/log/journal`에 영구 저장합니다. `dtoverlay=ramoops` 기반 pstore는 부팅 직후 `control-pstore-collect.service`가 `/var/log/pstore`로 복사합니다. 현재 pstore에 남은 크래시 조각이 없으면 이 서비스는 조건 불충족으로 `inactive`일 수 있으며 정상입니다.
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ API 응답의 `battery.enabled`는 false, `battery.removed`는 true로 내려가
|
|||||||
- WebSocket 끊김 시 HTTP fallback 상태 갱신: 2초마다
|
- WebSocket 끊김 시 HTTP fallback 상태 갱신: 2초마다
|
||||||
- 탭 복귀: 즉시 1회 상태 갱신
|
- 탭 복귀: 즉시 1회 상태 갱신
|
||||||
- UPS/배터리 잔여 시간: 현재 운영 구성에서는 표시하지 않음
|
- UPS/배터리 잔여 시간: 현재 운영 구성에서는 표시하지 않음
|
||||||
- dmesg: 진단 패널에서 열었을 때만 1초마다 갱신, 닫으면 중지
|
- 시놀로지 원격 로그/dmesg: 진단 패널에서 열었을 때만 1초마다 갱신, 닫으면 중지
|
||||||
- WiFi 관측 보정 timer: 10초마다 `bin/wifi_observe.php` 실행
|
- WiFi 관측 보정 timer: 10초마다 `bin/wifi_observe.php` 실행
|
||||||
- 외출/귀가 팬 정책: 팬 정책 적용 루프에서 기본 30초마다 서울 HA의 휴대폰 device tracker를 확인
|
- 외출/귀가 팬 정책: 팬 정책 적용 루프에서 기본 30초마다 서울 HA의 휴대폰 device tracker를 확인
|
||||||
- WebSocket 소스 변경 감지: 15초마다 확인 후 재시작
|
- WebSocket 소스 변경 감지: 15초마다 확인 후 재시작
|
||||||
@@ -187,7 +188,7 @@ control-wifi-observe.service
|
|||||||
13. 저전압/스로틀링 상태 파일에는 최근 episode 시작/종료를 보관하고, 최근 10분 발생 횟수, 감지 누적시간, 감지 비율을 계산합니다.
|
13. 저전압/스로틀링 상태 파일에는 최근 episode 시작/종료를 보관하고, 최근 10분 발생 횟수, 감지 누적시간, 감지 비율을 계산합니다.
|
||||||
14. UPS/배터리 장치가 제거되어 배터리 센서 조회와 잔여 시간 학습 계산은 기본적으로 돌리지 않습니다.
|
14. UPS/배터리 장치가 제거되어 배터리 센서 조회와 잔여 시간 학습 계산은 기본적으로 돌리지 않습니다.
|
||||||
15. System Notice 조건이 맞으면 Control이 `system_notice_logs`에 이력을 저장합니다.
|
15. System Notice 조건이 맞으면 Control이 `system_notice_logs`에 이력을 저장합니다.
|
||||||
16. 대시보드 첫 화면에는 팬/전원/스로틀링/WiFi/핵심 차트를 우선 배치하고, 상세 차트, 프로세스 후보, 사용자 서비스, dmesg는 접힘 영역으로 분리합니다.
|
16. 대시보드 첫 화면에는 팬/전원/스로틀링/WiFi/핵심 차트를 우선 배치하고, 상세 차트, 프로세스 후보, 사용자 서비스, 시놀로지 원격 로그, dmesg는 접힘 영역으로 분리합니다.
|
||||||
|
|
||||||
## 팬 판단 온도
|
## 팬 판단 온도
|
||||||
|
|
||||||
|
|||||||
+63
-1
@@ -16,11 +16,12 @@ require_once __DIR__ . '/../public/api.php';
|
|||||||
|
|
||||||
final class ControlWebSocket implements MessageComponentInterface
|
final class ControlWebSocket implements MessageComponentInterface
|
||||||
{
|
{
|
||||||
/** @var SplObjectStorage<ConnectionInterface, array{dmesg: bool, ip: string}> */
|
/** @var SplObjectStorage<ConnectionInterface, array{dmesg: bool, synology_log: bool, ip: string}> */
|
||||||
private SplObjectStorage $clients;
|
private SplObjectStorage $clients;
|
||||||
|
|
||||||
private bool $statusBusy = false;
|
private bool $statusBusy = false;
|
||||||
private bool $dmesgBusy = false;
|
private bool $dmesgBusy = false;
|
||||||
|
private bool $synologyLogBusy = false;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -47,6 +48,7 @@ final class ControlWebSocket implements MessageComponentInterface
|
|||||||
|
|
||||||
$this->clients[$conn] = [
|
$this->clients[$conn] = [
|
||||||
'dmesg' => false,
|
'dmesg' => false,
|
||||||
|
'synology_log' => false,
|
||||||
'ip' => $ip,
|
'ip' => $ip,
|
||||||
];
|
];
|
||||||
$this->sendStatus($conn);
|
$this->sendStatus($conn);
|
||||||
@@ -61,6 +63,7 @@ final class ControlWebSocket implements MessageComponentInterface
|
|||||||
|
|
||||||
$state = $this->clients[$from] ?? [
|
$state = $this->clients[$from] ?? [
|
||||||
'dmesg' => false,
|
'dmesg' => false,
|
||||||
|
'synology_log' => false,
|
||||||
'ip' => '127.0.0.1',
|
'ip' => '127.0.0.1',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -73,6 +76,15 @@ final class ControlWebSocket implements MessageComponentInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (($data['type'] ?? '') === 'synology_log') {
|
||||||
|
$state['synology_log'] = !empty($data['open']);
|
||||||
|
$this->clients[$from] = $state;
|
||||||
|
if ($state['synology_log']) {
|
||||||
|
$this->sendSynologyLog($from);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (($data['type'] ?? '') === 'status_refresh') {
|
if (($data['type'] ?? '') === 'status_refresh') {
|
||||||
$this->sendStatus($from);
|
$this->sendStatus($from);
|
||||||
}
|
}
|
||||||
@@ -147,6 +159,40 @@ final class ControlWebSocket implements MessageComponentInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function broadcastSynologyLog(): void
|
||||||
|
{
|
||||||
|
if ($this->clients->count() === 0 || $this->synologyLogBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targets = [];
|
||||||
|
foreach ($this->clients as $client) {
|
||||||
|
$state = $this->clients[$client];
|
||||||
|
if (!empty($state['synology_log'])) {
|
||||||
|
$targets[] = $client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($targets === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->synologyLogBusy = true;
|
||||||
|
try {
|
||||||
|
$payload = $this->json([
|
||||||
|
'type' => 'synology_log',
|
||||||
|
'data' => synology_remote_log(),
|
||||||
|
]);
|
||||||
|
foreach ($targets as $client) {
|
||||||
|
$client->send($payload);
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->broadcastError($e);
|
||||||
|
} finally {
|
||||||
|
$this->synologyLogBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function sendStatus(ConnectionInterface $conn): void
|
private function sendStatus(ConnectionInterface $conn): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -174,6 +220,21 @@ final class ControlWebSocket implements MessageComponentInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function sendSynologyLog(ConnectionInterface $conn): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$conn->send($this->json([
|
||||||
|
'type' => 'synology_log',
|
||||||
|
'data' => synology_remote_log(),
|
||||||
|
]));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$conn->send($this->json([
|
||||||
|
'type' => 'error',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function statusPayload(): string
|
private function statusPayload(): string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -280,6 +341,7 @@ $startedSignature = $watchSignature($watchFiles);
|
|||||||
|
|
||||||
$loop->addPeriodicTimer(1.0, static fn() => $app->broadcastStatus());
|
$loop->addPeriodicTimer(1.0, static fn() => $app->broadcastStatus());
|
||||||
$loop->addPeriodicTimer(1.0, static fn() => $app->broadcastDmesg());
|
$loop->addPeriodicTimer(1.0, static fn() => $app->broadcastDmesg());
|
||||||
|
$loop->addPeriodicTimer(1.0, static fn() => $app->broadcastSynologyLog());
|
||||||
$loop->addPeriodicTimer(15.0, static function () use ($watchFiles, $watchSignature, $startedSignature): void {
|
$loop->addPeriodicTimer(15.0, static function () use ($watchFiles, $watchSignature, $startedSignature): void {
|
||||||
if ($watchSignature($watchFiles) === $startedSignature) {
|
if ($watchSignature($watchFiles) === $startedSignature) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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
|
function command_first_line(array $cmd, bool $root = false, int $timeout = 3): string
|
||||||
{
|
{
|
||||||
$result = sh($cmd, $root, $timeout);
|
$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') {
|
if ($action === 'status') {
|
||||||
json_out([
|
json_out([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
|
|||||||
@@ -60,6 +60,9 @@
|
|||||||
dmesgToggle: $('#dmesgToggle'),
|
dmesgToggle: $('#dmesgToggle'),
|
||||||
dmesgOutput: $('#dmesgOutput'),
|
dmesgOutput: $('#dmesgOutput'),
|
||||||
dmesgMeta: $('#dmesgMeta'),
|
dmesgMeta: $('#dmesgMeta'),
|
||||||
|
synologyLogToggle: $('#synologyLogToggle'),
|
||||||
|
synologyLogOutput: $('#synologyLogOutput'),
|
||||||
|
synologyLogMeta: $('#synologyLogMeta'),
|
||||||
processDetails: $('#processDetails'),
|
processDetails: $('#processDetails'),
|
||||||
diagnosticDetails: $('#diagnosticDetails'),
|
diagnosticDetails: $('#diagnosticDetails'),
|
||||||
};
|
};
|
||||||
@@ -79,6 +82,10 @@
|
|||||||
dmesgOpen: false,
|
dmesgOpen: false,
|
||||||
dmesgTimer: null,
|
dmesgTimer: null,
|
||||||
dmesgLatestKey: null,
|
dmesgLatestKey: null,
|
||||||
|
synologyLogOpen: false,
|
||||||
|
synologyLogTimer: null,
|
||||||
|
synologyLogLatestKey: null,
|
||||||
|
synologyLogScrollHoldUntil: 0,
|
||||||
ws: null,
|
ws: null,
|
||||||
wsConnected: false,
|
wsConnected: false,
|
||||||
wsFallbackTimer: null,
|
wsFallbackTimer: null,
|
||||||
@@ -281,6 +288,9 @@
|
|||||||
fanUpdateFailed: 'fan update failed',
|
fanUpdateFailed: 'fan update failed',
|
||||||
dmesgUnavailable: 'dmesg log is not available yet.',
|
dmesgUnavailable: 'dmesg log is not available yet.',
|
||||||
dmesgRefreshFailed: 'dmesg refresh failed',
|
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.',
|
rebootPrompt: 'Type reboot below to continue.',
|
||||||
rebootPhrase: 'reboot',
|
rebootPhrase: 'reboot',
|
||||||
rebootTitle: 'System Reboot',
|
rebootTitle: 'System Reboot',
|
||||||
@@ -473,6 +483,9 @@
|
|||||||
fanUpdateFailed: '팬 갱신 실패',
|
fanUpdateFailed: '팬 갱신 실패',
|
||||||
dmesgUnavailable: 'dmesg 로그를 아직 사용할 수 없습니다.',
|
dmesgUnavailable: 'dmesg 로그를 아직 사용할 수 없습니다.',
|
||||||
dmesgRefreshFailed: 'dmesg 갱신 실패',
|
dmesgRefreshFailed: 'dmesg 갱신 실패',
|
||||||
|
synologyLog: '시놀로지 로그',
|
||||||
|
synologyLogUnavailable: '시놀로지 로그를 아직 사용할 수 없습니다.',
|
||||||
|
synologyLogRefreshFailed: '시놀로지 로그 갱신 실패',
|
||||||
rebootPrompt: '재부팅을 계속하려면 아래에 재부팅을 입력하세요.',
|
rebootPrompt: '재부팅을 계속하려면 아래에 재부팅을 입력하세요.',
|
||||||
rebootPhrase: '재부팅',
|
rebootPhrase: '재부팅',
|
||||||
rebootTitle: '시스템 재부팅',
|
rebootTitle: '시스템 재부팅',
|
||||||
@@ -527,6 +540,9 @@
|
|||||||
if (els.dmesgToggle) {
|
if (els.dmesgToggle) {
|
||||||
els.dmesgToggle.textContent = state.dmesgOpen ? t('hide') : t('show');
|
els.dmesgToggle.textContent = state.dmesgOpen ? t('hide') : t('show');
|
||||||
}
|
}
|
||||||
|
if (els.synologyLogToggle) {
|
||||||
|
els.synologyLogToggle.textContent = state.synologyLogOpen ? t('hide') : t('show');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initPreferences() {
|
function initPreferences() {
|
||||||
@@ -1163,6 +1179,10 @@
|
|||||||
stopDmesgFallback();
|
stopDmesgFallback();
|
||||||
sendWs({ type: 'dmesg', open: true });
|
sendWs({ type: 'dmesg', open: true });
|
||||||
}
|
}
|
||||||
|
if (state.synologyLogOpen) {
|
||||||
|
stopSynologyLogFallback();
|
||||||
|
sendWs({ type: 'synology_log', open: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
state.ws.addEventListener('message', event => {
|
state.ws.addEventListener('message', event => {
|
||||||
@@ -1184,6 +1204,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message?.type === 'synology_log') {
|
||||||
|
renderSynologyLog(message.data || {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message?.type === 'error') {
|
if (message?.type === 'error') {
|
||||||
console.error(message.message || 'websocket error');
|
console.error(message.message || 'websocket error');
|
||||||
}
|
}
|
||||||
@@ -1195,6 +1220,9 @@
|
|||||||
if (state.dmesgOpen) {
|
if (state.dmesgOpen) {
|
||||||
startDmesgFallback();
|
startDmesgFallback();
|
||||||
}
|
}
|
||||||
|
if (state.synologyLogOpen) {
|
||||||
|
startSynologyLogFallback();
|
||||||
|
}
|
||||||
const delay = state.wsReconnectDelay;
|
const delay = state.wsReconnectDelay;
|
||||||
state.wsReconnectDelay = Math.min(15000, state.wsReconnectDelay * 1.7);
|
state.wsReconnectDelay = Math.min(15000, state.wsReconnectDelay * 1.7);
|
||||||
state.wsReconnectTimer = setTimeout(connectControlSocket, delay);
|
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() {
|
async function refreshStatus() {
|
||||||
if (state.loading) return;
|
if (state.loading) return;
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
@@ -2697,6 +2803,15 @@
|
|||||||
els.dmesgToggle?.addEventListener('click', () => {
|
els.dmesgToggle?.addEventListener('click', () => {
|
||||||
setDmesgOpen(!state.dmesgOpen);
|
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 = () => {
|
const holdCustomServiceScroll = () => {
|
||||||
state.customServiceScrollHoldUntil = Date.now() + 4000;
|
state.customServiceScrollHoldUntil = Date.now() + 4000;
|
||||||
};
|
};
|
||||||
@@ -2776,6 +2891,9 @@
|
|||||||
if (state.dmesgOpen && !sendWs({ type: 'dmesg', open: true })) {
|
if (state.dmesgOpen && !sendWs({ type: 'dmesg', open: true })) {
|
||||||
refreshDmesg();
|
refreshDmesg();
|
||||||
}
|
}
|
||||||
|
if (state.synologyLogOpen && !sendWs({ type: 'synology_log', open: true })) {
|
||||||
|
refreshSynologyLog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', event => {
|
document.addEventListener('keydown', event => {
|
||||||
|
|||||||
@@ -344,6 +344,15 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
|||||||
</div>
|
</div>
|
||||||
</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="spike-log-box">
|
||||||
<div class="dmesg-head">
|
<div class="dmesg-head">
|
||||||
<h3>dmesg</h3>
|
<h3>dmesg</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user