/* ═══════════════════════════════════════════════════════════════
FOXBOT PRO — MOBILE DASHBOARD
Hero P&L · KPI 2x2 · Positions snap · Signaux · Activité
═══════════════════════════════════════════════════════════════ */
const { useState: useStateMD, useEffect: useEffectMD, useRef: useRefMD, useMemo: useMemoMD } = React;
function MobileDashboard({ onAction, onRoute }) {
const { KPI, POSITIONS, ACTIVITY, EQUITY, SIGNALS } = window.MOCK;
const top3 = useMemoMD(() =>
[...SIGNALS].filter(s => s.state === "active").sort((a, b) => b.score - a.score).slice(0, 3),
[SIGNALS]
);
return (
{/* ─── Hero P&L ─── */}
p.v)}/>
{/* ─── KPI Grid 2x2 ─── */}
{KPI.winRate.val.toFixed(1)}%>}
foot={
<>
{KPI.winRate.wins}W·{KPI.winRate.losses}L
>
}
/>
{KPI.trades.d}/ jour>}
foot={
7J {KPI.trades.w} · 30J {KPI.trades.m}
}
sparkline={makeWaveSpark(KPI.trades.w)}
/>
{fmtPct(KPI.drawdown.current)}>}
foot={
<>
max {fmtPct(KPI.drawdown.max)}
>
}
/>
{POSITIONS.length}actives>}
foot={
Exposition €2 420
}
stack={POSITIONS.slice(0, 3).map(p => p.pair)}
/>
{/* ─── Positions actives (horizontal snap) ─── */}
{POSITIONS.length > 0 && (
Positions {POSITIONS.length}
)}
{/* ─── Top opportunités ─── */}
Opportunités
{top3.map(s => )}
{/* ─── Activité ─── */}
Activité
1H
{ACTIVITY.slice(0, 5).map((a, i) => )}
);
}
/* ─── Hero P&L card ─── */
function MHero({ kpi, positions, sparkData }) {
const animated = useCountUp(kpi.pnlTotal.eur, 1100);
const animPct = useCountUp(kpi.pnlTotal.pct, 1100);
const up = kpi.pnlTotal.eur >= 0;
const sparkPath = useMemoMD(() => {
if (!sparkData || sparkData.length < 2) return "";
const min = Math.min(...sparkData), max = Math.max(...sparkData);
const range = max - min || 1;
const w = 380, h = 60;
return sparkData.map((v, i) => {
const x = (i / (sparkData.length - 1)) * w;
const y = h - 4 - ((v - min) / range) * (h - 8);
return (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
}).join(" ");
}, [sparkData]);
return (
P&L Total · 30J
LIVE
€
{up ? "+" : "−"}{Math.abs(animated).toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{up ? : }
{fmtPct(animPct)}
{positions} positions
{/* background sparkline */}
);
}
/* ─── KPI tile ─── */
function MKpi({ label, value, foot, sparkline, stack }) {
return (
{label}
{value}
{sparkline && (
)}
{stack && (
{stack.map((s, i) => (
{s.slice(0, 1)}
))}
)}
{foot}
);
}
/* ─── Solde live (USDC réel ou capital paper selon mode) ─── */
function MBalancePill() {
const store = window.useStore ? window.useStore() : (window.MOCK || {});
const st = store.STATUS || {};
const mode = store.MODE || "PAPER";
const isLive = mode === "LIVE";
const value = isLive ? Number(st.usdc || 0) : Number(st.capital || 200);
const animVal = useCountUp(value, 600, isLive ? 2 : 0);
return (
{isLive ? "Solde" : "Capital"}
{isLive
? "$" + animVal.toFixed(2)
: "€" + Math.round(animVal)}
);
}
function makeWaveSpark(seed) {
const pts = [];
for (let i = 0; i < 12; i++) {
pts.push(8 + Math.sin(i * 0.7 + seed * 0.1) * 4 + Math.cos(i * 0.4) * 2);
}
return pts.map((y, i) => (i === 0 ? "M" : "L") + (i * 10) + "," + y.toFixed(1)).join(" ");
}
/* ─── Positions horizontal scroll ─── */
function MPositionsScroll({ positions }) {
const [active, setActive] = useStateMD(0);
const ref = useRefMD(null);
useEffectMD(() => {
const el = ref.current; if (!el) return;
const onScroll = () => {
const cards = el.querySelectorAll(".m-pos-card");
const center = el.scrollLeft + el.clientWidth / 2;
let best = 0, bestDist = Infinity;
cards.forEach((c, i) => {
const cx = c.offsetLeft + c.offsetWidth / 2;
const d = Math.abs(cx - center);
if (d < bestDist) { bestDist = d; best = i; }
});
setActive(best);
};
el.addEventListener("scroll", onScroll, { passive: true });
return () => el.removeEventListener("scroll", onScroll);
}, []);
return (
{positions.map(p => )}
{positions.length > 1 && (
{positions.map((_, i) => )}
)}
);
}
function MPositionCard({ p }) {
const animPnl = useCountUp(p.pnlEur);
const animPct = useCountUp(p.pnlPct);
const up = p.pnlEur >= 0;
const sparkData = useMemoMD(() => {
// synthesize a short live shape from entry → current
const out = [];
const start = p.entry, end = p.current;
for (let i = 0; i < 24; i++) {
const t = i / 23;
const wobble = Math.sin(i * 0.8 + p.id.charCodeAt(1)) * (Math.abs(end - start) * 0.4);
out.push(start + (end - start) * t + wobble);
}
return out;
}, [p.entry, p.current, p.id]);
return (
{p.pair}/USDT
{p.direction} · ouvert {p.opened}
×{p.lev}
{up ? "+" : "−"}€{Math.abs(animPnl).toFixed(2)}
{fmtPct(animPct)}
);
}
function MiniLiveSpark({ data, up }) {
const path = useMemoMD(() => {
if (!data || data.length < 2) return "";
const min = Math.min(...data), max = Math.max(...data);
const range = max - min || 1;
const w = 300, h = 56;
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 = useMemoMD(() => "msp-" + Math.random().toString(36).slice(2, 8), []);
return (
);
}
/* ─── Compact signal row ─── */
function MSignalRow({ signal, onAction }) {
const up = signal.direction === "LONG";
return (
{ HAPTIC.tap(); onAction && onAction("preview", signal); }}>
{signal.pair}/USDT
{signal.note}
{fmtPct(signal.chg24h)}
);
}
/* ─── Activity row ─── */
function MActivityRow({ item }) {
const iconMap = {
"trend-up": Icon.TrendUp,
"trend-down": Icon.TrendDown,
"zap": Icon.Zap,
"shield": Icon.Shield,
"x": Icon.X,
};
const Ic = iconMap[item.icon] || Icon.Dot;
return (
);
}
Object.assign(window, { MobileDashboard });