/* ═══════════════════════════════════════════════════════════════ FOXBOT PRO — MOBILE SCREENS (Trading, Signaux, Charts, More) Phase 2 — implémentés côté coding (cohérent avec mobile-shell/dashboard) ═══════════════════════════════════════════════════════════════ */ const { useState: useStateS, useEffect: useEffectS, useRef: useRefS, useMemo: useMemoS } = React; /* ───────────────────────────────────────────────────────────────── MobileSignaux — liste compacte de signaux ───────────────────────────────────────────────────────────────── */ function MobileSignaux({ onAction }) { const store = window.useStore ? window.useStore() : (window.MOCK || {}); const SIGNALS = Array.isArray(store.SIGNALS) ? store.SIGNALS : []; const [filter, setFilter] = useStateS("all"); const [scanning, setScanning] = useStateS(false); const [expanded, setExpanded] = useStateS(null); const filtered = SIGNALS.filter(s => { if (filter === "all") return true; if (filter === "long") return s.direction === "LONG"; if (filter === "short") return s.direction === "SHORT"; if (filter === "premium") return s.score >= 8; return true; }); async function doScan() { setScanning(true); window.HAPTIC && window.HAPTIC.toggle(); try { await window.fbScan(); } catch {} window.HAPTIC && window.HAPTIC.success(); setScanning(false); emitToast({ type: "success", msg: "Scan terminé" }); } return (
{[ { id:"all", l:"Tous", n: SIGNALS.length }, { id:"long", l:"LONG", n: SIGNALS.filter(s=>s.direction==="LONG").length }, { id:"short", l:"SHORT", n: SIGNALS.filter(s=>s.direction==="SHORT").length }, { id:"premium", l:"≥ 8/10", n: SIGNALS.filter(s=>s.score>=8).length }, ].map(c => (
{ window.HAPTIC&&window.HAPTIC.tap(); setFilter(c.id); }}> {c.l} {c.n}
))}
{filtered.length === 0 ? ( ) : (
{filtered.map(s => ( { window.HAPTIC&&window.HAPTIC.tap(); setExpanded(expanded === s.id ? null : s.id); }} onAction={onAction} /> ))}
)}
); } function MSignalExpandable({ signal, expanded, onToggle, onAction }) { const up = signal.direction === "LONG"; return (
{signal.pair}/USDT
=0?"var(--bull)":"var(--bear)", fontFamily:"var(--font-mono)" }}>{fmtPct(signal.chg24h)}
{expanded && (
{/* Plan de trade compact */}
Entrée{fmtNum(signal.entry)}
SL{fmtNum(signal.sl)}
TP1{fmtNum(signal.tp1)}
TP2{fmtNum(signal.tp2)}
R/R net{signal.rr}
{/* Pills risque/gain */}
Perte max−€{(signal.riskEur||0).toFixed(2)}
Gain TP1+€{(signal.gainEur||0).toFixed(2)}
Gain TP2+€{(signal.gainTp2Eur||0).toFixed(2)}
{/* Tech (compact) + Risque */}
RSI {(signal.rsi||0).toFixed(1)} VOL {Math.round(signal.volume||0)}% ATR {(signal.atr||0).toFixed(2)}% MACD {signal.macd?.dir==="up"?"▲":"▼"}
{/* Actions 2x2 */}
)}
); } /* ───────────────────────────────────────────────────────────────── MobileTrading — cockpit positions ───────────────────────────────────────────────────────────────── */ function MobileTrading({ onAction }) { const store = window.useStore ? window.useStore() : (window.MOCK || {}); const positions = Array.isArray(store.POSITIONS) ? store.POSITIONS : []; const [filter, setFilter] = useStateS("all"); const filtered = positions.filter(p => { if (filter === "all") return true; if (filter === "paper") return !p.isReal; if (filter === "real") return p.isReal; return true; }); const totalPnl = positions.reduce((s, p) => s + (p.pnlEur || 0), 0); return (
P&L OUVERT
=0?"bull":"bear"}`}>{totalPnl>=0?"+":""}{totalPnl.toFixed(2)}€
POSITIONS
{positions.length}
{[ { id:"all", l:"Toutes", n: positions.length }, { id:"paper", l:"PAPER", n: positions.filter(p=>!p.isReal).length }, { id:"real", l:"RÉEL", n: positions.filter(p=> p.isReal).length }, ].map(c => (
{ window.HAPTIC&&window.HAPTIC.tap(); setFilter(c.id); }}> {c.l} {c.n}
))}
{filtered.length === 0 ? ( window.location.hash="#/signaux"}/> ) : (
{filtered.map(p => )}
)}
); } function MTradingCard({ p }) { const animPnl = useCountUp(p.pnlEur); const animPct = useCountUp(p.pnlPct); const up = p.pnlEur >= 0; const store = window.useStore ? window.useStore() : {}; const livePx = (store.PRICES && store.PRICES[p.pair]) || p.current; const [now, setNow] = useStateS(Date.now()); useEffectS(() => { const t = setInterval(()=>setNow(Date.now()), 1000); return ()=>clearInterval(t); }, []); const duration = p.raw?.entry_ts_ms ? (now - p.raw.entry_ts_ms) : 0; const isLong = p.direction === "LONG"; const sparkData = useMemoS(() => { const out = [], start = p.entry, end = livePx || p.current; for (let i = 0; i < 24; i++) { const t = i / 23; const wobble = Math.sin(i * 0.8 + (p.id||"x").charCodeAt(0)) * (Math.abs(end - start) * 0.4); out.push(start + (end - start) * t + wobble); } return out; }, [p.entry, livePx, p.id]); async function closeAll() { if (!confirm(`Fermer ${p.pair} ${p.direction} ?`)) return; window.HAPTIC && window.HAPTIC.warn(); await window.fbClosePos(p); emitToast({ type:"success", msg:`Position ${p.pair} fermée` }); } async function closeHalf() { if (!p.isReal) { emitToast({ type:"warn", msg:"50% dispo en RÉEL uniquement" }); return; } if (!confirm(`Fermer 50% de ${p.pair} ?`)) return; window.HAPTIC && window.HAPTIC.toggle(); const K = new URLSearchParams(location.search).get("key"); try { await fetch(`/api/close_real_partial/${p.raw.instId}/50`, { method:"POST", credentials:"include" }); emitToast({ type:"success", msg:"50% fermé" }); } catch (e) { emitToast({ type:"error", msg:e.message }); } } async function breakEven() { if (p.isReal) { emitToast({ type:"warn", msg:"BE dispo en PAPER uniquement" }); return; } window.HAPTIC && window.HAPTIC.toggle(); const K = new URLSearchParams(location.search).get("key"); try { const j = await fetch(`/api/move_sl_be/${p.id}`, { method:"POST", credentials:"include" }).then(r=>r.json()); emitToast({ type: j.ok?"success":"error", msg: j.ok?"SL → BE" : (j.msg||"erreur") }); } catch (e) { emitToast({ type:"error", msg:e.message }); } } const slPct = p.sl ? ((p.sl - livePx) / livePx * 100) : 0; const tp1Pct = p.tp1 ? ((p.tp1 - livePx) / livePx * 100) : 0; return (
{p.pair}/USDT
×{p.lev} {p.isReal?"RÉEL":"PAPER"} {fmtDur(duration)}
{up?"+":"−"}€{Math.abs(animPnl).toFixed(2)} {fmtPct(animPct)}
Prix{fmtNum(livePx)}
Distance SL{slPct>=0?"+":""}{slPct.toFixed(2)}%
Distance TP1{tp1Pct>=0?"+":""}{tp1Pct.toFixed(2)}%
); } function MiniLiveSpark2({ data, up }) { const path = useMemoS(() => { if (!data || data.length < 2) return ""; const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const w = 300, h = 70; return data.map((v, i) => { const x = (i / (data.length - 1)) * w; const y = h - 3 - ((v - min) / range) * (h - 6); return (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1); }).join(" "); }, [data]); const c = up ? "#10B981" : "#EF4444"; const id = useMemoS(() => "mlsp-" + Math.random().toString(36).slice(2, 8), []); return ( ); } /* ───────────────────────────────────────────────────────────────── MobileCharts — chart plein écran TradingView ───────────────────────────────────────────────────────────────── */ const M_TF_BAR = { M1:"1m", M5:"5m", M15:"15m", H1:"1H", H4:"4H", D1:"1D" }; function MobileCharts() { const containerRef = useRefS(null); const candleSeriesRef = useRefS(null); const [pair, setPair] = useStateS("BTC"); const [tf, setTf] = useStateS("M15"); const [candles, setCandles] = useStateS([]); const [info, setInfo] = useStateS({ price: 0, chg: 0 }); const KEY = new URLSearchParams(location.search).get("key"); const store = window.useStore ? window.useStore() : {}; const livePx = store.PRICES && store.PRICES[pair]; const fullPair = pair + "/USDT:USDT"; useEffectS(() => { const bar = M_TF_BAR[tf] || "15m"; fetch(`/api/candles?pair=${encodeURIComponent(fullPair)}&bar=${bar}&limit=200`, { credentials: "include" }) .then(r => r.json()) .then(j => { const arr = (j.candles || []).map(c => ({ time: Math.floor(c.t / 1000), open: c.open, high: c.high, low: c.low, close: c.close })); setCandles(arr); if (arr.length > 0) { const last = arr[arr.length-1], first = arr[0]; setInfo({ price: last.close, chg: ((last.close - first.open) / first.open) * 100 }); } }).catch(()=>{}); }, [pair, tf]); useEffectS(() => { if (!containerRef.current || !window.LightweightCharts || candles.length < 2) return; const { createChart } = window.LightweightCharts; const chart = createChart(containerRef.current, { width: containerRef.current.clientWidth, height: containerRef.current.clientHeight, layout: { background:{ type:"solid", color:"transparent" }, textColor:"#6B678F", fontFamily:"Geist Mono, monospace" }, grid: { vertLines:{ color:"#1F1F3A" }, horzLines:{ color:"#1F1F3A" } }, crosshair: { mode:0 }, rightPriceScale: { borderColor:"#2A2A4D", textColor:"#6B678F" }, timeScale: { borderColor:"#2A2A4D", timeVisible:true, secondsVisible:false }, }); const cs = chart.addCandlestickSeries({ upColor:"#10B981", downColor:"#EF4444", wickUpColor:"#10B981", wickDownColor:"#EF4444", borderVisible: false, }); cs.setData(candles); candleSeriesRef.current = cs; chart.timeScale().fitContent(); const ro = new ResizeObserver(() => { if (containerRef.current) chart.applyOptions({ width: containerRef.current.clientWidth, height: containerRef.current.clientHeight }); }); ro.observe(containerRef.current); return () => { ro.disconnect(); candleSeriesRef.current = null; chart.remove(); }; }, [candles]); // Live tick useEffectS(() => { if (!livePx || !candleSeriesRef.current || candles.length === 0) return; const last = candles[candles.length - 1]; candleSeriesRef.current.update({ time: last.time, open: last.open, high: Math.max(last.high, livePx), low: Math.min(last.low, livePx), close: livePx, }); setInfo(prev => candles.length > 0 ? { ...prev, price: livePx, chg: ((livePx - candles[0].open) / candles[0].open) * 100 } : prev); }, [livePx]); const up = info.chg >= 0; const pairs = ["BTC","ETH","SOL","XAU","DOGE","XRP","ADA","SUI","LTC"]; return (
{pair}/USDT
OKX · {M_TF_BAR[tf]}
{fmtNum(info.price)} {fmtPct(info.chg)}
{pairs.map(p => (
{ window.HAPTIC&&window.HAPTIC.tap(); setPair(p); }}> {p}
))}
{Object.keys(M_TF_BAR).map(t => (
{ window.HAPTIC&&window.HAPTIC.tap(); setTf(t); }}>{t}
))}
); } /* ───────────────────────────────────────────────────────────────── MobileMore — container Journal / Stats / Settings / Volatile ───────────────────────────────────────────────────────────────── */ function MobileMore({ subpage, onAction }) { if (subpage === "journal") return ; if (subpage === "stats") return ; if (subpage === "settings") return ; if (subpage === "volatile") return ; return ; } function MMoreJournal() { const store = window.useStore ? window.useStore() : (window.MOCK || {}); const JOURNAL = Array.isArray(store.JOURNAL) ? store.JOURNAL : []; const wins = JOURNAL.filter(j => j && j.result === "win").length; const losses = JOURNAL.filter(j => j && j.result === "loss").length; const totalPnl = JOURNAL.reduce((acc, j) => acc + (Number(j && j.pnlEur) || 0), 0); const wr = (wins + losses) > 0 ? (wins / (wins + losses) * 100) : 0; return (
P&L PÉRIODE=0?"bull":"bear"}`}>{totalPnl>=0?"+":""}{totalPnl.toFixed(2)}€
WIN RATE{wr.toFixed(0)}%{wins}W · {losses}L

