Upgrade dashboard v1.1 with auto-refresh, dual signoff, and justice routing

This commit is contained in:
Chen Gu
2026-08-13 17:00:20 +08:00
parent ab90d10544
commit 5f278abcc0
4 changed files with 78 additions and 8 deletions
+56 -8
View File
@@ -85,6 +85,14 @@
<p id="updatedAt">等待加载数据...</p>
</div>
<div class="controls">
<label style="font-size:12px;color:var(--muted);display:flex;gap:6px;align-items:center;">
<input id="autoRefresh" type="checkbox" checked /> 自动刷新
</label>
<select id="refreshMs" style="background:#1a2550;color:#dce6ff;border:1px solid #3b4b86;border-radius:8px;padding:6px;">
<option value="5000">5s</option>
<option value="10000" selected>10s</option>
<option value="30000">30s</option>
</select>
<input id="fileInput" type="file" accept="application/json" />
<button id="loadSample">加载样例数据</button>
</div>
@@ -118,19 +126,36 @@
</div>
<script>
const state = { data: null };
const state = { data: null, timer: null, lastSource: 'sample' };
function riskClass(risk) {
return risk === 'high' ? 'risk-high' : risk === 'medium' ? 'risk-medium' : 'risk-low';
}
function applyWorkflowRules(tasks) {
// 规则1:失败任务自动流入刑部(Justice)
for (const t of tasks) {
if (t.failed === true) {
t.state = 'Audit';
t.owner = '刑部';
t.summary = `【自动流转】失败转刑部:${t.failReason || '待定位根因'}`;
}
}
return tasks;
}
function hasDualSignoff(task) {
return !!(task.signoff?.auditor && task.signoff?.guardian);
}
function render(data) {
state.data = data;
const columns = data.columns || [];
const tasks = data.tasks || [];
const tasks = applyWorkflowRules([...(data.tasks || [])]);
const unsignedExec = tasks.filter(t => t.state === 'Execute' && !hasDualSignoff(t)).length;
document.getElementById('boardName').textContent = data.boardName || '多 Agent 任务看板';
document.getElementById('updatedAt').textContent = `更新时间:${data.updatedAt || '未知'} · 任务数 ${tasks.length}`;
document.getElementById('updatedAt').textContent = `更新时间:${data.updatedAt || '未知'} · 任务数 ${tasks.length} · 双签缺失执行项 ${unsignedExec}`;
const statsWrap = document.getElementById('stats');
const running = tasks.filter(t => ['Dispatch','Execute','Verify'].includes(t.state)).length;
@@ -151,37 +176,60 @@
return `<div class="col">
<div class="col-head"><span>${col}</span><span>${list.length}</span></div>
<div class="col-body">
${list.length ? list.map(t => `
${list.length ? list.map(t => {
const dual = hasDualSignoff(t);
const signText = dual ? '双签✅' : '双签❌';
const signStyle = dual ? 'style="color:#22c55e"' : 'style="color:#ef4444"';
return `
<div class="card">
<h4>${t.id} · ${t.title}</h4>
<div class="meta">
<span class="pill">${t.owner || '未分配'}</span>
<span class="pill">P:${t.priority || 'n/a'}</span>
<span class="pill ${riskClass(t.risk)}">Risk:${t.risk || 'low'}</span>
<span class="pill" ${signStyle}>${signText}</span>
</div>
<div class="meta">${t.summary || ''}</div>
</div>
`).join('') : `<div class="meta">暂无任务</div>`}
`}).join('') : `<div class="meta">暂无任务</div>`}
</div>
</div>`;
}).join('');
}
async function loadSample() {
const res = await fetch('./tasks.sample.json');
const res = await fetch('./tasks.sample.json', { cache: 'no-store' });
const data = await res.json();
render(data);
}
document.getElementById('loadSample').addEventListener('click', loadSample);
function setupAutoRefresh() {
if (state.timer) clearInterval(state.timer);
const enabled = document.getElementById('autoRefresh').checked;
const ms = Number(document.getElementById('refreshMs').value || 10000);
if (!enabled || state.lastSource !== 'sample') return;
state.timer = setInterval(loadSample, ms);
}
document.getElementById('loadSample').addEventListener('click', async () => {
state.lastSource = 'sample';
await loadSample();
setupAutoRefresh();
});
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files?.[0];
if (!file) return;
state.lastSource = 'upload';
if (state.timer) clearInterval(state.timer);
const text = await file.text();
render(JSON.parse(text));
});
loadSample();
document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh);
document.getElementById('refreshMs').addEventListener('change', setupAutoRefresh);
loadSample().then(setupAutoRefresh);
</script>
</body>
</html>