]> git.digitality.be Git - pdw25-26/commitdiff
Mobile: dashboard modularisation + setting + socket client integration (client only)
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Mon, 23 Feb 2026 16:20:44 +0000 (17:20 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Mon, 23 Feb 2026 16:20:44 +0000 (17:20 +0100)
mobile-ui/package-lock.json
mobile-ui/package.json
mobile-ui/src/components/ActionsCard.tsx
mobile-ui/src/components/WalletCard.tsx
mobile-ui/src/config/env.ts [new file with mode: 0644]
mobile-ui/src/models/UserSettings.ts
mobile-ui/src/screens/DashboardScreen.tsx
mobile-ui/src/services/socketService.ts [new file with mode: 0644]
mobile-ui/src/types/Alert.ts [new file with mode: 0644]

index 29d59918d560b3e43e83d5ab0bbf6523b595c5b7..c917a217048275f79bdcf54b0c324fca41f47fe0 100644 (file)
@@ -16,7 +16,8 @@
         "react": "19.1.0",
         "react-native": "0.81.5",
         "react-native-safe-area-context": "~5.6.0",
-        "react-native-screens": "~4.16.0"
+        "react-native-screens": "~4.16.0",
+        "socket.io-client": "^4.8.3"
       },
       "devDependencies": {
         "@types/react": "~19.1.0",
         "@sinonjs/commons": "^3.0.0"
       }
     },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+      "license": "MIT"
+    },
     "node_modules/@types/babel__core": {
       "version": "7.20.5",
       "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
         "node": ">= 0.8"
       }
     },
+    "node_modules/engine.io-client": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
+      "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.18.3",
+        "xmlhttprequest-ssl": "~2.1.1"
+      }
+    },
+    "node_modules/engine.io-client/node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/env-editor": {
       "version": "0.4.2",
       "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
       "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
       "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=0.10.0"
       }
         "node": ">=8.0.0"
       }
     },
+    "node_modules/socket.io-client": {
+      "version": "4.8.3",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
+      "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1",
+        "engine.io-client": "~6.6.1",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
+      "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/source-map": {
       "version": "0.5.7",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
         "node": ">=8.0"
       }
     },
+    "node_modules/xmlhttprequest-ssl": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+      "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
index e7af7ea6784a14ae7ffb5b7e903cfc729137d780..b229db2bbc2c78fa80483ada364e5374d8e76306 100644 (file)
@@ -17,7 +17,8 @@
     "react": "19.1.0",
     "react-native": "0.81.5",
     "react-native-safe-area-context": "~5.6.0",
-    "react-native-screens": "~4.16.0"
+    "react-native-screens": "~4.16.0",
+    "socket.io-client": "^4.8.3"
   },
   "devDependencies": {
     "@types/react": "~19.1.0",
index f8fe47abb43d216b0376ba78c85030698daed958..eadcfeec796f9e43f471596451cfdf4b1f04537c 100644 (file)
@@ -1,60 +1,37 @@
-import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
+import { View, Text, TouchableOpacity } from "react-native";
+import { ui } from "./ui/uiStyles";
 
 type Props = {
   onGoSettings: () => void;
 };
 
+/**
+ * ActionsCard
+ * -----------
+ * Regroupe les actions rapides de l'utilisateur.
+ * Pour l'instant :
+ * - Voir stratégie (placeholder)
+ * - Historique (placeholder)
+ * - Paramètres (navigation réelle via callback)
+ */
 export default function ActionsCard({ onGoSettings }: Props) {
   return (
-    <View style={styles.card}>
-      <Text style={styles.sectionTitle}>Actions</Text>
+    <View style={ui.card}>
+      <Text style={ui.title}>Actions</Text>
 
-      <View style={styles.row}>
-        <TouchableOpacity style={styles.button}>
-          <Text style={styles.buttonText}>Voir stratégie</Text>
+      <View style={[ui.rowBetween, { flexWrap: "wrap", gap: 8 }]}>
+        <TouchableOpacity style={ui.button} onPress={() => {}}>
+          <Text style={ui.buttonText}>Voir stratégie</Text>
         </TouchableOpacity>
 
-        <TouchableOpacity style={styles.button}>
-          <Text style={styles.buttonText}>Historique</Text>
+        <TouchableOpacity style={ui.button} onPress={() => {}}>
+          <Text style={ui.buttonText}>Historique</Text>
         </TouchableOpacity>
 
-        <TouchableOpacity style={styles.button} onPress={onGoSettings}>
-          <Text style={styles.buttonText}>Paramètres</Text>
+        <TouchableOpacity style={ui.button} onPress={onGoSettings}>
+          <Text style={ui.buttonText}>Paramètres</Text>
         </TouchableOpacity>
       </View>
     </View>
   );
-}
-
-const styles = StyleSheet.create({
-  card: {
-    borderWidth: 1,
-    borderColor: "#aaa",
-    padding: 12,
-    marginBottom: 12,
-    borderRadius: 6,
-  },
-  sectionTitle: {
-    fontWeight: "bold",
-    marginBottom: 6,
-    fontSize: 16,
-  },
-  row: {
-    flexDirection: "row",
-    flexWrap: "wrap",
-    gap: 8,
-  },
-  button: {
-    backgroundColor: "#555",
-    paddingHorizontal: 10,
-    paddingVertical: 10,
-    borderRadius: 4,
-    flexGrow: 1,
-    flexBasis: "48%",
-    alignItems: "center",
-  },
-  buttonText: {
-    color: "#fff",
-    fontWeight: "bold",
-  },
-});
\ No newline at end of file
+}
\ No newline at end of file
index 4fbcb88391be686ba91092d6cab479a92c95dcf6..52e5162e53d5ea7389a68c759edcc8e6be7827c2 100644 (file)
@@ -1,55 +1,39 @@
-import { View, Text, StyleSheet } from "react-native";
+import { View, Text } from "react-native";
 import type { UserSettings } from "../models/UserSettings";