Historique

{JOURNAL.length === 0 ? ( ) : (
{JOURNAL.map(j => j && )}
)}
); } function MJournalRow({ entry }) { if (!entry) return null; const isOpen = entry.result === "open"; const isWin = entry.result === "win"; const pnlEur = Number(entry.pnlEur) || 0; const color = isOpen ? "var(--info)" : isWin ? "var(--bull)" : "var(--bear)"; return (
{entry.pair}/USDT {isOpen?"OPEN":isWin?"WIN":"LOSS"}
{entry.date || "—"} · {entry.duration || "—"}
=0?"bull":"bear"}`}>{pnlEur>=0?"+":"−"}€{Math.abs(pnlEur).toFixed(2)}
); } function MMoreStats() { const store = window.useStore ? window.useStore() : {}; const s = store.DBSTATS || {}; return (
SIGNAUX 24H{s.signals_24h ?? "—"}
TRADES 7J{s.trades_7d ?? "—"}
WIN RATE{s.winrate_pct != null ? s.winrate_pct.toFixed(1)+"%" : "—"}
PnL TOTAL=0?"bull":"bear"}`}>{s.pnl_total != null ? (s.pnl_total>=0?"+":"")+s.pnl_total.toFixed(2)+"€" : "—"}
WINS / LOSSES{(s.wins ?? 0)} / {(s.losses ?? 0)}
DRAWDOWN MAX{s.drawdown_max != null ? s.drawdown_max.toFixed(2)+"%" : "—"}
); } function MMoreSettings() { const store = window.useStore ? window.useStore() : {}; const st = store.STATUS || {}; const scanMode = st.scan_mode || "NORMAL"; return (

Scanner

Mode
{["SAFE","NORMAL","AVANCE"].map(m => ( ))}

Trading

Levier
{[5,10,0].map(v => ( ))}
Paper trading
Trades simulés
{ window.HAPTIC&&window.HAPTIC.toggle(); window.fbToggle("paper"); }}>
Auto-trade
Bot ouvre seul
{ window.HAPTIC&&window.HAPTIC.toggle(); window.fbToggle("auto"); }}>

