// EA-Commander JARVIS — Production app.jsx // Babel-standalone React 18, no ES module imports // All React hooks via destructuring from global React const { useState, useEffect, useMemo, useRef, useCallback, useReducer } = React; /* ───────────────────────────────────────────── API UTILITIES ───────────────────────────────────────────── */ const API_BASE = ""; async function apiFetch(path, opts = {}) { const token = localStorage.getItem("jwt"); const headers = { "Content-Type": "application/json", ...(opts.headers || {}) }; if (token) headers["Authorization"] = "Bearer " + token; const res = await fetch(API_BASE + path, { ...opts, headers }); if (res.status === 401) { localStorage.removeItem("jwt"); window.location.reload(); return null; } if (!res.ok) { let msg = "Request failed"; try { const d = await res.json(); msg = d.detail || d.message || msg; } catch (_) {} throw new Error(msg); } if (res.status === 204) return null; return res.json(); } /* ───────────────────────────────────────────── TOAST SYSTEM ───────────────────────────────────────────── */ let _toastSetState = null; function toast(msg, type = "info") { if (_toastSetState) _toastSetState(p => [...p, { id: Date.now() + Math.random(), msg, type }]); } function ToastContainer() { const [toasts, setToasts] = useState([]); _toastSetState = setToasts; useEffect(() => { if (toasts.length === 0) return; const t = setTimeout(() => { setToasts(p => p.slice(1)); }, 3000); return () => clearTimeout(t); }, [toasts]); const colorMap = { info: "#5ee0ff", success: "#5cf2a0", error: "#ff4d6d", warn: "#ffb547" }; return (
{toasts.map(t => (
{t.msg}
))}
); } /* ───────────────────────────────────────────── FORMAT HELPERS ───────────────────────────────────────────── */ function fmtMoney(n, sign = false) { if (n == null || isNaN(n)) return "—"; const v = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const prefix = sign ? (n >= 0 ? "+" : "-") : (n < 0 ? "-" : ""); return prefix + "$" + v; } function relTime(ts) { if (!ts) return "—"; const d = new Date(ts); const diff = (Date.now() - d.getTime()) / 1000; if (diff < 60) return Math.round(diff) + "s ago"; if (diff < 3600) return Math.round(diff / 60) + "m ago"; if (diff < 86400) return Math.round(diff / 3600) + "h ago"; return Math.round(diff / 86400) + "d ago"; } /* ───────────────────────────────────────────── TICKER DATA (decorative) ───────────────────────────────────────────── */ const TICKER_DATA = [ { sym: "XAU/USD", px: "4,476.20", ch: "+0.84%", up: true }, { sym: "EUR/USD", px: "1.0842", ch: "-0.12%", up: false }, { sym: "GBP/JPY", px: "192.41", ch: "+0.42%", up: true }, { sym: "NAS100", px: "20,418.5", ch: "+0.92%", up: true }, { sym: "GER40", px: "21,402.8", ch: "+0.18%", up: true }, { sym: "BTC/USD", px: "98,420", ch: "-1.20%", up: false }, { sym: "USD/JPY", px: "152.18", ch: "+0.05%", up: true }, { sym: "OIL", px: "72.40", ch: "-0.62%", up: false }, ]; /* ───────────────────────────────────────────── SPARKLINE ───────────────────────────────────────────── */ function Sparkline({ data, color, area = true }) { if (!data || data.length < 2) return null; const W = 200, H = 38; const min = Math.min(...data), max = Math.max(...data); const x = i => (i / (data.length - 1)) * W; const y = v => H - ((v - min) / (max - min || 1)) * (H - 4) - 2; const path = data.map((v, i) => `${i === 0 ? "M" : "L"} ${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(" "); const areaPath = path + ` L ${W} ${H} L 0 ${H} Z`; const uid = color.replace(/[^a-z0-9]/gi, ""); return ( {area && ( <> )} ); } /* ───────────────────────────────────────────── EQUITY CHART ───────────────────────────────────────────── */ function EquityChart({ data, accent, currentEquity }) { // Convert daily-pnl data [{day, pnl}] to cumulative equity curve const points = useMemo(() => { if (data && data.length >= 2) { // Work backwards: current equity minus sum of all pnl = starting equity const totalPnl = data.reduce((s, d) => s + (d.pnl || 0), 0); const baseEquity = Math.max(0, (currentEquity || 100000) - totalPnl); let cum = baseEquity; return data.map(d => { cum += (d.pnl || 0); return cum; }); } // No real trade data yet — flat line at current equity, no oscillation if (currentEquity && currentEquity > 0) { return [currentEquity * 0.9995, currentEquity]; } return null; }, [data, currentEquity]); const W = 800, H = 260, PAD = 28; if (!points || points.length < 2) { return (
KEINE HANDELSDATEN
); } const rawMin = Math.min(...points); const rawMax = Math.max(...points); const pad = (rawMax - rawMin) * 0.1 || rawMax * 0.01 || 100; const min = rawMin - pad; const max = rawMax + pad; const px = i => PAD + (i / (points.length - 1)) * (W - PAD * 2); const py = v => H - PAD - ((v - min) / (max - min || 1)) * (H - PAD * 2); const linePath = points.map((v, i) => `${i === 0 ? "M" : "L"} ${px(i).toFixed(1)} ${py(v).toFixed(1)}`).join(" "); const areaPath = linePath + ` L ${px(points.length - 1).toFixed(1)} ${H - PAD} L ${px(0)} ${H - PAD} Z`; const last = points[points.length - 1]; const lx = px(points.length - 1), ly = py(last); const gridY = []; for (let i = 0; i < 5; i++) { const yy = PAD + (i / 4) * (H - PAD * 2); const val = max - (i / 4) * (max - min); gridY.push({ y: yy, v: val }); } const xLabels = data && data.length >= 2 ? (() => { const idxs = [0, Math.floor(data.length / 4), Math.floor(data.length / 2), Math.floor(3 * data.length / 4), data.length - 1]; return idxs.map(i => (data[i].day || "").slice(5)); // "MM-DD" from "YYYY-MM-DD" })() : []; return (
{gridY.map((g, i) => ( {Math.round(g.v / 1000)}k ))} {xLabels.map((l, i) => { const xx = PAD + (i / (xLabels.length - 1)) * (W - PAD * 2); return {l}; })} {[Math.floor(points.length * 0.1), Math.floor(points.length * 0.25), Math.floor(points.length * 0.4), Math.floor(points.length * 0.57), Math.floor(points.length * 0.75), Math.floor(points.length * 0.88)].map(idx => ( ))} ${last.toLocaleString("en-US", { maximumFractionDigits: 0 })}
); } /* ───────────────────────────────────────────── RADIAL DIAL ───────────────────────────────────────────── */ function Dial({ value, max = 100, label, color = "var(--accent)", suffix = "%" }) { const R = 42, C = 2 * Math.PI * R; const pct = Math.min((value || 0) / max, 1); const off = C * (1 - pct); return (
{Array.from({ length: 24 }).map((_, i) => { const a = (i / 24) * Math.PI * 2 - Math.PI / 2; const r1 = 47, r2 = 49; return ; })}
{(value || 0).toFixed(value < 10 ? 1 : 0)}{suffix}
{label}
); } /* ───────────────────────────────────────────── NAV ICON ───────────────────────────────────────────── */ function NavIcon({ name }) { const map = { grid: <>, cpu: <>, pulse: <>, users: <>, wallet: <>, stack: <>, server: <>, log: <>, tag: <>, }; return ( {map[name] || null} ); } /* ───────────────────────────────────────────── HUD OVERLAY ───────────────────────────────────────────── */ function HudOverlay() { return (
{Array.from({ length: 30 }).map((_, i) => ( ))}
); } /* ───────────────────────────────────────────── TICKER ───────────────────────────────────────────── */ function Ticker() { const items = [...TICKER_DATA, ...TICKER_DATA, ...TICKER_DATA]; return (
▸ Markets
{items.map((t, i) => ( {t.sym} {t.px} {t.ch} ))}
); } /* ───────────────────────────────────────────── EA CARD ───────────────────────────────────────────── */ function EACard({ bot, accent }) { const name = bot.name || "Unknown EA"; const pl = bot.today_pl || 0; const dd = bot.drawdown || 0; const data = useMemo(() => Array.from({ length: 32 }, (_, i) => 50 + Math.sin(i / 3 + name.length) * 12 + Math.cos(i / 1.5) * 8 + (pl > 0 ? i * 0.4 : -i * 0.2) ), [name, pl]); return (
{name}
{bot.symbol || "—"} {bot.timeframe || ""}
{fmtMoney(pl, true)}
= 0 ? "#5cf2a0" : "#ff4d6d"} />
Symbol
{bot.symbol || "—"}
DD
3 ? "var(--bad)" : "var(--ink-0)" }}>{dd.toFixed(1)}%
TF
{bot.timeframe || "—"}
); } /* ───────────────────────────────────────────── ACCOUNT ROW ───────────────────────────────────────────── */ function AcctRow({ account, onConfig }) { const a = account; const dd = a.max_dd_today || 0; const tone = dd > 4 ? "bad" : dd > 2.5 ? "warn" : ""; const initials = (a.broker || a.account_name || "AC").substring(0, 3).toUpperCase(); return (
{initials}
{a.broker || a.account_name || "Unknown"}
{a.login} · {a.server || ""}
Equity
{fmtMoney(a.equity)}
Daily P&L
= 0 ? "var(--good)" : "var(--bad)" }}> {fmtMoney(a.daily_pl || 0, true)}
4 ? " bad" : dd > 2.5 ? " warn" : "")}>DD {dd.toFixed(1)}%
{relTime(a.last_seen)}
{onConfig && a.vm_id && ( )}
); } /* ───────────────────────────────────────────── VM CARD ───────────────────────────────────────────── */ function VMCard({ vm, cust, onStop, onStart, onConnect, onDelete, isAdmin, killLocked }) { const isRun = vm.status === "running"; const isStarting = vm.status === "creating" || vm.status === "starting"; const isGolden = vm.vm_type === "golden"; const custName = cust ? cust.name : (vm.customer_id ? `#${vm.customer_id}` : "—"); return (
{vm.name} {isGolden && ( ⭐ GOLDEN )}
ID · {vm.short_id || vm.id}
{isStarting ? (
STARTING…
) : (
{isRun ? "● RUNNING" : "○ STOPPED"}
)}
Customer
{(custName || "—").slice(0, 12)}
MT5
{vm.mt5_login || "—"}
Server
{(vm.mt5_server || "—").slice(0, 12)}
{!isStarting && (
{isRun ? : ( ) } {isRun && vm.caddy_subdomain && ( )} {isAdmin && ( )}
)} {isStarting && (
VM wird eingerichtet, bitte warten…
)}
); } /* ───────────────────────────────────────────── GOLDEN VM PANEL (admin only) ───────────────────────────────────────────── */ function GoldenVMPanel({ onRefresh }) { const [status, setStatus] = useState(null); // null | "running" | "stopped" const [subdomain, setSubdomain] = useState(null); const [loading, setLoading] = useState(false); const fetchStatus = async () => { try { const d = await apiFetch("/api/vms/golden/status"); setStatus(d.status); setSubdomain(d.subdomain); } catch (_) { setStatus("unknown"); } }; useEffect(() => { fetchStatus(); }, []); const act = async (endpoint, msg) => { setLoading(true); try { await apiFetch(endpoint, { method: "POST" }); toast(msg, "success"); await fetchStatus(); onRefresh && onRefresh(); } catch (e) { toast("Fehler: " + e.message, "error"); } finally { setLoading(false); } }; const isRunning = status === "running"; return (
{/* Icon + info */}
GOLDEN VM TEMPLATE
{status === null ? "Laden…" : isRunning ? "Läuft — Änderungen werden übernommen wenn du speicherst" : "Gestoppt — Starten zum Bearbeiten"}
{/* Status pill */}
{status === null ? "…" : isRunning ? "● RUNNING" : "○ STOPPED"}
{/* Actions */}
{!isRunning ? ( ) : ( <> {subdomain && ( )} )}
); } /* ───────────────────────────────────────────── SIDEBAR ───────────────────────────────────────────── */ function Sidebar({ active, setActive, user }) { const isAdmin = user && user.is_admin; const initials = user && user.full_name ? user.full_name.split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase() : "??"; const groups = [ { group: "Übersicht", entries: [ { id: "dash", label: "Dashboard", icon: "grid" }, { id: "bots", label: "Expert Advisors", icon: "cpu" }, { id: "accounts", label: "Accounts", icon: "wallet" }, { id: "vms", label: "Virtual Machines", icon: "server" }, ] }, ...(isAdmin ? [{ group: "Admin", entries: [ { id: "customers", label: "Customers", icon: "users" }, { id: "trials", label: "Free Trials", icon: "tag" }, { id: "auditlog", label: "Audit Log", icon: "log" }, ] }] : []), ]; return ( ); } /* ───────────────────────────────────────────── MODAL BASE ───────────────────────────────────────────── */ function Modal({ children, onClose, width = 660 }) { useEffect(() => { const handler = e => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, [onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }}>
{children}
); } const modalInput = { background: "#070b12", border: "1px solid rgba(94,224,255,0.15)", borderRadius: 6, color: "#d8ecff", padding: "8px 12px", fontSize: 13, fontFamily: "var(--mono)", width: "100%", outline: "none", }; const modalLabel = { fontSize: 11, color: "var(--ink-2)", fontFamily: "var(--mono)", marginBottom: 4, display: "block", textTransform: "uppercase", letterSpacing: "0.06em", }; const modalBtn = { background: "rgba(94,224,255,0.12)", border: "1px solid rgba(94,224,255,0.3)", borderRadius: 6, color: "var(--accent)", padding: "9px 20px", fontSize: 13, fontFamily: "var(--mono)", cursor: "pointer", }; const modalBtnDanger = { ...modalBtn, background: "rgba(255,77,109,0.12)", border: "1px solid rgba(255,77,109,0.3)", color: "var(--bad)", }; /* ───────────────────────────────────────────── EA CONFIG MODAL ───────────────────────────────────────────── */ function EAConfigModal({ vmId, onClose }) { const [tab, setTab] = useState("risiko"); const [cfg, setCfg] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); useEffect(() => { setLoading(true); apiFetch(`/api/vms/${vmId}/ea-config`) .then(d => { if (d && d.ea_config) setCfg(d.ea_config); else if (d) setCfg(d); }) .catch(e => toast("Config laden fehlgeschlagen: " + e.message, "error")) .finally(() => setLoading(false)); }, [vmId]); const set = (k, v) => setCfg(p => ({ ...p, [k]: v })); const save = async () => { setSaving(true); try { await apiFetch(`/api/vms/${vmId}/ea-config`, { method: "PATCH", body: JSON.stringify({ ea_config: cfg }), }); toast("Konfiguration gespeichert", "success"); onClose(); } catch (e) { toast("Fehler: " + e.message, "error"); } finally { setSaving(false); } }; const TABS = [ { id: "risiko", label: "Risiko" }, { id: "strategie", label: "Strategie" }, { id: "zeiten", label: "Zeiten" }, { id: "propfirm", label: "Prop Firm" }, ]; const WEEKDAYS = [ { key: "allow_monday", label: "Mo" }, { key: "allow_tuesday", label: "Di" }, { key: "allow_wednesday", label: "Mi" }, { key: "allow_thursday", label: "Do" }, { key: "allow_friday", label: "Fr" }, ]; const TF_OPTIONS = ["M5", "M15", "M30", "H1", "H2", "H4", "D1", "W1"]; const TF_OPTIONS2 = ["M1", "M2", "M3", "M4", "M5", "M15", "M30", "H1", "H4", "D1"]; const FRow = ({ label, children }) => (
{children}
); const FNum = ({ k, step = 0.1, min }) => ( set(k, parseFloat(e.target.value) || 0)} /> ); const FToggle = ({ k, label }) => (
{label}
); const grid2 = { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }; const grid3 = { display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12 }; return (
EA Konfiguration
VM-ID: {vmId}{cfg && cfg.version ? ` · v${cfg.version}` : ""}
{TABS.map(t => ( ))}
{loading ? (
Laden...
) : !cfg ? (
Konfiguration nicht verfügbar
) : ( <> {tab === "risiko" && (
▸ Teilschließungen
)} {tab === "strategie" && (
{cfg.enable_htf && (
)}
)} {tab === "zeiten" && (
Handelstage
{WEEKDAYS.map(w => ( ))}
)} {tab === "propfirm" && (
{cfg.enable_random && (
)}
set("target_equity", parseFloat(e.target.value) || 0)} />
set("symbol", e.target.value)} />
)} )}
); } /* ───────────────────────────────────────────── SPAWN VM MODAL ───────────────────────────────────────────── */ function SpawnVMModal({ customers, isAdmin, onClose, onSpawned }) { const [form, setForm] = useState({ customer_id: "", mt5_login: "", mt5_password: "", vm_type: "standard" }); const [loading, setLoading] = useState(false); const set = (k, v) => setForm(p => ({ ...p, [k]: v })); const submit = async () => { if (!form.mt5_login || !form.mt5_password) { toast("MT5 Login und Passwort erforderlich", "warn"); return; } setLoading(true); try { const body = { ...form }; if (body.customer_id) body.customer_id = parseInt(body.customer_id); else delete body.customer_id; await apiFetch("/api/vms/spawn", { method: "POST", body: JSON.stringify(body) }); toast("VM wird gestartet…", "success"); onSpawned && onSpawned(); onClose(); } catch (e) { toast("Fehler: " + e.message, "error"); } finally { setLoading(false); } }; return (
VM Starten
{isAdmin && customers && customers.length > 0 && (
)}
{[ { value: "standard", label: "Standard", icon: "🖥", desc: "1 Account" }, { value: "golden", label: "Golden", icon: "⭐", desc: "Multi-Account" }, ].map(opt => ( ))}
set("mt5_login", e.target.value)} placeholder="z.B. 12345678" />
set("mt5_password", e.target.value)} placeholder="••••••••" />
); } /* ───────────────────────────────────────────── KILL SWITCH MODAL ───────────────────────────────────────────── */ function KillSwitchModal({ onClose, onDone }) { const [loading, setLoading] = useState(false); const [confirmed, setConfirmed] = useState(false); const [lockStatus, setLockStatus] = useState(null); // null | {locked, locked_until} useEffect(() => { apiFetch("/api/vms/kill-switch/status") .then(d => setLockStatus(d)) .catch(() => {}); }, []); const execute = async () => { setLoading(true); try { const res = await apiFetch("/api/vms/kill-switch", { method: "POST" }); toast(`Kill Switch aktiviert — ${res.stopped} VM(s) gestoppt. Neustart ab 00:00 Uhr.`, "warn"); onDone && onDone(); onClose(); } catch (e) { toast("Fehler: " + e.message, "error"); } finally { setLoading(false); } }; const isLocked = lockStatus && lockStatus.locked; const lockedUntil = lockStatus && lockStatus.locked_until ? new Date(lockStatus.locked_until).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }) : "00:00"; return (
KILL SWITCH
{isLocked ? (
⚠ Kill Switch ist aktiv
Deine VMs sind gesperrt bis
{lockedUntil} Uhr
) : (
Stoppt alle deine laufenden VMs sofort.
VMs können erst wieder um 00:00 Uhr gestartet werden.
)}
{!isLocked && ( )}
{!isLocked && ( )}
); } /* ───────────────────────────────────────────── CUSTOMER MODAL ───────────────────────────────────────────── */ function CustomerModal({ customer, onClose, onSaved }) { const isEdit = !!customer; const [form, setForm] = useState({ name: customer?.name || "", license_key: customer?.license_key || "", email: customer?.email || "", max_accounts: customer?.max_accounts || 1, is_active: customer?.is_active !== false, }); const [loading, setLoading] = useState(false); const set = (k, v) => setForm(p => ({ ...p, [k]: v })); const submit = async () => { if (!form.name) { toast("Name erforderlich", "warn"); return; } setLoading(true); try { if (isEdit) { await apiFetch(`/api/customers/${customer.id}`, { method: "PATCH", body: JSON.stringify(form) }); toast("Customer aktualisiert", "success"); } else { await apiFetch("/api/customers", { method: "POST", body: JSON.stringify(form) }); toast("Customer erstellt", "success"); } onSaved && onSaved(); onClose(); } catch (e) { toast("Fehler: " + e.message, "error"); } finally { setLoading(false); } }; return (
{isEdit ? "Customer bearbeiten" : "Neuer Customer"}
set("name", e.target.value)} />
set("email", e.target.value)} />
set("license_key", e.target.value)} placeholder="Wird automatisch generiert wenn leer" />
set("max_accounts", parseInt(e.target.value) || 1)} />
Aktiv
); } /* ───────────────────────────────────────────── TRIAL CAMPAIGN MODAL ───────────────────────────────────────────── */ function TrialModal({ campaign, onClose, onSaved }) { const isEdit = !!campaign; const [form, setForm] = useState({ name: campaign?.name || "", slots_total: campaign?.slots_total || 10, trial_days: campaign?.trial_days || 14, trial_max_accounts: campaign?.trial_max_accounts || 1, signup_deadline: campaign?.signup_deadline ? campaign.signup_deadline.slice(0, 10) : "", is_active: campaign?.is_active !== false, }); const [loading, setLoading] = useState(false); const set = (k, v) => setForm(p => ({ ...p, [k]: v })); const submit = async () => { if (!form.name) { toast("Name erforderlich", "warn"); return; } setLoading(true); try { const body = { ...form, slots_total: parseInt(form.slots_total), trial_days: parseInt(form.trial_days), trial_max_accounts: parseInt(form.trial_max_accounts) }; if (isEdit) { await apiFetch(`/api/admin/trial-campaigns/${campaign.id}`, { method: "PATCH", body: JSON.stringify(body) }); toast("Kampagne aktualisiert", "success"); } else { await apiFetch("/api/admin/trial-campaigns", { method: "POST", body: JSON.stringify(body) }); toast("Kampagne erstellt", "success"); } onSaved && onSaved(); onClose(); } catch (e) { toast("Fehler: " + e.message, "error"); } finally { setLoading(false); } }; return (
{isEdit ? "Kampagne bearbeiten" : "Neue Kampagne"}
set("name", e.target.value)} />
set("slots_total", e.target.value)} />
set("trial_days", e.target.value)} />
set("trial_max_accounts", e.target.value)} />
set("signup_deadline", e.target.value)} />
Aktiv
); } /* ───────────────────────────────────────────── LOGIN PAGE ───────────────────────────────────────────── */ function LoginPage({ onLogin }) { const [telegramId, setTelegramId] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const tgRef = useRef(null); // Inject Telegram Login Widget script once on mount useEffect(() => { window.onTelegramAuth = async (user) => { setLoading(true); setError(""); try { const data = await apiFetch("/api/auth/telegram", { method: "POST", body: JSON.stringify(user), }); if (data && data.access_token) { localStorage.setItem("jwt", data.access_token); onLogin(data.access_token); } else { setError("Telegram Login fehlgeschlagen."); } } catch (e) { setError(e.message || "Kein Zugang. Bitte abonniere EA Commander."); } finally { setLoading(false); } }; if (tgRef.current && tgRef.current.childNodes.length === 0) { const s = document.createElement("script"); s.src = "https://telegram.org/js/telegram-widget.js?22"; s.setAttribute("data-telegram-login", "NQBladeBladebot"); s.setAttribute("data-size", "large"); s.setAttribute("data-radius", "8"); s.setAttribute("data-onauth", "onTelegramAuth(user)"); s.setAttribute("data-request-access", "write"); s.async = true; tgRef.current.appendChild(s); } return () => { delete window.onTelegramAuth; }; }, []); const submit = async e => { e.preventDefault(); if (!telegramId || !password) { setError("Bitte alle Felder ausfüllen."); return; } setLoading(true); setError(""); try { const data = await apiFetch("/api/auth/login", { method: "POST", body: JSON.stringify({ telegram_id: parseInt(telegramId), password }), }); if (data && data.access_token) { localStorage.setItem("jwt", data.access_token); onLogin(data.access_token); } else { setError("Login fehlgeschlagen."); } } catch (e) { setError(e.message || "Login fehlgeschlagen."); } finally { setLoading(false); } }; return (
{/* Brand */}
EA-Commander
JARVIS · OPERATIONS
{/* Telegram Login Widget */}
Direkt mit deinem Telegram-Account anmelden
{/* Divider */}
ODER
{/* Password fallback form */}
setTelegramId(e.target.value)} />
setPassword(e.target.value)} />
{error && (
{error}
)}
EA-Commander · Secure Operations Panel
); } /* ───────────────────────────────────────────── DASHBOARD PAGE ───────────────────────────────────────────── */ function DashboardPage({ accounts, kpis, bots, vms, dailyPnl, customers, accent, user, onShowKill, onEaConfig }) { const [range, setRange] = useState("30D"); const isAdmin = user && user.is_admin; // KPI field names match the backend response exactly const totalEquity = kpis ? kpis.total_equity : accounts.reduce((s, a) => s + (a.equity || 0), 0); const totalFloat = kpis ? kpis.floating_pl : accounts.reduce((s, a) => s + (a.floating_pl || 0), 0); const dailyPnlVal = kpis ? kpis.daily_pl : accounts.reduce((s, a) => s + (a.daily_pl || 0), 0); const maxDD = kpis ? kpis.max_dd_today : 0; const runBots = kpis ? kpis.bots_running : bots.length; const pf = kpis ? kpis.profit_factor : 0; const wr = kpis ? kpis.win_rate : 0; // Sparklines from real daily-pnl data (or empty if no data yet) const sparkEquity = useMemo(() => { if (dailyPnl && dailyPnl.length >= 2) { let cum = totalEquity - dailyPnl.reduce((s, d) => s + (d.pnl || 0), 0); return dailyPnl.map(d => { cum += (d.pnl || 0); return cum; }); } return accounts.length ? [totalEquity * 0.98, totalEquity * 0.99, totalEquity] : []; }, [dailyPnl, totalEquity, accounts]); const sparkPnl = useMemo(() => { if (dailyPnl && dailyPnl.length >= 2) return dailyPnl.map(d => d.pnl || 0); return []; }, [dailyPnl]); const sparkDD = useMemo(() => { if (accounts.length) return accounts.map(a => a.max_dd_today || 0); return []; }, [accounts]); // On the dashboard always show only the logged-in user's own accounts, // even if user is admin (admin can use the Accounts page for all accounts). const myCustomerId = user && user.customer ? user.customer.id : null; const myAccounts = myCustomerId ? accounts.filter(a => a.customer_id === myCustomerId) : accounts; const accountsForDD = myAccounts.slice().sort((a, b) => (b.max_dd_today || 0) - (a.max_dd_today || 0)).slice(0, 4); return ( <>
▸ Live Operations

Dashboard {accounts.length} Accounts

Bots{runBots}
Equity{fmtMoney(totalEquity)}
Today= 0 ? "var(--good)" : "var(--bad)" }}>{fmtMoney(dailyPnlVal, true)}
▸ Total Equity
$ {totalEquity.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
= 0 ? "pos" : "neg"}>{fmtMoney(totalFloat, true)} floating = 0 ? "pos" : "neg"}>{fmtMoney(dailyPnlVal, true)} today
PF
{(pf || 0).toFixed(2)}
Win
{(wr || 0).toFixed(1)}%
MaxDD
5 ? "var(--bad)" : maxDD > 2.5 ? "var(--warn)" : "var(--good)" }}>{(maxDD || 0).toFixed(1)}%
{["7D", "14D", "30D", "90D", "ALL"].map(r => (
setRange(r)}>{r}
))}

