feat(ui): always-visible live activity panel - real-time sync log for all repos
This commit is contained in:
@@ -64,6 +64,7 @@ function card(r) {
|
||||
${r.conflicts && r.conflicts.length ? `<span class="badge b-conflict">冲突 ${r.conflicts.length}</span>` : ''}
|
||||
</div>
|
||||
<div class="last">上次同步:${esc(last)}</div>
|
||||
${r.last_sync && r.last_sync.detail ? `<div class="last-detail" title="${esc(r.last_sync.detail)}">${esc(r.last_sync.detail)}</div>` : ''}
|
||||
${r.status === 'error' && r.last_error ? `<div class="err" title="${esc(r.last_error)}">${esc(r.last_error)}</div>` : ''}
|
||||
<div class="card-btns">
|
||||
<button class="btn" data-act="sync" ${r.running ? '' : 'disabled'}>立即同步</button>
|
||||
@@ -106,6 +107,7 @@ function connect() {
|
||||
if (i >= 0) repos[i] = msg.data; else repos.push(msg.data);
|
||||
render();
|
||||
} else if (msg.type === 'log') {
|
||||
appendLiveLine(msg);
|
||||
if (current === msg.repo && !$('#tab-log').hidden) appendLogLine(`[${msg.time}] ${msg.msg}`);
|
||||
}
|
||||
};
|
||||
@@ -115,8 +117,55 @@ function setConn(ok) {
|
||||
const el = $('#conn');
|
||||
el.textContent = ok ? '● 已连接' : '● 未连接';
|
||||
el.className = 'conn ' + (ok ? 'on' : 'off');
|
||||
const live = $('#live-conn');
|
||||
if (live) {
|
||||
live.textContent = ok ? '● 已连接' : '● 未连接';
|
||||
live.className = 'conn ' + (ok ? 'on' : 'off');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 全局实时活动面板 ----------
|
||||
const LIVE_MAX = 400;
|
||||
function appendLiveLine(msg) {
|
||||
const box = $('#live-log');
|
||||
if (!box) return;
|
||||
let level = 'info';
|
||||
if (msg.level === 'error') level = 'err';
|
||||
else if (msg.level === 'warn') level = 'warn';
|
||||
const line = document.createElement('div');
|
||||
line.className = 'live-line lv-' + level;
|
||||
line.innerHTML = `<span class="ll-time">${esc(msg.time)}</span><span class="ll-repo">${esc(msg.repo)}</span><span class="ll-msg">${esc(msg.msg)}</span>`;
|
||||
box.appendChild(line);
|
||||
while (box.childElementCount > LIVE_MAX) box.removeChild(box.firstChild);
|
||||
box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
|
||||
function clearLive() {
|
||||
const box = $('#live-log');
|
||||
if (box) box.innerHTML = '';
|
||||
}
|
||||
|
||||
// 载入各仓库最近日志作为初始上下文,避免面板一开始空白(仅启动播种一次)
|
||||
let liveSeeded = false;
|
||||
async function seedLive() {
|
||||
if (liveSeeded) return;
|
||||
liveSeeded = true;
|
||||
const names = repos.map(r => r.name);
|
||||
await Promise.all(names.map(async (name) => {
|
||||
try {
|
||||
const d = await api(`/api/repos/${name}/log?lines=8`);
|
||||
if (d.lines && d.lines.length) {
|
||||
d.lines.forEach(t => {
|
||||
const m = t.match(/^\[(.+?)\] (.*)$/);
|
||||
appendLiveLine({ time: m ? m[1] : '', repo: name, msg: m ? m[2] : t, level: 'info', seed: true });
|
||||
});
|
||||
}
|
||||
} catch (_) { /* 忽略 */ }
|
||||
}));
|
||||
}
|
||||
|
||||
$('#btn-live-clear').addEventListener('click', clearLive);
|
||||
|
||||
// ---------- 登录 ----------
|
||||
function showLogin() {
|
||||
const el = $('#login');
|
||||
@@ -411,6 +460,7 @@ async function refresh() {
|
||||
const d = await api('/api/status');
|
||||
repos = d.repos || [];
|
||||
render();
|
||||
seedLive();
|
||||
} catch (e) { /* 忽略,SSE 会重试 */ }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user