1b2b935832
- 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
263 lines
13 KiB
HTML
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,'&').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)} · ${esc(a.host)} ${a.pid ? '· 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) ? '▲' : '▼'}</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>
|