+import { ui } from "./ui/uiStyles";
 
 type Props = {
   settings: UserSettings;
 };
 
+/**
+ * WalletCard
+ * ----------
+ * Step 1 : placeholder portefeuille.
+ * Pour l'instant on affiche une valeur totale fixe (mock),
+ * mais on utilise déjà la devise des settings.
+ *
+ * Plus tard (Step 3) :
+ * - portefeuille multi-cryptos
+ * - valeur globale calculée
+ */
 export default function WalletCard({ settings }: Props) {
+  const totalValue = 10_000; // mock (à remplacer plus tard)
+
   return (
-    <View style={styles.card}>
-      <Text style={styles.sectionTitle}>Portefeuille</Text>
+    <View style={ui.card}>
+      <Text style={ui.title}>Portefeuille</Text>
 
-      <View style={styles.priceRow}>
-        <Text style={styles.value}>Valeur totale</Text>
-        <Text style={styles.valueBold}>10 000 {settings.currency}</Text>
+      <View style={ui.rowBetween}>
+        <Text style={ui.value}>Valeur totale</Text>
+        <Text style={ui.valueBold}>
+          {totalValue.toLocaleString("fr-BE")} {settings.currency}
+        </Text>
       </View>
 
-      <Text style={styles.updated}>Step 1 : mono-utilisateur / mono-crypto</Text>
+      <Text style={[ui.muted, { marginTop: 10, textAlign: "center" }]}>
+        Step 1 : mono-utilisateur / mono-crypto
+      </Text>
     </View>
   );
-}
-
-const styles = StyleSheet.create({
-  card: {
-    borderWidth: 1,
-    borderColor: "#aaa",
-    padding: 12,
-    marginBottom: 12,
-    borderRadius: 6,
-  },
-  sectionTitle: {
-    fontWeight: "bold",
-    marginBottom: 6,
-    fontSize: 16,
-  },
-  priceRow: {
-    flexDirection: "row",
-    justifyContent: "space-between",
-    alignItems: "center",
-    marginVertical: 4,
-  },
-  value: {
-    fontSize: 16,
-  },
-  valueBold: {
-    fontSize: 16,
-    fontWeight: "bold",
-  },
-  updated: {
-    fontSize: 12,
-    opacity: 0.6,
-    marginTop: 6,
-    textAlign: "center",
-  },
-});
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/mobile-ui/src/config/env.ts b/mobile-ui/src/config/env.ts
new file mode 100644 (file)
index 0000000..799b6a2
--- /dev/null
@@ -0,0 +1 @@
+export const SERVER_URL = "http://192.168.129.121:3000";
\ No newline at end of file
index c8e552eb303d31b7963c72785f5dbc83e6f0dc0b..fd54d3dee0f0a3288bb95913125efec5f0844ae0 100644 (file)
@@ -10,6 +10,7 @@ export type RefreshMode =
   | "auto";
 
 export interface UserSettings {
+  userId?: string; // ✅ pour Socket.IO (optionnel, Step 4 sera l'auth)
   currency: Currency;
   favoriteSymbol: string;
   alertPreference: AlertPreference;
index 5fe177bebdbc65bc530feb5b4d1d577f0a7713ae..168536f299170f1ce46575392f67b9f851a983c2 100644 (file)
@@ -1,53 +1,58 @@
-import {
-  View,
-  Text,
-  StyleSheet,
-  ScrollView,
-  TouchableOpacity,
-} from "react-native";
+import { View, Text, StyleSheet, ScrollView } from "react-native";
 import { useState, useCallback, useEffect } from "react";
 import { SafeAreaView } from "react-native-safe-area-context";
 import { useFocusEffect, useNavigation } from "@react-navigation/native";
 
-import type {
-  DashboardSummary,
-  TradeDecision,
-  AlertLevel,
-} from "../types/DashboardSummary";
+import type { DashboardSummary } from "../types/DashboardSummary";
 import { fetchDashboardSummary } from "../services/dashboardService";
 import { loadSettings } from "../utils/settingsStorage";
 import type { UserSettings } from "../models/UserSettings";
 
+import MarketCard from "../components/MarketCard";
+import StrategyCard from "../components/StrategyCard";
+import WalletCard from "../components/WalletCard";
+import ActionsCard from "../components/ActionsCard";
+import { ui } from "../components/ui/uiStyles";
+
+// ✅ Socket.IO
+import { socketService } from "../services/socketService";
+import { SERVER_URL } from "../config/env";
+import type { Alert } from "../types/Alert";
+
 /**
  * DashboardScreen
- * --------------
- * Écran principal mobile (Step 1).
- * Rôle : afficher des données (consommateur API) :
- * - Marché (prix)
- * - Signal (BUY/SELL/HOLD/STOP_LOSS) + niveau d'alerte (CRITICAL/WARNING/INFO)
- * - Portefeuille (placeholder Step 1)
- * - Actions rapides (dont accès aux Paramètres)
+ * ----------------
+ * Écran principal mobile.
+ *
+ * Responsabilités :
+ * - Charger les données (mock/API REST)
+ * - Charger les paramètres utilisateur
+ * - Gérer le refresh automatique (settings.refreshMode)
+ * - Gérer la navigation
+ * - Recevoir les alertes temps réel via Socket.IO (non bloquant)
  *
- * Spécificité :
- * - On recharge les données quand l'écran reprend le focus (retour depuis Settings).
- * - Si refreshMode === "auto", on rafraîchit le dashboard périodiquement.
+ * L'affichage "business" est délégué aux composants (Cards).
  */
 export default function DashboardScreen() {
-  // Données du dashboard (mock pour l'instant)
   const [summary, setSummary] = useState<DashboardSummary | null>(null);
-
-  // Paramètres utilisateur (AsyncStorage)
   const [settings, setSettings] = useState<UserSettings | null>(null);
 
-  // États UI (loading / erreur)
   const [loading, setLoading] = useState<boolean>(true);
+
+  // erreurs REST/refresh
   const [error, setError] = useState<string | null>(null);
 
-  // Navigation (Stack)
+  // état Socket.IO (non bloquant)
+  const [socketConnected, setSocketConnected] = useState<boolean>(false);
+  const [socketError, setSocketError] = useState<string | null>(null);
+
+  // alertes reçues (optionnel mais utile)
+  const [liveAlerts, setLiveAlerts] = useState<Alert[]>([]);
+
   const navigation = useNavigation();
 
   /**
-   * Charge dashboard + settings à chaque focus sur l'écran.
+   * Chargement initial + rechargement à chaque focus
    * (utile quand on revient des Paramètres)
    */
   useFocusEffect(
@@ -68,7 +73,7 @@ export default function DashboardScreen() {
             setSummary(dashboardData);
             setSettings(userSettings);
           }
-        } catch (e) {
+        } catch {
           if (isActive) {
             setError("Impossible de charger le dashboard.");
           }
@@ -88,15 +93,10 @@ export default function DashboardScreen() {
   );
 
   /**
-   * Rafraîchissement automatique si l'utilisateur l'a activé.
-   * On ne relance pas le loading global (pour éviter que l'écran "clignote"),
-   * on met juste à jour le summary.
+   * Refresh automatique si activé
    */
   useEffect(() => {
-    // Pas de settings => impossible de savoir le mode
     if (!settings) return;
-
-    // Mode manuel => pas d'interval
     if (settings.refreshMode !== "auto") return;
 
     let cancelled = false;
@@ -106,11 +106,9 @@ export default function DashboardScreen() {
         const data = await fetchDashboardSummary();
         if (!cancelled) setSummary(data);
       } catch {
-        // On évite de spammer l'utilisateur : on peut juste logguer
-        // ou afficher une erreur discrète.
         if (!cancelled) setError("Erreur lors du rafraîchissement automatique.");
       }
-    }, 5000); // toutes les 5 secondes
+    }, 5000);
 
     return () => {
       cancelled = true;
@@ -118,157 +116,107 @@ export default function DashboardScreen() {
     };
   }, [settings]);
 
