- local_rig/ 完整包(7 源文件 + ui.html + CHANGELOG) - tests/test_local_rig.py + test_rig_real_agent.py(纯 local_rig 测试) - scripts/15-criterion4-live.sh(判据④活环境门) - README.md(定位/快速开始/与 tutti-selfhost 的关系) - requirements.txt(唯一外部依赖 pyyaml) - .gitignore 来源: figmar/tutti-selfhost @ 3b23853 (2026-10-06) 原 PR #43 已合入 tutti-selfhost main
166 lines
6.2 KiB
HTML
166 lines
6.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Rig Dashboard</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#e6edf3;--dim:#8b949e;--green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff}
|
|
body{font:14px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);padding:20px}
|
|
h1{font-size:18px;margin-bottom:4px}
|
|
.sub{color:var(--dim);font-size:12px;margin-bottom:20px}
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-bottom:20px}
|
|
.card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:14px}
|
|
.card h3{font-size:13px;color:var(--dim);margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
|
|
.member{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border)}
|
|
.member:last-child{border:none}
|
|
.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
.dot.running{background:var(--green)}.dot.stopped{background:var(--dim)}.dot.ready{background:var(--blue)}
|
|
.mname{font-weight:600;font-size:13px}
|
|
.mmeta{color:var(--dim);font-size:11px;margin-left:auto}
|
|
.task{padding:8px 0;border-bottom:1px solid var(--border);font-size:13px}
|
|
.task:last-child{border:none}
|
|
.tid{color:var(--dim);font-size:11px;font-family:monospace}
|
|
.tprompt{margin:2px 0}
|
|
.tresult{color:var(--green);font-size:12px;max-height:60px;overflow:hidden}
|
|
.terror{color:var(--red);font-size:12px}
|
|
.badge{display:inline-block;padding:1px 6px;border-radius:10px;font-size:11px;font-weight:600}
|
|
.badge.pending{background:#3d1d00;color:var(--yellow)}
|
|
.badge.running{background:#0d1f2d;color:var(--blue)}
|
|
.badge.done{background:#0d2818;color:var(--green)}
|
|
.badge.failed{background:#2d0d0d;color:var(--red)}
|
|
.badge.timeout{background:#2d0d2d;color:var(--purple)}
|
|
form{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
|
|
select,input,button{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:6px;font-size:13px}
|
|
select:focus,input:focus{outline:none;border-color:var(--blue)}
|
|
button{cursor:pointer;background:var(--blue);color:#fff;border:none;font-weight:600}
|
|
button:hover{opacity:.85}
|
|
button.sec{background:var(--card);color:var(--text);border:1px solid var(--border)}
|
|
#log{margin-top:12px;font-family:monospace;font-size:11px;color:var(--dim);max-height:120px;overflow-y:auto;background:var(--card);border-radius:6px;padding:8px;display:none}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🔧 Rig Dashboard</h1>
|
|
<div class="sub" id="sub">connecting...</div>
|
|
|
|
<form id="assign-form" onsubmit="return assign(event)">
|
|
<select id="f-assignee"></select>
|
|
<input id="f-prompt" placeholder="任务描述..." style="flex:1;min-width:200px" required>
|
|
<button type="submit">派发</button>
|
|
</form>
|
|
|
|
<div class="grid">
|
|
<div class="card"><h3>Members</h3><div id="members"></div></div>
|
|
<div class="card"><h3>Tasks</h3><div id="tasks"><div style="color:var(--dim)">loading...</div></div></div>
|
|
</div>
|
|
|
|
<div id="log"></div>
|
|
|
|
<script>
|
|
const BASE = location.origin;
|
|
let members = {};
|
|
|
|
async function api(path, opts) {
|
|
const r = await fetch(BASE + path, opts);
|
|
return r.json();
|
|
}
|
|
|
|
function log(msg) {
|
|
const el = document.getElementById('log');
|
|
el.style.display = 'block';
|
|
el.innerHTML += `<div>[${new Date().toLocaleTimeString()}] ${msg}</div>`;
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
async function refresh() {
|
|
try {
|
|
const [rig, status, tasks] = await Promise.all([
|
|
api('/rig'), api('/status'), api('/tasks?limit=20')
|
|
]);
|
|
// sub
|
|
document.getElementById('sub').textContent =
|
|
`${rig.name} · ${new Date().toLocaleTimeString()}`;
|
|
// members
|
|
const all = {};
|
|
for (const pod of rig.pods || []) {
|
|
for (const m of pod.members || []) {
|
|
all[pod.id + '.' + m.id] = m;
|
|
}
|
|
}
|
|
members = all;
|
|
const el = document.getElementById('members');
|
|
el.innerHTML = '';
|
|
for (const [mid, m] of Object.entries(all)) {
|
|
const st = status[mid] || 'stopped';
|
|
const dot = st === 'running' ? 'running' : st === 'ready' ? 'ready' : 'stopped';
|
|
const meta = m.runtime + (m.profile && m.profile !== 'default' ? ' · ' + m.profile : '');
|
|
el.innerHTML += `<div class="member">
|
|
<div class="dot ${dot}"></div>
|
|
<span class="mname">${mid}</span>
|
|
<span class="mmeta">${meta}</span>
|
|
</div>`;
|
|
}
|
|
// assignee dropdown
|
|
const sel = document.getElementById('f-assignee');
|
|
if (!sel.options.length) {
|
|
for (const mid of Object.keys(all)) {
|
|
const o = document.createElement('option');
|
|
o.value = mid; o.textContent = mid;
|
|
sel.appendChild(o);
|
|
}
|
|
}
|
|
// tasks
|
|
const tel = document.getElementById('tasks');
|
|
if (!tasks.length) {
|
|
tel.innerHTML = '<div style="color:var(--dim)">无任务</div>';
|
|
} else {
|
|
tel.innerHTML = tasks.map(t => {
|
|
let extra = '';
|
|
if (t.result) extra = `<div class="tresult">${esc(t.result.slice(0,200))}</div>`;
|
|
if (t.error) extra = `<div class="terror">${esc(t.error.slice(0,200))}</div>`;
|
|
return `<div class="task">
|
|
<span class="badge ${t.status}">${t.status}</span>
|
|
<span class="tid">${t.id.slice(0,16)}...</span>
|
|
<div class="tprompt">${esc(t.prompt)}</div>
|
|
${extra}
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
} catch (e) {
|
|
log('refresh error: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
async function assign(e) {
|
|
e.preventDefault();
|
|
const assignee = document.getElementById('f-assignee').value;
|
|
const prompt = document.getElementById('f-prompt').value;
|
|
if (!assignee || !prompt) return false;
|
|
try {
|
|
const r = await api('/tasks', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({assignee, prompt})
|
|
});
|
|
log(`✅ ${r.id} → ${assignee}`);
|
|
document.getElementById('f-prompt').value = '';
|
|
refresh();
|
|
} catch (err) {
|
|
log('❌ ' + err.message);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
refresh();
|
|
setInterval(refresh, 3000);
|
|
</script>
|
|
</body>
|
|
</html>
|