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,51 +36,456 @@ 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=container>
<!-- SIDEBAR -->
<div class=sidebar>
<div class=sidebar-icon active title=Dashboard>📊</div>
<div class=sidebar-icon title=Settings></div>
<div class=sidebar-icon title=Alerts>🔔</div>
</div>
<!-- MAIN CONTENT -->
<div class=main>
<!-- HEADER -->
<div class=header>
<div class=logo>🤖 Trading Bot</div>
<div class=status-badge>
<div class=status-dot></div>
<span>LIVE</span>
</div>
</div>
<!-- CONTENT -->
<div class=content>
<!-- METRICS -->
<div class=metrics> <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-card>
<div class=metric><div class=metric-label>Trades Today</div><div class=metric-value id=trades>0</div></div> <div class=metric-label>Portfolio</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-value>$<span id=portfolio>0.00</span></div>
<div class=metric><div class=metric-label>Win Rate</div><div class=metric-value id=wr>0%</div></div> </div>
<div class=metric><div class=metric-label>Open Positions</div><div class=metric-value id=open>0</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> </div>
<div class=section> <!-- OPEN TRADES -->
<h2>Open Trades</h2> <div class=section-title>Open Positions</div>
<div id=open_trades><div class=empty>No open trades</div></div> <div class=trades-list id=open_trades>
<div class=empty-state>No open positions</div>
</div> </div>
<div class=section> <!-- CLOSED TRADES -->
<h2>Closed Trades</h2> <div class=section-title>Recent Closes</div>
<div id=closed_trades><div class=empty>No closed trades</div></div> <div class=trades-list id=closed_trades>
<div class=empty-state>No closed trades</div>
</div> </div>
<!-- FOOTER -->
<div class=footer> <div class=footer>
Last update: <span id=last>-</span> Last update: <span id=last>-</span> UTC
</div>
</div>
</div>
</div> </div>
<script> <script>
@ -92,41 +494,62 @@ async function refresh() {
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('usdt').textContent = usdt.toFixed(2);
portfolio += usdt;
// Crypto prices (approximation - in real would fetch current prices)
const prices = {
BTC: 63000,
ETH: 2500,
SOL: 140,
BNB: 600,
XRP: 2.5,
};
for (const [asset, balance] of Object.entries(balances)) {
if (asset !== 'USDT' && balance.free > 0) {
portfolio += balance.free * (prices[asset] || 0);
}
} }
document.getElementById('trades').textContent = (d.trades_today || 0); document.getElementById('portfolio').textContent = portfolio.toFixed(2);
const pnl = parseFloat(d.daily_pnl) || 0;
const pnlText = (pnl >= 0 ? '$' : '-$') + Math.abs(pnl).toFixed(2);
document.getElementById('pnl').textContent = pnlText;
// 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; const wr = d.trades_today > 0 ? Math.round(((d.wins_today || 0) / d.trades_today) * 100) : 0;
document.getElementById('wr').textContent = wr + '%'; document.getElementById('wr').textContent = wr + '%';
const openCount = Object.keys(d.current_trades || {}).length; const pnl = parseFloat(d.daily_pnl || 0);
document.getElementById('open').textContent = openCount; 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');
if (d.last_update) { document.getElementById('open').textContent = Object.keys(d.current_trades || {}).length;
const t = new Date(d.last_update);
document.getElementById('last').textContent = t.toLocaleTimeString();
}
// Open trades // Open trades
const openDiv = document.getElementById('open_trades'); const openDiv = document.getElementById('open_trades');
const trades = d.current_trades || {}; const trades = d.current_trades || {};
if (Object.keys(trades).length === 0) { if (Object.keys(trades).length === 0) {
openDiv.innerHTML = '<div class=empty>No open trades</div>'; openDiv.innerHTML = '<div class=empty-state>No open positions</div>';
} else { } else {
let html = ''; let html = '';
for (const pair in trades) { for (const pair in trades) {
const t = trades[pair]; const t = trades[pair];
html += '<div class=trade-card>'; html += '<div class=trade-item>';
html += '<div class=trade-info>';
html += '<div class=trade-pair>' + pair + '</div>'; 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 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>'; html += '</div>';
} }
openDiv.innerHTML = html; openDiv.innerHTML = html;
@ -136,19 +559,36 @@ async function refresh() {
const closedDiv = document.getElementById('closed_trades'); const closedDiv = document.getElementById('closed_trades');
const closed = d.completed_trades || []; const closed = d.completed_trades || [];
if (closed.length === 0) { if (closed.length === 0) {
closedDiv.innerHTML = '<div class=empty>No closed trades</div>'; closedDiv.innerHTML = '<div class=empty-state>No closed trades</div>';
} else { } else {
let html = ''; let html = '';
for (const trade of closed.slice(-5)) { for (const trade of closed.slice(-10)) {
const profit = parseFloat(trade.profit_usd) || 0; const profit = parseFloat(trade.profit_usd || 0);
const color = profit > 0 ? '#00ff88' : '#ff0047'; const profitPct = parseFloat(trade.profit_pct || 0) * 100;
html += '<div class=trade-card style=border-left-color: + color + >'; 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-pair>' + trade.pair + '</div>';
html += '<div class=trade-detail>' + profit.toFixed(2) + '$ (' + (parseFloat(trade.profit_pct) * 100).toFixed(1) + '%)</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>'; html += '</div>';
} }
closedDiv.innerHTML = html; 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) { } catch (e) {
console.error('Error:', e); console.error('Error:', e);
} }
@ -157,9 +597,9 @@ async function refresh() {
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