-  // UI : Chargement
+  /**
+   * Socket.IO : connexion / écoute des alertes
+   * - Non bloquant : si socket KO, l'app continue via REST
+   * - Se déclenche quand on a settings (souvent nécessaire pour userId)
+   *
+   * IMPORTANT :
+   * 👉 Version propre : on n'utilise PAS @ts-expect-error.
+   * 👉 On lit un userId optionnel (si présent), sinon on désactive le socket
+   *    et on affiche un message clair.
+   */
+  useEffect(() => {
+    if (!settings) return;
+
+    setSocketError(null);
+
+    // ✅ Proposition propre : un champ optionnel dans UserSettings : userId?: string
+    // Si pas dispo => on ne connecte pas (pas de fake "user-123" en prod)
+    const userId = settings.userId;
+
+    if (!userId) {
+      setSocketConnected(false);
+      setSocketError("Socket désactivé : userId absent (pas connecté).");
+      return;
+    }
+
+    try {
+      socketService.connect(SERVER_URL, userId);
+      setSocketConnected(true);
+    } catch {
+      setSocketConnected(false);
+      setSocketError("Socket: impossible de se connecter (URL/userId).");
+      return;
+    }
+
+    const unsubscribeAlert = socketService.onAlert((alert) => {
+      setLiveAlerts((prev) => [alert, ...prev].slice(0, 50));
+    });
+
+    // ping optionnel pour debug
+    socketService.ping();
+
+    return () => {
+      unsubscribeAlert();
+      socketService.disconnect();
+      setSocketConnected(false);
+    };
+  }, [settings]);
+
+  // Chargement
   if (loading) {
     return (
-      <View style={styles.container}>
+      <View style={styles.centered}>
         <Text>Chargement du dashboard…</Text>
       </View>
     );
   }
 
