import { SafeAreaView } from "react-native-safe-area-context";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
+import { ui } from "../components/ui/uiStyles";
+
import type { PortfolioState, PortfolioAsset } from "../models/Portfolio";
import type { UserSettings } from "../models/UserSettings";
+import type { DashboardSummary } from "../types/DashboardSummary";
import type { Alert as AlertType } from "../types/Alert";
import { loadSession } from "../utils/sessionStorage";
import { loadSettings } from "../utils/settingsStorage";
import { loadPortfolio, savePortfolio } from "../utils/portfolioStorage";
-import { SERVER_URL } from "../config/env";
+import { SERVER_URL, API_BASE_URL, ENV_MODE } from "../config/env";
import { socketService } from "../services/socketService";
import { alertStore } from "../services/alertStore";
import { showAlertNotification } from "../services/notificationService";
-import { getCurrentPrice } from "../services/api/priceApi";
+import { getDashboardSummary } from "../services/api/dashboardApi";
import { getAlertHistory } from "../services/api/alertsApi";
import { getPortfolio as getPortfolioFromApi } from "../services/api/walletApi";
-/**
- * DashboardScreen (aligné Web)
- * ---------------------------
- * Web consomme :
- * - GET /api/prices/current?pair=BTC/EUR -> { price }
- * - GET /api/wallet/:userId -> { balance }
- * - GET /api/alerts/history?userId=... -> Alert[]
- *
- * Mobile :
- * - UI identique dans l’esprit (crypto + adresse + buy/sell)
- * - "Acheter/Vendre" = simulation (local) -> pas de trading réel
- */
-
type CryptoSymbol = "BTC" | "ETH" | "LTC";
type TradeSide = "BUY" | "SELL";
const CRYPTOS: CryptoSymbol[] = ["BTC", "ETH", "LTC"];
}
function mergePortfolios(base: PortfolioState, patch: PortfolioState): PortfolioState {
- // patch écrase base sur les symboles présents dans patch
const map = new Map<string, number>();
-
for (const a of base.assets) map.set(normalizeSymbol(a.symbol), a.quantity);
for (const a of patch.assets) map.set(normalizeSymbol(a.symbol), a.quantity);
export default function DashboardScreen() {
const navigation = useNavigation();
- // session / settings
+ const [loading, setLoading] = useState(true);
+ const [softError, setSoftError] = useState<string | null>(null);
+
const [userId, setUserId] = useState<string | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
+ const [portfolio, setPortfolio] = useState<PortfolioState>({ assets: [], updatedAtMs: Date.now() });
- // dashboard data
const [selectedCrypto, setSelectedCrypto] = useState<CryptoSymbol>("BTC");
- const [currentPrice, setCurrentPrice] = useState<number | null>(null);
- const [lastPriceUpdateMs, setLastPriceUpdateMs] = useState<number | null>(null);
- const [portfolio, setPortfolio] = useState<PortfolioState>({ assets: [], updatedAtMs: Date.now() });
+ // dashboard summary = signal + prix
+ const [summary, setSummary] = useState<DashboardSummary | null>(null);
// wallet address
- const [walletAddress, setWalletAddress] = useState<string>("");
+ const [walletAddress, setWalletAddress] = useState("");
const [walletAddressInfo, setWalletAddressInfo] = useState<string | null>(null);
- // alerts
+ // alerts (live)
const [liveAlerts, setLiveAlerts] = useState<AlertType[]>([]);
- const [socketConnected, setSocketConnected] = useState<boolean>(false);
+ const [socketConnected, setSocketConnected] = useState(false);
const [socketInfo, setSocketInfo] = useState<string | null>(null);
- // loading / errors
- const [loading, setLoading] = useState<boolean>(true);
- const [softError, setSoftError] = useState<string | null>(null);
-
// modal trade
- const [tradeOpen, setTradeOpen] = useState<boolean>(false);
+ const [tradeOpen, setTradeOpen] = useState(false);
const [tradeSide, setTradeSide] = useState<TradeSide>("BUY");
- const [tradeQty, setTradeQty] = useState<string>("0.01");
+ const [tradeQty, setTradeQty] = useState("0.01");
const [tradeInfo, setTradeInfo] = useState<string | null>(null);
const currency: "EUR" | "USD" = settings?.currency === "USD" ? "USD" : "EUR";
const urgentAlert = useMemo(() => {
if (liveAlerts.length === 0) return null;
-
const critical = liveAlerts.find((a) => String(a.alertLevel).toUpperCase() === "CRITICAL");
if (critical) return critical;
-
const warning = liveAlerts.find((a) => String(a.alertLevel).toUpperCase() === "WARNING");
if (warning) return warning;
-
return liveAlerts[0];
}, [liveAlerts]);
- const fetchPrice = useCallback(
- async (p: string) => {
- const res = await getCurrentPrice(p);
- setCurrentPrice(res.price);
- setLastPriceUpdateMs(res.timestampMs ?? Date.now());
- },
- []
- );
-
const refreshAll = useCallback(async () => {
setSoftError(null);
const s = await loadSettings();
setSettings(s);
- // Local portfolio (fallback + base)
+ // local portfolio
const localPortfolio = await loadPortfolio();
setPortfolio(localPortfolio);
- // Adresse wallet (local, par userId)
+ // wallet address
if (uid) {
const addr = (await AsyncStorage.getItem(walletAddressKey(uid))) ?? "";
setWalletAddress(addr);
setWalletAddress("");
}
- // Prix (API)
+ // summary (signal+prix) -> API
try {
- await fetchPrice(`${selectedCrypto}/${s.currency === "USD" ? "USD" : "EUR"}`);
+ const dash = await getDashboardSummary();
+ setSummary(dash);
} catch {
- setSoftError("Prix indisponible pour le moment (API).");
+ setSummary(null);
+ setSoftError(
+ `Signal/Prix indisponibles (API). Vérifie si tu es en DEV: ${ENV_MODE}.`
+ );
}
- // Wallet (API) -> fusion pour ne pas écraser ETH/LTC locaux
+ // wallet API (si dispo) -> fusion
if (uid) {
try {
const apiPortfolio = await getPortfolioFromApi();
await savePortfolio(merged);
} catch {
// pas bloquant
- setSoftError((prev) => prev ?? "Wallet indisponible pour le moment (API).");
}
}
- // Alert history (API) + socket live ensuite
+ // alert history REST (si dispo)
if (uid) {
try {
- const history = await getAlertHistory();
- // On met en store + state
+ const history = await getAlertHistory(10);
for (const a of history) alertStore.add(a);
setLiveAlerts(history.slice(0, 50));
} catch {
// pas bloquant
}
}
- }, [fetchPrice, selectedCrypto]);
+ }, []);
useFocusEffect(
useCallback(() => {
- let active = true;
-
+ let alive = true;
(async () => {
try {
setLoading(true);
await refreshAll();
} finally {
- if (active) setLoading(false);
+ if (alive) setLoading(false);
}
})();
-
return () => {
- active = false;
+ alive = false;
};
}, [refreshAll])
);
- // Socket : alert live (non bloquant)
+ // Socket live (non bloquant)
useEffect(() => {
let unsub: null | (() => void) = null;
let alive = true;
setSocketConnected(true);
} catch {
if (!alive) return;
- setSocketInfo("Socket indisponible (URL ou serveur).");
+ setSocketInfo("Socket indisponible (URL / serveur).");
return;
}
alertStore.add(a);
setLiveAlerts((prev) => [a, ...prev].slice(0, 100));
- // notifications si activées
if (settings?.notificationsEnabled) {
void (async () => {
try {
}
});
- // Optionnel
socketService.ping();
})();
updatedAtMs: Date.now(),
};
- // Aujourd’hui : local (simulation)
await savePortfolio(nextPortfolio);
setPortfolio(nextPortfolio);
-
setTradeOpen(false);
RNAlert.alert("Transaction enregistrée ✅", `${tradeSide} ${qty} ${symbol}`);
}, [tradeQty, tradeSide, selectedCrypto, portfolio]);
- const handleManualRefreshPrice = useCallback(async () => {
- try {
- setSoftError(null);
- await fetchPrice(pair);
- } catch {
- setSoftError("Prix indisponible pour le moment (API).");
- }
- }, [fetchPrice, pair]);
-
if (loading) {
return (
- <View style={styles.centered}>
+ <View style={ui.centered}>
<ActivityIndicator />
<Text style={{ marginTop: 10 }}>Chargement…</Text>
</View>
return (
<SafeAreaView style={styles.safeArea}>
- <ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
+ <ScrollView contentContainerStyle={ui.container} showsVerticalScrollIndicator={false}>
{!!softError && (
- <View style={styles.banner}>
+ <View style={[ui.banner, styles.bannerWarn]}>
<Text style={styles.bannerText}>{softError}</Text>
+ <Text style={styles.bannerSub}>
+ Base REST : {API_BASE_URL}
+ </Text>
</View>
)}
- {/* COMPTE UTILISATEUR */}
- <View style={styles.card}>
- <View style={styles.rowBetween}>
- <Text style={styles.title}>Compte utilisateur</Text>
- <Text style={styles.muted}>{socketConnected ? "Socket: ON ✅" : "Socket: OFF ⚠️"}</Text>
+ {/* Compte */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Compte utilisateur</Text>
+ <View style={ui.rowBetween}>
+ <Text style={ui.muted}>UserId : <Text style={styles.bold}>{userId ?? "—"}</Text></Text>
+ <Text style={ui.muted}>Socket : {socketConnected ? "ON ✅" : "OFF ⚠️"}</Text>
</View>
-
- <Text style={styles.muted}>
- UserId : <Text style={styles.bold}>{userId ?? "—"}</Text>
- </Text>
-
- {!!socketInfo && <Text style={[styles.muted, { marginTop: 6 }]}>{socketInfo}</Text>}
+ {!!socketInfo && <Text style={[ui.muted, { marginTop: 6 }]}>{socketInfo}</Text>}
</View>
- {/* CHOIX CRYPTO + ADRESSE */}
- <View style={styles.card}>
- <Text style={styles.title}>Choisir une cryptomonnaie</Text>
+ {/* Crypto + adresse */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Choisir une cryptomonnaie</Text>
<View style={styles.cryptoRow}>
{CRYPTOS.map((c) => {
<TouchableOpacity
key={c}
style={[styles.cryptoBtn, active && styles.cryptoBtnActive]}
- onPress={async () => {
- setSelectedCrypto(c);
- try {
- await fetchPrice(`${c}/${currency}`);
- } catch {
- setSoftError("Prix indisponible pour le moment (API).");
- }
- }}
+ onPress={() => setSelectedCrypto(c)}
>
<Text style={[styles.cryptoText, active && styles.cryptoTextActive]}>{c}</Text>
</TouchableOpacity>
})}
</View>
- <Text style={[styles.muted, { marginTop: 12 }]}>Adresse du portefeuille</Text>
+ <Text style={[ui.muted, { marginTop: 12 }]}>Adresse du portefeuille</Text>
<TextInput
value={walletAddress}
onChangeText={(t) => {
<Text style={styles.secondaryButtonText}>Enregistrer l’adresse</Text>
</TouchableOpacity>
- {!!walletAddressInfo && <Text style={[styles.muted, { marginTop: 8 }]}>{walletAddressInfo}</Text>}
+ {!!walletAddressInfo && <Text style={[ui.muted, { marginTop: 8 }]}>{walletAddressInfo}</Text>}
</View>
- {/* SOLDE + PRIX (comme le Web) */}
- <View style={styles.rowGap}>
- <View style={[styles.card, styles.half]}>
- <Text style={styles.title}>Solde</Text>
- <Text style={styles.bigValue}>
- {selectedQty.toFixed(6)} {selectedCrypto}
- </Text>
- <Text style={styles.muted}>Source : wallet local (+ API BTC fusionné)</Text>
- </View>
-
- <View style={[styles.card, styles.half]}>
- <View style={styles.rowBetween}>
- <Text style={styles.title}>Prix</Text>
- <TouchableOpacity onPress={handleManualRefreshPrice} style={styles.refreshBtn}>
- <Text style={styles.refreshText}>Actualiser</Text>
- </TouchableOpacity>
- </View>
-
- <Text style={styles.bigValue}>
- {(currentPrice ?? 0).toFixed(2)} {currency}
- </Text>
+ {/* 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>
- <Text style={styles.muted}>
- Pair : {pair} — Maj : {lastPriceUpdateMs ? new Date(lastPriceUpdateMs).toLocaleTimeString() : "—"}
- </Text>
+ <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>
+
+ <TouchableOpacity style={[ui.button, { marginTop: 10 }]} onPress={() => void refreshAll()}>
+ <Text style={ui.buttonText}>Actualiser</Text>
+ </TouchableOpacity>
</View>
- {/* SIGNAL (minimal) */}
- <TouchableOpacity style={styles.card} onPress={() => navigation.navigate("Alerts" as never)} activeOpacity={0.85}>
- <View style={styles.rowBetween}>
- <Text style={styles.title}>Signal du marché</Text>
- <Text style={styles.muted}>Ouvrir alertes</Text>
- </View>
+ {/* Signal */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Signal du marché</Text>
- {urgentAlert ? (
+ {summary ? (
<>
- <Text style={styles.signalAction}>{String(urgentAlert.action ?? "HOLD")}</Text>
- <Text style={styles.muted} numberOfLines={2}>
- {String(urgentAlert.alertLevel ?? "INFO")} — {String(urgentAlert.reason ?? urgentAlert.message ?? "—")}
+ <Text style={styles.signalDecision}>{summary.decision}</Text>
+ <Text style={ui.muted}>
+ {summary.alertLevel} — Confiance {Math.round(summary.confidence * 100)}%
</Text>
- <Text style={styles.muted}>
- {String(urgentAlert.pair ?? "")} — Confiance{" "}
- {typeof urgentAlert.confidence === "number" ? `${Math.round(urgentAlert.confidence * 100)}%` : "—"}
+ <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={3}>
+ {summary.reason}
</Text>
</>
) : (
- <Text style={styles.muted}>Aucune alerte pour le moment.</Text>
+ <Text style={ui.muted}>Aucune donnée pour le moment.</Text>
)}
- </TouchableOpacity>
- {/* STRATÉGIE (bouton) */}
- <TouchableOpacity style={styles.card} onPress={() => navigation.navigate("Strategy" as never)} activeOpacity={0.85}>
- <View style={styles.rowBetween}>
- <Text style={styles.title}>Stratégie</Text>
- <Text style={styles.muted}>Configurer</Text>
- </View>
+ {/* Bonus : urgence live si dispo */}
+ {urgentAlert && (
+ <View style={styles.urgentBox}>
+ <Text style={styles.urgentTitle}>
+ {String(urgentAlert.alertLevel ?? "INFO")} — {String(urgentAlert.action ?? "HOLD")}
+ </Text>
+ <Text style={ui.muted} numberOfLines={2}>
+ {urgentAlert.reason ?? urgentAlert.message ?? "—"}
+ </Text>
+ </View>
+ )}
+ </View>
- <Text style={styles.muted} numberOfLines={2}>
- (L’écran Stratégie reste inchangé — ici on ne fait que naviguer.)
- </Text>
+ {/* Stratégie */}
+ <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 (Acheter / Vendre) */}
- <View style={styles.card}>
- <Text style={styles.title}>Actions</Text>
+ {/* Actions */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Actions</Text>
<View style={styles.buySellRow}>
<TouchableOpacity style={styles.buyBtn} onPress={() => openTrade("BUY")}>
</TouchableOpacity>
</View>
- <Text style={[styles.muted, { marginTop: 10 }]} numberOfLines={2}>
+ <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 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={styles.muted}>
- Prix : {(currentPrice ?? 0).toFixed(2)} {currency}
+ <Text style={ui.muted}>
+ Prix : {(summary?.price ?? 0).toFixed(2)} {currency}
</Text>
- <Text style={[styles.muted, { marginTop: 12 }]}>Quantité</Text>
+ <Text style={[ui.muted, { marginTop: 12 }]}>Quantité</Text>
<TextInput
value={tradeQty}
onChangeText={(t) => {
</KeyboardAvoidingView>
</View>
</Modal>
-
- {/* Bouton refresh global */}
- <TouchableOpacity style={[styles.secondaryButton, { marginTop: 4 }]} onPress={() => void refreshAll()}>
- <Text style={styles.secondaryButtonText}>Tout actualiser</Text>
- </TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
- safeArea: { flex: 1, backgroundColor: "#0b1220" },
- container: { padding: 16, paddingBottom: 28 },
+ safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
- centered: { flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: "#0b1220" },
+ bannerWarn: { borderColor: "#ca8a04" },
+ bannerText: { color: "#ca8a04", fontWeight: "900" },
+ bannerSub: { marginTop: 6, color: "#475569", fontWeight: "700" },
- banner: {
- borderWidth: 1,
- borderColor: "#f59e0b",
- backgroundColor: "rgba(245,158,11,0.12)",
- padding: 12,
- borderRadius: 12,
- marginBottom: 12,
- },
- bannerText: { color: "#f59e0b", fontWeight: "800" },
-
- card: {
- backgroundColor: "#ffffff",
- borderRadius: 16,
- padding: 14,
- marginBottom: 12,
- borderWidth: 1,
- borderColor: "#e5e7eb",
- },
-
- rowBetween: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
- rowGap: { flexDirection: "row", gap: 12, marginBottom: 12 },
- half: { flex: 1, marginBottom: 0 },
-
- title: { fontSize: 16, fontWeight: "900", color: "#0f172a" },
- muted: { marginTop: 6, color: "#475569", fontWeight: "600" },
bold: { fontWeight: "900", color: "#0f172a" },
bigValue: { marginTop: 10, fontSize: 22, fontWeight: "900", color: "#0f172a" },
- signalAction: { marginTop: 10, fontSize: 24, fontWeight: "900", color: "#0f172a" },
+ signalDecision: { marginTop: 10, fontSize: 26, fontWeight: "900", color: "#0f172a" },
cryptoRow: { flexDirection: "row", gap: 10, marginTop: 10 },
cryptoBtn: {
},
secondaryButtonText: { fontWeight: "900", color: "#0f172a", opacity: 0.9 },
- refreshBtn: {
- paddingHorizontal: 12,
- paddingVertical: 8,
- borderRadius: 12,
+ urgentBox: {
+ marginTop: 12,
borderWidth: 1,
borderColor: "#e5e7eb",
+ borderRadius: 12,
+ padding: 10,
backgroundColor: "#fff",
},
- refreshText: { fontWeight: "900", color: "#0f172a" },
+ urgentTitle: { fontWeight: "900", color: "#0f172a" },
buySellRow: { flexDirection: "row", gap: 10, marginTop: 10 },
buyBtn: { flex: 1, backgroundColor: "#16a34a", paddingVertical: 12, borderRadius: 12, alignItems: "center" },
modalCard: { backgroundColor: "#fff", borderRadius: 16, padding: 16, borderWidth: 1, borderColor: "#e5e7eb" },
modalTitle: { fontSize: 18, fontWeight: "900", color: "#0f172a" },
modalError: { marginTop: 10, fontWeight: "800", color: "#dc2626" },
-
modalButtonsRow: { flexDirection: "row", gap: 10, marginTop: 14 },
modalBtn: { flex: 1, paddingVertical: 12, borderRadius: 12, alignItems: "center" },
modalBtnSecondary: { backgroundColor: "#fff", borderWidth: 1, borderColor: "#e5e7eb" },
-import type { DashboardSummary } from "../../types/DashboardSummary";
-import type { Alert } from "../../types/Alert";
+import type { DashboardSummary, TradeDecision, AlertLevel } from "../../types/DashboardSummary";
import { apiGet } from "./http";
import { loadSession } from "../../utils/sessionStorage";
import { loadSettings } from "../../utils/settingsStorage";
/**
* dashboardApi (API-only)
* -----------------------
- * Construit un DashboardSummary en appelant les endpoints du contrat.
- * Aucun fallback mock.
+ * 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
*/
export async function getDashboardSummary(): Promise<DashboardSummary> {
const session = await loadSession();
if (!userId) throw new Error("Session absente : impossible de charger le dashboard.");
const settings = await loadSettings();
- const pair = "BTC/EUR"; // TODO: quand tu gères multi-paires sur dashboard, tu changes ici.
-
- // 1) Prix courant
- const priceData = await apiGet<{
- pair: string;
- timestamp_ms: number;
- current_price: number;
- }>(`/price/current?pair=${encodeURIComponent(pair)}`);
-
- // 2) Signal courant
- const signalData = await apiGet<{
- action: Alert["action"]; // BUY/SELL/HOLD/STOP_LOSS
- criticality: Alert["alertLevel"]; // CRITICAL/WARNING/INFO
- confidence: number; // 0..1
- reason: string;
- timestamp_ms: number;
- }>(`/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`);
+ 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 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 alertLevel: AlertLevel =
+ lvlStr === "CRITICAL" || lvlStr === "WARNING"
+ ? (lvlStr as AlertLevel)
+ : "INFO";
+
+ const confidence =
+ typeof signalRaw?.confidence === "number"
+ ? signalRaw.confidence
+ : typeof signalRaw?.data?.confidence === "number"
+ ? signalRaw.data.confidence
+ : 0;
+
+ const reason = String(
+ signalRaw?.reason ??
+ signalRaw?.message ??
+ signalRaw?.data?.reason ??
+ signalRaw?.data?.message ??
+ "—"
+ );
+
+ 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);
return {
pair,
- price: Number(priceData.current_price),
- strategy: settings.selectedStrategyKey, // affichage “mobile” (en attendant un champ API)
- decision: signalData.action,
- alertLevel: signalData.criticality,
- confidence: Number(signalData.confidence),
- reason: String(signalData.reason),
- timestamp: Number(signalData.timestamp_ms),
+ price: Number(price),
+ strategy: settings.selectedStrategyKey,
+ decision,
+ confidence: Number(confidence),
+ reason,
+ alertLevel,
+ timestamp: Number(tsSignal),
};
}
\ No newline at end of file
import type { PortfolioState } from "../../models/Portfolio";
/**
- * walletApi
- * ---------
- * Aligné Web :
- * - GET /api/wallet/:userId
- * Réponse attendue : { balance: number }
+ * walletApi (aligné serveur déployé)
+ * ---------------------------------
+ * Routes dispo :
+ * - GET /api/wallets?userId=...
+ * - GET /api/wallets/:walletId
+ * - GET /api/wallets/:walletId/events
*
- * Le web affiche le solde en BTC.
- * Côté mobile, on convertit en PortfolioState (assets).
+ * 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 WalletResponse = {
- balance?: number;
- // si un jour ils passent multi-assets, on ne casse pas :
- balances?: Record<string, number>;
+type WalletListItem = {
+ id?: string;
+ walletId?: string;
+ _id?: string;
};
-export async function getPortfolio(): Promise<PortfolioState> {
- const session = await loadSession();
- const userId = session?.userId;
- if (!userId) throw new Error("Session absente : impossible de charger le portefeuille.");
+type WalletListResponse =
+ | { wallets?: WalletListItem[] }
+ | { items?: WalletListItem[] }
+ | WalletListItem[]
+ | any;
- const data = await apiGet<WalletResponse>(`/wallet/${encodeURIComponent(userId)}`);
+type WalletDetailsResponse = any;
- // Format actuel web : balance BTC
- if (typeof data?.balance === "number" && Number.isFinite(data.balance)) {
- return {
- assets: [{ symbol: "BTC", quantity: data.balance }],
- updatedAtMs: Date.now(),
- };
+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 first = arr[0];
+ return (
+ (typeof first?.walletId === "string" && first.walletId) ||
+ (typeof first?.id === "string" && first.id) ||
+ (typeof first?._id === "string" && first._id) ||
+ 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 }
+ const assetsArr =
+ (Array.isArray(details?.portfolio?.assets) && details.portfolio.assets) ||
+ (Array.isArray(details?.assets) && details.assets) ||
+ null;
+
+ if (assetsArr) {
+ return assetsArr
+ .filter((a: any) => a && typeof a.symbol === "string" && typeof a.quantity === "number")
+ .map((a: any) => ({ symbol: String(a.symbol).toUpperCase(), quantity: Number(a.quantity) }))
+ .filter((a: any) => Number.isFinite(a.quantity) && a.quantity > 0)
+ .sort((a: any, b: any) => a.symbol.localeCompare(b.symbol));
}
- // Format futur possible : balances { BTC:..., ETH:..., LTC:... }
- const balances = data?.balances;
+ const balances = details?.balances;
if (balances && typeof balances === "object") {
- const assets = Object.entries(balances)
+ return Object.entries(balances)
.filter(([, qty]) => typeof qty === "number" && Number.isFinite(qty) && qty > 0)
- .map(([symbol, qty]) => ({ symbol: symbol.toUpperCase(), quantity: qty }))
+ .map(([symbol, qty]) => ({ symbol: String(symbol).toUpperCase(), quantity: Number(qty) }))
.sort((a, b) => a.symbol.localeCompare(b.symbol));
+ }
- return { assets, updatedAtMs: Date.now() };
+ // fallback : rien exploitable
+ return [];
+}
+
+export async function getPortfolio(): Promise<PortfolioState> {
+ const session = await loadSession();
+ 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() };
}
- // fallback safe
- return { assets: [], updatedAtMs: Date.now() };
+ // 2) récupérer le wallet détail
+ const details = await apiGet<WalletDetailsResponse>(`/wallets/${encodeURIComponent(walletId)}`);
+
+ const assets = extractAssetsFromWalletDetails(details);
+
+ return {
+ assets,
+ updatedAtMs: Date.now(),
+ };
}
\ No newline at end of file