/* ═══════════════════════════════════════════════════════════════ FoxBot Pro — app shell + entry ═══════════════════════════════════════════════════════════════ */ const { useState: useStateApp, useEffect: useEffectApp } = React; /* ─── Error Boundary : empêche un crash écran de tuer toute l'app ─── */ class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { err: null }; } static getDerivedStateFromError(err) { return { err }; } componentDidCatch(err, info) { console.error("[ErrorBoundary]", err, info); } render() { if (this.state.err) { return (

Cet écran a planté

{String(this.state.err && this.state.err.message || this.state.err)}

); } return this.props.children; } } function App() { const [route, setRoute] = useStateApp(() => { const h = window.location.hash.replace("#/", "") || "dashboard"; return ["dashboard","signaux","volatile","trading","charts","journal","stats","settings"].includes(h) ? h : "dashboard"; }); const [paletteOpen, setPaletteOpen] = useStateApp(false); const store = window.useStore ? window.useStore() : window.MOCK; const mode = store.MODE || "PAPER"; // Route → hash useEffectApp(() => { window.location.hash = "#/" + route; }, [route]); // ⌘K / Ctrl+K → ouvre command palette useEffectApp(() => { function onKey(e) { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); setPaletteOpen(o => !o); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); async function handleAction(kind, signal) { const isPaper = kind.startsWith("paper"); const dir = kind.endsWith("long") ? "LONG" : "SHORT"; try { const res = await window.fbAction(isPaper ? "paper" : "real", dir, signal.fullPair || (signal.pair + "/USDT:USDT")); const isReal = !isPaper; emitToast({ type: res.ok ? (isReal ? "success" : "info") : "error", msg: res.ok ? `${isPaper ? "Paper trade" : "Position réelle"} · ${signal.pair} ${dir}` : `Erreur · ${res.msg || "ouverture impossible"}`, sub: res.ok ? `Entry ${fmtNum(signal.entry)} · SL ${fmtNum(signal.sl)} · TP ${fmtNum(signal.tp1)}` : null, }); } catch (e) { emitToast({ type: "error", msg: `Erreur · ${e.message}` }); } } return (
setPaletteOpen(true)}/> {store.CONN_LOST && } {route === "dashboard" && } {route === "signaux" && } {route === "volatile" && } {route === "trading" && } {route === "charts" && } {route === "journal" && } {route === "stats" && } {route === "settings" && }
{paletteOpen && setPaletteOpen(false)} onRoute={setRoute}/>}
); } /* ─── Sidebar ─── */ function Sidebar({ route, onRoute, mode }) { const store = window.useStore ? window.useStore() : {}; const signalsCount = (store.SIGNALS || []).filter(s => s.state === "active").length; const volCount = (store.VOLATILE || []).filter(s => s.state === "active").length; const posCount = (store.POSITIONS || []).length; const capital = store.STATUS?.capital ?? "—"; const autoTrade = !!store.STATUS?.auto_trade; const minConf = store.STATUS?.auto_min_confidence ?? 8; const items = [ { id: "dashboard", icon: Icon.Home, label: "Dashboard" }, { id: "signaux", icon: Icon.Zap, label: "Signaux", badge: signalsCount || null }, { id: "volatile", icon: Icon.Flame, label: "Volatile", badge: volCount || null }, { id: "trading", icon: Icon.TrendUp, label: "Trading", badge: posCount || null }, { id: "charts", icon: Icon.Chart, label: "Charts" }, { id: "journal", icon: Icon.Book, label: "Journal" }, ]; const items2 = [ { id: "stats", icon: Icon.Stats, label: "Stats" }, { id: "settings", icon: Icon.Settings, label: "Settings" }, ]; return ( ); } function ToggleMini({ value, onClick }) { return (
); } function FoxMark({ size = 16 }) { return ( FoxBot Pro ); } /* ─── Topbar ─── */ function Topbar({ route, mode, onOpenPalette }) { const titles = { dashboard: "Dashboard", signaux: "Signaux", volatile: "Volatile", charts: "Charts", journal: "Journal", stats: "Stats", settings: "Settings" }; // Live ticker — re-render toutes les 500ms pour afficher le delta WS const store = window.useStore ? window.useStore() : {}; const [, force] = useStateApp(0); useEffectApp(() => { const t = setInterval(() => force(n => n + 1), 500); return () => clearInterval(t); }, []); const lastTs = store.LAST_WS_TS || 0; const deltaMs = lastTs ? Date.now() - lastTs : null; const live = deltaMs != null && deltaMs < 2000; return (
FoxBot

{titles[route] || route}

{deltaMs == null ? "WAIT" : deltaMs < 1000 ? `${deltaMs}ms` : `${(deltaMs/1000).toFixed(1)}s`}
Rechercher cryptos, pages, actions… ⌘K
FB
); } function ConnectionBanner() { return (
Connexion perdue. Tentative de reconnexion…
); } /* ─── Bottom nav (mobile) avec menu Plus ─── */ function BottomNav({ route, onRoute }) { const [moreOpen, setMoreOpen] = useStateApp(false); const items = [ { id:"dashboard", i:Icon.Home, l:"Home" }, { id:"signaux", i:Icon.Zap, l:"Signaux" }, { id:"trading", i:Icon.TrendUp, l:"Trading" }, { id:"charts", i:Icon.Chart, l:"Charts" }, { id:"__more", i:Icon.Menu, l:"Plus" }, ]; const extraPages = [ { id:"volatile", i:Icon.Flame, l:"Volatile" }, { id:"journal", i:Icon.Book, l:"Journal" }, { id:"stats", i:Icon.Stats, l:"Stats" }, { id:"settings", i:Icon.Settings, l:"Settings" }, ]; const isExtraActive = ["volatile","journal","stats","settings"].includes(route); return ( <> {moreOpen && (
setMoreOpen(false)} style={{ position:"fixed", inset:0, zIndex:50, background:"rgba(8,8,15,0.6)", backdropFilter:"blur(8px)", display:"flex", alignItems:"flex-end", }}>
e.stopPropagation()} style={{ width:"100%", background:"var(--bg-elevated)", borderTopLeftRadius:"var(--radius-xl)", borderTopRightRadius:"var(--radius-xl)", border:"1px solid var(--border-base)", paddingBottom: "calc(env(safe-area-inset-bottom) + 16px)", animation: "sheet-up 240ms cubic-bezier(0.2,0.8,0.2,1)", }}>
NAVIGATION
{extraPages.map(p => { const active = route === p.id; return (
{ onRoute(p.id); setMoreOpen(false); }} style={{ display:"flex", alignItems:"center", gap:14, padding:"14px 16px", borderRadius:"var(--radius-md)", background: active ? "var(--accent-soft)" : "transparent", color: active ? "var(--accent-400)" : "var(--text-primary)", fontWeight:500, fontSize:15, cursor:"pointer", }}> {p.l}
); })}
)} ); } // Animation slide-up — injecté une fois if (typeof document !== "undefined" && !document.getElementById("sheet-up-kf")) { const s = document.createElement("style"); s.id = "sheet-up-kf"; s.textContent = "@keyframes sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }"; document.head.appendChild(s); } /* ─── Stats + Settings (placeholders simples — vraies fonctionnalités via Dashboard/Journal) ─── */ function StatsScreen() { const store = window.useStore ? window.useStore() : {}; const s = store.DBSTATS || {}; return (

