From 77fa37c9f1d3fa32b03f8599710664b274949ca1 Mon Sep 17 00:00:00 2001 From: Thibaud Moustier Date: Wed, 25 Feb 2026 13:10:56 +0100 Subject: [PATCH] Mobile : Modification + AlertsScreen created --- Wallette/mobile/App.tsx | 8 + .../mobile/src/components/ActionsCard.tsx | 8 +- Wallette/mobile/src/screens/AlertsScreen.tsx | 140 ++++++++++++++++++ .../mobile/src/screens/DashboardScreen.tsx | 68 +++++++-- Wallette/mobile/src/services/alertStore.ts | 38 +++++ 5 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 Wallette/mobile/src/screens/AlertsScreen.tsx create mode 100644 Wallette/mobile/src/services/alertStore.ts diff --git a/Wallette/mobile/App.tsx b/Wallette/mobile/App.tsx index e491b2b..3498460 100644 --- a/Wallette/mobile/App.tsx +++ b/Wallette/mobile/App.tsx @@ -4,12 +4,15 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack"; import DashboardScreen from "./src/screens/DashboardScreen"; import SettingsScreen from "./src/screens/SettingsScreen"; import HistoryScreen from "./src/screens/HistoryScreen"; +import AlertsScreen from "./src/screens/AlertsScreen"; + // Types des routes (pour éviter les erreurs de navigation) export type RootStackParamList = { Dashboard: undefined; Settings: undefined; History: undefined; + Alerts: undefined; }; const Stack = createNativeStackNavigator(); @@ -35,6 +38,11 @@ export default function App() { component={HistoryScreen} options={{ title: "Historique" }} /> + + diff --git a/Wallette/mobile/src/components/ActionsCard.tsx b/Wallette/mobile/src/components/ActionsCard.tsx index dfc2a6c..d49b260 100644 --- a/Wallette/mobile/src/components/ActionsCard.tsx +++ b/Wallette/mobile/src/components/ActionsCard.tsx @@ -4,6 +4,7 @@ import { ui } from "./ui/uiStyles"; type Props = { onGoSettings: () => void; onGoHistory: () => void; + onGoAlerts: () => void; }; /** @@ -14,15 +15,16 @@ type Props = { * - Voir stratégie (placeholder) * - Historique (placeholder) * - Paramètres (navigation réelle via callback) + * - Alertes (navigation réelle via callback) */ -export default function ActionsCard({ onGoSettings, onGoHistory }: Props) { +export default function ActionsCard({ onGoSettings, onGoHistory, onGoAlerts }: Props) { return ( Actions - {}}> - Voir stratégie + + Alertes diff --git a/Wallette/mobile/src/screens/AlertsScreen.tsx b/Wallette/mobile/src/screens/AlertsScreen.tsx new file mode 100644 index 0000000..4333d5d --- /dev/null +++ b/Wallette/mobile/src/screens/AlertsScreen.tsx @@ -0,0 +1,140 @@ +import { View, Text, StyleSheet, FlatList } from "react-native"; +import { useEffect, useMemo, useState } from "react"; +import { SafeAreaView } from "react-native-safe-area-context"; + +import type { Alert } from "../types/Alert"; +import { alertStore } from "../services/alertStore"; +import { ui } from "../components/ui/uiStyles"; + +// Types locaux pour le tri/couleurs (évite dépendre d'exports manquants) +type AlertLevel = "CRITICAL" | "WARNING" | "INFO"; +type TradeDecision = "BUY" | "SELL" | "HOLD" | "STOP_LOSS"; + +function levelRank(level: AlertLevel) { + switch (level) { + case "CRITICAL": + return 3; + case "WARNING": + return 2; + case "INFO": + default: + return 1; + } +} + +function getLevelColor(level: AlertLevel): string { + switch (level) { + case "CRITICAL": + return "#dc2626"; + case "WARNING": + return "#ca8a04"; + case "INFO": + default: + return "#2563eb"; + } +} + +function getActionColor(action: TradeDecision): string { + switch (action) { + case "BUY": + return "#16a34a"; + case "SELL": + return "#dc2626"; + case "STOP_LOSS": + return "#991b1b"; + case "HOLD": + default: + return "#ca8a04"; + } +} + +export default function AlertsScreen() { + const [alerts, setAlerts] = useState([]); + + useEffect(() => { + const unsub = alertStore.subscribe(setAlerts); + return () => { + unsub(); + }; + }, []); + + const sorted = useMemo(() => { + return [...alerts].sort((a, b) => { + // cast car ton type Alert peut ne pas déclarer explicitement les unions + const la = a.alertLevel as AlertLevel; + const lb = b.alertLevel as AlertLevel; + + const byLevel = levelRank(lb) - levelRank(la); + if (byLevel !== 0) return byLevel; + + const ta = a.timestamp ?? 0; + const tb = b.timestamp ?? 0; + return tb - ta; + }); + }, [alerts]); + + return ( + + `alert-${idx}`} + ListEmptyComponent={ + + Aucune alerte + En attente d’alertes Socket.IO… + + } + renderItem={({ item }) => { + const lvl = item.alertLevel as AlertLevel; + const act = item.action as TradeDecision; + + const lvlColor = getLevelColor(lvl); + const actColor = getActionColor(act); + + return ( + + + {item.pair} + + {item.timestamp ? new Date(item.timestamp).toLocaleString() : "—"} + + + + + + {lvl} + + + + {act} + + + + + Confiance + {(item.confidence * 100).toFixed(0)}% + + + {typeof item.price === "number" && ( + + Prix + {item.price.toFixed(2)} + + )} + + {item.reason} + + ); + }} + /> + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: ui.screen.backgroundColor, + }, +}); \ No newline at end of file diff --git a/Wallette/mobile/src/screens/DashboardScreen.tsx b/Wallette/mobile/src/screens/DashboardScreen.tsx index a8a34b0..01da368 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -7,6 +7,7 @@ import type { DashboardSummary } from "../types/DashboardSummary"; import { fetchDashboardSummary } from "../services/dashboardService"; import { loadSettings } from "../utils/settingsStorage"; import type { UserSettings } from "../models/UserSettings"; +import { alertStore } from "../services/alertStore"; import MarketCard from "../components/MarketCard"; import StrategyCard from "../components/StrategyCard"; @@ -26,8 +27,13 @@ import type { Alert } from "../types/Alert"; * Écran principal mobile. * - Charge Dashboard + Settings * - Refresh auto si activé - * - Socket.IO optionnel (non bloquant) pour alertes live - * - UI cohérente via uiStyles/theme + * - Socket.IO non bloquant pour alertes live + * - Dashboard = résumé : on affiche UNE seule alerte (la dernière) + * + * + * Important : + * - Le Socket est NON BLOQUANT : si le serveur n'est pas dispo, l'app continue de marcher. + * - En Step 1/2/3, on n'a pas forcément de vrai userId => on utilise un userId de test. */ export default function DashboardScreen() { const [summary, setSummary] = useState(null); @@ -106,22 +112,16 @@ export default function DashboardScreen() { /** * Socket.IO (non bloquant) - * - Si userId absent => on désactive socket proprement. - * - Si serveur pas prêt => connect_error timeout => on affiche un message, sans crasher. + * - En Step 1/2/3 on utilise un userId de test (auth pas encore là) + * - Si serveur KO, on n'empêche pas l'app de fonctionner */ useEffect(() => { if (!settings) return; setSocketError(null); - // ✅ userId optionnel pour Step 1/2/3 (pas d'auth) - const userId = settings.userId; - - if (!userId) { - setSocketConnected(false); - setSocketError("Socket désactivé : userId absent."); - return; - } + // ✅ userId de test pour avancer maintenant + const userId = "test-user"; try { socketService.connect(SERVER_URL, userId); @@ -133,7 +133,8 @@ export default function DashboardScreen() { } const unsubscribeAlert = socketService.onAlert((alert) => { - setLiveAlerts((prev) => [alert, ...prev].slice(0, 50)); + alertStore.add(alert); // ✅ stock global + setLiveAlerts((prev) => [alert, ...prev].slice(0, 50)); // (optionnel) résumé dashboard }); socketService.ping(); @@ -194,6 +195,23 @@ export default function DashboardScreen() { + {/* Dashboard : afficher uniquement la DERNIÈRE alerte */} + {liveAlerts.length > 0 && ( + + + {liveAlerts[0].alertLevel} — {liveAlerts[0].action}{" "} + {liveAlerts[0].pair} + + + {(liveAlerts[0].confidence * 100).toFixed(0)}% —{" "} + {liveAlerts[0].reason} + + + Voir toutes les alertes dans “Alertes”. + + + )} + @@ -201,6 +219,7 @@ export default function DashboardScreen() { navigation.navigate("Settings" as never)} onGoHistory={() => navigation.navigate("History" as never)} + onGoAlerts={() => navigation.navigate("Alerts" as never)} /> @@ -231,4 +250,27 @@ const styles = StyleSheet.create({ socketTitle: { color: "#0f172a", }, + + // Dernière alerte affichée (résumé) + alertItem: { + borderWidth: 1, + borderColor: "#e5e7eb", + padding: 10, + borderRadius: 10, + marginBottom: 12, + backgroundColor: "#ffffff", + }, + alertHeader: { + fontWeight: "900", + color: "#0f172a", + }, + alertBody: { + marginTop: 4, + color: "#334155", + }, + alertHint: { + marginTop: 6, + fontSize: 12, + opacity: 0.7, + }, }); \ No newline at end of file diff --git a/Wallette/mobile/src/services/alertStore.ts b/Wallette/mobile/src/services/alertStore.ts new file mode 100644 index 0000000..4ac745d --- /dev/null +++ b/Wallette/mobile/src/services/alertStore.ts @@ -0,0 +1,38 @@ +import type { Alert } from "../types/Alert"; + +type Listener = (alerts: Alert[]) => void; + +class AlertStore { + private alerts: Alert[] = []; + private listeners = new Set(); + + add(alert: Alert) { + this.alerts = [alert, ...this.alerts].slice(0, 200); + this.emit(); + } + + getAll() { + return this.alerts; + } + + subscribe(listener: Listener) { + this.listeners.add(listener); + listener(this.alerts); + + // ✅ cleanup doit retourner void (pas boolean) + return () => { + this.listeners.delete(listener); + }; + } + + clear() { + this.alerts = []; + this.emit(); + } + + private emit() { + for (const l of this.listeners) l(this.alerts); + } +} + +export const alertStore = new AlertStore(); \ No newline at end of file -- 2.50.1