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');