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
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
import json
app = FastAPI()
@ -15,7 +13,6 @@ app.add_middleware(
allow_headers=['*'],
)
# Global state
state = {
'current_trades': {},
'completed_trades': [],
@ -39,127 +36,570 @@ async def get_state():
@app.get('/', response_class=HTMLResponse)
async def dashboard():
html = '''<!DOCTYPE html>
<html>
return '''<!DOCTYPE html>
<html lang=en>
<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>
* { box-sizing: border-box; }
body { background: #0a0e27; color: #fff; font-family: monospace; margin: 0; padding: 20px; }
h1 { color: #00ff88; margin: 0 0 30px 0; }
.metrics { display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px; margin-bottom: 40px; }
@media (max-width: 1200px) { .metrics { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 768px) { .metrics { grid-template-columns: repeat(2, 1fr); } }
.metric { background: rgba(255,255,255,0.1); border: 2px solid #00ff88; padding: 20px; border-radius: 8px; text-align: center; }
.metric-label { color: #aaa; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; }
.metric-value { font-size: 28px; color: #00ff88; margin-top: 12px; font-weight: bold; }
.section { margin: 40px 0; }
.section h2 { color: #00ff88; font-size: 18px; margin: 0 0 15px 0; border-bottom: 1px solid #333; padding-bottom: 10px; }
.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; }
.empty { color: #666; padding: 20px; text-align: center; }
.footer { margin-top: 50px; padding-top: 20px; border-top: 1px solid #333; color: #666; font-size: 12px; }
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #00ff88;
--bg-dark: #0a0e27;
--bg-card: #1a1f3a;
--border: #2a3050;
--text-main: #ffffff;
--text-muted: #8899aa;
--red: #ff3366;
--green: #00ff88;
}
body {
background: var(--bg-dark);
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>
</head>
<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>
<h2>Open Trades</h2>
<div id=open_trades><div class=empty>No open trades</div></div>
</div>
<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>
<div class=section>
<h2>Closed Trades</h2>
<div id=closed_trades><div class=empty>No closed trades</div></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>
<div class=footer>
Last update: <span id=last>-</span>
<!-- CONTENT -->
<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>
<script>
async function refresh() {
try {
const resp = await fetch('/api/state');
const d = await resp.json();
// Update metrics
if (d.balance && d.balance.USDT) {
const usdt = parseFloat(d.balance.USDT.free) || 0;
document.getElementById('usdt').textContent = '$' + usdt.toFixed(2);
}
document.getElementById('trades').textContent = (d.trades_today || 0);
const pnl = parseFloat(d.daily_pnl) || 0;
const pnlText = (pnl >= 0 ? '$' : '-$') + Math.abs(pnl).toFixed(2);
document.getElementById('pnl').textContent = pnlText;
const wr = d.trades_today > 0 ? Math.round(((d.wins_today || 0) / d.trades_today) * 100) : 0;
document.getElementById('wr').textContent = wr + '%';
const openCount = Object.keys(d.current_trades || {}).length;
document.getElementById('open').textContent = openCount;
if (d.last_update) {
const t = new Date(d.last_update);
document.getElementById('last').textContent = t.toLocaleTimeString();
}
// 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);
try {
const resp = await fetch('/api/state');
const d = await resp.json();
// Portfolio value calculation
let portfolio = 0;
const balances = d.balance || {};
// 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('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();
setInterval(refresh, 1000);
</script>
</body>
</html>'''
return html
if __name__ == '__main__':
import uvicorn