-  // UI : Erreur (erreur "bloquante")
+  // Erreur bloquante
   if (error && (!summary || !settings)) {
     return (
-      <View style={styles.container}>
+      <View style={styles.centered}>
         <Text style={styles.errorText}>{error}</Text>
       </View>
     );
   }
 
-  // Fallback sécurité
+  // Sécurité
   if (!summary || !settings) {
     return (
-      <View style={styles.container}>
+      <View style={styles.centered}>
         <Text>Initialisation…</Text>
       </View>
     );
   }
 
-  /**
-   * Helpers d'affichage (couleurs) pour respecter le contrat mobile
-   */
-  const getDecisionColor = (decision: TradeDecision) => {
-    switch (decision) {
-      case "BUY":
-        return "#16a34a";
-      case "SELL":
-        return "#dc2626";
-      case "STOP_LOSS":
-        return "#991b1b";
-      case "HOLD":
-      default:
-        return "#ca8a04";
-    }
-  };
-
-  const getAlertColor = (level: AlertLevel) => {
-    switch (level) {
-      case "CRITICAL":
-        return "#b91c1c";
-      case "WARNING":
-        return "#ca8a04";
-      case "INFO":
-      default:
-        return "#2563eb";
-    }
-  };
-
   return (
     <SafeAreaView style={styles.safeArea}>
       <ScrollView contentContainerStyle={styles.container}>
-        {/* Petit warning non bloquant si l'auto-refresh a eu un souci */}
+        {/* Warning REST/refresh (non bloquant) */}
         {error && (
-          <View style={styles.warningBanner}>
+          <View style={styles.warningCard}>
             <Text style={styles.warningText}>{error}</Text>
           </View>
         )}
 
-        {/* Carte Marché */}
-        <View style={styles.card}>
-          <Text style={styles.sectionTitle}>Marché</Text>
-
-          <Text style={styles.price}>{summary.pair}</Text>
-
-          <View style={styles.priceRow}>
-            <Text style={styles.value}>Prix actuel</Text>
-            <Text style={styles.valueBold}>
-              {summary.price.toFixed(2)} {settings.currency}
-            </Text>
-          </View>
-
-          <Text style={styles.updated}>
-            Dernière mise à jour : {new Date(summary.timestamp).toLocaleString()}
-          </Text>
-        </View>
-
-        {/* Carte Stratégie + Signal */}
-        <View style={styles.card}>
-          <Text style={styles.sectionTitle}>Stratégie</Text>
-
-          <Text style={styles.valueBold}>{summary.strategy}</Text>
-
-          <Text
-            style={[
-              styles.decision,
-              { color: getDecisionColor(summary.decision) },
-            ]}
-          >
-            {summary.decision}
+        {/* Socket status (non bloquant) */}
+        <View style={styles.socketCard}>
+          <Text style={styles.socketTitle}>
+            Socket.IO : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"}
           </Text>
 
-          <Text
-            style={[
-              styles.alertLevel,
-              { color: getAlertColor(summary.alertLevel) },
-            ]}
-          >
-            {summary.alertLevel}
-          </Text>
+          {!!socketError && <Text style={styles.socketMuted}>{socketError}</Text>}
 
-          <View style={styles.priceRow}>
-            <Text style={styles.value}>Confiance</Text>
-            <Text style={styles.valueBold}>
-              {(summary.confidence * 100).toFixed(1)} %
-            </Text>
-          </View>
-
-          <Text style={styles.reason}>{summary.reason}</Text>
+          <Text style={styles.socketMuted}>Alertes reçues : {liveAlerts.length}</Text>
         </View>
 
-        {/* Carte Portefeuille (placeholder Step 1) */}
-        <View style={styles.card}>
-          <Text style={styles.sectionTitle}>Portefeuille</Text>
+        <MarketCard summary={summary} settings={settings} />
+        <StrategyCard summary={summary} />
+        <WalletCard settings={settings} />
 
-          <View style={styles.priceRow}>
-            <Text style={styles.value}>Valeur totale</Text>
-            <Text style={styles.valueBold}>10 000 {settings.currency}</Text>
-          </View>
-
-          <Text style={styles.updated}>Step 1 : mono-utilisateur / mono-crypto</Text>
-        </View>
-
-        {/* Carte Actions */}
-        <View style={styles.card}>
-          <Text style={styles.sectionTitle}>Actions</Text>
-
-          <View style={styles.row}>
-            <TouchableOpacity style={styles.button}>
-              <Text style={styles.buttonText}>Voir stratégie</Text>
-            </TouchableOpacity>
-
-            <TouchableOpacity style={styles.button}>
-              <Text style={styles.buttonText}>Historique</Text>
-            </TouchableOpacity>
-
-            <TouchableOpacity
-              style={styles.button}
-              onPress={() => navigation.navigate("Settings" as never)}
-            >
-              <Text style={styles.buttonText}>Paramètres</Text>
-            </TouchableOpacity>
-          </View>
-        </View>
+        <ActionsCard onGoSettings={() => navigation.navigate("Settings" as never)} />
       </ScrollView>
     </SafeAreaView>
   );
