/* ═══════════════════════════════════════════════════════════════
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 (
);
}
/* ─── 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 }) => (
);
const Row = ({ label, hint, children }) => (
{label}
{hint && {hint}}
{children}
);
return (
Settings
Configuration FoxBot Pro · {Object.keys(st).length ? "synchronisé" : "chargement..."}
{[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}
- Ouvre Telegram → cherche @{code.bot}
- Envoie :
/link {code.value}
- 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 === "login" && isWebauthnSupported() && (
)}
);
}
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();