Statut

USDC réel{(st.usdc||0).toFixed(2)}$
Trades aujourd'hui{st.trades_today||0} / {st.max_trades||5}
Daily P&L=0?"bull":"bear"}`}>{(st.daily_pnl||0)>=0?"+":""}{(st.daily_pnl||0).toFixed(2)}€

🔗 Connexion OKX

🔔 Telegram

🆔 Face ID / Touch ID

👤 Compte

Email
{store.AUTH?.user?.email || "—"}
); } /* ─── Mobile OKX section ─── */ function MOkxSection() { const [status, setStatus] = useStateS(null); const [show, setShow] = useStateS(false); const [apiKey, setApiKey] = useStateS(""); const [secret, setSecret] = useStateS(""); const [pass, setPass] = useStateS(""); const [isDemo, setIsDemo] = useStateS(false); const [busy, setBusy] = useStateS(false); async function load() { try { setStatus(await fetch("/api/okx/credentials/status", { credentials:"include" }).then(r=>r.json())); } catch {} } useEffectS(() => { load(); }, []); async function save() { setBusy(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é" }); setApiKey(""); setSecret(""); setPass(""); setShow(false); load(); } else emitToast({ type:"error", msg: j.msg || "Erreur" }); } catch(e) { emitToast({ type:"error", msg:e.message }); } setBusy(false); } async function del() { if (!confirm("Supprimer tes clés OKX ?")) return; await fetch("/api/okx/credentials", { method:"DELETE", credentials:"include" }); load(); } if (status?.connected) { return (
{status.is_demo?"DEMO":"LIVE"} · {status.api_key_masked}
); } if (!show) { return
Clés OKX
Non configurées
; } return (
setApiKey(e.target.value)} style={mInput}/> setSecret(e.target.value)} style={mInput}/> setPass(e.target.value)} style={mInput}/>
); } /* ─── Mobile Telegram section ─── */ function MTelegramSection() { const [status, setStatus] = useStateS(null); const [code, setCode] = useStateS(null); const [cd, setCd] = useStateS(0); async function load() { try { setStatus(await fetch("/api/telegram/link/status", { credentials:"include" }).then(r=>r.json())); } catch {} } useEffectS(() => { load(); }, []); useEffectS(() => { if (!code) return; setCd(600); const t = setInterval(() => setCd(c => Math.max(0, c-1)), 1000); const poll = setInterval(load, 3000); return () => { clearInterval(t); clearInterval(poll); }; }, [code]); useEffectS(() => { if (status?.linked && code) { setCode(null); emitToast({type:"success",msg:"Telegram lié !"}); } }, [status?.linked]); useEffectS(() => { if (cd === 0 && code) setCode(null); }, [cd, code]); async function gen() { 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 }); } async function unlink() { if (!confirm("Délier Telegram ?")) return; await fetch("/api/telegram/link", { method:"DELETE", credentials:"include" }); load(); } if (status?.linked) { return
Lié{status.username?` à @${status.username}`:""}
{status.notifications_enabled?"Notifs ON":"Pause"}
; } if (code) { const mm = String(Math.floor(cd/60)).padStart(2,"0"), ss = String(cd%60).padStart(2,"0"); return
EXPIRE DANS {mm}:{ss}
{code.value}
  1. Telegram → @{code.bot}
  2. Envoie : /link {code.value}
; } return
Non lié
Notifs trading
; } /* ─── Mobile WebAuthn section ─── */ function MWebauthnSection() { const [creds, setCreds] = useStateS([]); const isHttps = window.location.protocol === "https:" || window.location.hostname === "localhost"; const isSupp = typeof window.PublicKeyCredential !== "undefined"; async function load() { try { setCreds((await fetch("/api/webauthn/credentials", { credentials:"include" }).then(r=>r.json())).credentials || []); } catch {} } useEffectS(() => { load(); }, []); async function enroll() { if (!isHttps) { emitToast({ type:"warn", msg:"Face ID nécessite HTTPS" }); return; } try { const nick = prompt("Nom de cet appareil ?", "iPhone Face ID") || "Device"; const j = await window.fbWebauthnRegister(nick); if (j.ok) { emitToast({ type:"success", msg:"Biométrie OK" }); load(); } else emitToast({ type:"error", msg: j.msg }); } catch(e) { emitToast({ type:"error", msg:e.message }); } } async function remove(id) { if (!confirm("Supprimer ?")) return; await fetch(`/api/webauthn/credentials/${id}`, { method:"DELETE", credentials:"include" }); load(); } return
{!isHttps && ⚠️ Requiert HTTPS} {!isSupp && ❌ Non supporté par ce navigateur} {creds.length === 0 ? Aucun appareil : creds.map(c =>
{c.nickname}
)}
; } const mInput = { 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-mono)", width:"100%", }; function MMoreVolatile({ onAction }) { const store = window.useStore ? window.useStore() : {}; const VOLATILE = Array.isArray(store.VOLATILE) ? store.VOLATILE : []; const [expanded, setExpanded] = useStateS(null); const [filter, setFilter] = useStateS("all"); const [scanning, setScanning] = useStateS(false); const filtered = VOLATILE.filter(s => { if (filter === "all") return true; if (filter === "hot") return s.hot; if (filter === "long") return s.direction === "LONG"; if (filter === "short") return s.direction === "SHORT"; return true; }); async function doScan() { setScanning(true); window.HAPTIC && window.HAPTIC.toggle(); try { await window.fbScan(); } catch {} window.HAPTIC && window.HAPTIC.success(); setScanning(false); emitToast({ type:"success", msg:"Scan volatile terminé" }); } const atrAvg = VOLATILE.length ? (VOLATILE.reduce((s,v)=>s+(v.atr||0),0)/VOLATILE.length) : 0; return (
ATR MOYEN
2 ? "var(--bear)" : atrAvg>1 ? "var(--warn)" : "var(--bull)" }}>{atrAvg.toFixed(2)}%
PAIRES
{VOLATILE.length}
{[ { id:"all", l:"Toutes", n: VOLATILE.length }, { id:"hot", l:"🔥 HOT", n: VOLATILE.filter(s=>s.hot).length }, { id:"long", l:"LONG", n: VOLATILE.filter(s=>s.direction==="LONG").length }, { id:"short", l:"SHORT", n: VOLATILE.filter(s=>s.direction==="SHORT").length }, ].map(c => (
{ window.HAPTIC&&window.HAPTIC.tap(); setFilter(c.id); }}> {c.l} {c.n}
))}
{filtered.length === 0 ? ( ) : (
{filtered.map(s => ( { window.HAPTIC&&window.HAPTIC.tap(); setExpanded(expanded === s.id ? null : s.id); }} onAction={onAction} /> ))}
)}
); } function MVolatileExpandable({ signal, expanded, onToggle, onAction }) { const isWait = signal.state === "wait" || !signal.entry; return (
{signal.pair}/USDT {signal.hot && 🔥 HOT}
=0?"var(--bull)":"var(--bear)", fontFamily:"var(--font-mono)" }}>{fmtPct(signal.chg24h)} 2 ? "var(--bear)" : signal.atr>1 ? "var(--warn)" : "var(--text-muted)", fontFamily:"var(--font-mono)" }}>ATR {signal.atr?.toFixed(2)}%
{expanded && (
{isWait ? (
WAIT — {signal.waitReason || "filtres pas tous validés, scénario en cours"}
) : null}
Entrée{fmtNum(signal.entry)}
SL{fmtNum(signal.sl)}
TP1{fmtNum(signal.tp1)}
TP2{fmtNum(signal.tp2)}
R/R net{signal.rr}
Perte max−€{(signal.riskEur||0).toFixed(2)}
Gain TP1+€{(signal.gainEur||0).toFixed(2)}
Gain TP2+€{(signal.gainTp2Eur||0).toFixed(2)}
RSI {(signal.rsi||0).toFixed(1)} VOL {Math.round(signal.volume||0)}% ATR 2?"bear":signal.atr>1?"warn":""}>{(signal.atr||0).toFixed(2)}% MACD {signal.macd?.dir==="up"?"▲":"▼"}
{/* Actions toujours visibles : même en WAIT, l'user peut trader manuel (Entry/SL/TP restent valides). */} {signal.entry > 0 && (
)}
)}
); } /* ───────────────────────────────────────────────────────────────── Helpers partagés ───────────────────────────────────────────────────────────────── */ function MEmpty({ icon: I, title, desc, cta, onCta }) { return (

{title}

{desc}

{cta && }
); } function fmtDur(ms) { if (!ms || ms <= 0) return "—"; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; if (s < 3600) return `${Math.floor(s/60)}m`; if (s < 86400) return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`; return `${Math.floor(s/86400)}j`; } Object.assign(window, { MobileSignaux, MobileTrading, MobileCharts, MobileMore });