/* global React, AURA_DATA, Icon, Spark, EquityChart */
const D2 = window.AURA_DATA;
const { useState, useEffect, useMemo } = React;
function Dashboard({ state }) {
const { botStatus, activeAccount, openTrades, balance, equity, dayPnL } = state;
const [perf, setPerf] = useState(null);
const [signals, setSignals] = useState([]);
const [sigQuality, setSigQuality] = useState(null);
const [confirmStart, setConfirmStart] = useState(false);
const [confirmStop, setConfirmStop] = useState(false);
const [toast, setToast] = useState(null);
const [actionLoading, setActionLoading] = useState(false);
const botActive = botStatus ? botStatus.bot_active : false;
const accountId = activeAccount ? activeAccount.id : null;
useEffect(() => {
if (!accountId) return;
window.api.performance(accountId, 60).then(setPerf).catch(() => {});
window.api.signals(accountId, 20).then(setSignals).catch(() => {});
window.api.signalQuality(accountId, 60).then(setSigQuality).catch(() => {});
// Poll signals + bot status every 30 s so the feed stays live
const poll = setInterval(() => {
window.api.signals(accountId, 20).then(setSignals).catch(() => {});
window.api.botStatus(accountId).then((s) => { if (s) state.setBotStatus(s); }).catch(() => {});
}, 30000);
return () => clearInterval(poll);
}, [accountId]);
const equityCurve = useMemo(() => D2.generateEquityCurve(perf ? perf.equity_curve : []), [perf]);
const pnlSpark = useMemo(() => D2.generateSpark(perf ? perf.equity_curve : [], 24), [perf]);
const showToast = (msg, type = "success") => {
setToast({ msg, type });
setTimeout(() => setToast(null), 2400);
};
const startBot = async () => {
if (!accountId) return;
setActionLoading(true);
try {
await window.api.startBot(accountId);
showToast("Bot started — scanning markets");
setConfirmStart(false);
const s = await window.api.botStatus(accountId);
if (s) state.setBotStatus(s);
} catch (err) {
showToast(err.message || "Failed to start bot", "error");
} finally {
setActionLoading(false);
}
};
const stopBot = async () => {
if (!accountId) return;
setActionLoading(true);
try {
await window.api.stopBot(accountId);
showToast("Stop signal sent — open positions held", "error");
setConfirmStop(false);
const s = await window.api.botStatus(accountId);
if (s) state.setBotStatus(s);
} catch (err) {
showToast(err.message || "Failed to stop bot", "error");
} finally {
setActionLoading(false);
}
};
const lastCycle = botStatus && botStatus.last_heartbeat
? Math.round((Date.now() - D2.parseUTC(botStatus.last_heartbeat).getTime()) / 1000)
: null;
// bot_active can be stale in DB if the worker crashed without calling the finish endpoint
const botStale = botStatus && botStatus.bot_active && lastCycle != null && lastCycle > 180;
return (
<>
Dashboard
{activeAccount ? `Live · ${activeAccount.nickname || activeAccount.login} · ${activeAccount.server}` : "No account selected"}
window.location.reload()}>{Icon.refresh} Refresh
{ /* account selector */ }}>{Icon.acct} {activeAccount ? activeAccount.nickname || activeAccount.login : "Select account"}
{Icon.alert}
Trading involves substantial risk of loss. Past performance does not guarantee future results.
{/* Stale bot warning */}
{botStale && (
{Icon.alert}
Bot worker appears offline — last heartbeat was {Math.round(lastCycle / 60)} min ago.
Click Start Trading to restart it, or check the AURA-Worker service on the VPS.
)}
{/* Bot control */}
Bot Status
{botActive && !botStale ? "ACTIVE" : botStale ? "STALE" : "IDLE"}v1.0
{activeAccount ? `Account #${activeAccount.login} · ${activeAccount.leverage || "1:?"}` : "No account"}
Strategy
Gold Momentum · EMA 8/21 · M15
Spread Gate
{botStatus ? `${botStatus.spread != null ? botStatus.spread.toFixed(1) : "—"} pips` : "—"}
Last Cycle
180 ? "var(--red)" : "var(--text-2)" }}>
{lastCycle != null ? `● ${lastCycle < 60 ? lastCycle + "s" : Math.round(lastCycle/60) + "m"} ago` : botActive ? "● starting…" : "—"}
{!botActive ? (
setConfirmStart(true)} disabled={!activeAccount || actionLoading}>
{Icon.play} Start Trading
) : (
setConfirmStop(true)} disabled={actionLoading}>
{Icon.stop} Stop Trading
)}
{/* Stats */}
Account Balance {Icon.dollar}
$ {balance != null ? balance.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : "—"}
Equity ${equity != null ? equity.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : "—"}
= 0 ? "pos" : "neg"}`}>
Today's P&L {Icon.activity}
= 0 ? "pos" : "neg"}`}>{D2.fmtSigned(dayPnL)}
{openTrades.length} open · Unrealized
Win Rate · 30D {Icon.target}
{perf ? perf.win_rate.toFixed(1) : "—"}%
PF {perf ? perf.profit_factor.toFixed(2) : "—"}
{perf ? perf.total_trades : "—"} trades
Open Positions {Icon.zap}
{openTrades.length}/ 5 max
Unrealized {D2.fmtSigned(openTrades.reduce((a, b) => a + (b.pnl || 0), 0))}
{/* Equity chart + Strategy telemetry */}
Performance · Last 60 Days
{[
{ k: "Net P&L", v: perf ? D2.fmtUSD(perf.net_pnl) : "—", c: perf && perf.net_pnl >= 0 ? "pos" : "neg" },
{ k: "Max Drawdown", v: perf ? `-${perf.max_drawdown_pct.toFixed(2)}%` : "—", c: "neg" },
{ k: "Avg Win", v: perf ? D2.fmtUSD(perf.avg_win) : "—", c: "pos" },
{ k: "Avg Loss", v: perf ? D2.fmtUSD(-perf.avg_loss) : "—", c: "neg" },
].map((m) => (
))}
ATR (M15) {botStatus && botStatus.atr != null ? `${botStatus.atr.toFixed(2)} pts` : "—"}
Spread {botStatus && botStatus.spread != null ? `● ${botStatus.spread.toFixed(1)} pips` : "—"}
Loss Streak {botStatus ? `${botStatus.loss_streak} / 4` : "—"}
Daily P&L = 0 ? "pos" : "neg"}`}>{botStatus ? D2.fmtSigned(botStatus.daily_pnl) : "—"}
Circuit Breaker {botStatus ? (botStatus.circuit_broken ? "● TRIPPED" : "● OK") : "—"}
Cycle Latency {botStatus && botStatus.cycle_latency_ms ? `${botStatus.cycle_latency_ms}ms` : "—"}
Account Status {activeAccount ? `● ${activeAccount.status}` : "—"}
{/* Signal Quality Analysis */}
{sigQuality && (
Signal Quality · Last 60 Days
{sigQuality.total_signals} signals
{Object.entries(sigQuality.action_counts || {}).map(([action, count]) => (
{action.replace(/_/g, " ")}
{count}
))}
{sigQuality.by_direction && sigQuality.by_direction.length > 0 && (
<>
Trade Outcomes by Bucket
Bucket Group Trades Win % PF Avg P&L Net P&L
{[
...( sigQuality.by_direction || []).map(r => ({ ...r, group: "Direction" })),
...( sigQuality.by_session || []).map(r => ({ ...r, group: "Session" })),
...( sigQuality.by_rsi_bucket || []).map(r => ({ ...r, group: "RSI" })),
...( sigQuality.by_spread_bucket || []).map(r => ({ ...r, group: "Spread" })),
].map((row, i) => (
{row.bucket}
{row.group}
{row.count}
= 50 ? "pos" : "neg"}`}>{row.win_rate.toFixed(1)}%
= 1 ? "pos" : "neg"}`}>{row.profit_factor.toFixed(2)}
= 0 ? "pos" : "neg"}`}>{D2.fmtSigned(row.avg_pnl)}
= 0 ? "pos" : "neg"}`}>{D2.fmtSigned(row.net_pnl)}
))}
>
)}
{(!sigQuality.by_direction || sigQuality.by_direction.length === 0) && (
No executed trades yet — analysis will appear once trades have P&L data
)}
)}
{/* Open positions + Signal feed */}
Open Positions
window.api.openTrades(accountId).then(state.setOpenTrades)}>{Icon.refresh}
{openTrades.length === 0 ? (
No open positions
) : (
Ticket Dir Entry SL / TP Vol P&L Duration
{openTrades.map((p) => (
#{p.ticket}
{p.direction}
{p.entry_price.toFixed(2)}
{p.sl.toFixed(2)} / {p.tp.toFixed(2)}
{p.volume.toFixed(2)}
= 0 ? "pos" : "neg"}`} style={{ fontWeight: 600 }}>{D2.fmtSigned(p.pnl || 0)}
{D2.fmtDuration(p.open_time)}
))}
)}
Recent Signals
auto-refresh 30s
{signals.length === 0 ? (
{botStale ? "Bot offline — no signals received" : "No signals yet — bot will log activity here"}
) : (() => {
const interesting = signals.filter(s => s.action !== "no_signal");
const idle = signals.filter(s => s.action === "no_signal");
return (
<>
{interesting.length === 0 && idle.length > 0 && (
{idle.length} idle cycles — EMA8/21 trending without crossover · last {D2.fmtDateTime(idle[0].created_at)}
)}
{interesting.slice(0, 10).map((s, i) => (
{D2.fmtDateTime(s.created_at)}
{s.reasoning}
{s.direction || s.action}
))}
{interesting.length === 0 && (
{idle.slice(0, 3).map((s, i) => (
{D2.fmtDateTime(s.created_at)}
{s.reasoning}
))}
)}
>
);
})()}
{confirmStart && (
setConfirmStart(false)}>
e.stopPropagation()}>
Start the bot?
The bot will begin scanning markets and may open XAUUSD positions per your configured strategy.
setConfirmStart(false)}>Cancel
Start Trading
)}
{confirmStop && (
setConfirmStop(false)}>
e.stopPropagation()}>
Stop the bot?
New trades will be paused. Open positions will remain — close them manually from Active Trades.
setConfirmStop(false)}>Cancel
Stop Trading
)}
{toast && {toast.msg}
}
>
);
}
window.Dashboard = Dashboard;