Stats

Statistiques SQLite · cumul historique
SIGNAUX 24H
{s.signals_24h ?? "—"}
TRADES 7J
{s.trades_7d ?? "—"}
WIN RATE
{s.winrate_pct != null ? s.winrate_pct.toFixed(1) + "%" : "—"}
PnL TOTAL
=0?"var(--bull)":"var(--bear)" }}>{s.pnl_total != null ? (s.pnl_total>=0?"+":"")+s.pnl_total.toFixed(2)+"€" : "—"}
); } function SettingsScreen() { const store = window.useStore ? window.useStore() : {}; const st = store.STATUS || {}; const scanMode = st.scan_mode || "NORMAL"; const [minConf, setMinConf] = useStateApp(st.auto_min_confidence ?? 8); const [interval, setInterval_]= useStateApp(st.scan_interval_s ?? 120); const [capital, setCapital] = useStateApp(st.capital ?? 200); useEffectApp(() => { if (st.auto_min_confidence != null) setMinConf(st.auto_min_confidence); }, [st.auto_min_confidence]); useEffectApp(() => { if (st.scan_interval_s != null) setInterval_(st.scan_interval_s); }, [st.scan_interval_s]); useEffectApp(() => { if (st.capital != null) setCapital(st.capital); }, [st.capital]); const Section = ({ title, danger, children }) => (

{title}

{children}
); const Row = ({ label, hint, children }) => (
{label} {hint && {hint}}
{children}
); return (

Settings

Configuration FoxBot Pro · {Object.keys(st).length ? "synchronisé" : "chargement..."}
{["SAFE","NORMAL","AVANCE"].map(m => ( ))} setMinConf(parseInt(e.target.value))} onMouseUp={()=>fetch(`/api/set/auto_confidence/${minConf}`, { method:"POST", credentials:"include" }).then(()=>window.fbRefresh && window.fbRefresh())} style={{ width:140 }}/> {minConf} setInterval_(parseInt(e.target.value))} onMouseUp={()=>fetch(`/api/set/scan_interval/${interval}`, { method:"POST", credentials:"include" }).then(()=>window.fbRefresh && window.fbRefresh())} style={{ width:140 }}/> {interval}s
{[5, 10, 0].map(v => ( ))} setCapital(parseInt(e.target.value) || 0)} style={{ background:"var(--bg-base)", color:"var(--text-primary)", border:"1px solid var(--border-base)", borderRadius:6, padding:"6px 10px", fontSize:13, width:100, fontFamily:"var(--font-mono)" }}/> {(st.usdc || 0).toFixed(2)} $
window.fbToggle("paper")}/> window.fbToggle("auto")}/>
{st.trades_today || 0}/{st.max_trades || 5} = 2 ? "bear" : "primary"}`}>{st.consecutive_losses || 0} = 0 ? "bull" : "bear"}`}>{(st.daily_pnl||0) >= 0 ? "+" : ""}{(st.daily_pnl||0).toFixed(2)}€
{store.CONN_LOST ? "Reconnexion..." : "Live"}
); } /* ─── WebAuthn UI section (Phase 4) ─── */ function WebauthnSection() { const [creds, setCreds] = useStateApp([]); const [busy, setBusy] = useStateApp(false); async function load() { try { const j = await fetch("/api/webauthn/credentials", { credentials: "include" }).then(r => r.json()); setCreds(j.credentials || []); } catch {} } useEffectApp(() => { load(); }, []); const isHttps = window.location.protocol === "https:" || window.location.hostname === "localhost"; async function enroll() { if (!isHttps) { emitToast({ type:"warn", msg:"Face ID nécessite HTTPS — pas activable sur connexion HTTP" }); return; } setBusy(true); try { const nickname = prompt("Nom de cet appareil ?", "iPhone Face ID") || "Device"; const j = await window.fbWebauthnRegister(nickname); if (j.ok) { emitToast({ type:"success", msg:"Biométrie enregistrée" }); load(); } else emitToast({ type:"error", msg: j.msg || "Erreur" }); } catch (e) { emitToast({ type:"error", msg: e.message }); } setBusy(false); } async function remove(id) { if (!confirm("Supprimer cet enregistrement ?")) return; await fetch(`/api/webauthn/credentials/${id}`, { method:"DELETE", credentials:"include" }); load(); } return (
{!isHttps && (
⚠️ Face ID / Touch ID nécessite HTTPS. Activez-le après avoir configuré un domaine + Let's Encrypt sur votre VPS.
)} {!isWebauthnSupported() && (
❌ Votre navigateur ne supporte pas WebAuthn.
)} {creds.length === 0 ? (

Aucun appareil biométrique enregistré.

) : (
{creds.map(c => (
{c.nickname || "Device"}
Enregistré le {c.created_at ? new Date(c.created_at).toLocaleDateString("fr-FR") : "—"} {c.last_used_at ? ` · utilisé ${new Date(c.last_used_at).toLocaleDateString("fr-FR")}` : ""}
))}
)}
); } /* ─── WebAuthn / Face ID helpers (Phase 4) ─── */ // WebAuthn nécessite HTTPS (sauf localhost). Le client le détecte via window.PublicKeyCredential. function isWebauthnSupported() { return typeof window !== "undefined" && !!window.PublicKeyCredential; } function b64urlDecode(s) { s = s.replace(/-/g, "+").replace(/_/g, "/"); while (s.length % 4) s += "="; return Uint8Array.from(atob(s), c => c.charCodeAt(0)); } function b64urlEncode(buf) { let s = ""; const bytes = new Uint8Array(buf); for (let i = 0; i < bytes.byteLength; i++) s += String.fromCharCode(bytes[i]); return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } async function webauthnRegister(nickname) { if (!isWebauthnSupported()) throw new Error("WebAuthn non supporté par ce navigateur"); const optsRaw = await fetch("/api/webauthn/register/begin", { method: "POST", credentials: "include" }); const opts = await optsRaw.json(); // Convertir challenge + user.id + excludeCredentials.id en ArrayBuffer opts.challenge = b64urlDecode(opts.challenge); opts.user.id = b64urlDecode(opts.user.id); if (opts.excludeCredentials) { opts.excludeCredentials = opts.excludeCredentials.map(c => ({ ...c, id: b64urlDecode(c.id) })); } const cred = await navigator.credentials.create({ publicKey: opts }); const att = cred.response; const credJson = { id: cred.id, rawId: b64urlEncode(cred.rawId), type: cred.type, response: { clientDataJSON: b64urlEncode(att.clientDataJSON), attestationObject: b64urlEncode(att.attestationObject), }, }; const r = await fetch("/api/webauthn/register/complete", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ expectedChallenge: b64urlEncode(opts.challenge), credential: credJson, nickname, }), }); return r.json(); } async function webauthnLogin(email) { if (!isWebauthnSupported()) throw new Error("WebAuthn non supporté par ce navigateur"); const optsRaw = await fetch("/api/webauthn/login/begin", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email || "" }), }); const opts = await optsRaw.json(); opts.challenge = b64urlDecode(opts.challenge); if (opts.allowCredentials) { opts.allowCredentials = opts.allowCredentials.map(c => ({ ...c, id: b64urlDecode(c.id) })); } const cred = await navigator.credentials.get({ publicKey: opts }); const a = cred.response; const credJson = { id: cred.id, rawId: b64urlEncode(cred.rawId), type: cred.type, response: { clientDataJSON: b64urlEncode(a.clientDataJSON), authenticatorData: b64urlEncode(a.authenticatorData), signature: b64urlEncode(a.signature), userHandle: a.userHandle ? b64urlEncode(a.userHandle) : null, }, }; const r = await fetch("/api/webauthn/login/complete", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ expectedChallenge: b64urlEncode(opts.challenge), credential: credJson, }), }); return r.json(); } window.fbWebauthnRegister = webauthnRegister; window.fbWebauthnLogin = webauthnLogin; /* ─── Telegram link form (Phase 3) ─── */ function TelegramLinkForm() { const [status, setStatus] = useStateApp(null); const [code, setCode] = useStateApp(null); const [countdown, setCD] = useStateApp(0); const [loading, setLoad] = useStateApp(false); async function loadStatus() { try { setStatus(await fetch("/api/telegram/link/status", { credentials: "include" }).then(r=>r.json())); } catch {} } useEffectApp(() => { loadStatus(); }, []); // Countdown du code + polling status pendant la durée de vie useEffectApp(() => { if (!code) return; setCD(600); const t = setInterval(() => setCD(c => Math.max(0, c-1)), 1000); const poll = setInterval(loadStatus, 3000); return () => { clearInterval(t); clearInterval(poll); }; }, [code]); useEffectApp(() => { if (status?.linked && code) { setCode(null); emitToast({ type:"success", msg:"Telegram lié !" }); } }, [status?.linked]); useEffectApp(() => { if (countdown === 0 && code) setCode(null); }, [countdown, code]); async function generate() { setLoad(true); try { const j = await fetch("/api/telegram/link/generate", { method:"POST", credentials:"include" }).then(r=>r.json()); if (j.ok) setCode({ value: j.code, bot: j.bot_username }); else emitToast({ type:"error", msg: j.msg || "Erreur" }); } catch (e) { emitToast({ type:"error", msg:e.message }); } setLoad(false); } async function unlink() { if (!confirm("Délier Telegram ?")) return; await fetch("/api/telegram/link", { method:"DELETE", credentials:"include" }); emitToast({ type:"info", msg:"Telegram délié" }); loadStatus(); } async function testNotif() { const j = await fetch("/api/telegram/test", { method:"POST", credentials:"include" }).then(r=>r.json()); emitToast({ type: j.ok ? "success" : "error", msg: j.ok ? "Test envoyé sur Telegram" : (j.msg || "Erreur") }); } if (status?.linked) { return (
Lié{status.username ? ` à @${status.username}` : ""}
{status.notifications_enabled ? "Notifs activées" : "Notifs en pause"}
); } if (code) { const mm = String(Math.floor(countdown / 60)).padStart(2, "0"); const ss = String(countdown % 60).padStart(2, "0"); return (
CODE DE LIAISON · EXPIRE DANS {mm}:{ss}
{code.value}
  1. Ouvre Telegram → cherche @{code.bot}
  2. Envoie : /link {code.value}
  3. Reviens ici, c'est automatique
); } return (

Reçois tes notifications de trading (signaux, trades, TP/SL atteints) directement sur Telegram.

); } /* ─── OKX credentials form (Phase 2) ─── */ function OkxCredsForm() { const [status, setStatus] = useStateApp(null); const [apiKey, setApiKey] = useStateApp(""); const [secret, setSecret] = useStateApp(""); const [pass, setPass] = useStateApp(""); const [isDemo, setIsDemo] = useStateApp(false); const [show, setShow] = useStateApp({ k: false, s: false, p: false }); const [testing, setTesting] = useStateApp(false); const [saving, setSaving] = useStateApp(false); const [testRes, setTestRes] = useStateApp(null); async function loadStatus() { try { const j = await fetch("/api/okx/credentials/status", { credentials: "include" }).then(r => r.json()); setStatus(j); } catch {} } useEffectApp(() => { loadStatus(); }, []); async function doTest() { setTesting(true); setTestRes(null); try { const j = await fetch("/api/okx/test-connection", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: apiKey, secret, passphrase: pass, is_demo: isDemo }), }).then(r => r.json()); setTestRes(j); } catch (e) { setTestRes({ ok: false, msg: e.message }); } setTesting(false); } async function doSave() { setSaving(true); try { const j = await fetch("/api/okx/credentials", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: apiKey, secret, passphrase: pass, is_demo: isDemo }), }).then(r => r.json()); if (j.ok) { emitToast({ type: "success", msg: "OKX connecté", sub: j.usdc != null ? `USDC: ${j.usdc.toFixed(2)}$` : "" }); setApiKey(""); setSecret(""); setPass(""); await loadStatus(); } else { emitToast({ type: "error", msg: j.msg || "Erreur" }); } } catch (e) { emitToast({ type: "error", msg: e.message }); } setSaving(false); } async function doDelete() { if (!confirm("Supprimer tes clés OKX ?")) return; try { await fetch("/api/okx/credentials", { method: "DELETE", credentials: "include" }); emitToast({ type: "info", msg: "Clés OKX supprimées" }); await loadStatus(); } catch (e) { emitToast({ type: "error", msg: e.message }); } } if (status?.connected) { return (
Connecté · {status.is_demo ? "DEMO" : "LIVE"} {!status.is_valid && "(à re-tester)"}
{status.api_key_masked}
); } return (

Crée une API Key OKX avec permissions Read + Trade uniquement (PAS Withdraw). Configure une whitelist IP côté OKX pour sécurité maximale.

{[ { key:"k", label:"API Key", v: apiKey, set: setApiKey, type:"text" }, { key:"s", label:"Secret Key", v: secret, set: setSecret, type:"password" }, { key:"p", label:"Passphrase", v: pass, set: setPass, type:"password" }, ].map(f => ( ))} {testRes && (
{testRes.ok ? `✓ Connexion validée${testRes.usdc != null ? ` · USDC : ${testRes.usdc.toFixed(2)}$` : ""}` : `✗ ${testRes.msg}`}
)}
); } /* ─── Command Palette ⌘K ─────────────────────────────────────────────────── */ function CommandPalette({ onClose, onRoute }) { const [q, setQ] = useStateApp(""); const [idx, setIdx] = useStateApp(0); const inputRef = React.useRef(null); const store = window.useStore ? window.useStore() : {}; useEffectApp(() => { inputRef.current && inputRef.current.focus(); }, []); // Build searchable items const items = React.useMemo(() => { const pages = ["dashboard","signaux","volatile","trading","charts","journal","stats","settings"].map(p => ({ kind:"page", label: p.charAt(0).toUpperCase()+p.slice(1), sub:"Page · navigation", action:()=>{ onRoute(p); onClose(); } })); const cryptos = ["BTC","ETH","SOL","XAU","DOGE","XRP","ADA","SUI","LTC"].map(s => ({ kind:"crypto", label:`${s}/USDT`, sub:`Chart · ${s}`, action:()=>{ onRoute("charts"); onClose(); } })); const actions = [ { kind:"action", label:"Lancer un scan", sub:"Force un scan immédiat", action:()=>{ window.fbScan().then(()=>emitToast({ type:"success", msg:"Scan terminé" })); onClose(); } }, { kind:"action", label:"Refresh complet", sub:"Recharge toutes les données", action:()=>{ window.fbRefresh(); onClose(); } }, { kind:"action", label:"Toggle Paper trading", sub:"Bascule paper / live", action:()=>{ window.fbToggle("paper"); onClose(); } }, { kind:"action", label:"Toggle Auto-trade", sub:"Active/désactive l'auto", action:()=>{ window.fbToggle("auto"); onClose(); } }, { kind:"action", label:"🛑 Emergency Stop", sub:"Stop auto + ferme tous les réels", action:()=>{ if(confirm("Confirmer ?")) window.fbEmergencyStop(); onClose(); } }, ]; return [...pages, ...cryptos, ...actions]; }, []); const filtered = React.useMemo(() => { if (!q) return items; const lower = q.toLowerCase(); return items.filter(it => it.label.toLowerCase().includes(lower) || it.sub.toLowerCase().includes(lower)); }, [q, items]); React.useEffect(() => { setIdx(0); }, [q]); function onKey(e) { if (e.key === "Escape") { onClose(); } else if (e.key === "ArrowDown") { setIdx(i => Math.min(i+1, filtered.length-1)); e.preventDefault(); } else if (e.key === "ArrowUp") { setIdx(i => Math.max(i-1, 0)); e.preventDefault(); } else if (e.key === "Enter") { filtered[idx] && filtered[idx].action(); e.preventDefault(); } } const grouped = filtered.reduce((acc, it) => { (acc[it.kind] = acc[it.kind] || []).push(it); return acc; }, {}); const groupOrder = ["page","crypto","action"]; const groupLabels = { page:"PAGES", crypto:"CRYPTOS", action:"ACTIONS" }; let runningIdx = -1; return (
e.stopPropagation()} onKeyDown={onKey} style={{ background:"var(--bg-elevated)", border:"1px solid var(--border-base)", borderRadius:"var(--radius-lg)", width:"min(540px, 92vw)", overflow:"hidden", boxShadow:"var(--shadow-lg)", }}>
setQ(e.target.value)} placeholder="Rechercher cryptos, pages, actions…" style={{ flex:1, background:"transparent", border:"none", outline:"none", color:"var(--text-primary)", fontSize:14, fontFamily:"var(--font-sans)" }}/> ESC
{filtered.length === 0 ? (
Aucun résultat
) : groupOrder.map(g => grouped[g] && (
{groupLabels[g]}
{grouped[g].map(it => { runningIdx++; const active = runningIdx === idx; return (
setIdx(runningIdx)} style={{ display:"flex", alignItems:"center", justifyContent:"space-between", padding:"8px 18px", cursor:"pointer", background: active ? "var(--accent-soft)" : "transparent", borderLeft: active ? "2px solid var(--accent-500)" : "2px solid transparent", }}>
{it.label} {it.sub}
{active && }
); })}
))}
↑↓ naviguer valider esc fermer
); } window.StatsScreen = StatsScreen; window.SettingsScreen = SettingsScreen; /* ─── AuthScreen : login + signup quand non authentifié ─── */ function AuthScreen() { const [mode, setMode] = useStateApp("login"); // 'login' | 'signup' const [email, setEmail] = useStateApp(""); const [pwd, setPwd] = useStateApp(""); const [name, setName] = useStateApp(""); const [loading, setLoading] = useStateApp(false); const [err, setErr] = useStateApp(""); async function submit(e) { e.preventDefault(); setErr(""); setLoading(true); try { const fn = mode === "signup" ? window.fbSignup : window.fbLogin; const res = mode === "signup" ? await fn(email, pwd, name) : await fn(email, pwd); if (!res.ok) setErr(res.msg || "Erreur"); } catch (e) { setErr(e.message); } setLoading(false); } const isMobile = window.matchMedia && window.matchMedia("(max-width: 880px)").matches; return (

FoxBot Pro

{mode === "signup" ? "CRÉATION DE COMPTE" : "CONNEXION"}
{mode === "signup" && ( )} {err && (
{err}
)}
{mode === "login" && isWebauthnSupported() && ( )}
{mode === "login" ? "Pas de compte ?" : "Déjà inscrit ?"}{" "} {e.preventDefault(); setMode(mode==="login"?"signup":"login"); setErr("");}} style={{ color:"var(--accent-400)", cursor:"pointer", textDecoration:"none", fontWeight:500 }}> {mode === "login" ? "Créer un compte" : "Se connecter"}
); } const inputStyle = { background:"var(--bg-base)", color:"var(--text-primary)", border:"1px solid var(--border-base)", borderRadius:8, padding:"10px 12px", fontSize:14, outline:"none", fontFamily:"var(--font-sans)", }; /* ─── Root : bascule entre App (desktop) et MobileApp selon largeur écran ─── */ function Root() { // Re-render sur changement STORE (sinon les écrans figés) const store = window.useStore ? window.useStore() : window.MOCK; const auth = store.AUTH || { authenticated: false, checking: true }; const isMobile = window.useIsMobile ? window.useIsMobile() : false; const mode = store.MODE || "PAPER"; // Si non authentifié ET pas de KEY URL → écran de login if (!auth.authenticated && !window.KEY) { if (auth.checking) { return
Chargement…
; } return ; } // Tweaks compat pour le shell mobile (qui attend tweaks.mode + setTweak) const tweaks = { mode, autoTrade: !!store.STATUS?.auto_trade }; function setTweak(patch) { if (patch.mode !== undefined) window.fbToggle("paper"); // toggle paper côté serveur } async function handleAction(kind, signal) { const isPaper = kind.startsWith("paper"); const dir = kind.endsWith("long") ? "LONG" : "SHORT"; try { const res = await window.fbAction(isPaper ? "paper" : "real", dir, signal.fullPair || (signal.pair + "/USDT:USDT")); const isReal = !isPaper; if (window.HAPTIC) (res.ok ? window.HAPTIC.success : window.HAPTIC.err)(); emitToast({ type: res.ok ? (isReal ? "success" : "info") : "error", msg: res.ok ? `${isPaper ? "Paper trade" : "Position réelle"} · ${signal.pair} ${dir}` : `Erreur · ${res.msg || "ouverture impossible"}`, sub: res.ok ? `Entry ${fmtNum(signal.entry)} · SL ${fmtNum(signal.sl)} · TP ${fmtNum(signal.tp1)}` : null, }); } catch (e) { emitToast({ type: "error", msg: `Erreur · ${e.message}` }); } } if (isMobile && window.MobileApp) { return ; } return ; } ReactDOM.createRoot(document.getElementById("root")).render();