Resilience : ecran pause, reconnexion WS auto, watchdog systemd, WiFi monitor

- WebSocket : reconnexion avec backoff exponentiel au lieu de reload page
- Ecran pause avec tasse de cafe quand le backend est injoignable
- Endpoint /api/health pour monitoring externe
- Watchdog systemd (sd_notify READY=1 + WATCHDOG=1 toutes les 10s)
- Service systemd durci (Type=notify, WatchdogSec=30, KillMode, limites)
- WiFi watchdog : script + timer systemd, verifie la gateway toutes les 60s
- Destinations : fallback url_tunnel (WireGuard) en priorite

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 19:16:39 +02:00
parent 48f54ab70a
commit 4e431507fc
10 changed files with 273 additions and 99 deletions

View File

@@ -2,6 +2,7 @@ import asyncio
import base64
import json
import logging
import os
import threading
import time
from pathlib import Path
@@ -273,6 +274,24 @@ def _appliquer_config_camera():
camera.configurer_flash(flash_integre)
async def _watchdog_systemd():
"""Notifie systemd que le service est vivant + surveille la santé."""
try:
import socket
addr = os.environ.get("NOTIFY_SOCKET")
if not addr:
return
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
if addr[0] == "@":
addr = "\0" + addr[1:]
sock.sendto(b"READY=1", addr)
while True:
await asyncio.sleep(10)
sock.sendto(b"WATCHDOG=1", addr)
except Exception:
pass
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Demarrage et arret de l'application."""
@@ -287,10 +306,12 @@ async def lifespan(app: FastAPI):
task_dslr = asyncio.create_task(surveiller_dslr())
task_push = asyncio.create_task(_pusher_preview())
task_spool = asyncio.create_task(tache_spool_demarrage())
task_watchdog = asyncio.create_task(_watchdog_systemd())
yield
task_dslr.cancel()
task_push.cancel()
task_spool.cancel()
task_watchdog.cancel()
log.info("Arret du photobooth")
camera.deconnecter()
@@ -311,6 +332,18 @@ async def page_admin():
return FileResponse(str(RACINE / "frontend" / "index.html"))
@app.get("/api/health")
async def api_health():
return {
"status": "ok",
"camera": camera.mode if camera.connectee else "disconnected",
"clients": len(clients_ws),
"uptime": int(time.time() - _start_time),
}
_start_time = time.time()
@app.get("/")
async def page_principale(request: Request):
from fastapi.responses import RedirectResponse