From 5f1a96ab8aa168d7c8b790ca3e88319084fa8be2 Mon Sep 17 00:00:00 2001 From: Jules Date: Fri, 18 Sep 2026 12:32:31 +0200 Subject: [PATCH] Watchdog WiFi + reconnexion auto + fix timing surprise capture - Watchdog connectivite toutes les 60s (check TCP 1.1.1.1:53) - Si offline : scan WiFi et reconnexion auto aux reseaux enregistres - Si internet restaure : flush immediat des spools (email + galerie) - Surprise : capture declenchee 500ms apres affichage (au lieu de 1.7s apres) pour capturer la reaction naturelle des gens Co-Authored-By: Claude Opus 4.6 --- backend/mailer.py | 52 +++++++++++++++++++++++++------------- backend/wifi.py | 59 +++++++++++++++++++++++++++++++++++++++++++ frontend/index.html | 2 +- frontend/js/camera.js | 36 +++++++++++++------------- 4 files changed, 113 insertions(+), 36 deletions(-) diff --git a/backend/mailer.py b/backend/mailer.py index fe7999b..1bb3325 100644 --- a/backend/mailer.py +++ b/backend/mailer.py @@ -179,28 +179,46 @@ def envoyer_rapport_spool(nb_envoyes: int, destinataires: list[str], nb_echoues: RETRY_INTERVAL = 300 # 5 minutes +WATCHDOG_INTERVAL = 60 # 1 minute + + +async def _flush_spools(): + """Vide les spools email + galerie.""" + from backend.destinations import traiter_booth_spool, taille_booth_spool + loop = asyncio.get_event_loop() + total_mail = taille_spool() + if total_mail > 0: + log.info(f"Spool email: retry ({total_mail} en attente)") + nb, dests = await loop.run_in_executor(None, traiter_spool) + if nb > 0: + echoues = total_mail - nb + await loop.run_in_executor(None, envoyer_rapport_spool, nb, dests, echoues) + total_booth = taille_booth_spool() + if total_booth > 0: + log.info(f"Spool galerie: retry ({total_booth} en attente)") + await loop.run_in_executor(None, traiter_booth_spool) async def tache_spool_periodique(): - """Retente emails + galerie en spool toutes les 5 min.""" - from backend.destinations import traiter_booth_spool, taille_booth_spool + """Watchdog connectivite (60s) + retry spool (5 min).""" + from backend.wifi import watchdog_tick + from backend.destinations import taille_booth_spool await asyncio.sleep(15) + loop = asyncio.get_event_loop() + ticks_depuis_flush = 0 while True: - loop = asyncio.get_event_loop() - # Emails - total_mail = taille_spool() - if total_mail > 0: - log.info(f"Spool email: retry ({total_mail} en attente)") - nb, dests = await loop.run_in_executor(None, traiter_spool) - if nb > 0: - echoues = total_mail - nb - await loop.run_in_executor(None, envoyer_rapport_spool, nb, dests, echoues) - # Galerie booth - total_booth = taille_booth_spool() - if total_booth > 0: - log.info(f"Spool galerie: retry ({total_booth} en attente)") - await loop.run_in_executor(None, traiter_booth_spool) - await asyncio.sleep(RETRY_INTERVAL) + result = await loop.run_in_executor(None, watchdog_tick) + if result == "restored": + await _flush_spools() + ticks_depuis_flush = 0 + else: + ticks_depuis_flush += 1 + if ticks_depuis_flush >= RETRY_INTERVAL // WATCHDOG_INTERVAL: + has_spool = taille_spool() > 0 or taille_booth_spool() > 0 + if has_spool: + await _flush_spools() + ticks_depuis_flush = 0 + await asyncio.sleep(WATCHDOG_INTERVAL) async def tache_spool_demarrage(): diff --git a/backend/wifi.py b/backend/wifi.py index 1906ae8..a7ac188 100644 --- a/backend/wifi.py +++ b/backend/wifi.py @@ -201,3 +201,62 @@ def wifi_forget(ssid: str) -> dict: def wifi_get_password(ssid: str) -> str | None: return _charger_mdp().get(ssid) + + +# --- Watchdog connectivité --- + +_internet_ok = False + + +def check_internet(timeout: int = 5) -> bool: + """Teste la connectivité internet (DNS + HTTP rapide).""" + import socket + try: + socket.create_connection(("1.1.1.1", 53), timeout=timeout).close() + return True + except OSError: + return False + + +def watchdog_tick() -> str | None: + """Vérifie internet. Retourne 'restored' si passage offline→online, None sinon.""" + global _internet_ok + now_ok = check_internet() + if now_ok and not _internet_ok: + _internet_ok = True + log.info("Internet restauré — flush spool") + return "restored" + _internet_ok = now_ok + if not now_ok: + _tenter_reconnexion_wifi() + return None + + +def _tenter_reconnexion_wifi(): + """Si déconnecté du WiFi, tente de se reconnecter à un réseau enregistré visible.""" + status = wifi_status() + if status["connecte"]: + return + log.info("Pas de WiFi — scan des réseaux enregistrés") + saved = {n["ssid"] for n in wifi_saved_list()} + if not saved: + return + try: + r = subprocess.run( + ["nmcli", "-t", "-f", "SSID,SIGNAL", "dev", "wifi", "list", "--rescan", "yes"], + capture_output=True, text=True, timeout=15, + ) + candidates = [] + for line in r.stdout.strip().split("\n"): + p = _nmcli_split(line) + if len(p) >= 2 and p[0] in saved and p[1].isdigit(): + candidates.append((p[0], int(p[1]))) + candidates.sort(key=lambda x: -x[1]) + for ssid, sig in candidates: + log.info(f"Tentative reconnexion WiFi: {ssid} (signal {sig}%)") + result = wifi_connect(ssid) + if result.get("succes"): + log.info(f"Reconnecté à {ssid}") + return + except Exception as e: + log.warning(f"Reconnexion WiFi échouée: {e}") diff --git a/frontend/index.html b/frontend/index.html index 597aa7e..da777a9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1146,7 +1146,7 @@ - + diff --git a/frontend/js/camera.js b/frontend/js/camera.js index 61aa2e1..b87ea8b 100644 --- a/frontend/js/camera.js +++ b/frontend/js/camera.js @@ -22,13 +22,12 @@ function prechargerSurprise() { _surprisePreloaded = img; } -async function afficherSurprise() { +function montrerSurprise() { const surprise = (config || {}).surprise; - if (!surprise || !surprise.actif || !surprise.fichier) return; - const delai = surprise.delai_ms || 1500; + if (!surprise || !surprise.actif || !surprise.fichier) return false; const overlay = document.getElementById('surprise-overlay'); - if (!overlay) return; + if (!overlay) return false; if (surprise.type === 'video') { overlay.innerHTML = ''; @@ -39,26 +38,24 @@ async function afficherSurprise() { overlay.appendChild(_surprisePreloaded); } else { overlay.innerHTML = ''; - const img = overlay.querySelector('img'); - if (img && !img.complete) { - await new Promise(r => { img.onload = r; img.onerror = r; setTimeout(r, 500); }); - } } } overlay.style.opacity = '0'; overlay.classList.remove('cache'); - overlay.style.transition = 'opacity 0.2s'; + overlay.style.transition = 'opacity 0.15s'; overlay.style.opacity = '1'; + return true; +} - await new Promise(r => setTimeout(r, delai)); - +function cacherSurprise() { + const overlay = document.getElementById('surprise-overlay'); + if (!overlay) return; overlay.style.opacity = '0'; - await new Promise(r => setTimeout(r, 200)); - overlay.classList.add('cache'); - overlay.style.transition = ''; - overlay.style.opacity = ''; - - prechargerSurprise(); + setTimeout(() => { + overlay.classList.add('cache'); + overlay.style.transition = ''; + overlay.style.opacity = ''; + }, 200); } // --- Preview live --- @@ -320,7 +317,9 @@ async function lancerCapture() { await compteARebours(avecPreMessages); // Surprise : seulement en photo simple (pas strip/multi) - if (modeActuel === 'simple') await afficherSurprise(); + // Affiche la surprise PUIS capture pendant que les gens reagissent + const aSurprise = (modeActuel === 'simple') && montrerSurprise(); + if (aSurprise) await pause(500); // Flash blanc immédiat + lancer capture en parallèle const flash = document.getElementById('flash-blanc'); @@ -331,6 +330,7 @@ async function lancerCapture() { arreterPreview(); let resultat = await apiPost('/api/capturer').catch(() => null); + if (aSurprise) { cacherSurprise(); prechargerSurprise(); } if (!resultat && !resultat?.erreur) { flash.classList.add('cache');