Align Worker logs page with df-autotest design
Docker Build and Push / check-secrets (push) Successful in 22s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
Build Windows CUDA 11.8 / build (push) Has been cancelled
Build Windows CUDA 12.6 / build (push) Has been cancelled
Build Windows CUDA 12.8 / build (push) Has been cancelled
Build Windows DirectML / build (push) Has been cancelled

- Extract Worker logs as separate nav page (page-worker-logs)
- Dark terminal style (#0b1320 bg, level-based colors: DEBUG/INFO/WARN/ERROR)
- Auto-poll every 2s via setInterval, starts on page enter, stops on leave
- Pause/Resume and Clear buttons, live line count
- Auto-scroll only when already at bottom
- Remove old embedded log panel from Settings page

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
liuchen
2026-06-27 14:29:39 +08:00
co-authored by Claude Haiku 4.5
parent 273f03ee9f
commit e512a842e4
+96 -31
View File
@@ -439,6 +439,25 @@
/* ── Divider ────────────────────────────────────────────── */
.divider { border: none; border-top: 1px solid var(--line); }
/* ── Worker log terminal ─────────────────────────────────── */
.worker-log-wrap {
height: 60vh; min-height: 280px; overflow-y: auto;
background: #0b1320; border-radius: var(--radius-sm); border: 1px solid #1e2d42;
}
.worker-log-pre {
margin: 0; padding: 12px 16px; color: #d6e2f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12.5px; line-height: 1.7; white-space: pre-wrap; overflow-wrap: anywhere;
}
.worker-log-pre .ll-WARNING { color: #fbbf24; }
.worker-log-pre .ll-ERROR,
.worker-log-pre .ll-CRITICAL { color: #f87171; }
.worker-log-pre .ll-INFO { color: #d6e2f0; }
.worker-log-pre .ll-DEBUG { color: #64748b; }
.log-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.log-toolbar .subtle { flex: 1; }
#wl-pause-btn.paused { color: var(--warn); border-color: rgba(181,113,10,.5); }
/* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 1100px) {
.shell { grid-template-columns: 1fr; }
@@ -475,6 +494,9 @@
<button class="nav-item" type="button" data-page="page-settings">
<span class="nav-icon">⚙</span>系统设置
</button>
<button class="nav-item" type="button" data-page="page-worker-logs">
<span class="nav-icon">▦</span>Worker 日志
</button>
</nav>
<div class="sidebar-footer">
@@ -574,19 +596,25 @@
</div>
</div>
</div>
<!-- 页面:Worker 日志 -->
<div class="page" id="page-worker-logs">
<div class="panel">
<div class="panel-head">
<h2>Worker 运行日志</h2>
<button class="button secondary" style="min-height:32px;padding:0 12px;font-size:12px" onclick="refreshWorkerLogs()">刷新</button>
<h2>Worker 日志</h2>
<div class="actions">
<span id="wl-status" class="subtle">未启动</span>
</div>
</div>
<div class="panel-body" style="padding:0">
<div id="workerLogs" style="
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
font-size:12px; line-height:1.7; color:#3d4856;
background:#f7f9fc; padding:14px 16px;
max-height:280px; overflow-y:auto;
">
<div class="empty" style="border:none;background:none">尚无日志。Worker 运行后日志会出现在这里。</div>
<div class="panel-body" style="display:grid;gap:10px">
<div class="log-toolbar">
<button id="wl-pause-btn" class="button secondary" style="min-height:34px;padding:0 14px;font-size:13px" type="button">暂停</button>
<button id="wl-clear-btn" class="button secondary" style="min-height:34px;padding:0 14px;font-size:13px" type="button">清空</button>
<span class="subtle" id="wl-line-count">0 条</span>
</div>
<div class="worker-log-wrap" id="wl-wrap">
<pre class="worker-log-pre" id="wl-pre"></pre>
</div>
</div>
</div>
@@ -757,7 +785,6 @@ const S = {
wsRetry: 1000,
regions: [],
mode: 'sttn-det',
workerLogSeq: 0,
};
// ── Init ───────────────────────────────────────────────────────
@@ -781,9 +808,13 @@ document.addEventListener('DOMContentLoaded', () => {
});
// ── Page switching ─────────────────────────────────────────────
// Worker 日志轮询状态 — 在 switchPage 之前声明,避免 TDZ 报错
let _wlSeq = 0, _wlTimer = null, _wlPaused = false, _wlLineCount = 0;
const PAGE_META = {
'page-queue': ['任务队列', '提交视频链接,去除字幕或水印,实时查看处理进度。', true],
'page-settings': ['系统设置', '配置平台 Worker 连接,查看运行日志。', false],
'page-queue': ['任务队列', '提交视频链接,去除字幕或水印,实时查看处理进度。', true],
'page-settings': ['系统设置', '配置平台 Worker 连接,保存后立即生效。', false],
'page-worker-logs': ['Worker 日志', '实时查看 Worker 轮询、认领任务及上报进度的日志(进程内缓冲,重启清空)。', false],
};
function switchPage(id) {
document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
@@ -795,6 +826,7 @@ function switchPage(id) {
document.getElementById('page-subtitle').textContent = sub;
document.getElementById('page-actions').style.display = showActions ? '' : 'none';
if (id === 'page-settings') loadSettings();
if (id === 'page-worker-logs') startWlPolling(); else stopWlPolling();
}
// ── Status / Alert ─────────────────────────────────────────────
@@ -1068,7 +1100,6 @@ async function loadSettings() {
el.innerHTML = '<span class="bdot"></span>未运行';
}
} catch(e) {}
refreshWorkerLogs();
}
async function saveSettings() {
@@ -1090,28 +1121,62 @@ async function saveSettings() {
el.innerHTML = '<span class="bdot"></span>未运行';
}
showToast(r.worker_running ? 'Worker 已启动' : '设置已保存(Worker 未启动)');
refreshWorkerLogs();
} catch(e) { showToast('保存失败: ' + e.message); }
}
async function refreshWorkerLogs() {
try {
const d = await api(`/api/worker/logs?since=${S.workerLogSeq}`);
S.workerLogSeq = d.next_seq || S.workerLogSeq;
const el = document.getElementById('workerLogs');
if (d.lines && d.lines.length) {
if (el.querySelector('.empty')) el.innerHTML = '';
d.lines.forEach(l => {
const div = document.createElement('div');
div.style.color = l.level === 'WARNING' ? 'var(--warn)' : l.level === 'ERROR' ? 'var(--bad)' : 'var(--ink-2)';
div.textContent = `${new Date(l.ts*1000).toLocaleTimeString('zh-CN')} ${l.msg}`;
el.appendChild(div);
});
el.scrollTop = el.scrollHeight;
}
} catch(e) {}
// ── Worker 日志轮询(同 df-autotest)──────────────────────────
const LEVEL_CLS = { WARNING: 'll-WARNING', ERROR: 'll-ERROR', CRITICAL: 'll-CRITICAL', DEBUG: 'll-DEBUG' };
function _wlAppend(lines) {
if (!lines.length) return;
const pre = document.getElementById('wl-pre');
const wrap = document.getElementById('wl-wrap');
const atBottom = wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight < 40;
for (const entry of lines) {
const span = document.createElement('span');
span.className = LEVEL_CLS[entry.level] || 'll-INFO';
span.textContent = entry.msg + '\n';
pre.appendChild(span);
}
_wlLineCount += lines.length;
document.getElementById('wl-line-count').textContent = `${_wlLineCount} 条`;
if (atBottom) wrap.scrollTop = wrap.scrollHeight;
}
async function _wlPoll() {
if (_wlPaused) return;
try {
const data = await api(`/api/worker/logs?since=${_wlSeq}`);
if (data.lines?.length) { _wlAppend(data.lines); _wlSeq = data.next_seq; }
document.getElementById('wl-status').textContent = '运行中';
} catch(e) {
document.getElementById('wl-status').textContent = '轮询失败';
}
}
function startWlPolling() {
if (_wlTimer) return;
_wlPoll();
_wlTimer = setInterval(_wlPoll, 2000);
}
function stopWlPolling() {
if (_wlTimer) { clearInterval(_wlTimer); _wlTimer = null; }
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('wl-pause-btn').addEventListener('click', () => {
_wlPaused = !_wlPaused;
document.getElementById('wl-pause-btn').textContent = _wlPaused ? '继续' : '暂停';
document.getElementById('wl-pause-btn').classList.toggle('paused', _wlPaused);
});
document.getElementById('wl-clear-btn').addEventListener('click', () => {
document.getElementById('wl-pre').textContent = '';
_wlLineCount = 0;
document.getElementById('wl-line-count').textContent = '0 条';
});
});
// ── Mode select ────────────────────────────────────────────────
function selMode(val, el) {
S.mode = val;