"""Opérations de flotte (fan-out) pour Kazeia-central. §9 du contrat : toute action parc se fait par **fan-out séquentiel par device**, et les commandes adb **échouent silencieusement par tablette** → on capture le résultat OU l'erreur de CHAQUE device, jamais un échec global qui masquerait les autres. Le rapport renvoyé est une liste `{serial, ok, result|error}` exploitable telle quelle. Séquentiel (pas parallèle) volontairement : conforme au contrat, et un `adb` partagé sature vite ; pour un parc de quelques tablettes le coût est négligeable. """ from __future__ import annotations from typing import Any, Callable from .adb import Adb from .provider import ProviderClient def fan_out(adb: Adb, fn: Callable[[str], Any], *, serials: list[str] | None = None) -> list[dict[str, Any]]: """Exécute `fn(serial)` sur chaque tablette prête (ou la sous-liste `serials`), en isolant les échecs. Renvoie un rapport par device.""" ready = adb.ready_devices() if serials is not None: wanted = set(serials) ready = [d for d in ready if d.serial in wanted] report: list[dict[str, Any]] = [] for d in ready: try: report.append({"serial": d.serial, "ok": True, "result": fn(d.serial)}) except Exception as e: # adb/provider peut échouer pour CE device seulement report.append({"serial": d.serial, "ok": False, "error": str(e)}) return report def device_overview(adb: Adb, serial: str) -> dict[str, Any]: """Résumé santé d'une tablette pour la vue parc. Chaque sous-requête est isolée : une section indisponible ne fait pas tomber tout le résumé (renvoi partiel).""" c = ProviderClient(adb, serial) def safe(f, default=None): try: return f() except Exception: return default state = safe(lambda: c.state().model_dump()) updates = safe(lambda: c.updates().model_dump()) rag = safe(lambda: c.rag_status().model_dump()) profiles = safe(lambda: c.profiles(), []) sessions = safe(lambda: c.sessions(), []) crashes = safe(lambda: c.crashes(), []) g = lambda d, k: (d or {}).get(k) return { "serial": serial, "online": state is not None, "uptime_s": g(state, "uptime_seconds"), "mem_available_mb": g(state, "mem_available_mb"), "update_phase": g(updates, "phase"), "app_update_available": g(updates, "app_update_available"), "rag_ready": g(rag, "ready"), "rag_docs": g(rag, "doc_count"), "profiles": len(profiles), "sessions": len(sessions), "crashes": len(crashes), }