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:
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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); }
|
||||
Reference in New Issue
Block a user