▸ Risk Console

{accountsForDD.length === 0 ? (
Keine Accounts
) : accountsForDD.map(a => { const dd = a.max_dd_today || 0; const cls = dd > 4 ? " bad" : dd > 2.5 ? " warn" : ""; const name = (a.account_name || a.broker || "Account").slice(0, 10); return (
{name}
{dd.toFixed(1)}%
); })}
Gesamt MaxDD
{(maxDD || 0).toFixed(1)}%
▸ Total Equity
LIVE
{fmtMoney(totalEquity)}
{fmtMoney(dailyPnlVal, true)} heute
= 2 ? sparkEquity : null} color={accent} />
▸ Daily P&L
24H
= 0 ? "var(--good)" : "var(--bad)" }}>{fmtMoney(dailyPnlVal)}
= 0 ? "pos" : "neg")}>{fmtMoney(totalFloat, true)} floating
= 2 ? sparkPnl : null} color="#5cf2a0" />
▸ Max DD Today
24H
{(maxDD || 0).toFixed(1)}%
4 ? "var(--bad)" : "var(--ink-2)" }}> {maxDD > 4 ? "⚠ Kritisch" : maxDD > 2 ? "Erhöht" : "Normal"}
= 2 ? sparkDD : null} color="#ffb547" />
▸ Win Rate
LIVE
{(wr || 0).toFixed(1)}%
Profit Factor: {(pf || 0).toFixed(2)}
= 2 ? sparkPnl : null} color={accent} />
{bots.length > 0 && (

Active Expert Advisors · {runBots} live

{bots.slice(0, 6).map(bot => )}
)} {myAccounts.length > 0 && (

Meine Accounts · {myAccounts.length} total

{myAccounts.map(a => )}
)} ); } /* ───────────────────────────────────────────── VMs PAGE ───────────────────────────────────────────── */ function VMsPage({ vms, customers, user, onRefresh, onEaConfig }) { const [showSpawn, setShowSpawn] = useState(false); const [killLocked, setKillLocked] = useState(false); const [killUntil, setKillUntil] = useState(null); useEffect(() => { apiFetch("/api/vms/kill-switch/status") .then(d => { setKillLocked(d.locked); setKillUntil(d.locked_until || null); }) .catch(() => {}); }, [vms]); const custMap = useMemo(() => { const m = {}; if (customers) customers.forEach(c => { m[c.id] = c; }); return m; }, [customers]); const handleStop = async id => { try { await apiFetch(`/api/vms/${id}/stop`, { method: "POST" }); toast("VM gestoppt", "success"); onRefresh && onRefresh(); } catch (e) { toast("Fehler: " + e.message, "error"); } }; const handleStart = async id => { try { await apiFetch(`/api/vms/${id}/start`, { method: "POST" }); toast("VM gestartet", "success"); onRefresh && onRefresh(); } catch (e) { toast("Fehler: " + e.message, "error"); } }; const handleDelete = async id => { if (!window.confirm("VM wirklich löschen?")) return; try { await apiFetch(`/api/vms/${id}`, { method: "DELETE" }); toast("VM gelöscht", "success"); onRefresh && onRefresh(); } catch (e) { toast("Fehler: " + e.message, "error"); } }; const isAdmin = user && user.is_admin; return ( <>
▸ Infrastruktur

Virtual Machines {vms.length} total · {vms.filter(v => v.status === "running").length} running

{isAdmin && } {killLocked && (
KILL SWITCH AKTIV
VM-Start gesperrt bis{" "} {killUntil ? new Date(killUntil).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }) : "00:00"} Uhr
)}
{vms.length === 0 ? (
Keine VMs vorhanden.
) : vms.map(vm => ( v.caddy_subdomain && window.open("https://" + v.caddy_subdomain, "_blank")} /> ))}
{showSpawn && ( setShowSpawn(false)} onSpawned={onRefresh} /> )} ); } /* ───────────────────────────────────────────── ACCOUNTS PAGE ───────────────────────────────────────────── */ function AccountsPage({ accounts, onEaConfig }) { return ( <>
▸ Trading

Accounts {accounts.length} verbunden

{accounts.length === 0 ? (
Keine Accounts.
) : accounts.map(a => ( ))}
); } /* ───────────────────────────────────────────── BOTS PAGE ───────────────────────────────────────────── */ function BotsPage({ bots, accent }) { return ( <>
▸ Automation

Expert Advisors {bots.length} bots

{bots.length === 0 ? (
Keine Bots aktiv.
) : (
{bots.map(bot => )}
)}
{bots.length > 0 && (

Statistiken

▸ Total Today P&L
24H
s + (b.today_pl || 0), 0) >= 0 ? "var(--good)" : "var(--bad)" }}> {fmtMoney(bots.reduce((s, b) => s + (b.today_pl || 0), 0))}
▸ Aktive Bots
LIVE
{bots.length}
▸ Max Drawdown
24H
{Math.max(...bots.map(b => b.drawdown || 0)).toFixed(1)}%
)} ); } /* ───────────────────────────────────────────── CUSTOMERS PAGE (admin) ───────────────────────────────────────────── */ function CustomersPage({ customers, onRefresh }) { const [showModal, setShowModal] = useState(false); const [editCustomer, setEditCustomer] = useState(null); const openCreate = () => { setEditCustomer(null); setShowModal(true); }; const openEdit = c => { setEditCustomer(c); setShowModal(true); }; const thStyle = { padding: "10px 14px", textAlign: "left", fontSize: 11, color: "var(--ink-2)", fontFamily: "var(--mono)", borderBottom: "1px solid rgba(94,224,255,0.08)", whiteSpace: "nowrap" }; const tdStyle = { padding: "10px 14px", fontSize: 13, color: "var(--ink-1)", fontFamily: "var(--mono)", borderBottom: "1px solid rgba(94,224,255,0.05)" }; return ( <>
▸ Admin

Customers {customers.length} registriert

{customers.length === 0 ? ( ) : customers.map(c => ( ))}
Name E-Mail License Key Max Accts VMs Accounts Status Aktionen
Keine Customers vorhanden.
{c.name} {c.email || "—"} {(c.license_key || "—").slice(0, 20)}{c.license_key && c.license_key.length > 20 ? "…" : ""} {c.max_accounts} {c.vm_count || 0} {c.registered_accounts || 0} {c.is_active ? "AKTIV" : "INAKTIV"}
{showModal && ( setShowModal(false)} onSaved={onRefresh} /> )} ); } /* ───────────────────────────────────────────── TRIALS PAGE (admin) ───────────────────────────────────────────── */ function TrialsPage() { const [campaigns, setCampaigns] = useState([]); const [loading, setLoading] = useState(true); const [showModal, setShowModal] = useState(false); const [editCampaign, setEditCampaign] = useState(null); const load = useCallback(async () => { setLoading(true); try { const data = await apiFetch("/api/admin/trial-campaigns"); setCampaigns(data || []); } catch (e) { toast("Fehler beim Laden: " + e.message, "error"); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const openCreate = () => { setEditCampaign(null); setShowModal(true); }; const openEdit = c => { setEditCampaign(c); setShowModal(true); }; const handleDelete = async id => { if (!window.confirm("Kampagne löschen?")) return; try { await apiFetch(`/api/admin/trial-campaigns/${id}`, { method: "DELETE" }); toast("Kampagne gelöscht", "success"); load(); } catch (e) { toast("Fehler: " + e.message, "error"); } }; const thStyle = { padding: "10px 14px", textAlign: "left", fontSize: 11, color: "var(--ink-2)", fontFamily: "var(--mono)", borderBottom: "1px solid rgba(94,224,255,0.08)" }; const tdStyle = { padding: "10px 14px", fontSize: 13, color: "var(--ink-1)", fontFamily: "var(--mono)", borderBottom: "1px solid rgba(94,224,255,0.05)" }; return ( <>
▸ Admin

Trial Campaigns {campaigns.length} gesamt

{loading ? (
Laden...
) : (
{campaigns.length === 0 ? ( ) : campaigns.map(c => ( ))}
Name Slots Tage Max Accts Deadline Status Erstellt Aktionen
Keine Kampagnen vorhanden.
{c.name} {c.slots_used || 0} / {c.slots_total} {c.trial_days} {c.trial_max_accounts} {c.signup_deadline ? c.signup_deadline.slice(0, 10) : "—"} {c.is_active ? "AKTIV" : "INAKTIV"} {c.created_at ? c.created_at.slice(0, 10) : "—"}
)}
{showModal && ( setShowModal(false)} onSaved={load} /> )} ); } /* ───────────────────────────────────────────── AUDIT LOG PAGE ───────────────────────────────────────────── */ function AuditLogPage() { return ( <>
▸ Admin

Audit Log

Audit log coming soon
); } /* ───────────────────────────────────────────── MAIN APP ───────────────────────────────────────────── */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "#5ee0ff", "hud": true, "density": "comfortable" }/*EDITMODE-END*/; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [authed, setAuthed] = useState(false); const [user, setUser] = useState(null); const [authChecked, setAuthChecked] = useState(false); // Data state const [accounts, setAccounts] = useState([]); const [vms, setVms] = useState([]); const [bots, setBots] = useState([]); const [kpis, setKpis] = useState(null); const [dailyPnl, setDailyPnl] = useState(null); const [customers, setCustomers] = useState([]); // UI state const [active, setActive] = useState("dash"); const [showKill, setShowKill] = useState(false); const [eaConfigVmId, setEaConfigVmId] = useState(null); const [showTweaks, setShowTweaks] = useState(false); // Auth check on mount useEffect(() => { const token = localStorage.getItem("jwt"); if (!token) { setAuthChecked(true); return; } apiFetch("/api/auth/me") .then(u => { if (u) { setUser(u); setAuthed(true); } else { localStorage.removeItem("jwt"); } }) .catch(() => { localStorage.removeItem("jwt"); }) .finally(() => setAuthChecked(false)); setAuthChecked(true); }, []); // Apply accent CSS var useEffect(() => { document.documentElement.style.setProperty("--accent", t.accent); document.documentElement.style.setProperty("--accent-2", t.accent); }, [t.accent]); // Keyboard shortcut Ctrl+Shift+T to open TweaksPanel useEffect(() => { const handler = e => { if (e.ctrlKey && e.shiftKey && e.key === "T") { e.preventDefault(); window.parent.postMessage({ type: "__activate_edit_mode" }, "*"); } }; document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, []); // Data loading const loadData = useCallback(async () => { if (!authed) return; try { const [acctData, vmData, botData, kpiData, pnlData] = await Promise.all([ apiFetch("/api/accounts").catch(() => []), apiFetch("/api/vms").catch(() => []), apiFetch("/api/bots?status=running").catch(() => []), apiFetch("/api/dashboard/kpis").catch(() => null), apiFetch("/api/dashboard/daily-pnl?days=30").catch(() => null), ]); setAccounts(acctData || []); setVms(vmData || []); setBots(botData || []); if (kpiData) setKpis(kpiData); if (pnlData) setDailyPnl(pnlData); } catch (e) { // silent — individual catches handle per-endpoint } }, [authed]); const loadCustomers = useCallback(async () => { if (!authed || !user?.is_admin) return; try { const data = await apiFetch("/api/customers"); setCustomers(data || []); } catch (_) {} }, [authed, user]); useEffect(() => { if (authed) { loadData(); loadCustomers(); } }, [authed, loadData, loadCustomers]); // Auto-refresh every 30s useEffect(() => { if (!authed) return; const interval = setInterval(() => { loadData(); }, 30000); return () => clearInterval(interval); }, [authed, loadData]); const handleLogin = useCallback(async token => { try { const u = await apiFetch("/api/auth/me"); if (u) { setUser(u); setAuthed(true); } } catch (_) { localStorage.removeItem("jwt"); } }, []); const handleEaConfig = useCallback(vmId => { setEaConfigVmId(vmId); }, []); if (!authChecked && !authed) { return (
Verbinde...
); } if (!authed) { return ( <> ); } const isAdmin = user && user.is_admin; const renderPage = () => { switch (active) { case "dash": return ( setShowKill(true)} onEaConfig={handleEaConfig} /> ); case "bots": return ; case "accounts": return ; case "vms": return ; case "customers": return isAdmin ? : null; case "trials": return isAdmin ? : null; case "auditlog": return ; default: return
Seite nicht gefunden.
; } }; return (
{renderPage()}
{t.hud && } {showKill && setShowKill(false)} onDone={loadData} />} {eaConfigVmId && setEaConfigVmId(null)} />} setTweak("accent", v)} /> setTweak("hud", v)} /> setTweak("density", v)} />
); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(); // Hide boot screen once React has painted its first frame. // (onload on type="text/babel" fires at network-fetch time, not after Babel // transpiles — so we do it here instead.) requestAnimationFrame(() => { requestAnimationFrame(() => { const boot = document.getElementById("boot-screen"); if (boot) boot.classList.add("hidden"); }); });