]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Refractor + modif
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Sat, 28 Feb 2026 19:17:44 +0000 (20:17 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Sat, 28 Feb 2026 19:17:44 +0000 (20:17 +0100)
Wallette/mobile/src/config/api.ts
Wallette/mobile/src/config/env.ts
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/services/api/alertsApi.ts
Wallette/mobile/src/services/api/dashboardApi.ts
Wallette/mobile/src/services/api/http.ts
Wallette/mobile/src/services/api/priceApi.ts
Wallette/mobile/src/services/api/walletApi.ts
Wallette/mobile/src/services/socketService.ts
Wallette/mobile/src/types/Alert.ts

index 17e74187940eaed12b19e7c7c2263b1ca6786ddc..15bff3d1edbcae05fd2ee3f3eaa21cf8836c2abd 100644 (file)
@@ -1,32 +1,31 @@
 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
index bd7d115acbbed4115e63df5e5cf9787ae19b14dc..15bff3d1edbcae05fd2ee3f3eaa21cf8836c2abd 100644 (file)
@@ -3,38 +3,29 @@ import { Platform } from "react-native";
 /**
  * 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
index 6dde816b63693620b9b7c8f02bc84404b9d870b8..b0aab8d95fbbe622d6e158c17347aa7155bbf4e2 100644 (file)
@@ -43,11 +43,9 @@ const CRYPTOS: CryptoSymbol[] = ["BTC", "ETH", "LTC"];
 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);
@@ -73,19 +71,15 @@ export default function DashboardScreen() {
 
   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");
@@ -118,11 +112,9 @@ export default function DashboardScreen() {
     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);
@@ -130,18 +122,14 @@ export default function DashboardScreen() {
       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();
@@ -149,18 +137,17 @@ export default function DashboardScreen() {
         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
       }
     }
   }, []);
@@ -182,7 +169,7 @@ export default function DashboardScreen() {
     }, [refreshAll])
   );
 
-  // Socket live (non bloquant)
+  // ✅ FIX string|null : on connecte socket seulement si uid existe
   useEffect(() => {
     let unsub: null | (() => void) = null;
     let alive = true;
@@ -192,7 +179,7 @@ export default function DashboardScreen() {
       setSocketConnected(false);
 
       const session = await loadSession();
-      const uid = session?.userId;
+      const uid = session?.userId ?? null;
 
       if (!uid) {
         setSocketInfo("Socket désactivé : session absente.");
@@ -275,9 +262,8 @@ export default function DashboardScreen() {
 
     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;
@@ -290,10 +276,7 @@ export default function DashboardScreen() {
       .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);
@@ -317,13 +300,9 @@ export default function DashboardScreen() {
         {!!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}>
@@ -333,7 +312,6 @@ export default function DashboardScreen() {
           {!!socketInfo && <Text style={[ui.muted, { marginTop: 6 }]}>{socketInfo}</Text>}
         </View>
 
-        {/* Crypto + adresse */}
         <View style={ui.card}>
           <Text style={ui.title}>Choisir une cryptomonnaie</Text>
 
@@ -372,51 +350,27 @@ export default function DashboardScreen() {
           {!!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}>
@@ -429,43 +383,30 @@ export default function DashboardScreen() {
           )}
         </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}>
@@ -474,9 +415,7 @@ export default function DashboardScreen() {
                   {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
@@ -512,13 +451,10 @@ export default function DashboardScreen() {
 
 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" },
 
index 8c1cf87dd1884ba11e501f52ba14002638ad4d9a..43abea05d4ec51b2800c9a346656c9f0d93fdd71 100644 (file)
@@ -3,19 +3,6 @@ import { apiGet } from "./http";
 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;
@@ -31,8 +18,7 @@ export async function getAlertHistory(limit = 10): Promise<Alert[]> {
     `/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> {
index 4f4e64c84b977acbc53157098070b7c06cc5697f..f37605fcf5b30ed4c5f032590f76c8a009f28ee5 100644 (file)
 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
index a699996e95578703043d445ede018e71f8c9a3c0..68149033274407b132d23a977eca3586da0b0a20 100644 (file)
@@ -5,7 +5,7 @@ import { API_BASE_URL } from "../../config/env";
  * -------------------
  * 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) {
@@ -23,14 +23,11 @@ function buildUrl(path: string) {
 }
 
 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;
 }
 
index 9fbf20b50dc384f4b4233bc39501a4bd9a5c0209..c91a3780d4f205409f95cb7e4f3b6b0b6eace44b 100644 (file)
@@ -1,23 +1,5 @@
 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;
index dbb11e22b8388d3b464027ef9fb59bd1a7468808..7cf5c7deecb6ee2998fa2f6333b87e2ef5082a31 100644 (file)
@@ -2,41 +2,19 @@ import { apiGet } from "./http";
 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) ||
@@ -45,11 +23,7 @@ function pickWalletId(list: any): string | null {
   );
 }
 
-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) ||
@@ -71,7 +45,6 @@ function extractAssetsFromWalletDetails(details: any): { symbol: string; quantit
       .sort((a, b) => a.symbol.localeCompare(b.symbol));
   }
 
-  // fallback : rien exploitable
   return [];
 }
 
@@ -80,22 +53,13 @@ export async function getPortfolio(): Promise<PortfolioState> {
   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
index 4d93f28078e8ba58f89406b5052086fa939b5dfa..76d9fd78a4ddb44515e80b3f9fa72a01b81c9dd7 100644 (file)
@@ -1,19 +1,6 @@
 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>();
@@ -24,7 +11,6 @@ class SocketService {
     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 &&
@@ -34,65 +20,73 @@ class SocketService {
       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);
+        });
+      }
     });
   }
 
@@ -105,11 +99,6 @@ class SocketService {
     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();
index ac89884886bf8f5fb83a1d19ce1112a059abfb8a..d1b7caabbc100cdf8a91965e17528058892bc6f3 100644 (file)
@@ -1,28 +1,16 @@
-/**
- * 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