Files
AgentsMeeting/gateway/scripts/templates/dashboard.html
T
hmo 1b2b935832 Initial: multi-agent XMPP communication system with dashboard
- Platform-based architecture (Windows/Linux/Mac)
- Agent instance registry (agents.yaml)
- Management dashboard with cross-platform monitoring
- xmpp_bot with HTTP bridge + health endpoints
- wechat_agent with WeChat-Hermes bridging
- Platform services: ProcessGuardian, HealthProbe, APIRouter, ChannelBridge
- Deployment: systemd (Linux) + PowerShell (Windows)
- Monitoring: SSH+ejabberdctl for cross-platform presence
2026-06-12 21:51:36 +08:00

263 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AgentsMeeting Dashboard</title>
<style>
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--text: #c9d1d9;
--dim: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--red: #f85149;
--yellow: #d29922;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { background:var(--bg); color:var(--text); font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; padding:24px; min-height:100vh; }
h1 { font-size:20px; font-weight:600; color:var(--accent); margin-bottom:4px; }
.subtitle { color:var(--dim); font-size:12px; margin-bottom:20px; }
/* stats bar */
.stats { display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap; }
.stat-card { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:12px 16px; flex:1; min-width:140px; }
.stat-card .num { font-size:28px; font-weight:700; }
.stat-card .label { font-size:11px; color:var(--dim); text-transform:uppercase; letter-spacing:.5px; }
.stat-card.online .num { color:var(--green); }
.stat-card.offline .num { color:var(--red); }
.stat-card.errors .num { color:var(--yellow); }
.stat-card.total .num { color:var(--accent); }
/* agent cards */
.agents { display:flex; flex-direction:column; gap:12px; }
.agent-card { background:var(--card); border:1px solid var(--border); border-radius:10px; overflow:hidden; }
.agent-card.online { border-left:3px solid var(--green); }
.agent-card.degraded { border-left:3px solid var(--yellow); }
.agent-card.offline { border-left:3px solid var(--red); opacity:.7; }
.agent-header { display:flex; align-items:center; padding:14px 16px; gap:12px; cursor:pointer; }
.agent-header:hover { background:rgba(255,255,255,.02); }
.status-dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
.status-dot.online { background:var(--green); box-shadow:0 0 6px var(--green); }
.status-dot.degraded { background:var(--yellow); box-shadow:0 0 6px var(--yellow); }
.status-dot.offline { background:var(--red); }
.agent-info { flex:1; min-width:0; }
.agent-name { font-size:15px; font-weight:600; }
.agent-meta { font-size:12px; color:var(--dim); margin-top:2px; }
.badge { display:inline-block; padding:1px 8px; border-radius:10px; font-size:10px; font-weight:600; margin-left:8px; }
.badge.windows { background:#0078d420; color:#60cdff; }
.badge.linux { background:#fcc62420; color:#fcc624; }
.badge.mac { background:#99999920; color:#ccc; }
.agent-stats { display:flex; gap:16px; align-items:center; padding-right:8px; flex-shrink:0; }
.agent-stat { text-align:right; }
.agent-stat .val { font-size:13px; font-weight:600; }
.agent-stat .lbl { font-size:10px; color:var(--dim); }
.agent-actions { display:flex; gap:4px; flex-shrink:0; }
.btn { padding:5px 12px; border:1px solid var(--border); border-radius:5px; background:#21262d; color:var(--text); cursor:pointer; font-size:11px; transition:all .15s; }
.btn:hover { background:#30363d; }
.btn.start { border-color:var(--green); color:var(--green); }
.btn.stop { border-color:var(--red); color:var(--red); }
.btn.restart { border-color:var(--yellow); color:var(--yellow); }
.btn.log { border-color:var(--accent); color:var(--accent); }
.btn:disabled { opacity:.4; cursor:not-allowed; }
/* services row */
.services { display:flex; gap:8px; padding:0 16px 10px; flex-wrap:wrap; }
.service-tag { display:inline-flex; align-items:center; gap:4px; padding:3px 10px; border-radius:12px; font-size:11px; background:#0d1117; border:1px solid var(--border); }
.service-tag .dot { width:6px; height:6px; border-radius:50%; }
.service-tag .dot.running { background:var(--green); }
.service-tag .dot.stopped { background:var(--dim); }
.service-tag .port { color:var(--accent); font-family:monospace; font-size:10px; margin-left:4px; }
/* log panel */
.log-panel { display:none; border-top:1px solid var(--border); padding:12px 16px; background:#0a0e14; }
.log-panel.open { display:block; }
.log-header { font-size:12px; color:var(--dim); margin-bottom:8px; display:flex; justify-content:space-between; align-items:center; }
.log-content { font:11px/1.6 'Cascadia Code','Consolas',monospace; color:var(--dim); max-height:300px; overflow-y:auto; background:#06080c; border:1px solid var(--border); border-radius:6px; padding:10px; white-space:pre-wrap; word-break:break-all; }
.log-content::-webkit-scrollbar { width:6px; }
.log-content::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
/* toast */
.toast { position:fixed; top:16px; right:16px; padding:10px 16px; border-radius:6px; font-size:13px; z-index:999; opacity:0; transition:opacity .3s; pointer-events:none; }
.toast.show { opacity:1; }
.toast.ok { background:#238636; color:#fff; }
.toast.err { background:#da3633; color:#fff; }
/* loading */
.loading { text-align:center; padding:40px; color:var(--dim); }
.spinner { display:inline-block; width:20px; height:20px; border:2px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin .6s linear infinite; margin-right:8px; vertical-align:middle; }
@keyframes spin { to { transform:rotate(360deg); } }
.expand { color:var(--dim); font-size:18px; margin-left:4px; }
.expand.open { color:var(--accent); }
/* platform section */
.platform-section { margin-top:24px; }
.platform-section h2 { font-size:16px; color:var(--dim); margin-bottom:12px; padding-bottom:8px; border-bottom:1px solid var(--border); }
.platform-svcs { display:flex; gap:8px; flex-wrap:wrap; }
.platform-svc { background:var(--card); border:1px solid var(--border); border-radius:6px; padding:8px 12px; display:flex; align-items:center; gap:8px; font-size:12px; }
.platform-svc .dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
.platform-svc .dot.running { background:var(--green); }
.platform-svc .dot.stopped { background:var(--dim); }
.platform-svc .type { color:var(--accent); font-size:10px; text-transform:uppercase; }
</style>
</head>
<body>
<h1>AgentsMeeting Dashboard</h1>
<div class="subtitle" id="subtitle">Loading...</div>
<div class="stats" id="stats"></div>
<div class="agents" id="agents"><div class="loading"><span class="spinner"></span>Loading agents...</div></div>
<div class="platform-section">
<h2>Infrastructure</h2>
<div class="platform-svcs" id="platform"></div>
<div class="platform-svcs" id="ejabberd-status" style="margin-top:8px;"></div>
</div>
<div id="toast" class="toast"></div>
<script>
const API = '/api/agents';
let agentsData = [];
const openLogs = new Set();
function toast(msg, type) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast show ' + type;
setTimeout(() => t.className = 'toast', 2500);
}
async function doAction(id, action) {
try {
const r = await fetch(API + '/' + id + '/' + action, { method:'POST' });
const d = await r.json();
toast(action + (d.ok ? ' OK' : ' FAILED'), d.ok ? 'ok' : 'err');
setTimeout(fetchAgents, 1500);
} catch(e) { toast('Error: ' + e.message, 'err'); }
}
function toggleLogs(id) {
if (openLogs.has(id)) openLogs.delete(id);
else openLogs.add(id);
render();
if (openLogs.has(id)) fetchLogs(id);
}
async function fetchLogs(id) {
const el = document.getElementById('log-' + id);
if (!el || !openLogs.has(id)) return;
el.style.opacity = '0.5';
try {
const r = await fetch(API + '/' + id + '/logs?lines=50');
const d = await r.json();
el.innerHTML = (d.lines || []).map(l => '<div>' + esc(l) + '</div>').join('') || '<span style="color:var(--dim)">no log data</span>';
} catch(e) {
el.innerHTML = '<span style="color:var(--red)">Failed: ' + esc(e.message) + '</span>';
}
el.style.opacity = '1';
el.scrollTop = el.scrollHeight;
}
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'<').replace(/>/g,'>'); }
function render() {
if (!agentsData.length) {
document.getElementById('agents').innerHTML = '<div class="loading">No agents registered</div>';
return;
}
document.getElementById('agents').innerHTML = agentsData.map(a => `
<div class="agent-card ${a.status}">
<div class="agent-header" onclick="toggleLogs('${a.id}')">
<span class="status-dot ${a.status}"></span>
<div class="agent-info">
<span class="agent-name">${esc(a.display_name)}</span>
<span class="badge ${a.platform}">${a.platform}</span>
<div class="agent-meta">${esc(a.jid)} &middot; ${esc(a.host)} ${a.pid ? '&middot; PID '+a.pid : ''}</div>
</div>
<div class="agent-stats">
${a.last_message ? '<div class="agent-stat"><div class="val">'+esc(a.last_message)+'</div><div class="lbl">last msg</div></div>' : ''}
<div class="agent-stat"><div class="val">${a.message_count_5min}</div><div class="lbl">msgs/5min</div></div>
${a.errors ? '<div class="agent-stat"><div class="val" style="color:var(--red)">'+a.errors+'</div><div class="lbl">errors</div></div>' : ''}
</div>
<div class="agent-actions" onclick="event.stopPropagation()">
<button class="btn start" onclick="doAction('${a.id}','start')" ${(a.status!=='offline'||!a.restartable)?'disabled':''}>Start</button>
<button class="btn stop" onclick="doAction('${a.id}','stop')" ${(a.status==='offline'||!a.restartable)?'disabled':''}>Stop</button>
<button class="btn restart" onclick="doAction('${a.id}','restart')" ${!a.restartable?'disabled':''}>Restart</button>
</div>
<span class="expand">${openLogs.has(a.id) ? '&#9650;' : '&#9660;'}</span>
</div>
<div class="services">
${(a.services || []).map(s => '<span class="service-tag"><span class="dot '+s.status+'"></span>'+esc(s.type)+(s.port?' <span class="port">:'+s.port+'</span>':'')+(s.pid?' PID:'+s.pid:'')+'</span>').join('')}
</div>
<div class="log-panel ${openLogs.has(a.id) ? 'open' : ''}" id="log-${a.id}">
<div class="log-header">
<span>Agent Logs</span>
<button class="btn log" onclick="fetchLogs('${a.id}');event.stopPropagation()">Refresh</button>
</div>
<div class="log-content">Click arrow to load logs</div>
</div>
</div>
`).join('');
}
function renderStats() {
const online = agentsData.filter(a => a.status === 'online').length;
const offline = agentsData.filter(a => a.status === 'offline').length;
const degraded = agentsData.filter(a => a.status === 'degraded').length;
const errors = agentsData.reduce((s, a) => s + (a.errors || 0), 0);
document.getElementById('stats').innerHTML = `
<div class="stat-card online"><div class="num">${online}</div><div class="label">Online</div></div>
<div class="stat-card"><div class="num" style="color:${degraded?'var(--yellow)':'var(--dim)'}">${degraded}</div><div class="label">Degraded</div></div>
<div class="stat-card offline"><div class="num">${offline}</div><div class="label">Offline</div></div>
<div class="stat-card errors"><div class="num">${errors}</div><div class="label">Errors</div></div>
<div class="stat-card total"><div class="num">${agentsData.length}</div><div class="label">Total</div></div>
`;
}
async function fetchAgents() {
try {
const r = await fetch(API);
agentsData = await r.json();
renderStats();
render();
document.getElementById('subtitle').textContent = 'Last update: ' + new Date().toLocaleTimeString();
openLogs.forEach(id => fetchLogs(id));
} catch(e) {
document.getElementById('agents').innerHTML = '<div class="loading" style="color:var(--red)"><span class="spinner" style="border-top-color:var(--red)"></span>Cannot connect to backend (:5803). Is dashboard.py running?</div>';
}
}
async function fetchPlatform() {
try {
const r = await fetch('/api/platform');
const data = await r.json();
document.getElementById('platform').innerHTML = data.map(s => '<div class="platform-svc"><span class="dot '+s.status+'"></span><strong>'+esc(s.name)+'</strong><span class="type">'+s.type+'</span><span style="margin-left:auto;color:var(--dim)">'+(s.pid?'PID:'+s.pid:'')+'</span></div>').join('');
} catch(e) {}
}
async function fetchEjabberd() {
try {
const r = await fetch('/api/ejabberd');
const d = await r.json();
const alive = d.alive || d.xmpp_bot_connected;
document.getElementById('ejabberd-status').innerHTML = '<div class="platform-svc"><span class="dot '+(alive?'running':'stopped')+'"></span><strong>Ejabberd XMPP Server</strong><span class="type">'+(alive?'ALIVE':'DOWN')+'</span><span style="margin-left:auto;color:var(--dim)">online: '+(d.online_jids||[]).join(', ')+'</span></div>';
} catch(e) {}
}
fetchAgents();
fetchPlatform();
fetchEjabberd();
setInterval(fetchAgents, 5000);
setInterval(fetchPlatform, 10000);
setInterval(fetchEjabberd, 10000);
setInterval(() => openLogs.forEach(id => fetchLogs(id)), 3000);
</script>
</body>
</html>