import { Platform } from "react-native";
-import { API_BASE_URL, SERVER_URL, ENV_MODE, PLATFORM } from "./env";
/**
- * gatewayUrls.ts (ou apiConfig.ts)
- * -------------------------------
- * Source de vérité = env.ts
+ * env.ts
+ * ------
+ * 1 seul point de config réseau.
*
- * - REST : API_BASE_URL (déjà contient /api)
- * - Socket : SERVER_URL (gateway root, sans /api)
+ * IMPORTANT :
+ * - REST = https://wallette.duckdns.org/api
+ * - Socket.IO = https://wallette.duckdns.org (PAS /api)
*
- * NOTE :
- * - En DEV, env.ts pointe vers ton gateway local (http://IP:3000)
- * - En PROD, env.ts pointe vers duckdns (https://wallette.duckdns.org)
+ * Expo = __DEV__ => on peut forcer PROD pour tester le serveur déployé.
*/
+// ✅ PROD (duckdns)
+const PROD_GATEWAY = "https://wallette.duckdns.org";
+
+// ✅ DEV (pour test Expo sur tel) : force PROD
+// Si tu veux revenir au local plus tard : remets ton IP LAN + http://IP:3000
+const DEV_GATEWAY = "https://wallette.duckdns.org";
+
+export const GATEWAY_BASE_URL = __DEV__ ? DEV_GATEWAY : PROD_GATEWAY;
+
// REST (via gateway)
-export const REST_BASE_URL = API_BASE_URL;
+export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`;
-// Socket.IO (via gateway)
-export const SOCKET_BASE_URL = SERVER_URL;
+// Socket.IO (via gateway) -> RACINE (pas /api)
+export const SERVER_URL = GATEWAY_BASE_URL;
-/**
- * Debug helper : pratique pour afficher dans un screen "About / Debug"
- */
-export const DEBUG_NETWORK_INFO = {
- env: ENV_MODE,
- platform: PLATFORM,
- rest: REST_BASE_URL,
- socket: SOCKET_BASE_URL,
- rnPlatform: Platform.OS,
-};
\ No newline at end of file
+export const ENV_MODE = __DEV__ ? "DEV" : "PROD";
+export const PLATFORM = Platform.OS;
\ No newline at end of file
/**
* env.ts
* ------
- * Objectif : 1 seul point de config réseau.
+ * 1 seul point de config réseau.
*
- * DEV : IP LAN (PC qui fait tourner gateway en local)
- * PROD : URL publique (duckdns / serveur)
+ * IMPORTANT :
+ * - REST = https://wallette.duckdns.org/api
+ * - Socket.IO = https://wallette.duckdns.org (PAS /api)
*
- * Le mobile parle UNIQUEMENT au Gateway :
- * - REST : <GATEWAY>/api/...
- * - Socket : <GATEWAY> (socket.io proxy via gateway)
+ * Expo = __DEV__ => on peut forcer PROD pour tester le serveur déployé.
*/
-// ✅ DEV (chez toi / en classe quand tu lances sur ton PC)
-const DEV_LAN_IP = "192.168.129.121";
-const DEV_GATEWAY = `http://${DEV_LAN_IP}:3000`;
-
// ✅ PROD (duckdns)
const PROD_GATEWAY = "https://wallette.duckdns.org";
-/**
- * Pour l'instant :
- * - en Expo / dev => DEV_GATEWAY
- * - en build (APK/IPA) => PROD_GATEWAY
- */
+// ✅ DEV (pour test Expo sur tel) : force PROD
+// Si tu veux revenir au local plus tard : remets ton IP LAN + http://IP:3000
+const DEV_GATEWAY = "https://wallette.duckdns.org";
+
export const GATEWAY_BASE_URL = __DEV__ ? DEV_GATEWAY : PROD_GATEWAY;
// REST (via gateway)
export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`;
-// Socket.IO (via gateway)
+// Socket.IO (via gateway) -> RACINE (pas /api)
export const SERVER_URL = GATEWAY_BASE_URL;
-/**
- * Helpers (debug)
- */
export const ENV_MODE = __DEV__ ? "DEV" : "PROD";
export const PLATFORM = Platform.OS;
\ No newline at end of file
function walletAddressKey(userId: string) {
return `walletAddress:${userId}`;
}
-
function normalizeSymbol(s: string) {
return s.trim().toUpperCase();
}
-
function mergePortfolios(base: PortfolioState, patch: PortfolioState): PortfolioState {
const map = new Map<string, number>();
for (const a of base.assets) map.set(normalizeSymbol(a.symbol), a.quantity);
const [selectedCrypto, setSelectedCrypto] = useState<CryptoSymbol>("BTC");
- // dashboard summary = signal + prix
const [summary, setSummary] = useState<DashboardSummary | null>(null);
- // wallet address
const [walletAddress, setWalletAddress] = useState("");
const [walletAddressInfo, setWalletAddressInfo] = useState<string | null>(null);
- // alerts (live)
const [liveAlerts, setLiveAlerts] = useState<AlertType[]>([]);
const [socketConnected, setSocketConnected] = useState(false);
const [socketInfo, setSocketInfo] = useState<string | null>(null);
- // modal trade
const [tradeOpen, setTradeOpen] = useState(false);
const [tradeSide, setTradeSide] = useState<TradeSide>("BUY");
const [tradeQty, setTradeQty] = useState("0.01");
const s = await loadSettings();
setSettings(s);
- // local portfolio
const localPortfolio = await loadPortfolio();
setPortfolio(localPortfolio);
- // wallet address
if (uid) {
const addr = (await AsyncStorage.getItem(walletAddressKey(uid))) ?? "";
setWalletAddress(addr);
setWalletAddress("");
}
- // summary (signal+prix) -> API
try {
const dash = await getDashboardSummary();
setSummary(dash);
} catch {
setSummary(null);
- setSoftError(
- `Signal/Prix indisponibles (API). Vérifie si tu es en DEV: ${ENV_MODE}.`
- );
+ setSoftError(`Signal/Prix indisponibles (API). DEV=${ENV_MODE}. Base REST: ${API_BASE_URL}`);
}
- // wallet API (si dispo) -> fusion
if (uid) {
try {
const apiPortfolio = await getPortfolioFromApi();
setPortfolio(merged);
await savePortfolio(merged);
} catch {
- // pas bloquant
+ // non bloquant
}
}
- // alert history REST (si dispo)
if (uid) {
try {
const history = await getAlertHistory(10);
for (const a of history) alertStore.add(a);
setLiveAlerts(history.slice(0, 50));
} catch {
- // pas bloquant
+ // non bloquant
}
}
}, []);
}, [refreshAll])
);
- // Socket live (non bloquant)
+ // ✅ FIX string|null : on connecte socket seulement si uid existe
useEffect(() => {
let unsub: null | (() => void) = null;
let alive = true;
setSocketConnected(false);
const session = await loadSession();
- const uid = session?.userId;
+ const uid = session?.userId ?? null;
if (!uid) {
setSocketInfo("Socket désactivé : session absente.");
let nextQty = currentQty;
- if (tradeSide === "BUY") {
- nextQty = currentQty + qty;
- } else {
+ if (tradeSide === "BUY") nextQty = currentQty + qty;
+ else {
if (qty > currentQty) {
setTradeInfo(`Vente impossible : tu n'as que ${currentQty.toFixed(6)} ${symbol}.`);
return;
.concat(nextQty > 0 ? [{ symbol, quantity: nextQty }] : [])
.sort((a, b) => normalizeSymbol(a.symbol).localeCompare(normalizeSymbol(b.symbol)));
- const nextPortfolio: PortfolioState = {
- assets: nextAssets,
- updatedAtMs: Date.now(),
- };
+ const nextPortfolio: PortfolioState = { assets: nextAssets, updatedAtMs: Date.now() };
await savePortfolio(nextPortfolio);
setPortfolio(nextPortfolio);
{!!softError && (
<View style={[ui.banner, styles.bannerWarn]}>
<Text style={styles.bannerText}>{softError}</Text>
- <Text style={styles.bannerSub}>
- Base REST : {API_BASE_URL}
- </Text>
</View>
)}
- {/* Compte */}
<View style={ui.card}>
<Text style={ui.title}>Compte utilisateur</Text>
<View style={ui.rowBetween}>
{!!socketInfo && <Text style={[ui.muted, { marginTop: 6 }]}>{socketInfo}</Text>}
</View>
- {/* Crypto + adresse */}
<View style={ui.card}>
<Text style={ui.title}>Choisir une cryptomonnaie</Text>
{!!walletAddressInfo && <Text style={[ui.muted, { marginTop: 8 }]}>{walletAddressInfo}</Text>}
</View>
- {/* Solde + Prix */}
<View style={ui.card}>
- <Text style={ui.title}>Solde</Text>
- <Text style={styles.bigValue}>
- {selectedQty.toFixed(6)} {selectedCrypto}
- </Text>
- <Text style={ui.muted}>Portefeuille local (+ sync serveur si dispo)</Text>
- </View>
-
- <View style={ui.card}>
- <View style={ui.rowBetween}>
- <Text style={ui.title}>Prix</Text>
- <Text style={ui.muted}>{pair}</Text>
- </View>
- <Text style={styles.bigValue}>
- {(summary?.price ?? 0).toFixed(2)} {currency}
- </Text>
- <Text style={ui.muted}>
- Maj : {summary?.timestamp ? new Date(summary.timestamp).toLocaleTimeString() : "—"}
- </Text>
-
+ <Text style={ui.title}>Prix</Text>
+ <Text style={ui.muted}>{pair}</Text>
+ <Text style={styles.bigValue}>{(summary?.price ?? 0).toFixed(2)} {currency}</Text>
<TouchableOpacity style={[ui.button, { marginTop: 10 }]} onPress={() => void refreshAll()}>
<Text style={ui.buttonText}>Actualiser</Text>
</TouchableOpacity>
</View>
- {/* Signal */}
<View style={ui.card}>
<Text style={ui.title}>Signal du marché</Text>
-
{summary ? (
<>
<Text style={styles.signalDecision}>{summary.decision}</Text>
- <Text style={ui.muted}>
- {summary.alertLevel} — Confiance {Math.round(summary.confidence * 100)}%
- </Text>
- <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={3}>
- {summary.reason}
- </Text>
+ <Text style={ui.muted}>{summary.alertLevel} — Confiance {Math.round(summary.confidence * 100)}%</Text>
+ <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={3}>{summary.reason}</Text>
</>
) : (
<Text style={ui.muted}>Aucune donnée pour le moment.</Text>
)}
- {/* Bonus : urgence live si dispo */}
{urgentAlert && (
<View style={styles.urgentBox}>
<Text style={styles.urgentTitle}>
)}
</View>
- {/* Stratégie */}
- <TouchableOpacity
- activeOpacity={0.85}
- onPress={() => navigation.navigate("Strategy" as never)}
- >
+ <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Strategy" as never)}>
<View style={ui.card}>
<View style={ui.rowBetween}>
<Text style={ui.title}>Stratégie</Text>
<Text style={ui.muted}>Configurer</Text>
</View>
-
<Text style={[ui.muted, { marginTop: 6 }]}>
Sélection : <Text style={styles.bold}>{settings?.selectedStrategyKey ?? "—"}</Text>
</Text>
</View>
</TouchableOpacity>
- {/* Actions */}
<View style={ui.card}>
<Text style={ui.title}>Actions</Text>
-
<View style={styles.buySellRow}>
<TouchableOpacity style={styles.buyBtn} onPress={() => openTrade("BUY")}>
<Text style={styles.buySellText}>Acheter</Text>
</TouchableOpacity>
-
<TouchableOpacity style={styles.sellBtn} onPress={() => openTrade("SELL")}>
<Text style={styles.buySellText}>Vendre</Text>
</TouchableOpacity>
</View>
-
- <Text style={[ui.muted, { marginTop: 10 }]} numberOfLines={2}>
- Note : Acheter/Vendre = simulation (registre local). Pas de trading réel.
- </Text>
</View>
- {/* Modal BUY/SELL */}
<Modal visible={tradeOpen} transparent animationType="fade" onRequestClose={() => setTradeOpen(false)}>
<View style={styles.modalBackdrop}>
<KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : undefined} style={styles.modalWrap}>
{tradeSide === "BUY" ? "Acheter" : "Vendre"} {selectedCrypto}
</Text>
- <Text style={ui.muted}>
- Prix : {(summary?.price ?? 0).toFixed(2)} {currency}
- </Text>
+ <Text style={ui.muted}>Prix : {(summary?.price ?? 0).toFixed(2)} {currency}</Text>
<Text style={[ui.muted, { marginTop: 12 }]}>Quantité</Text>
<TextInput
const styles = StyleSheet.create({
safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
-
bannerWarn: { borderColor: "#ca8a04" },
bannerText: { color: "#ca8a04", fontWeight: "900" },
- bannerSub: { marginTop: 6, color: "#475569", fontWeight: "700" },
bold: { fontWeight: "900", color: "#0f172a" },
-
bigValue: { marginTop: 10, fontSize: 22, fontWeight: "900", color: "#0f172a" },
signalDecision: { marginTop: 10, fontSize: 26, fontWeight: "900", color: "#0f172a" },
import { loadSession } from "../../utils/sessionStorage";
import { alertStore } from "../alertStore";
-/**
- * alertsApi (aligné serveur déployé)
- * ---------------------------------
- * Route dispo :
- * - GET /api/alerts/events?userId=...&limit=10
- *
- * Réponse :
- * {
- * "ok": true,
- * "data": { "events": [...], "count": 10, "limit": 10 }
- * }
- */
-
type AlertsEventsResponse = {
events?: Alert[];
count?: number;
`/alerts/events?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`
);
- const events = Array.isArray(data?.events) ? data.events : [];
- return events;
+ return Array.isArray(data?.events) ? (data!.events as Alert[]) : [];
}
export async function clearAlertsLocal(): Promise<void> {
import type { DashboardSummary, TradeDecision, AlertLevel } from "../../types/DashboardSummary";
-import { apiGet } from "./http";
+import type { Alert } from "../../types/Alert";
+
import { loadSession } from "../../utils/sessionStorage";
import { loadSettings } from "../../utils/settingsStorage";
-/**
- * dashboardApi (API-only)
- * -----------------------
- * Construit un DashboardSummary via les endpoints gateway.
- *
- * Aligné Web pour le prix :
- * - GET /api/prices/current?pair=BTC/EUR -> { price }
- *
- * Signal :
- * - GET /api/signal/current?userId=...&pair=BTC/EUR
- */
+import { getCurrentPrice } from "./priceApi";
+import { getAlertHistory } from "./alertsApi";
+
+function safeDecision(action: any): TradeDecision {
+ const a = String(action ?? "HOLD").toUpperCase();
+ if (a === "BUY" || a === "SELL" || a === "STOP_LOSS") return a as TradeDecision;
+ return "HOLD";
+}
+
+function safeLevel(level: any): AlertLevel {
+ const l = String(level ?? "INFO").toUpperCase();
+ if (l === "CRITICAL" || l === "WARNING") return l as AlertLevel;
+ return "INFO";
+}
+
export async function getDashboardSummary(): Promise<DashboardSummary> {
const session = await loadSession();
const userId = session?.userId;
if (!userId) throw new Error("Session absente : impossible de charger le dashboard.");
const settings = await loadSettings();
- const currency = settings.currency === "USD" ? "USD" : "EUR";
- const pair = `BTC/${currency}`;
-
- // 1) Prix courant (aligné Web)
- const priceRaw = await apiGet<any>(`/prices/current?pair=${encodeURIComponent(pair)}`);
+ const currency: "EUR" | "USD" = settings.currency === "USD" ? "USD" : "EUR";
- const price =
- (typeof priceRaw?.price === "number" ? priceRaw.price : null) ??
- (typeof priceRaw?.current_price === "number" ? priceRaw.current_price : null) ??
- (typeof priceRaw?.data?.price === "number" ? priceRaw.data.price : null) ??
- (typeof priceRaw?.data?.current_price === "number" ? priceRaw.data.current_price : null);
-
- if (typeof price !== "number" || !Number.isFinite(price)) {
- throw new Error("Prix invalide (API).");
- }
-
- const tsPrice =
- (typeof priceRaw?.timestampMs === "number" ? priceRaw.timestampMs : null) ??
- (typeof priceRaw?.timestamp_ms === "number" ? priceRaw.timestamp_ms : null) ??
- Date.now();
-
- // 2) Signal courant (API)
- const signalRaw = await apiGet<any>(
- `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`
- );
-
- // action (défaut HOLD)
- const actionStr = String(
- signalRaw?.action ??
- signalRaw?.data?.action ??
- "HOLD"
- ).toUpperCase();
-
- const decision: TradeDecision =
- actionStr === "BUY" || actionStr === "SELL" || actionStr === "STOP_LOSS"
- ? (actionStr as TradeDecision)
- : "HOLD";
-
- // alertLevel (défaut INFO)
- const lvlStr = String(
- signalRaw?.alertLevel ??
- signalRaw?.criticality ??
- signalRaw?.data?.alertLevel ??
- signalRaw?.data?.criticality ??
- "INFO"
- ).toUpperCase();
+ const pair = `BTC/${currency}`;
- const alertLevel: AlertLevel =
- lvlStr === "CRITICAL" || lvlStr === "WARNING"
- ? (lvlStr as AlertLevel)
- : "INFO";
+ const price = await getCurrentPrice(pair);
- const confidence =
- typeof signalRaw?.confidence === "number"
- ? signalRaw.confidence
- : typeof signalRaw?.data?.confidence === "number"
- ? signalRaw.data.confidence
- : 0;
+ let decision: TradeDecision = "HOLD";
+ let alertLevel: AlertLevel = "INFO";
+ let confidence = 0;
+ let reason = "Aucune alerte récente.";
+ let timestamp = price.timestampMs;
- const reason = String(
- signalRaw?.reason ??
- signalRaw?.message ??
- signalRaw?.data?.reason ??
- signalRaw?.data?.message ??
- "—"
- );
+ try {
+ const events = await getAlertHistory(10);
+ const last: Alert | undefined = events[0];
- const tsSignal =
- (typeof signalRaw?.timestamp === "number" ? signalRaw.timestamp : null) ??
- (typeof signalRaw?.timestamp_ms === "number" ? signalRaw.timestamp_ms : null) ??
- (typeof signalRaw?.data?.timestamp === "number" ? signalRaw.data.timestamp : null) ??
- (typeof signalRaw?.data?.timestamp_ms === "number" ? signalRaw.data.timestamp_ms : null) ??
- Number(tsPrice);
+ if (last) {
+ decision = safeDecision(last.action);
+ alertLevel = safeLevel(last.alertLevel);
+ confidence = typeof last.confidence === "number" ? last.confidence : 0;
+ reason = String(last.reason ?? last.message ?? "—");
+ timestamp = typeof last.timestamp === "number" ? last.timestamp : timestamp;
+ }
+ } catch {
+ // non bloquant
+ }
return {
pair,
- price: Number(price),
+ price: price.price,
strategy: settings.selectedStrategyKey,
decision,
- confidence: Number(confidence),
+ confidence,
reason,
alertLevel,
- timestamp: Number(tsSignal),
+ timestamp,
};
}
\ No newline at end of file
* -------------------
* Compatible avec 2 formats :
* A) "wrap" : { ok:true, data: ... } / { ok:false, error:{message} }
- * B) "raw" : { price: ... } / { balance: ... } / Alert[]
+ * B) "raw" : { ... } / Alert[] etc.
*/
async function parseJsonSafe(res: Response) {
}
function unwrapOrRaw<T>(json: any): T {
- // Format "wrap"
if (json && typeof json === "object" && "ok" in json) {
if (json.ok === true) return json.data as T;
const msg = json?.error?.message ?? "Réponse API invalide (ok=false)";
throw new Error(msg);
}
-
- // Format "raw"
return json as T;
}
import { apiGet } from "./http";
-/**
- * priceApi (aligné serveur déployé)
- * ---------------------------------
- * Route dispo :
- * - GET /api/price/current?pair=BTC/EUR
- *
- * Réponse :
- * {
- * "ok": true,
- * "data": {
- * "pair": "BTC/EUR",
- * "current_price": 42150.23,
- * "timestamp_ms": 1700000000000,
- * "source": "..."
- * }
- * }
- */
-
export type PriceCurrent = {
pair: string;
timestampMs: number;
import { loadSession } from "../../utils/sessionStorage";
import type { PortfolioState } from "../../models/Portfolio";
-/**
- * walletApi (aligné serveur déployé)
- * ---------------------------------
- * Routes dispo :
- * - GET /api/wallets?userId=...
- * - GET /api/wallets/:walletId
- * - GET /api/wallets/:walletId/events
- *
- * Objectif côté mobile (pour l’instant) :
- * - Récupérer un portefeuille (assets + quantités) si possible.
- *
- * Si le backend ne fournit pas (encore) un format multi-assets clair,
- * on renvoie un portefeuille vide plutôt que casser l’app.
- */
-
-type WalletListItem = {
- id?: string;
- walletId?: string;
- _id?: string;
-};
-
-type WalletListResponse =
- | { wallets?: WalletListItem[] }
- | { items?: WalletListItem[] }
- | WalletListItem[]
- | any;
-
-type WalletDetailsResponse = any;
+type WalletListItem = { id?: string; walletId?: string; _id?: string };
+type WalletListResponse = any;
function pickWalletId(list: any): string | null {
- // cas 1: data = array
- const arr = Array.isArray(list) ? list : Array.isArray(list?.wallets) ? list.wallets : Array.isArray(list?.items) ? list.items : null;
- if (!arr || arr.length === 0) return null;
+ const arr =
+ Array.isArray(list) ? list :
+ Array.isArray(list?.wallets) ? list.wallets :
+ Array.isArray(list?.items) ? list.items :
+ null;
+ if (!arr || arr.length === 0) return null;
const first = arr[0];
+
return (
(typeof first?.walletId === "string" && first.walletId) ||
(typeof first?.id === "string" && first.id) ||
);
}
-function extractAssetsFromWalletDetails(details: any): { symbol: string; quantity: number }[] {
- // On tente plusieurs formats possibles :
- // - details.portfolio.assets
- // - details.assets
- // - details.balances { BTC: 0.1, ETH: 2 }
+function extractAssets(details: any): { symbol: string; quantity: number }[] {
const assetsArr =
(Array.isArray(details?.portfolio?.assets) && details.portfolio.assets) ||
(Array.isArray(details?.assets) && details.assets) ||
.sort((a, b) => a.symbol.localeCompare(b.symbol));
}
- // fallback : rien exploitable
return [];
}
const userId = session?.userId;
if (!userId) throw new Error("Session absente : impossible de charger le portefeuille.");
- // 1) récupérer la liste des wallets de l’utilisateur
const list = await apiGet<WalletListResponse>(`/wallets?userId=${encodeURIComponent(userId)}`);
const walletId = pickWalletId(list);
- if (!walletId) {
- // Aucun wallet côté serveur : on renvoie vide (l’app reste stable)
- return { assets: [], updatedAtMs: Date.now() };
- }
-
- // 2) récupérer le wallet détail
- const details = await apiGet<WalletDetailsResponse>(`/wallets/${encodeURIComponent(walletId)}`);
+ if (!walletId) return { assets: [], updatedAtMs: Date.now() };
- const assets = extractAssetsFromWalletDetails(details);
+ const details = await apiGet<any>(`/wallets/${encodeURIComponent(walletId)}`);
+ const assets = extractAssets(details);
- return {
- assets,
- updatedAtMs: Date.now(),
- };
+ return { assets, updatedAtMs: Date.now() };
}
\ No newline at end of file
import { io, Socket } from "socket.io-client";
import type { Alert } from "../types/Alert";
-/**
- * socketService.ts
- * ----------------
- * Objectif :
- * - Connexion Socket.IO via le Gateway (duckdns ou local)
- * - Auth par event "auth" (payload = userId)
- * - Réception des alertes via event "alert"
- *
- * Important :
- * - Le Gateway doit proxy /socket.io/* vers alerts-service.
- * - En prod (https), socket.io bascule en wss automatiquement.
- */
-
class SocketService {
private socket: Socket | null = null;
private listeners = new Set<(alert: Alert) => void>();
if (!serverUrl) throw new Error("serverUrl is required");
if (!userId) throw new Error("userId is required");
- // Si on est déjà connecté au même serveur avec le même userId -> rien à faire
if (
this.socket &&
this.socket.connected &&
return;
}
- // Si on change de serveur ou de userId, on repart proprement
- if (this.socket) {
- this.disconnect();
- }
+ if (this.socket) this.disconnect();
this.currentServerUrl = serverUrl;
this.currentUserId = userId;
- this.socket = io(serverUrl, {
- // Très important en environnement proxy
- path: "/socket.io",
+ const attachHandlers = (sock: Socket) => {
+ const emitAuth = () => sock.emit("auth", userId);
- // Mobile : websocket + fallback polling
- transports: ["websocket", "polling"],
+ sock.on("connect", () => {
+ console.log("✅ Socket connecté:", sock.id);
+ emitAuth();
+ });
- reconnection: true,
- reconnectionAttempts: 10,
- reconnectionDelay: 800,
- reconnectionDelayMax: 3000,
- timeout: 10000,
- });
-
- const emitAuth = () => {
- if (!this.socket || !this.currentUserId) return;
- this.socket.emit("auth", this.currentUserId);
- };
+ sock.on("auth_success", (data: any) => {
+ console.log("✅ Auth success:", data?.message ?? data);
+ });
- this.socket.on("connect", () => {
- console.log("✅ Socket connecté:", this.socket?.id);
- emitAuth();
- });
+ sock.on("alert", (alert: Alert) => {
+ for (const cb of this.listeners) cb(alert);
+ });
- // Si le serveur supporte le message
- this.socket.on("auth_success", (data: any) => {
- console.log("✅ Auth success:", data?.message ?? data);
- });
+ sock.on("disconnect", (reason: string) => {
+ console.log("⚠️ Socket disconnect:", reason);
+ });
- // Alertes live
- this.socket.on("alert", (alert: Alert) => {
- for (const cb of this.listeners) cb(alert);
- });
-
- // Debug utile
- this.socket.on("connect_error", (err: any) => {
- console.log("❌ Socket connect_error:", err?.message ?? err);
- });
-
- this.socket.on("error", (err: any) => {
- console.log("❌ Socket error:", err?.message ?? err);
- });
+ sock.on("error", (err: any) => {
+ console.log("❌ Socket error:", err?.message ?? err);
+ });
+ };
- this.socket.on("reconnect", (attempt: number) => {
- console.log("🔁 Socket reconnect:", attempt);
- // Après reconnexion, on renvoie auth (certains backends oublient la session socket)
- emitAuth();
+ // tentative websocket + polling
+ const sock = io(serverUrl, {
+ path: "/socket.io",
+ transports: ["websocket", "polling"],
+ reconnection: true,
+ reconnectionAttempts: 10,
+ timeout: 10000,
});
- this.socket.on("disconnect", (reason: string) => {
- console.log("⚠️ Socket disconnect:", reason);
+ this.socket = sock;
+ attachHandlers(sock);
+
+ sock.on("connect_error", (err: any) => {
+ const msg = String(err?.message ?? err);
+ console.log("❌ Socket connect_error:", msg);
+
+ // fallback polling-only si websocket échoue
+ if (msg.toLowerCase().includes("websocket")) {
+ console.log("↩️ Fallback: polling-only");
+ this.disconnect();
+
+ const sockPolling = io(serverUrl, {
+ path: "/socket.io",
+ transports: ["polling"],
+ upgrade: false,
+ reconnection: true,
+ reconnectionAttempts: 10,
+ timeout: 10000,
+ });
+
+ this.socket = sockPolling;
+ attachHandlers(sockPolling);
+
+ sockPolling.on("connect_error", (e: any) => {
+ console.log("❌ Socket polling connect_error:", e?.message ?? e);
+ });
+ }
});
}
this.socket?.emit("ping_alerts");
}
- onPong(callback: (data: any) => void) {
- this.socket?.on("pong_alerts", callback);
- return () => this.socket?.off("pong_alerts", callback);
- }
-
disconnect() {
if (!this.socket) return;
this.socket.removeAllListeners();
-/**
- * Alert.ts
- * --------
- * Format d'alerte utilisé par :
- * - Socket.IO (event "alert")
- * - REST history : GET /api/alerts/history?userId=...
- *
- * Le Web utilise parfois `alert.message`.
- * Donc on rend ce champ optionnel (pour compatibilité).
- */
-
export type AlertLevel = "CRITICAL" | "WARNING" | "INFO";
export type AlertAction = "BUY" | "SELL" | "HOLD" | "STOP_LOSS";
export interface Alert {
- action?: AlertAction; // BUY / SELL / HOLD / STOP_LOSS
- pair?: string; // ex: BTC/EUR
- confidence?: number; // 0..1
- reason?: string; // texte explicatif
+ action?: AlertAction;
+ pair?: string;
+ confidence?: number;
+ reason?: string;
- // ✅ Compatibilité Web (script.js affiche alert.message si présent)
+ // compat web
message?: string;
- alertLevel?: AlertLevel; // CRITICAL / WARNING / INFO
- timestamp?: number; // ms
-
- price?: number; // prix au moment de l’alerte
+ alertLevel?: AlertLevel;
+ timestamp?: number;
+ price?: number;
}
\ No newline at end of file