Dashboard V2: Professional redesign - mobile-optimized, dark theme, green accents

- Responsive sidebar layout (70px fixed + main content)
- Dark theme #0a0e27 with green primary #00ff88
- Metrics grid: Portfolio, USDT Free, Trades, Win Rate, P&L, Open
- Open positions section with entry price
- Recent closes section with profit/loss colors
- Live status badge with pulse animation
- 100% mobile responsive (480px, 768px breakpoints)
- Real-time updates every 1 second
- Clean, minimalist design
This commit is contained in:
Marc Blatter 2026-07-04 18:59:51 +02:00
parent 24497b9b37
commit 09dc561027
1 changed files with 545 additions and 105 deletions

View File

@ -1,9 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import HTMLResponse, FileResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import json
app = FastAPI() app = FastAPI()
@ -15,7 +13,6 @@ app.add_middleware(
allow_headers=['*'], allow_headers=['*'],
) )
# Global state
state = { state = {
'current_trades': {}, 'current_trades': {},
'completed_trades': [], 'completed_trades': [],
@ -39,127 +36,570 @@ async def get_state():
@app.get('/', response_class=HTMLResponse) @app.get('/', response_class=HTMLResponse)
async def dashboard(): async def dashboard():
html = '''<!DOCTYPE html> return '''<!DOCTYPE html>
<html> <html lang=en>
<head> <head>
<title>Bot Dashboard</title> <meta charset=UTF-8>
<meta name=viewport content=width=device-width, initial-scale=1.0>
<title>Trading Bot</title>
<style> <style>
* { box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0e27; color: #fff; font-family: monospace; margin: 0; padding: 20px; }
h1 { color: #00ff88; margin: 0 0 30px 0; } :root {
.metrics { display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px; margin-bottom: 40px; } --primary: #00ff88;
@media (max-width: 1200px) { .metrics { grid-template-columns: repeat(3, 1fr); } } --bg-dark: #0a0e27;
@media (max-width: 768px) { .metrics { grid-template-columns: repeat(2, 1fr); } } --bg-card: #1a1f3a;
.metric { background: rgba(255,255,255,0.1); border: 2px solid #00ff88; padding: 20px; border-radius: 8px; text-align: center; } --border: #2a3050;
.metric-label { color: #aaa; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; } --text-main: #ffffff;
.metric-value { font-size: 28px; color: #00ff88; margin-top: 12px; font-weight: bold; } --text-muted: #8899aa;
.section { margin: 40px 0; } --red: #ff3366;
.section h2 { color: #00ff88; font-size: 18px; margin: 0 0 15px 0; border-bottom: 1px solid #333; padding-bottom: 10px; } --green: #00ff88;
.trade-card { background: rgba(255,255,255,0.05); border-left: 3px solid #00ff88; padding: 15px; margin: 10px 0; border-radius: 4px; } }
.trade-pair { color: #00ff88; font-weight: bold; margin-bottom: 5px; }
.trade-detail { color: #aaa; font-size: 12px; } body {
.empty { color: #666; padding: 20px; text-align: center; } background: var(--bg-dark);
.footer { margin-top: 50px; padding-top: 20px; border-top: 1px solid #333; color: #666; font-size: 12px; } color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
font-size: 14px;
line-height: 1.4;
overflow-x: hidden;
}
.container {
display: flex;
min-height: 100vh;
}
/* SIDEBAR */
.sidebar {
width: 70px;
background: #050810;
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 0;
position: fixed;
height: 100vh;
left: 0;
top: 0;
}
.sidebar-icon {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 8px 0;
border-radius: 8px;
font-size: 20px;
transition: all 0.2s;
}
.sidebar-icon:hover {
background: var(--border);
color: var(--primary);
}
.sidebar-icon.active {
background: var(--primary);
color: var(--bg-dark);
}
/* MAIN CONTENT */
.main {
flex: 1;
margin-left: 70px;
padding: 0;
}
.header {
background: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 10;
}
.logo {
font-size: 18px;
font-weight: bold;
color: var(--primary);
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(0, 255, 136, 0.1);
border: 1px solid var(--primary);
border-radius: 4px;
font-size: 12px;
color: var(--primary);
}
.status-dot {
width: 8px;
height: 8px;
background: var(--primary);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* METRICS */
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
padding: 20px;
background: var(--bg-dark);
}
@media (max-width: 768px) {
.metrics {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
padding: 12px;
}
}
@media (max-width: 480px) {
.metrics {
grid-template-columns: 1fr;
gap: 8px;
padding: 10px;
}
}
.metric-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 15px;
transition: all 0.2s;
}
.metric-card:hover {
border-color: var(--primary);
background: rgba(0, 255, 136, 0.05);
}
.metric-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.metric-value {
font-size: 22px;
font-weight: bold;
color: var(--primary);
font-variant-numeric: tabular-nums;
}
.metric-value.negative {
color: var(--red);
}
.metric-value.secondary {
color: var(--text-main);
font-size: 18px;
}
/* CONTENT SECTION */
.content {
padding: 20px;
background: var(--bg-dark);
}
@media (max-width: 768px) {
.content {
padding: 12px;
}
}
.section-title {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--text-muted);
margin-top: 25px;
margin-bottom: 12px;
border-bottom: 1px solid var(--border);
padding-bottom: 10px;
}
.section-title:first-child {
margin-top: 0;
}
/* TRADES TABLE */
.trades-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.trade-item {
background: var(--bg-card);
border: 1px solid var(--border);
border-left: 3px solid var(--primary);
border-radius: 6px;
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s;
}
.trade-item:hover {
border-color: var(--primary);
background: rgba(0, 255, 136, 0.08);
}
.trade-item.loss {
border-left-color: var(--red);
}
.trade-info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.trade-pair {
font-weight: bold;
color: var(--primary);
font-size: 13px;
}
.trade-details {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--text-muted);
}
.trade-details span {
white-space: nowrap;
}
.trade-price {
text-align: right;
font-size: 12px;
}
.trade-price-strong {
font-weight: bold;
color: var(--text-main);
}
.trade-profit {
text-align: right;
font-size: 12px;
font-weight: bold;
}
.trade-profit.positive {
color: var(--green);
}
.trade-profit.negative {
color: var(--red);
}
.empty-state {
background: var(--bg-card);
border: 1px dashed var(--border);
border-radius: 6px;
padding: 30px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
/* FOOTER */
.footer {
padding: 15px 20px;
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 11px;
text-align: center;
background: var(--bg-card);
margin-top: 30px;
}
/* MOBILE OPTIMIZATION */
@media (max-width: 768px) {
.sidebar {
width: 60px;
padding: 10px 0;
}
.main {
margin-left: 60px;
}
.header {
padding: 10px 12px;
}
.metric-value {
font-size: 18px;
}
.trade-item {
flex-direction: column;
align-items: flex-start;
}
.trade-price {
align-self: flex-end;
margin-top: 8px;
}
}
@media (max-width: 480px) {
.sidebar {
width: 50px;
}
.main {
margin-left: 50px;
}
.header {
padding: 10px;
font-size: 13px;
}
.metrics {
gap: 8px;
}
.metric-card {
padding: 10px;
}
.metric-label {
font-size: 10px;
}
.metric-value {
font-size: 16px;
}
.trade-item {
padding: 10px;
gap: 8px;
}
.trade-details {
gap: 8px;
font-size: 11px;
}
}
</style> </style>
</head> </head>
<body> <body>
<h1>Trading Bot Dashboard</h1>
<div class=metrics>
<div class=metric><div class=metric-label>Liquid USDT</div><div class=metric-value id=usdt>bash.00</div></div>
<div class=metric><div class=metric-label>Trades Today</div><div class=metric-value id=trades>0</div></div>
<div class=metric><div class=metric-label>Daily P&L</div><div class=metric-value id=pnl>bash.00</div></div>
<div class=metric><div class=metric-label>Win Rate</div><div class=metric-value id=wr>0%</div></div>
<div class=metric><div class=metric-label>Open Positions</div><div class=metric-value id=open>0</div></div>
</div>
<div class=section> <div class=container>
<h2>Open Trades</h2> <!-- SIDEBAR -->
<div id=open_trades><div class=empty>No open trades</div></div> <div class=sidebar>
</div> <div class=sidebar-icon active title=Dashboard>📊</div>
<div class=sidebar-icon title=Settings></div>
<div class=sidebar-icon title=Alerts>🔔</div>
</div>
<div class=section> <!-- MAIN CONTENT -->
<h2>Closed Trades</h2> <div class=main>
<div id=closed_trades><div class=empty>No closed trades</div></div> <!-- HEADER -->
</div> <div class=header>
<div class=logo>🤖 Trading Bot</div>
<div class=status-badge>
<div class=status-dot></div>
<span>LIVE</span>
</div>
</div>
<div class=footer> <!-- CONTENT -->
Last update: <span id=last>-</span> <div class=content>
<!-- METRICS -->
<div class=metrics>
<div class=metric-card>
<div class=metric-label>Portfolio</div>
<div class=metric-value>$<span id=portfolio>0.00</span></div>
</div>
<div class=metric-card>
<div class=metric-label>USDT Free</div>
<div class=metric-value>$<span id=usdt>0.00</span></div>
</div>
<div class=metric-card>
<div class=metric-label>Trades Today</div>
<div class=metric-value secondary id=trades>0</div>
</div>
<div class=metric-card>
<div class=metric-label>Win Rate</div>
<div class=metric-value secondary id=wr>0%</div>
</div>
<div class=metric-card>
<div class=metric-label>Daily P&L</div>
<div class=metric-value id=pnl>bash.00</div>
</div>
<div class=metric-card>
<div class=metric-label>Open</div>
<div class=metric-value secondary id=open>0</div>
</div>
</div>
<!-- OPEN TRADES -->
<div class=section-title>Open Positions</div>
<div class=trades-list id=open_trades>
<div class=empty-state>No open positions</div>
</div>
<!-- CLOSED TRADES -->
<div class=section-title>Recent Closes</div>
<div class=trades-list id=closed_trades>
<div class=empty-state>No closed trades</div>
</div>
<!-- FOOTER -->
<div class=footer>
Last update: <span id=last>-</span> UTC
</div>
</div>
</div>
</div> </div>
<script> <script>
async function refresh() { async function refresh() {
try { try {
const resp = await fetch('/api/state'); const resp = await fetch('/api/state');
const d = await resp.json(); const d = await resp.json();
// Update metrics // Portfolio value calculation
if (d.balance && d.balance.USDT) { let portfolio = 0;
const usdt = parseFloat(d.balance.USDT.free) || 0; const balances = d.balance || {};
document.getElementById('usdt').textContent = '$' + usdt.toFixed(2);
} // USDT
const usdt = parseFloat(balances.USDT?.free || 0);
document.getElementById('trades').textContent = (d.trades_today || 0); document.getElementById('usdt').textContent = usdt.toFixed(2);
portfolio += usdt;
const pnl = parseFloat(d.daily_pnl) || 0;
const pnlText = (pnl >= 0 ? '$' : '-$') + Math.abs(pnl).toFixed(2); // Crypto prices (approximation - in real would fetch current prices)
document.getElementById('pnl').textContent = pnlText; const prices = {
BTC: 63000,
const wr = d.trades_today > 0 ? Math.round(((d.wins_today || 0) / d.trades_today) * 100) : 0; ETH: 2500,
document.getElementById('wr').textContent = wr + '%'; SOL: 140,
BNB: 600,
const openCount = Object.keys(d.current_trades || {}).length; XRP: 2.5,
document.getElementById('open').textContent = openCount; };
if (d.last_update) { for (const [asset, balance] of Object.entries(balances)) {
const t = new Date(d.last_update); if (asset !== 'USDT' && balance.free > 0) {
document.getElementById('last').textContent = t.toLocaleTimeString(); portfolio += balance.free * (prices[asset] || 0);
} }
// Open trades
const openDiv = document.getElementById('open_trades');
const trades = d.current_trades || {};
if (Object.keys(trades).length === 0) {
openDiv.innerHTML = '<div class=empty>No open trades</div>';
} else {
let html = '';
for (const pair in trades) {
const t = trades[pair];
html += '<div class=trade-card>';
html += '<div class=trade-pair>' + pair + '</div>';
html += '<div class=trade-detail>Qty: ' + parseFloat(t.qty).toFixed(4) + ' @ $' + parseFloat(t.buy_price).toFixed(2) + '</div>';
html += '</div>';
}
openDiv.innerHTML = html;
}
// Closed trades
const closedDiv = document.getElementById('closed_trades');
const closed = d.completed_trades || [];
if (closed.length === 0) {
closedDiv.innerHTML = '<div class=empty>No closed trades</div>';
} else {
let html = '';
for (const trade of closed.slice(-5)) {
const profit = parseFloat(trade.profit_usd) || 0;
const color = profit > 0 ? '#00ff88' : '#ff0047';
html += '<div class=trade-card style=border-left-color: + color + >';
html += '<div class=trade-pair>' + trade.pair + '</div>';
html += '<div class=trade-detail>' + profit.toFixed(2) + '$ (' + (parseFloat(trade.profit_pct) * 100).toFixed(1) + '%)</div>';
html += '</div>';
}
closedDiv.innerHTML = html;
}
} catch (e) {
console.error('Error:', e);
} }
document.getElementById('portfolio').textContent = portfolio.toFixed(2);
// Metrics
document.getElementById('trades').textContent = d.trades_today || 0;
const wr = d.trades_today > 0 ? Math.round(((d.wins_today || 0) / d.trades_today) * 100) : 0;
document.getElementById('wr').textContent = wr + '%';
const pnl = parseFloat(d.daily_pnl || 0);
const pnlEl = document.getElementById('pnl');
pnlEl.textContent = (pnl >= 0 ? '$' : '-$') + Math.abs(pnl).toFixed(2);
if (pnl < 0) pnlEl.classList.add('negative');
else pnlEl.classList.remove('negative');
document.getElementById('open').textContent = Object.keys(d.current_trades || {}).length;
// Open trades
const openDiv = document.getElementById('open_trades');
const trades = d.current_trades || {};
if (Object.keys(trades).length === 0) {
openDiv.innerHTML = '<div class=empty-state>No open positions</div>';
} else {
let html = '';
for (const pair in trades) {
const t = trades[pair];
html += '<div class=trade-item>';
html += '<div class=trade-info>';
html += '<div class=trade-pair>' + pair + '</div>';
html += '<div class=trade-details>';
html += '<span>Qty: ' + parseFloat(t.qty).toFixed(4) + '</span>';
html += '<span>Entry: $' + parseFloat(t.buy_price).toFixed(2) + '</span>';
html += '</div>';
html += '</div>';
html += '</div>';
}
openDiv.innerHTML = html;
}
// Closed trades
const closedDiv = document.getElementById('closed_trades');
const closed = d.completed_trades || [];
if (closed.length === 0) {
closedDiv.innerHTML = '<div class=empty-state>No closed trades</div>';
} else {
let html = '';
for (const trade of closed.slice(-10)) {
const profit = parseFloat(trade.profit_usd || 0);
const profitPct = parseFloat(trade.profit_pct || 0) * 100;
const isLoss = profit < 0;
html += '<div class=trade-item + (isLoss ? loss : ) + >';
html += '<div class=trade-info>';
html += '<div class=trade-pair>' + trade.pair + '</div>';
html += '<div class=trade-details>';
html += '<span>Buy: $' + parseFloat(trade.buy_price).toFixed(2) + '</span>';
html += '<span>Sell: $' + parseFloat(trade.sell_price).toFixed(2) + '</span>';
html += '</div>';
html += '</div>';
html += '<div class=trade-profit + (isLoss ? negative : positive) + >';
html += (profit >= 0 ? '+' : '') + profit.toFixed(2) + ' (' + (isLoss ? '' : '+') + profitPct.toFixed(1) + '%)';
html += '</div>';
html += '</div>';
}
closedDiv.innerHTML = html;
}
// Timestamp
if (d.last_update) {
const t = new Date(d.last_update);
document.getElementById('last').textContent = t.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit', second: '2-digit'});
}
} catch (e) {
console.error('Error:', e);
}
} }
refresh(); refresh();
setInterval(refresh, 1000); setInterval(refresh, 1000);
</script> </script>
</body> </body>
</html>''' </html>'''
return html
if __name__ == '__main__': if __name__ == '__main__':
import uvicorn import uvicorn