<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BBSBot Swarm Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
<style>
:root {
--bg: #0d1117;
--bg2: #161b22;
--bg3: #21262d;
--fg: #c9d1d9;
--fg2: #8b949e;
--green: #3fb950;
--red: #f85149;
--yellow: #d29922;
--blue: #58a6ff;
--cyan: #39c5cf;
--border: #30363d;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
background: var(--bg);
color: var(--fg);
font-size: 14px;
}
.header {
background: var(--bg2);
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 { font-size: 16px; color: var(--cyan); font-weight: 600; }
.header .status { font-size: 12px; color: var(--fg2); }
.header .status .dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
vertical-align: middle;
}
.dot.connected { background: var(--green); }
.dot.disconnected { background: var(--red); }
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
padding: 16px 24px;
}
.card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px;
}
.card .label { font-size: 11px; color: var(--fg2); text-transform: uppercase; letter-spacing: 0.5px; }
.card .value { font-size: 24px; font-weight: 700; margin-top: 4px; }
.card .value.green { color: var(--green); }
.card .value.red { color: var(--red); }
.card .value.blue { color: var(--blue); }
.card .value.cyan { color: var(--cyan); }
.card .value.yellow { color: var(--yellow); }
.table-wrap {
padding: 0 24px 24px;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
thead th {
text-align: left;
padding: 10px 14px;
background: var(--bg3);
color: var(--fg2);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
thead th:hover { color: var(--fg); }
thead th:last-child { cursor: default; }
tbody td {
padding: 8px 14px;
border-bottom: 1px solid var(--border);
font-size: 13px;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:nth-child(even) { background: rgba(255,255,255,0.02); }
tbody tr:hover { background: var(--bg3); }
.state { font-weight: 600; }
.state.running { color: var(--green); }
.state.completed { color: var(--blue); }
.state.error { color: var(--red); }
.state.stopped { color: var(--fg2); }
.state.queued { color: var(--yellow); }
.state.warning { color: #f0883e; }
.numeric { text-align: right; font-variant-numeric: tabular-nums; }
/* Hijack indicator */
.hijack-badge {
display: inline-block;
background: rgba(248, 81, 73, 0.2);
color: var(--red);
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid var(--red);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Action buttons */
.actions { white-space: nowrap; }
.btn {
display: inline-block;
padding: 3px 10px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg3);
color: var(--fg2);
font-size: 11px;
font-family: inherit;
cursor: pointer;
margin-right: 4px;
transition: background 0.15s, color 0.15s;
}
.btn:hover { background: var(--bg); color: var(--fg); }
.btn:disabled { opacity: 0.3; cursor: not-allowed; }
.btn.logs { border-color: var(--cyan); color: var(--cyan); }
.btn.logs:hover { background: rgba(57,197,207,0.15); }
.btn.restart { border-color: var(--green); color: var(--green); }
.btn.restart:hover { background: rgba(63,185,80,0.15); }
.btn.kill { border-color: var(--red); color: var(--red); }
.btn.kill:hover { background: rgba(248,81,73,0.15); }
/* Log modal */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 100;
justify-content: center;
align-items: center;
}
.modal-overlay.open { display: flex; }
.modal {
width: 90vw;
height: 85vh;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
}
.modal-header h2 {
font-size: 14px;
color: var(--cyan);
font-weight: 600;
}
.modal-header .close-btn {
background: none;
border: none;
color: var(--fg2);
font-size: 20px;
cursor: pointer;
padding: 0 6px;
line-height: 1;
}
.modal-header .close-btn:hover { color: var(--fg); }
.modal-status {
padding: 4px 16px;
font-size: 11px;
color: var(--fg2);
background: var(--bg2);
border-bottom: 1px solid var(--border);
}
.modal-status .live { color: var(--green); }
.log-content {
flex: 1;
overflow-y: auto;
padding: 8px 16px;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
color: var(--fg);
}
.log-content .log-line { margin: 0; }
.log-content .log-line:nth-child(even) { background: rgba(255,255,255,0.015); }
/* Terminal modal */
.term-modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 200;
justify-content: center;
align-items: center;
}
.term-modal-overlay.open { display: flex; }
.term-modal {
width: 92vw;
height: 88vh;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.term-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
gap: 12px;
}
.term-modal-header h2 {
font-size: 14px;
color: var(--cyan);
font-weight: 600;
white-space: nowrap;
}
.term-stats {
display: flex;
gap: 12px;
align-items: center;
flex: 1;
font-size: 12px;
color: var(--fg2);
overflow-x: auto;
padding: 0 8px;
}
.term-stats .stat {
display: inline-flex;
gap: 4px;
align-items: center;
white-space: nowrap;
}
.term-stats .stat-label {
color: var(--fg2);
opacity: 0.7;
}
.term-stats .stat-value {
color: var(--fg);
font-weight: 500;
}
.term-stats .stat-value.credits { color: var(--green); }
.term-stats .stat-value.sector { color: var(--yellow); }
.term-stats .stat-value.turns { color: var(--cyan); }
.term-actions {
display: flex;
gap: 8px;
align-items: center;
}
.term-status {
padding: 6px 16px;
font-size: 11px;
color: var(--fg2);
background: var(--bg2);
border-bottom: 1px solid var(--border);
}
.term-status .live { color: var(--green); }
.term-status .warn { color: var(--yellow); }
.term-status .bad { color: var(--red); }
.term-container {
flex: 1;
padding: 8px;
background: #0b0f14;
}
#term {
width: 100%;
height: 100%;
}
.term-analysis {
border-top: 1px solid var(--border);
background: var(--bg2);
}
.term-analysis summary {
padding: 6px 16px;
font-size: 11px;
color: var(--fg2);
cursor: pointer;
user-select: none;
}
.term-analysis pre {
margin: 0;
padding: 8px 16px;
font-size: 12px;
line-height: 1.35;
color: var(--fg);
white-space: pre;
overflow: auto;
max-height: 32vh;
border-top: 1px solid var(--border);
}
/* Activity badge */
.activity-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.activity-badge.trading { background: rgba(63,185,80,0.2); color: var(--green); }
.activity-badge.exploring { background: rgba(88,166,255,0.2); color: var(--blue); }
.activity-badge.battling { background: rgba(248,81,73,0.2); color: var(--red); }
.activity-badge.orienting { background: rgba(210,153,34,0.3); color: var(--yellow); animation: pulse 1s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
.activity-badge.selecting { background: rgba(210,153,34,0.2); color: var(--yellow); }
.activity-badge.connecting { background: rgba(57,197,207,0.2); color: var(--cyan); }
.activity-badge.queued { background: rgba(210,153,34,0.15); color: var(--yellow); }
.activity-badge.dead { background: rgba(139,148,158,0.15); color: var(--fg2); }
.activity-badge.idle { background: rgba(139,148,158,0.2); color: var(--fg2); }
/* Error badge */
.error-badge {
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--red);
color: var(--bg);
font-weight: 700;
text-align: center;
line-height: 20px;
font-size: 12px;
cursor: pointer;
}
/* Error detail modal */
.error-modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 150;
justify-content: center;
align-items: center;
}
.error-modal-overlay.open { display: flex; }
.error-modal {
width: 500px;
max-height: 80vh;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.error-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
}
.error-modal-header h3 {
font-size: 14px;
color: var(--red);
font-weight: 600;
margin: 0;
}
.error-modal-close {
background: none;
border: none;
color: var(--fg2);
font-size: 20px;
cursor: pointer;
padding: 0;
line-height: 1;
}
.error-modal-close:hover { color: var(--fg); }
.error-modal-content {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
font-size: 12px;
}
.error-modal-content .field {
margin-bottom: 12px;
}
.error-modal-content .label {
color: var(--fg2);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.error-modal-content .value {
color: var(--fg);
font-family: 'SF Mono', monospace;
word-break: break-all;
white-space: pre-wrap;
line-height: 1.4;
}
/* Action feed */
.action-feed {
display: none;
margin-top: 12px;
padding: 8px;
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
font-size: 11px;
}
.action-feed.show { display: block; }
.action-item {
padding: 4px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
margin-bottom: 4px;
}
.action-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.action-time { color: var(--fg2); margin-right: 8px; }
.action-type { color: var(--cyan); font-weight: 600; }
.action-result { margin-left: 8px; }
.action-result.success { color: var(--green); }
.action-result.failure { color: var(--red); }
/* Toast notification */
.toast {
position: fixed;
bottom: 24px;
right: 24px;
padding: 10px 20px;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-size: 13px;
z-index: 200;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1; }
.toast.success { border-color: var(--green); color: var(--green); }
.toast.error { border-color: var(--red); color: var(--red); }
</style>
</head>
<body>
<div class="header">
<h1>BBSBOT SWARM DASHBOARD</h1>
<div style="display:flex;align-items:center;gap:12px;">
<div style="display:flex;align-items:center;gap:6px;">
<input type="number" id="spawn-count" min="1" max="100" value="5" style="width:60px;background:var(--bg);border:1px solid var(--border);color:var(--fg);padding:4px 8px;border-radius:4px;font-family:inherit;font-size:12px;" placeholder="Count">
<select id="spawn-config" style="background:var(--bg);border:1px solid var(--border);color:var(--fg);padding:4px 8px;border-radius:4px;font-family:inherit;font-size:12px;">
<option value="swarm_demo">swarm_demo</option>
<option value="swarm_demo_ai">swarm_demo_ai (AI)</option>
</select>
<button class="btn" id="btn-spawn" style="border-color:var(--green);color:var(--green);" title="Spawn bots">Spawn</button>
</div>
<div style="border-left:1px solid var(--border);height:24px;"></div>
<button class="btn kill" id="btn-kill-all" title="Kill all running bots">Kill All</button>
<button class="btn" id="btn-clear" title="Clear all bot entries" style="border-color:var(--yellow);color:var(--yellow);">Clear</button>
</div>
<div class="status">
<span class="dot disconnected" id="dot"></span>
<span id="conn-status">Connecting...</span>
<span id="uptime"></span>
</div>
</div>
<div class="summary">
<div class="card"><div class="label">Running</div><div class="value green" id="running">-</div></div>
<div class="card"><div class="label">Total</div><div class="value" id="total">-</div></div>
<div class="card"><div class="label">Completed</div><div class="value blue" id="completed">-</div></div>
<div class="card"><div class="label">Errors</div><div class="value red" id="errors">-</div></div>
<div class="card"><div class="label">Credits</div><div class="value cyan" id="credits">-</div></div>
<div class="card"><div class="label">Turns</div><div class="value" id="turns">-</div></div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th data-sort="bot_id">Bot ID</th>
<th data-sort="state">State</th>
<th>Activity</th>
<th data-sort="is_hijacked">Status</th>
<th data-sort="username">Username</th>
<th data-sort="sector">Sector</th>
<th data-sort="credits">Credits</th>
<th data-sort="turns_executed">Turns</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="bot-table"></tbody>
</table>
</div>
<!-- Terminal modal -->
<div class="term-modal-overlay" id="term-modal">
<div class="term-modal" role="dialog" aria-modal="true" aria-label="Terminal">
<div class="term-modal-header">
<h2 id="term-title">Terminal</h2>
<div class="term-stats" id="term-stats"></div>
<div class="term-actions">
<button class="btn" id="term-resync" title="Request a fresh snapshot">Resync</button>
<button class="btn kill" id="term-hijack" title="Pause bot automation and take control">Hijack</button>
<button class="btn" id="term-step" title="Run one automation step, then pause again" disabled>Step</button>
<button class="btn restart" id="term-release" title="Release control and resume automation" disabled>Release</button>
<button class="btn" id="term-analyze" title="Request prompt detection analysis">Analyze</button>
<button class="btn" id="term-close">Close</button>
</div>
</div>
<div class="term-status" id="term-status">Disconnected</div>
<div class="term-container">
<div id="term"></div>
</div>
<details class="term-analysis" id="term-analysis-details">
<summary>Screen analysis</summary>
<pre id="term-analysis"></pre>
</details>
</div>
</div>
<!-- Error detail modal -->
<div class="error-modal-overlay" id="error-modal-overlay">
<div class="error-modal">
<div class="error-modal-header">
<h3>Error Details</h3>
<button class="error-modal-close" id="error-modal-close">×</button>
</div>
<div class="error-modal-content" id="error-modal-content"></div>
</div>
</div>
<!-- Log viewer modal -->
<div class="modal-overlay" id="log-modal">
<div class="modal">
<div class="modal-header">
<h2 id="log-title">Logs: ---</h2>
<button class="close-btn" id="log-close">×</button>
</div>
<div class="modal-status">
<span id="log-status">Connecting...</span>
</div>
<div class="log-content" id="log-content"></div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script src="/static/dashboard.js"></script>
</body>
</html>