BrainSteel Fin v1.0 — 3-agent BTC trading pipeline

- Brief: Market Intelligence (Binance data + LLM analysis)
- Decio: Strategy decision (BUY/HOLD/SELL)
- PaperT: Order executor (Binance API)
- Anime-style Flask dashboard
- Traefik-ready Docker deployment
This commit is contained in:
Hermes - BrainSteel VPS
2026-05-17 16:25:55 +00:00
commit 50960bab21
24 changed files with 3285 additions and 0 deletions
+608
View File
@@ -0,0 +1,608 @@
/**
* BrainSteel Fin — Dashboard JavaScript
* 4-Agent Pipeline: Brief → Decio → PaperT → Audit
*/
// ── State ─────────────────────────────────────────────────────────────────────
let autoMode = false;
let autoInterval = null;
let pollInterval = null;
const AGENT_EMOJIS = { brief: "📊", decio: "🎯", papert: "⚡", audit: "🔍" };
const AGENT_COLORS = { brief: "#4fc3f7", decio: "#ffd54f", papert: "#69f0ae", audit: "#b388ff" };
// ── Init ─────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
updateClock();
setInterval(updateClock, 1000);
startPolling();
loadAgentRegistry();
loadMetrics();
loadServices();
loadPipelineVisual();
loadTokenChart();
updateStatus('Sistema operacional', 'online');
});
// ── Clock ─────────────────────────────────────────────────────────────────────
function updateClock() {
const now = new Date();
document.getElementById('clock').textContent = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
// ── Polling ───────────────────────────────────────────────────────────────────
function startPolling() {
pollState();
pollInterval = setInterval(pollState, 3000);
}
function pollState() {
fetch('/api/state')
.then(r => r.json())
.then(data => {
updateAgentUI('brief', data.brief);
updateAgentUI('decio', data.decio);
updateAgentUI('papert', data.papert);
updateAgentUI('audit', data.audit);
document.getElementById('btnRun').disabled = data.pipeline_running;
updateStatus(data.pipeline_running ? 'Pipeline em execução' : 'Aguardando', data.pipeline_running ? 'running' : 'online');
if (!data.pipeline_running && data.last_pipeline) {
showDecisionBanner(data);
}
}).catch(err => console.error('Poll error:', err));
}
function updateAgentUI(agent, data) {
const node = document.getElementById(`node-${agent}`);
const statusEl = document.getElementById(`status-${agent}`);
const msgEl = document.getElementById(`msg-${agent}`);
if (!node) return;
node.className = `pipeline-node ${data.status || 'idle'}`;
if (statusEl) {
const labels = { idle: 'Aguardando', working: 'Trabalhando', done: 'Concluído', error: 'Erro', waiting: 'Aguardando' };
statusEl.textContent = labels[data.status] || data.status;
statusEl.className = `agent-status ${data.status}`;
}
if (msgEl) msgEl.textContent = data.message || '';
}
// ── Status ────────────────────────────────────────────────────────────────────
function updateStatus(text, type) {
const dot = document.getElementById('statusDot');
const txt = document.getElementById('statusText');
if (dot) dot.className = `status-dot ${type}`;
if (txt) txt.textContent = text;
}
// ── Pipeline ─────────────────────────────────────────────────────────────────
function runPipeline() {
fetch('/api/run', { method: 'POST' })
.then(r => r.json())
.then(d => {
if (d.error) { alert(d.error); return; }
addLog('SYS', 'Pipeline iniciado');
}).catch(err => console.error(err));
}
function toggleAuto() {
autoMode = !autoMode;
const el = document.getElementById('autoStatus');
if (el) el.textContent = autoMode ? 'ON' : 'OFF';
if (autoMode) {
addLog('SYS', 'Modo automático ativado — a cada 5 min');
autoInterval = setInterval(runPipeline, 5 * 60 * 1000);
} else {
clearInterval(autoInterval);
addLog('SYS', 'Modo automático desativado');
}
}
// ── Decision Banner ────────────────────────────────────────────────────────────
function showDecisionBanner(data) {
const banner = document.getElementById('lastDecisionBanner');
if (!banner) return;
const decision = data.decio?.message || 'HOLD';
const cls = decision.includes('BUY') ? 'buy' : decision.includes('SELL') ? 'sell' : 'hold';
banner.className = `decision-banner ${cls}`;
banner.textContent = `Última decisão: ${decision}`;
banner.style.display = 'block';
}
// ── Item 1: Agent Registry ────────────────────────────────────────────────────
function loadAgentRegistry() {
fetch('/api/agents')
.then(r => r.json())
.then(agents => {
const container = document.getElementById('agentCards');
if (!container) return;
container.innerHTML = agents.map(a => `
<div class="agent-card" id="card-${a.id}">
<div class="agent-card-header">
<span class="agent-card-emoji">${AGENT_EMOJIS[a.id] || '🤖'}</span>
<div>
<div class="agent-card-name">${a.name}</div>
<div class="agent-card-role">${a.role || ''}</div>
</div>
</div>
<div class="agent-card-intent">${a.intent || '—'}</div>
<div class="agent-card-stats">
<span class="stat-badge runs">⏱ ${a.cycle_count || 0} ciclos</span>
<span class="stat-badge ok">✓ ${a.success_count || 0}</span>
<span class="stat-badge fail">✗ ${a.fail_count || 0}</span>
</div>
<div class="agent-card-model">${a.model || '—'} @ ${a.provider || '?'}</div>
<div class="agent-card-actions">
<button onclick="editAgent('${a.id}')">✏️ Editar</button>
<button onclick="deleteAgent('${a.id}')" style="color:#ff6b6b">🗑️ Remover</button>
</div>
</div>
`).join('');
}).catch(err => console.error('Agent registry error:', err));
}
function showAddAgent() {
document.getElementById('modalAgentTitle').textContent = 'Novo Agente';
['agentId','agentName','agentRole','agentIntent','agentModel','agentProvider'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = id === 'agentId' ? '' : '';
if (id === 'agentModel') el.value = 'deepseek/deepseek-v4-flash:free';
if (id === 'agentProvider') el.value = 'openrouter';
});
document.getElementById('modalAgent').style.display = 'flex';
}
function editAgent(id) {
fetch(`/api/agents/${id}`).then(r => r.json()).then(a => {
document.getElementById('modalAgentTitle').textContent = `Editar: ${a.name}`;
document.getElementById('agentId').value = a.id;
document.getElementById('agentId').readOnly = true;
document.getElementById('agentName').value = a.name || '';
document.getElementById('agentRole').value = a.role || '';
document.getElementById('agentIntent').value = a.intent || '';
document.getElementById('agentModel').value = a.model || '';
document.getElementById('agentProvider').value = a.provider || '';
document.getElementById('modalAgent').style.display = 'flex';
});
}
function saveAgent() {
const id = document.getElementById('agentId').value.trim();
if (!id) { alert('ID é obrigatório'); return; }
const data = {
id, name: document.getElementById('agentName').value.trim(),
role: document.getElementById('agentRole').value.trim(),
intent: document.getElementById('agentIntent').value.trim(),
model: document.getElementById('agentModel').value.trim(),
provider: document.getElementById('agentProvider').value.trim()
};
fetch(`/api/agents/${id}`, {
method: id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(r => r.json()).then(() => {
closeAgent();
loadAgentRegistry();
addLog('SYS', `Agente ${id} atualizado`);
});
}
function deleteAgent(id) {
if (!confirm(`Remover agente ${id}?`)) return;
fetch(`/api/agents/${id}`, { method: 'DELETE' }).then(r => r.json()).then(() => {
loadAgentRegistry();
addLog('SYS', `Agente ${id} removido`);
});
}
function closeAgent() {
document.getElementById('modalAgent').style.display = 'none';
const el = document.getElementById('agentId');
if (el) el.readOnly = false;
}
// ── Item 2: Agent Metrics ────────────────────────────────────────────────────
function loadMetrics() {
fetch('/api/agent_metrics')
.then(r => r.json())
.then(metrics => {
const grid = document.getElementById('metricsGrid');
if (!grid) return;
grid.innerHTML = metrics.map(m => {
const color = AGENT_COLORS[m.id] || '#888';
const rate = m.success_rate || 0;
const avg = m.avg_duration_ms ? `${(m.avg_duration_ms/1000).toFixed(1)}s` : '—';
return `
<div class="metric-card ${m.id}">
<div class="metric-name">${AGENT_EMOJIS[m.id] || '🤖'} ${m.name}</div>
<div class="metric-value">${rate}%</div>
<div class="metric-sub">${m.cycle_count || 0} ciclos · avg ${avg}</div>
<div class="metric-bar"><div class="metric-bar-fill" style="width:${rate}%;background:${color}"></div></div>
</div>`;
}).join('');
}).catch(err => console.error('Metrics error:', err));
}
// ── Item 3: Services ─────────────────────────────────────────────────────────
function loadServices() {
fetch('/api/services')
.then(r => r.json())
.then(services => {
const list = document.getElementById('servicesList');
if (!list) return;
if (!services.length) {
list.innerHTML = '<div class="service-item" style="color:var(--text-secondary)">Nenhum serviço registado. Clique em + Novo Serviço.</div>';
return;
}
list.innerHTML = services.map(s => `
<div class="service-item" id="svc-${s.id}">
<span class="service-status-dot ${s.status || 'unknown'}"></span>
<span class="service-name">${s.name}</span>
<span class="service-type">${s.type || ''}</span>
<span class="service-endpoint">${s.endpoint ? s.endpoint.slice(0,40) : ''}</span>
<div class="service-actions">
<button onclick="pingService('${s.id}')">🏓</button>
<button onclick="deleteService('${s.id}')" style="color:#ff6b6b">×</button>
</div>
</div>
`).join('');
}).catch(err => console.error('Services error:', err));
}
function showAddService() {
['serviceName','serviceType','serviceEndpoint','serviceStatus'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
document.getElementById('modalService').style.display = 'flex';
}
function saveService() {
const name = document.getElementById('serviceName').value.trim();
if (!name) { alert('Nome é obrigatório'); return; }
const data = {
name,
type: document.getElementById('serviceType').value.trim(),
endpoint: document.getElementById('serviceEndpoint').value.trim(),
status: document.getElementById('serviceStatus').value.trim() || 'unknown'
};
fetch('/api/services', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(r => r.json()).then(() => {
closeService();
loadServices();
addLog('SYS', `Serviço ${name} registado`);
});
}
function pingService(id) {
fetch(`/api/services/${id}/ping`, { method: 'POST' })
.then(r => r.json())
.then(d => {
loadServices();
addLog('SYS', `Ping ${id}: ${d.status}`);
});
}
function pingAllServices() {
fetch('/api/services').then(r => r.json()).then(svcs => {
svcs.forEach(s => pingService(s.id));
addLog('SYS', 'Ping em todos os serviços');
});
}
function deleteService(id) {
fetch(`/api/services/${id}`, { method: 'DELETE' }).then(() => {
loadServices();
addLog('SYS', `Serviço ${id} removido`);
});
}
function closeService() {
document.getElementById('modalService').style.display = 'none';
}
// ── Item 4: Pipeline Visual ───────────────────────────────────────────────────
function loadPipelineVisual() {
fetch('/api/pipeline/visual')
.then(r => r.json())
.then(data => {
// Visual flow
const flow = document.getElementById('visualFlow');
if (flow) {
flow.innerHTML = data.nodes.map((n, i) => `
<div class="visual-node" id="vis-${n.id}">
<div style="font-size:1.4rem">${AGENT_EMOJIS[n.id] || '🤖'}</div>
<div class="visual-node-label">${n.label}</div>
<div class="visual-node-status">${n.status || 'idle'}</div>
${n.total_runs ? `<div style="font-size:0.6rem;color:var(--text-secondary)">${n.total_runs}x</div>` : ''}
</div>
${i < data.nodes.length - 1 ? '<span class="visual-arrow">→</span>' : ''}
`).join('');
}
// Timeline
const tl = document.getElementById('visualTimeline');
if (tl) {
tl.innerHTML = data.recent_runs.map(r => `
<div class="timeline-item">
<span class="timeline-ts">${r.started_at ? r.started_at.slice(11,19) : '—'}</span>
<span class="timeline-decision ${r.decio_decision}">${r.decio_decision || '?'}</span>
<span style="font-size:0.65rem;color:var(--text-secondary)">${r.status}</span>
</div>
`).join('');
}
}).catch(err => console.error('Pipeline visual error:', err));
}
// ── History Modal ─────────────────────────────────────────────────────────────
function showHistory() {
fetch('/api/pipeline_history')
.then(r => r.json())
.then(data => {
const content = document.getElementById('historyContent');
if (!content) return;
content.innerHTML = data.map(r => `
<div class="history-item" style="padding:8px;border-bottom:1px solid rgba(255,255,255,0.05)">
<div style="display:flex;justify-content:space-between">
<span style="font-weight:700">${r.decio_decision || '?'}</span>
<span style="color:var(--text-secondary);font-size:0.75rem">${r.started_at ? r.started_at.slice(0,16).replace('T',' ') : '—'}</span>
</div>
<div style="font-size:0.75rem;color:var(--text-secondary);margin-top:4px">${(r.brief_result || '').slice(0,80)}</div>
</div>
`).join('');
document.getElementById('modalHistory').style.display = 'flex';
});
}
function closeHistory() {
document.getElementById('modalHistory').style.display = 'none';
}
// ── Log ───────────────────────────────────────────────────────────────────────
function addLog(agent, message) {
const container = document.getElementById('logContainer');
if (!container) return;
const now = new Date();
const ts = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const div = document.createElement('div');
div.className = 'log-entry';
div.innerHTML = `<span class="log-time">${ts}</span><span class="log-agent">${agent}</span><span class="log-message">${message}</span>`;
container.prepend(div);
while (container.children.length > 50) container.removeChild(container.lastChild);
}
// Refresh all sections periodically
setInterval(() => {
loadAgentRegistry();
loadMetrics();
loadServices();
loadPipelineVisual();
loadPortfolio();
}, 15000);
// ── Portfolio ───────────────────────────────────────────────────────────────
function loadPortfolio() {
fetch('/api/portfolio')
.then(r => r.json())
.then(data => {
if (data.error) return;
const bal = data.current_balance || 0;
const pnl = data.total_pnl || 0;
const pnlPct = data.total_pnl_pct || 0;
const trades = data.total_trades || 0;
const wins = data.wins || 0;
const losses = data.losses || 0;
const wr = data.win_rate || 0;
const init = data.initial_balance || 10000;
document.getElementById('pfBalance').textContent = `$${bal.toLocaleString('pt-BR', {minimumFractionDigits:2})}`;
const pnlEl = document.getElementById('pfPnl');
pnlEl.textContent = `P&L: ${pnl >= 0 ? '+' : ''}$${pnl.toLocaleString('pt-BR',{minimumFractionDigits:2})} (${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%)`;
pnlEl.style.color = pnl >= 0 ? '#69f0ae' : '#ff6b6b';
document.getElementById('pfTrades').textContent = `${trades} trades | ${wins}W/${losses}L | WR: ${wr}%`;
// Draw chart
drawChart(data.chart_data || [], init, bal);
// Recent trades list
const tradesEl = document.getElementById('portfolioTrades');
const recent = data.recent_trades || [];
if (recent.length === 0) {
tradesEl.innerHTML = '<div style="color:var(--text-secondary);font-size:0.8rem;padding:8px">Nenhum trade ainda. Execute o pipeline para começar.</div>';
} else {
tradesEl.innerHTML = recent.slice(0, 10).map(t => {
const cls = t.action === 'BUY' ? '#69f0ae' : t.action === 'SELL' ? (t.result === 'WIN' ? '#69f0ae' : '#ff6b6b') : '#888';
return `<div style="display:flex;gap:10px;padding:6px 8px;background:var(--bg-card-hover);border-radius:6px;margin-bottom:4px;font-size:0.78rem">
<span style="color:var(--text-secondary)">${t.date ? t.date.slice(5) : '—'}</span>
<span style="font-weight:700;color:${cls}">${t.action}</span>
<span>${t.btc_amount ? t.btc_amount.toFixed(6)+' BTC' : '—'}</span>
<span>@ $${Number(t.entry_price).toLocaleString('pt-BR',{minimumFractionDigits:0})}</span>
<span style="color:${t.result==='WIN'?'#69f0ae':t.result==='LOSS'?'#ff6b6b':'#888'}">${t.result || ''}</span>
</div>`;
}).join('');
}
}).catch(err => console.error('Portfolio error:', err));
}
function drawChart(chartData, initial, current) {
const canvas = document.getElementById('chartCanvas');
if (!canvas || !chartData.length) return;
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw grid
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.lineWidth = 1;
for (let i = 0; i < 5; i++) {
const y = (H / 4) * i;
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
// Draw data line
if (chartData.length < 2) return;
const allBalances = chartData.map(d => d[1]);
const minBal = Math.min(...allBalances, initial) * 0.98;
const maxBal = Math.max(...allBalances, initial) * 1.02;
const range = maxBal - minBal || 1;
const toX = (i) => (i / (chartData.length - 1)) * W;
const toY = (v) => H - ((v - minBal) / range) * H;
// Area fill
ctx.beginPath();
ctx.moveTo(toX(0), H);
chartData.forEach(([_, bal], i) => ctx.lineTo(toX(i), toY(bal)));
ctx.lineTo(toX(chartData.length - 1), H);
ctx.closePath();
ctx.fillStyle = 'rgba(79,195,247,0.15)';
ctx.fill();
// Line
ctx.beginPath();
ctx.moveTo(toX(0), toY(chartData[0][1]));
chartData.forEach(([_, bal], i) => ctx.lineTo(toX(i), toY(bal)));
ctx.strokeStyle = '#4fc3f7';
ctx.lineWidth = 2;
ctx.stroke();
// Initial balance reference
const y0 = toY(initial);
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(179,136,255,0.5)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, y0); ctx.lineTo(W, y0); ctx.stroke();
ctx.setLineDash([]);
// Current dot
const lastX = toX(chartData.length - 1);
const lastY = toY(chartData[chartData.length - 1][1]);
ctx.beginPath();
ctx.arc(lastX, lastY, 4, 0, Math.PI * 2);
ctx.fillStyle = current >= initial ? '#69f0ae' : '#ff6b6b';
ctx.fill();
}
function resetPortfolio() {
if (!confirm('Resetar portfolio para $10.000? Todos os trades serão apagados.')) return;
fetch('/api/portfolio/reset', { method: 'POST' })
.then(r => r.json())
.then(() => {
loadPortfolio();
addLog('SYS', 'Portfolio resetado para $10.000');
});
}
// ── Token Chart ─────────────────────────────────────────────────────────────
const TOKEN_AGENTS = [
{ key: 'Brief', color: '#4fc3f7' },
{ key: 'Decio', color: '#ffd54f' },
{ key: 'PaperT', color: '#69f0ae' },
{ key: 'Audit', color: '#b388ff' },
];
function loadTokenChart() {
const days = parseInt(document.getElementById('tokenDays')?.value || 7);
Promise.all([
fetch(`/api/token_stats?days=${days}`).then(r => r.json()),
fetch('/api/token_totals').then(r => r.json()),
]).then(([stats, totals]) => {
drawTokenBarChart(stats);
renderTokenTotals(totals);
document.getElementById('tokenTotals').textContent =
`Total: ${(stats.total_all_time || 0).toLocaleString()} tok | $${(stats.total_cost_usd || 0).toFixed(4)}`;
}).catch(() => {});
}
function drawTokenBarChart(stats) {
const canvas = document.getElementById('tokenChart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const W = canvas.width = 800;
const H = canvas.height = 200;
ctx.clearRect(0, 0, W, H);
const data = stats.chart_data || [];
if (!data.length) {
ctx.fillStyle = '#556';
ctx.font = '13px monospace';
ctx.textAlign = 'center';
ctx.fillText('Sem dados de tokens ainda. Os agentes precisam ser executados.', W / 2, H / 2);
return;
}
const labels = data.map(d => d.date.slice(5)); // MM-DD
const days = data.length;
const groupW = (W - 80) / Math.max(days, 1);
const barW = Math.min((groupW - 8) / TOKEN_AGENTS.length, 28);
const maxVal = Math.max(...TOKEN_AGENTS.map(a => Math.max(...data.map(d => d[a.key] || 0))), 1);
// Grid
ctx.strokeStyle = 'rgba(99,110,200,0.15)';
ctx.lineWidth = 1;
ctx.font = '11px monospace';
ctx.fillStyle = '#556';
for (let i = 0; i <= 4; i++) {
const y = 10 + (H - 30) * (1 - i / 4);
ctx.beginPath(); ctx.moveTo(40, y); ctx.lineTo(W - 10, y); ctx.stroke();
ctx.textAlign = 'right';
const val = Math.round(maxVal * i / 4);
ctx.fillText(val.toLocaleString(), 38, y + 4);
}
// Bars
data.forEach((d, i) => {
const x = 50 + i * groupW;
TOKEN_AGENTS.forEach((ag, j) => {
const val = d[ag.key] || 0;
const barH = val / maxVal * (H - 30);
const bx = x + j * (barW + 2);
const by = H - 20 - barH;
ctx.fillStyle = ag.color;
ctx.fillRect(bx, by, barW - 1, barH);
// Value label
if (val > 0 && barH > 12) {
ctx.fillStyle = '#fff';
ctx.font = '9px monospace';
ctx.textAlign = 'center';
ctx.fillText(val > 999 ? Math.round(val/1000)+'k' : val, bx + barW/2, by + 10);
}
});
// X label
ctx.fillStyle = '#778';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText(labels[i], x + groupW / 2, H - 4);
});
// Legend
TOKEN_AGENTS.forEach((ag, i) => {
const lx = W - 160 + (i % 2) * 80;
const ly = 16 + Math.floor(i / 2) * 14;
ctx.fillStyle = ag.color;
ctx.fillRect(lx, ly - 9, 10, 10);
ctx.fillStyle = '#aab';
ctx.font = '10px monospace';
ctx.textAlign = 'left';
ctx.fillText(ag.key, lx + 14, ly);
});
}
function renderTokenTotals(totals) {
const tbody = document.getElementById('tokenTotalsBody');
if (!tbody) return;
tbody.innerHTML = '';
(totals || []).forEach(r => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="color:${AGENT_COLORS[r.agent.toLowerCase()] || '#aab'}">${r.agent}</td>
<td>${r.prompt ? r.prompt.toLocaleString() : '-'}</td>
<td>${r.completion ? r.completion.toLocaleString() : '-'}</td>
<td><b>${r.total ? r.total.toLocaleString() : '-'}</b></td>
<td>${r.calls || 0}</td>
<td>$${r.cost ? r.cost.toFixed(4) : '0.0000'}</td>
`;
tbody.appendChild(tr);
});
}
+407
View File
@@ -0,0 +1,407 @@
/* BrainSteel Fin — Anime-style Dashboard */
/* ── Variables ─────────────────────────────────────────── */
:root {
--bg-dark: #0f0f1a;
--bg-card: #1a1a2e;
--bg-card-hover: #222240;
--accent-blue: #4fc3f7;
--accent-red: #ff6b6b;
--accent-green: #69f0ae;
--accent-yellow: #ffd54f;
--accent-purple: #b388ff;
--text-primary: #e8e8f0;
--text-secondary: #8888aa;
--border-radius: 16px;
--shadow: 0 8px 32px rgba(0,0,0,0.4);
}
/* ── Reset & Base ───────────────────────────────────────── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Exo 2', 'Noto Sans JP', sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
/* ── Dashboard Layout ───────────────────────────────────── */
.dashboard {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* ── Header ──────────────────────────────────────────────── */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: var(--bg-card);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
border: 1px solid rgba(79, 195, 247, 0.1);
}
.logo { display: flex; align-items: center; gap: 12px; font-size: 1.5rem; }
.logo-icon { font-size: 2rem; }
.logo-text strong { color: var(--accent-blue); font-weight: 700; }
.header-status { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); }
.status-dot {
width: 12px; height: 12px; border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 8px var(--accent-green);
animation: pulse 2s infinite;
}
.status-dot.running { background: var(--accent-yellow); box-shadow: 0 0 8px var(--accent-yellow); }
.status-dot.error { background: var(--accent-red); box-shadow: 0 0 8px var(--accent-red); }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
.header-time { font-variant-numeric: tabular-nums; color: var(--accent-blue); font-size: 1.1rem; }
/* ── Buttons ─────────────────────────────────────────────── */
.btn {
padding: 10px 20px;
border: none;
border-radius: 10px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary { background: var(--accent-blue); color: #000; }
.btn-primary:hover { background: #7dd3fc; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(79,195,247,0.4); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-secondary { background: var(--bg-card-hover); color: var(--text-primary); border: 1px solid rgba(255,255,255,0.1); }
.btn-secondary:hover { background: #2a2a48; }
.btn-small { padding: 6px 14px; font-size: 0.8rem; background: var(--bg-card-hover); color: var(--text-primary); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; cursor: pointer; }
.btn-small:hover { background: #2a2a48; }
/* ── Pipeline Section ────────────────────────────────────── */
.pipeline-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); border: 1px solid rgba(79,195,247,0.1); }
.pipeline-section h2 { margin-bottom: 16px; color: var(--text-primary); }
.pipeline-flow { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; }
.pipeline-arrow { font-size: 2rem; color: var(--text-secondary); }
/* ── Pipeline Node ────────────────────────────────────────── */
.pipeline-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
border-radius: 14px;
background: var(--bg-card-hover);
border: 2px solid transparent;
min-width: 120px;
transition: all 0.3s;
cursor: pointer;
position: relative;
}
.pipeline-node:hover { transform: translateY(-4px); border-color: var(--accent-blue); }
.pipeline-node.idle { border-color: rgba(136,136,170,0.3); }
.pipeline-node.working { border-color: var(--accent-yellow); box-shadow: 0 0 20px rgba(255,213,79,0.3); animation: glow-yellow 1.5s infinite; }
.pipeline-node.waiting { border-color: rgba(136,136,170,0.5); }
.pipeline-node.done { border-color: var(--accent-green); box-shadow: 0 0 15px rgba(105,240,174,0.3); }
.pipeline-node.error { border-color: var(--accent-red); }
@keyframes glow-yellow { 0%,100% { box-shadow: 0 0 20px rgba(255,213,79,0.3); } 50% { box-shadow: 0 0 35px rgba(255,213,79,0.6); } }
/* ── Agent Avatar ─────────────────────────────────────────── */
.agent-avatar {
width: 56px; height: 56px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1.8rem;
position: relative;
}
.avatar-ring {
position: absolute; inset: -3px; border-radius: 50%;
border: 2px solid;
opacity: 0.6;
animation: spin 4s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.pipeline-node.working .avatar-ring { border-color: var(--accent-yellow); }
.pipeline-node.done .avatar-ring { border-color: var(--accent-green); animation-duration: 8s; }
.pipeline-node.error .avatar-ring { border-color: var(--accent-red); animation-duration: 0.5s; }
.agent-info { text-align: center; }
.agent-name { font-weight: 700; font-size: 1rem; }
.agent-role { font-size: 0.7rem; color: var(--text-secondary); display: block; }
.agent-status {
font-size: 0.7rem;
padding: 2px 10px;
border-radius: 20px;
background: rgba(255,255,255,0.1);
}
.agent-status.working { background: rgba(255,213,79,0.2); color: var(--accent-yellow); }
.agent-status.done { background: rgba(105,240,174,0.2); color: var(--accent-green); }
.agent-status.error { background: rgba(255,107,107,0.2); color: var(--accent-red); }
.agent-status.waiting { background: rgba(136,136,170,0.2); }
.agent-message { font-size: 0.65rem; color: var(--text-secondary); text-align: center; max-width: 100px; overflow: hidden; }
/* ── Pipeline Controls ────────────────────────────────────── */
.pipeline-controls { display: flex; gap: 10px; margin-top: 16px; justify-content: center; flex-wrap: wrap; }
/* ── Decision Banner ─────────────────────────────────────── */
.decision-banner {
margin-top: 12px;
padding: 10px 20px;
border-radius: 10px;
text-align: center;
font-weight: 700;
font-size: 1rem;
}
.decision-banner.buy { background: rgba(105,240,174,0.15); color: var(--accent-green); border: 1px solid var(--accent-green); }
.decision-banner.sell { background: rgba(255,107,107,0.15); color: var(--accent-red); border: 1px solid var(--accent-red); }
.decision-banner.hold { background: rgba(255,213,79,0.15); color: var(--accent-yellow); border: 1px solid var(--accent-yellow); }
/* ── Office Section ───────────────────────────────────────── */
.office-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); }
.office-section h2 { margin-bottom: 16px; }
.office-floor { position: relative; }
.floor-grid { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
.agent-desk {
flex: 1; min-width: 140px;
padding: 16px;
background: var(--bg-card-hover);
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.05);
display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.desk-icon { font-size: 2rem; }
.desk-status { font-size: 0.7rem; color: var(--text-secondary); text-align: center; }
.chat-bubble {
margin-top: 16px; padding: 12px 20px;
background: var(--bg-card-hover);
border-radius: 12px; border: 1px solid rgba(79,195,247,0.2);
font-size: 0.85rem; color: var(--text-secondary);
}
/* ── Registry Section ─────────────────────────────────────── */
.registry-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); }
.registry-section h2 { margin-bottom: 12px; }
.registry-controls { display: flex; gap: 10px; margin-bottom: 16px; }
.agent-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.agent-card {
padding: 16px;
background: var(--bg-card-hover);
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.05);
transition: all 0.2s;
}
.agent-card:hover { border-color: var(--accent-blue); transform: translateY(-2px); }
.agent-card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.agent-card-emoji { font-size: 1.5rem; }
.agent-card-name { font-weight: 700; font-size: 1rem; }
.agent-card-role { font-size: 0.75rem; color: var(--text-secondary); }
.agent-card-intent { font-size: 0.8rem; color: var(--text-secondary); margin: 8px 0; line-height: 1.4; }
.agent-card-stats { display: flex; gap: 12px; font-size: 0.75rem; margin-top: 8px; }
.stat-badge { padding: 2px 8px; border-radius: 6px; background: rgba(255,255,255,0.05); }
.stat-badge.runs { color: var(--accent-blue); }
.stat-badge.ok { color: var(--accent-green); }
.stat-badge.fail { color: var(--accent-red); }
.agent-card-model { font-size: 0.65rem; color: var(--accent-purple); margin-top: 4px; font-family: monospace; }
.agent-card-actions { display: flex; gap: 6px; margin-top: 10px; }
.agent-card-actions button { flex: 1; padding: 5px; font-size: 0.7rem; border-radius: 6px; border: none; cursor: pointer; }
/* ── Two Column Layout ────────────────────────────────────── */
.two-col-section { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
@media (max-width: 900px) { .two-col-section { grid-template-columns: 1fr; } }
/* ── Metrics Section ─────────────────────────────────────── */
.metrics-section, .services-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); }
.metrics-section h2, .services-section h2 { margin-bottom: 12px; }
.metrics-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
.metric-card {
padding: 14px;
background: var(--bg-card-hover);
border-radius: 10px;
border-left: 3px solid;
}
.metric-card.brief { border-left-color: var(--accent-blue); }
.metric-card.decio { border-left-color: var(--accent-yellow); }
.metric-card.papert { border-left-color: var(--accent-green); }
.metric-card.audit { border-left-color: var(--accent-purple); }
.metric-name { font-weight: 600; font-size: 0.85rem; margin-bottom: 6px; }
.metric-value { font-size: 1.2rem; font-weight: 700; }
.metric-sub { font-size: 0.7rem; color: var(--text-secondary); }
.metric-bar { height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin-top: 6px; overflow: hidden; }
.metric-bar-fill { height: 100%; border-radius: 2px; transition: width 0.5s; }
/* ── Services Section ─────────────────────────────────────── */
.services-controls { display: flex; gap: 10px; margin-bottom: 12px; }
.services-list { display: flex; flex-direction: column; gap: 8px; max-height: 300px; overflow-y: auto; }
.service-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 14px;
background: var(--bg-card-hover);
border-radius: 8px;
font-size: 0.85rem;
}
.service-status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.service-status-dot.healthy { background: var(--accent-green); }
.service-status-dot.degraded { background: var(--accent-yellow); }
.service-status-dot.unknown { background: var(--text-secondary); }
.service-status-dot.unreachable { background: var(--accent-red); }
.service-name { font-weight: 600; flex: 1; }
.service-type { font-size: 0.7rem; color: var(--text-secondary); }
.service-endpoint { font-size: 0.7rem; color: var(--accent-purple); font-family: monospace; }
.service-actions { display: flex; gap: 4px; }
.service-actions button { padding: 3px 8px; font-size: 0.65rem; border-radius: 4px; border: none; cursor: pointer; background: rgba(255,255,255,0.1); color: var(--text-secondary); }
.service-actions button:hover { background: rgba(255,255,255,0.2); }
/* ── Visual Pipeline ─────────────────────────────────────── */
.visual-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); }
.visual-section h2 { margin-bottom: 16px; }
.visual-container { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; }
@media (max-width: 900px) { .visual-container { grid-template-columns: 1fr; } }
.visual-flow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.visual-node {
display: flex; flex-direction: column; align-items: center; gap: 4px;
padding: 12px 16px;
background: var(--bg-card-hover);
border-radius: 10px;
min-width: 90px;
position: relative;
}
.visual-node-label { font-size: 0.8rem; font-weight: 700; }
.visual-node-status { font-size: 0.65rem; color: var(--text-secondary); }
.visual-arrow { color: var(--text-secondary); font-size: 1.2rem; }
.visual-timeline { display: flex; flex-direction: column; gap: 4px; max-height: 200px; overflow-y: auto; }
.timeline-item {
padding: 8px 12px;
background: var(--bg-card-hover);
border-radius: 6px;
font-size: 0.75rem;
display: flex; justify-content: space-between; gap: 8px;
}
.timeline-ts { color: var(--text-secondary); flex-shrink: 0; }
.timeline-decision { font-weight: 700; }
.timeline-decision.BUY { color: var(--accent-green); }
.timeline-decision.SELL { color: var(--accent-red); }
.timeline-decision.HOLD { color: var(--accent-yellow); }
/* ── Log Section ──────────────────────────────────────────── */
.log-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); }
.log-section h2 { margin-bottom: 12px; }
.log-container { max-height: 200px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; }
.log-entry {
display: flex; gap: 10px; align-items: baseline;
padding: 6px 10px;
background: var(--bg-card-hover);
border-radius: 6px;
font-size: 0.78rem;
}
.log-time { color: var(--text-secondary); flex-shrink: 0; font-variant-numeric: tabular-nums; }
.log-agent { font-weight: 700; min-width: 40px; color: var(--accent-blue); }
.log-message { color: var(--text-primary); flex: 1; }
.log-entry.system .log-agent { color: var(--accent-purple); }
.log-entry.error .log-agent { color: var(--accent-red); }
/* ── Modals ──────────────────────────────────────────────── */
.modal {
position: fixed; inset: 0;
background: rgba(0,0,0,0.7);
display: flex; align-items: center; justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.modal-content {
background: var(--bg-card);
border-radius: 16px;
padding: 24px;
width: 90%; max-width: 480px;
max-height: 90vh;
overflow-y: auto;
border: 1px solid rgba(79,195,247,0.2);
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.modal-header h3 { color: var(--text-primary); }
.modal-close { background: none; border: none; color: var(--text-secondary); font-size: 1.5rem; cursor: pointer; }
.modal-body { display: flex; flex-direction: column; gap: 10px; }
.modal-body label { font-size: 0.8rem; color: var(--text-secondary); font-weight: 600; }
.modal-body input, .modal-body textarea {
background: var(--bg-card-hover);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
padding: 10px 12px;
color: var(--text-primary);
font-family: inherit;
font-size: 0.9rem;
outline: none;
}
.modal-body input:focus, .modal-body textarea:focus { border-color: var(--accent-blue); }
.modal-footer { display: flex; gap: 10px; margin-top: 16px; justify-content: flex-end; }
/* ── Scrollbar ───────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg-dark); }
::-webkit-scrollbar-thumb { background: var(--bg-card-hover); border-radius: 3px; }
/* ── Portfolio Section ─────────────────────────────────────── */
.portfolio-section {
background: var(--bg-card);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
border: 1px solid rgba(105,240,174,0.2);
}
.portfolio-header {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.portfolio-header h2 { margin: 0; flex: 1; }
.portfolio-summary {
display: flex;
gap: 20px;
align-items: center;
font-size: 0.85rem;
}
.portfolio-balance { font-size: 1.2rem; font-weight: 700; color: var(--accent-blue); }
.portfolio-pnl { font-weight: 600; }
.portfolio-trades { color: var(--text-secondary); }
.portfolio-actions { display: flex; gap: 8px; }
.portfolio-chart {
background: var(--bg-card-hover);
border-radius: 10px;
padding: 8px;
margin-bottom: 12px;
}
.portfolio-chart canvas { width: 100%; height: 180px; display: block; }
.portfolio-trades-list { max-height: 200px; overflow-y: auto; }
::-webkit-scrollbar-thumb:hover { background: #333; }
/* ── Token Tracker ──────────────────────────────────────────── */
.tokens-section {
background: rgba(20, 25, 50, 0.95);
border: 1px solid rgba(99, 110, 200, 0.3);
border-radius: 12px;
padding: 18px;
margin-bottom: 16px;
}
.tokens-controls {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.tokens-controls label { color: #b0b8d0; font-size: 13px; }
.tokens-chart-wrap {
background: rgba(10, 12, 28, 0.8);
border-radius: 8px;
padding: 10px;
margin-bottom: 12px;
}
.tokens-table-wrap { overflow-x: auto; }
.tokens-table { width: 100%; border-collapse: collapse; font-size: 13px; color: #c8d0e8; }
.tokens-table th { background: rgba(99, 110, 200, 0.2); color: #a0aaff; padding: 8px 10px; text-align: left; }
.tokens-table td { padding: 6px 10px; border-bottom: 1px solid rgba(99, 110, 200, 0.1); }
.tokens-table tr:hover td { background: rgba(99, 110, 200, 0.08); }