"""Déclencheur automatique d'enrôlement à la connexion d'une tablette. Piloté depuis l'interface : démarre **désarmé** (`enabled=False`). Quand l'opérateur l'arme, toute tablette qui se **connecte** déclenche un `sync_admin` (enrôle + déploie les voix en attente, verrou-conscient). Statut par tablette consultable. Sécurité : ne fait rien si le store est verrouillé (les PII restent protégées) ; la suppression du WAV device est opt-in (`delete_source`, défaut False). Détecte les **nouvelles** connexions ; pour traiter une tablette déjà branchée, l'UI utilise le bouton « synchroniser » (endpoint sync manuel). """ from __future__ import annotations import threading import time from typing import Any from .orchestrator import sync_admin class VoiceAutoSync: def __init__(self, adb, store, bridge, *, interval: float = 3.0) -> None: self.adb = adb self.store = store self.bridge = bridge self.interval = interval self.enabled = False self.delete_source = False self._known: set[str] = set() self._status: dict[str, dict] = {} self._lock = threading.Lock() self._thread: threading.Thread | None = None self._stop = threading.Event() @staticmethod def _now() -> int: return int(time.time() * 1000) # ---- cycle de vie ----------------------------------------------------- def start(self) -> None: if self._thread and self._thread.is_alive(): return self._stop.clear() self._thread = threading.Thread(target=self._loop, name="voice-autosync", daemon=True) self._thread.start() def stop(self) -> None: self._stop.set() def set_enabled(self, enabled: bool, *, delete_source: bool | None = None) -> None: self.enabled = bool(enabled) if delete_source is not None: self.delete_source = bool(delete_source) def status(self) -> dict[str, Any]: with self._lock: return {"enabled": self.enabled, "delete_source": self.delete_source, "running": bool(self._thread and self._thread.is_alive()), "devices": dict(self._status)} # ---- boucle ----------------------------------------------------------- def _loop(self) -> None: while not self._stop.wait(self.interval): try: ready = {d.serial for d in self.adb.ready_devices()} except Exception: continue new = ready - self._known self._known = ready if not self.enabled: continue for serial in sorted(new): self.sync_now(serial) def sync_now(self, serial: str) -> dict: """Lance une sync pour une tablette (utilisé par le watcher ET par le bouton manuel). Ne lève pas : renvoie/enregistre un statut.""" if not self.store.is_unlocked: st = {"state": "store_verrouille", "at": self._now()} self._set(serial, st) return st self._set(serial, {"state": "sync_en_cours", "at": self._now()}) try: rep = sync_admin(self.adb, self.store, self.bridge, serial, delete_source=self.delete_source, now=self._now(), actor="auto") ok = sum(1 for r in rep if r.get("ok")) st = {"state": "termine", "at": self._now(), "enrolled": ok, "total": len(rep), "report": rep} except Exception as e: st = {"state": "erreur", "at": self._now(), "error": str(e)} self._set(serial, st) return st def _set(self, serial: str, st: dict) -> None: with self._lock: self._status[serial] = st