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<RootStackParamList>();
component={HistoryScreen}
options={{ title: "Historique" }}
/>
+
+ <Stack.Screen
+ name="Alerts"
+ component={AlertsScreen}
+ options={{ title: "Alertes" }} />
</Stack.Navigator>
</NavigationContainer>
type Props = {
onGoSettings: () => void;
onGoHistory: () => void;
+ onGoAlerts: () => void;
};
/**
* - 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 (
<View style={ui.card}>
<Text style={ui.title}>Actions</Text>
<View style={[ui.rowBetween, { flexWrap: "wrap", gap: 8 }]}>
- <TouchableOpacity style={ui.button} onPress={() => {}}>
- <Text style={ui.buttonText}>Voir stratégie</Text>
+ <TouchableOpacity style={ui.button} onPress={onGoAlerts}>
+ <Text style={ui.buttonText}>Alertes</Text>
</TouchableOpacity>
<TouchableOpacity style={ui.button} onPress={onGoHistory}>
--- /dev/null
+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<Alert[]>([]);
+
+ 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 (
+ <SafeAreaView style={styles.safeArea}>
+ <FlatList
+ contentContainerStyle={ui.container}
+ data={sorted}
+ keyExtractor={(_, idx) => `alert-${idx}`}
+ ListEmptyComponent={
+ <View style={ui.card}>
+ <Text style={ui.title}>Aucune alerte</Text>
+ <Text style={ui.muted}>En attente d’alertes Socket.IO…</Text>
+ </View>
+ }
+ renderItem={({ item }) => {
+ const lvl = item.alertLevel as AlertLevel;
+ const act = item.action as TradeDecision;
+
+ const lvlColor = getLevelColor(lvl);
+ const actColor = getActionColor(act);
+
+ return (
+ <View style={ui.card}>
+ <View style={ui.rowBetween}>
+ <Text style={ui.valueBold}>{item.pair}</Text>
+ <Text style={ui.muted}>
+ {item.timestamp ? new Date(item.timestamp).toLocaleString() : "—"}
+ </Text>
+ </View>
+
+ <View style={{ flexDirection: "row", gap: 8, flexWrap: "wrap", marginTop: 10 }}>
+ <View style={[ui.badge, { backgroundColor: `${lvlColor}22`, marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: lvlColor }]}>{lvl}</Text>
+ </View>
+
+ <View style={[ui.badge, { backgroundColor: `${actColor}22`, marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: actColor }]}>{act}</Text>
+ </View>
+ </View>
+
+ <View style={[ui.rowBetween, { marginTop: 10 }]}>
+ <Text style={ui.value}>Confiance</Text>
+ <Text style={ui.valueBold}>{(item.confidence * 100).toFixed(0)}%</Text>
+ </View>
+
+ {typeof item.price === "number" && (
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Prix</Text>
+ <Text style={ui.valueBold}>{item.price.toFixed(2)}</Text>
+ </View>
+ )}
+
+ <Text style={[ui.muted, { marginTop: 10 }]}>{item.reason}</Text>
+ </View>
+ );
+ }}
+ />
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: ui.screen.backgroundColor,
+ },
+});
\ No newline at end of file
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";
* É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<DashboardSummary | null>(null);
/**
* 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);
}
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();
</Text>
</View>
+ {/* Dashboard : afficher uniquement la DERNIÈRE alerte */}
+ {liveAlerts.length > 0 && (
+ <View style={styles.alertItem}>
+ <Text style={styles.alertHeader}>
+ {liveAlerts[0].alertLevel} — {liveAlerts[0].action}{" "}
+ {liveAlerts[0].pair}
+ </Text>
+ <Text style={styles.alertBody}>
+ {(liveAlerts[0].confidence * 100).toFixed(0)}% —{" "}
+ {liveAlerts[0].reason}
+ </Text>
+ <Text style={styles.alertHint}>
+ Voir toutes les alertes dans “Alertes”.
+ </Text>
+ </View>
+ )}
+
<MarketCard summary={summary} settings={settings} />
<StrategyCard summary={summary} />
<WalletCard settings={settings} />
<ActionsCard
onGoSettings={() => navigation.navigate("Settings" as never)}
onGoHistory={() => navigation.navigate("History" as never)}
+ onGoAlerts={() => navigation.navigate("Alerts" as never)}
/>
</ScrollView>
</SafeAreaView>
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
--- /dev/null
+import type { Alert } from "../types/Alert";
+
+type Listener = (alerts: Alert[]) => void;
+
+class AlertStore {
+ private alerts: Alert[] = [];
+ private listeners = new Set<Listener>();
+
+ 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