@@ -279,111 +227,51 @@ export default function DashboardScreen() {
 const styles = StyleSheet.create({
   safeArea: {
     flex: 1,
-    backgroundColor: "#fff",
+    backgroundColor: ui.screen.backgroundColor,
   },
 
   container: {
     padding: 16,
   },
 
-  card: {
-    borderWidth: 1,
-    borderColor: "#aaa",
-    padding: 12,
-    marginBottom: 12,
-    borderRadius: 6,
-  },
-
-  sectionTitle: {
-    fontWeight: "bold",
-    marginBottom: 6,
-    fontSize: 16,
-  },
-
-  price: {
-    fontSize: 26,
-    fontWeight: "bold",
-    textAlign: "center",
-    marginVertical: 6,
-  },
-
-  decision: {
-    fontSize: 32,
-    fontWeight: "bold",
-    textAlign: "center",
-    marginVertical: 10,
-  },
-
-  alertLevel: {
-    textAlign: "center",
-    fontWeight: "bold",
-    marginBottom: 6,
-  },
-
-  row: {
-    flexDirection: "row",
-    flexWrap: "wrap",
-    gap: 8,
-  },
-
-  priceRow: {
-    flexDirection: "row",
-    justifyContent: "space-between",
-    alignItems: "center",
-    marginVertical: 4,
-  },
-
-  value: {
-    fontSize: 16,
-  },
-
-  valueBold: {
-    fontSize: 16,
-    fontWeight: "bold",
-  },
-
-  reason: {
-    marginTop: 6,
-    fontStyle: "italic",
-  },
-
-  button: {
-    backgroundColor: "#555",
-    paddingHorizontal: 10,
-    paddingVertical: 10,
-    borderRadius: 4,
-    flexGrow: 1,
-    flexBasis: "48%",
+  centered: {
+    flex: 1,
+    justifyContent: "center",
     alignItems: "center",
-  },
-
-  buttonText: {
-    color: "#fff",
-    fontWeight: "bold",
-  },
-
-  updated: {
-    fontSize: 12,
-    opacity: 0.6,
-    marginTop: 6,
-    textAlign: "center",
+    backgroundColor: ui.screen.backgroundColor,
   },
 
   errorText: {
     color: "#dc2626",
-    fontWeight: "bold",
+    fontWeight: "800",
   },
 
-  warningBanner: {
+  // Bannières en style "card" comme les mockups
+  warningCard: {
+    ...ui.card,
     borderWidth: 1,
     borderColor: "#ca8a04",
-    padding: 10,
-    borderRadius: 6,
-    marginBottom: 12,
   },
 
   warningText: {
     color: "#ca8a04",
-    fontWeight: "bold",
+    fontWeight: "800",
+  },
+
+  socketCard: {
+    ...ui.card,
+    borderWidth: 1,
+    borderColor: "#cbd5e1",
+  },
+
+  socketTitle: {
+    fontWeight: "800",
+    color: "#0f172a",
+    marginBottom: 6,
+  },
+
+  socketMuted: {
+    ...ui.muted,
+    marginTop: 2,
   },
 });
\ No newline at end of file
diff --git a/mobile-ui/src/services/socketService.ts b/mobile-ui/src/services/socketService.ts
new file mode 100644 (file)
index 0000000..27bf531
--- /dev/null
@@ -0,0 +1,68 @@
+import { io, Socket } from "socket.io-client";
+import type { Alert } from "../types/Alert";
+
+class SocketService {
+  private socket: Socket | null = null;
+  private listeners = new Set<(alert: Alert) => void>();
+
+  connect(serverUrl: string, userId: string) {
+    if (!serverUrl) throw new Error("serverUrl is required");
+    if (!userId) throw new Error("userId is required");
+
+    // évite d'ouvrir plusieurs connexions si connect() est appelé plusieurs fois
+    if (this.socket?.connected) return;
+
+    this.socket = io(serverUrl, {
+      transports: ["websocket", "polling"], // plus fiable sur mobile
+      reconnection: true,
+      reconnectionAttempts: 5,
+      timeout: 10000,
+    });
+
+    this.socket.on("connect", () => {
+      console.log("✅ Socket connecté:", this.socket?.id);
+      // tuto de ton camarade : auth via userId
+      this.socket?.emit("auth", userId);
+    });
+
+    this.socket.on("auth_success", (data: any) => {
+      console.log("✅ Auth success:", data?.message ?? data);
+    });
+
+    this.socket.on("alert", (alert: Alert) => {
+      for (const cb of this.listeners) cb(alert);
+    });
+
+    this.socket.on("connect_error", (err) => {
+      console.log("❌ Socket connect_error:", err?.message ?? err);
+    });
+
+    this.socket.on("disconnect", (reason) => {
+      console.log("⚠️ Socket disconnect:", reason);
+    });
+  }
+
+  onAlert(callback: (alert: Alert) => void) {
+    this.listeners.add(callback);
+    return () => this.listeners.delete(callback);
+  }
+
+  ping() {
+    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();
+    this.socket.disconnect();
+    this.socket = null;
+    this.listeners.clear();
+  }
+}
+
+export const socketService = new SocketService();
\ No newline at end of file
diff --git a/mobile-ui/src/types/Alert.ts b/mobile-ui/src/types/Alert.ts
new file mode 100644 (file)
index 0000000..a59d4b4
--- /dev/null
@@ -0,0 +1,9 @@
+export interface Alert {
+  action: "BUY" | "SELL" | "HOLD";
+  pair: string;            // ex: "BTC/EUR"
+  confidence: number;      // 0..1
+  reason: string;
+  alertLevel: "INFO" | "WARNING" | "CRITICAL";
+  timestamp: number;       // Unix ms
+  price?: number;
+}
\ No newline at end of file