diff --git a/backend/main.py b/backend/main.py index 96854e3..10a8a12 100644 --- a/backend/main.py +++ b/backend/main.py @@ -38,6 +38,7 @@ from backend.evenements import ( ) from backend.evenements import DOSSIER_EVENEMENTS from backend.eclairage import analyser_frame as analyser_eclairage, dernier_resultat as dernier_eclairage +from backend.wifi import wifi_status, wifi_scan, wifi_connect, wifi_saved_list, wifi_forget, wifi_get_password logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s") log = logging.getLogger("photobooth") @@ -1402,6 +1403,48 @@ async def api_quitter_navigateur(): return {"succes": True} +# --- API WiFi --- + +@app.get("/api/wifi/status") +async def api_wifi_status(): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, wifi_status) + + +@app.get("/api/wifi/scan") +async def api_wifi_scan(): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, wifi_scan) + + +@app.post("/api/wifi/connect") +async def api_wifi_connect(donnees: dict): + ssid = donnees.get("ssid", "") + password = donnees.get("password") + if not ssid: + return JSONResponse({"erreur": "SSID requis"}, status_code=400) + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, wifi_connect, ssid, password) + + +@app.get("/api/wifi/saved") +async def api_wifi_saved(): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, wifi_saved_list) + + +@app.delete("/api/wifi/saved/{ssid}") +async def api_wifi_forget(ssid: str): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, wifi_forget, ssid) + + +@app.get("/api/wifi/password/{ssid}") +async def api_wifi_password(ssid: str): + mdp = wifi_get_password(ssid) + return {"ssid": ssid, "password": mdp} + + # --- WebSocket --- @app.websocket("/ws") diff --git a/backend/wifi.py b/backend/wifi.py new file mode 100644 index 0000000..1906ae8 --- /dev/null +++ b/backend/wifi.py @@ -0,0 +1,203 @@ +"""Gestion WiFi via nmcli — scan, connexion, réseaux enregistrés.""" + +import json +import logging +import subprocess +from pathlib import Path + +log = logging.getLogger("photobooth.wifi") + +WIFI_PASSWORDS_FILE = Path(__file__).parent.parent / "data" / "wifi_passwords.json" + + +def _nmcli_split(line): + """Split nmcli -t output, gère les ':' échappés par '\\'.""" + parts = [] + cur = [] + i = 0 + while i < len(line): + if line[i] == '\\' and i + 1 < len(line): + cur.append(line[i + 1]) + i += 2 + elif line[i] == ':': + parts.append(''.join(cur)) + cur = [] + i += 1 + else: + cur.append(line[i]) + i += 1 + parts.append(''.join(cur)) + return parts + + +def _charger_mdp() -> dict: + if WIFI_PASSWORDS_FILE.exists(): + try: + return json.loads(WIFI_PASSWORDS_FILE.read_text()) + except Exception: + return {} + return {} + + +def _sauvegarder_mdp(data: dict): + WIFI_PASSWORDS_FILE.parent.mkdir(parents=True, exist_ok=True) + WIFI_PASSWORDS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False)) + + +def wifi_status() -> dict: + result = {"connecte": False, "ssid": None, "signal": None, "ip": None, "interface": None} + try: + r = subprocess.run( + ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], + capture_output=True, text=True, timeout=10, + ) + for line in r.stdout.strip().split("\n"): + p = _nmcli_split(line) + if len(p) >= 4 and p[1] == "wifi" and p[2] == "connected": + result.update(connecte=True, interface=p[0], ssid=p[3]) + break + + if result["connecte"] and result["interface"]: + r2 = subprocess.run( + ["nmcli", "-t", "-f", "IP4.ADDRESS", "device", "show", result["interface"]], + capture_output=True, text=True, timeout=5, + ) + for line in r2.stdout.strip().split("\n"): + if "IP4.ADDRESS" in line: + result["ip"] = line.split(":", 1)[1].strip().split("/")[0] + break + + r3 = subprocess.run( + ["nmcli", "-t", "-f", "ACTIVE,SIGNAL", "dev", "wifi", "list"], + capture_output=True, text=True, timeout=5, + ) + for line in r3.stdout.strip().split("\n"): + p = _nmcli_split(line) + if len(p) >= 2 and p[0] in ("yes", "oui") and p[1].isdigit(): + result["signal"] = int(p[1]) + break + except Exception as e: + log.warning(f"wifi_status: {e}") + return result + + +def wifi_scan() -> list: + saved_ssids = {n["ssid"] for n in wifi_saved_list()} + networks = [] + try: + r = subprocess.run( + ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,ACTIVE", "dev", "wifi", "list", "--rescan", "yes"], + capture_output=True, text=True, timeout=20, + ) + merged = {} + for line in r.stdout.strip().split("\n"): + if not line.strip(): + continue + p = _nmcli_split(line) + if len(p) < 4: + continue + ssid = p[0] + if not ssid: + continue + sig = int(p[1]) if p[1].isdigit() else 0 + is_active = p[3] in ("yes", "oui") + if ssid in merged: + if sig > merged[ssid]["signal"]: + merged[ssid]["signal"] = sig + if is_active: + merged[ssid]["actif"] = True + else: + merged[ssid] = { + "ssid": ssid, + "signal": sig, + "securise": bool(p[2] and p[2] not in ("--", "")), + "securite": p[2] if p[2] not in ("--", "") else "", + "actif": is_active, + "enregistre": ssid in saved_ssids, + } + networks = list(merged.values()) + networks.sort(key=lambda n: (-n["actif"], -n["signal"])) + except Exception as e: + log.warning(f"wifi_scan: {e}") + return networks + + +def wifi_connect(ssid: str, password: str | None = None) -> dict: + mdp = _charger_mdp() + + if not password and ssid in mdp: + password = mdp[ssid] + + # Réseau enregistré sans nouveau mot de passe → activer la connexion existante + if not password: + try: + r = subprocess.run( + ["nmcli", "connection", "up", ssid], + capture_output=True, text=True, timeout=30, + ) + if r.returncode == 0: + return {"succes": True, "message": f"Connecté à {ssid}"} + except Exception: + pass + + if password: + try: + # Supprimer l'ancienne connexion pour éviter les doublons + subprocess.run(["nmcli", "connection", "delete", ssid], + capture_output=True, text=True, timeout=5) + r = subprocess.run( + ["nmcli", "dev", "wifi", "connect", ssid, "password", password], + capture_output=True, text=True, timeout=30, + ) + if r.returncode == 0: + mdp[ssid] = password + _sauvegarder_mdp(mdp) + return {"succes": True, "message": f"Connecté à {ssid}"} + err = r.stderr.strip() or r.stdout.strip() + if "secret" in err.lower() or "no suitable" in err.lower(): + return {"succes": False, "message": "Mot de passe incorrect"} + return {"succes": False, "message": err} + except subprocess.TimeoutExpired: + return {"succes": False, "message": "Timeout — le réseau ne répond pas"} + except Exception as e: + return {"succes": False, "message": str(e)} + + return {"succes": False, "message": "Mot de passe requis"} + + +def wifi_saved_list() -> list: + saved = [] + mdp = _charger_mdp() + try: + r = subprocess.run( + ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"], + capture_output=True, text=True, timeout=5, + ) + for line in r.stdout.strip().split("\n"): + p = _nmcli_split(line) + if len(p) >= 2 and p[1] == "802-11-wireless": + saved.append({"ssid": p[0], "mdp_connu": p[0] in mdp}) + except Exception as e: + log.warning(f"wifi_saved_list: {e}") + return saved + + +def wifi_forget(ssid: str) -> dict: + mdp = _charger_mdp() + try: + r = subprocess.run( + ["nmcli", "connection", "delete", ssid], + capture_output=True, text=True, timeout=10, + ) + if ssid in mdp: + del mdp[ssid] + _sauvegarder_mdp(mdp) + if r.returncode == 0: + return {"succes": True} + return {"succes": False, "message": r.stderr.strip()} + except Exception as e: + return {"succes": False, "message": str(e)} + + +def wifi_get_password(ssid: str) -> str | None: + return _charger_mdp().get(ssid) diff --git a/frontend/css/style.css b/frontend/css/style.css index e35e2f0..495d503 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -2036,6 +2036,94 @@ h3 { to { opacity: 1; transform: translateY(0); } } +/* === WiFi === */ +.wifi-statut { + display: flex; + align-items: center; + gap: 1rem; + background: rgba(255,255,255,.04); + border-radius: var(--rayon); + padding: 1rem 1.2rem; +} +.wifi-statut-icon { font-size: 2rem; } +.wifi-statut-info { flex: 1; } +.wifi-ssid { font-size: 1.1rem; font-weight: 600; } +.wifi-detail { font-size: .8rem; color: var(--texte-secondaire); margin-top: .15rem; } + +.wifi-signal-bars { + display: flex; + align-items: flex-end; + gap: 3px; + height: 24px; +} +.wifi-signal-bars .bar { + width: 5px; + border-radius: 2px; + background: rgba(255,255,255,.15); + transition: background .3s; +} +.wifi-signal-bars .bar.actif { background: var(--succes); } +.wifi-signal-bars .bar:nth-child(1) { height: 6px; } +.wifi-signal-bars .bar:nth-child(2) { height: 11px; } +.wifi-signal-bars .bar:nth-child(3) { height: 16px; } +.wifi-signal-bars .bar:nth-child(4) { height: 22px; } + +.wifi-liste { display: flex; flex-direction: column; gap: .4rem; } + +.wifi-reseau { + display: flex; + align-items: center; + gap: .8rem; + padding: .7rem 1rem; + background: rgba(255,255,255,.03); + border: 1px solid rgba(255,255,255,.06); + border-radius: 10px; + cursor: pointer; + transition: background .2s, border-color .2s; +} +.wifi-reseau:active { background: rgba(255,255,255,.08); } +.wifi-reseau.wifi-actif { + border-color: var(--succes); + background: rgba(76,175,80,.08); +} +.wifi-reseau-ssid { flex: 1; font-size: .95rem; font-weight: 500; } +.wifi-reseau-meta { font-size: .75rem; color: var(--texte-secondaire); } +.wifi-reseau-icons { display: flex; align-items: center; gap: .5rem; font-size: .9rem; } +.wifi-badge-connecte { + font-size: .7rem; + padding: .15rem .5rem; + border-radius: 6px; + background: rgba(76,175,80,.2); + color: var(--succes); + font-weight: 600; +} +.wifi-badge-enregistre { + font-size: .7rem; + padding: .15rem .5rem; + border-radius: 6px; + background: rgba(255,255,255,.08); + color: var(--texte-secondaire); +} +.wifi-enregistre-item { + display: flex; + align-items: center; + gap: .8rem; + padding: .6rem 1rem; + background: rgba(255,255,255,.03); + border: 1px solid rgba(255,255,255,.06); + border-radius: 10px; +} +.wifi-enregistre-item .wifi-reseau-ssid { flex: 1; } +.wifi-enregistre-mdp { + font-family: monospace; + font-size: .8rem; + color: var(--texte-secondaire); + background: rgba(255,255,255,.05); + padding: .2rem .5rem; + border-radius: 4px; + cursor: pointer; +} + /* Responsive tactile */ @media (max-width: 800px) { .accueil-contenu h1 { font-size: 2.5rem; } diff --git a/frontend/index.html b/frontend/index.html index ee95f8f..58bff7a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ - + @@ -374,6 +374,7 @@ + @@ -691,6 +692,112 @@ + +
+
+
📶
+
+
--
+
Non connecté
+
+
+
+ +
+ +
+ +
+
+ Recherche en cours... +
+ +
+ +

Réseaux enregistrés

+
+
+ + + +
@@ -809,6 +916,6 @@ - + diff --git a/frontend/js/admin.js b/frontend/js/admin.js index 81ca67b..a46c299 100644 --- a/frontend/js/admin.js +++ b/frontend/js/admin.js @@ -1159,6 +1159,210 @@ async function regenererBoothPassword() { } } +// === WIFI === + +let _wifiMajActif = false; +let _wifiSsidCible = null; + +function _signalBars(signal, actif) { + const seuils = [20, 40, 60, 80]; + return seuils.map(s => + `
` + ).join(''); +} + +async function chargerWifi() { + const statut = await apiGet('/api/wifi/status'); + const icon = document.getElementById('wifi-statut-icon'); + const ssid = document.getElementById('wifi-ssid'); + const detail = document.getElementById('wifi-detail'); + const bars = document.getElementById('wifi-signal-bars'); + + if (statut.connecte) { + icon.textContent = '\u{1F4F6}'; + ssid.textContent = statut.ssid; + detail.textContent = `IP : ${statut.ip || '—'}`; + bars.innerHTML = _signalBars(statut.signal || 0, true); + } else { + icon.textContent = '\u{1F4F4}'; + ssid.textContent = 'Non connecté'; + detail.textContent = 'Aucun réseau WiFi'; + bars.innerHTML = _signalBars(0, false); + } + + await chargerWifiEnregistres(); +} + +async function scannerWifi() { + const liste = document.getElementById('wifi-liste-reseaux'); + const loading = document.getElementById('wifi-scan-loading'); + liste.innerHTML = ''; + loading.classList.remove('cache'); + + try { + const reseaux = await apiGet('/api/wifi/scan'); + loading.classList.add('cache'); + if (reseaux.length === 0) { + liste.innerHTML = '

Aucun réseau détecté

'; + return; + } + for (const r of reseaux) { + const div = document.createElement('div'); + div.className = 'wifi-reseau' + (r.actif ? ' wifi-actif' : ''); + div.onclick = () => onWifiReseauClick(r); + div.innerHTML = ` +
+ ${r.securise ? '🔒' : '🔓'} +
+
${_escHtml(r.ssid)}
+
${r.securite || 'Ouvert'}
+ ${r.actif ? 'Connecté' : ''} + ${!r.actif && r.enregistre ? 'Enregistré' : ''} +
${_signalBars(r.signal)}
+ `; + liste.appendChild(div); + } + } catch (e) { + loading.classList.add('cache'); + liste.innerHTML = '

Erreur scan WiFi

'; + } +} + +function onWifiReseauClick(reseau) { + if (reseau.actif) return; + if (reseau.enregistre) { + connecterWifiDirect(reseau.ssid); + } else if (!reseau.securise) { + connecterWifiDirect(reseau.ssid); + } else { + afficherPopupWifiMdp(reseau.ssid); + } +} + +async function connecterWifiDirect(ssid) { + afficherStatut(`Connexion à ${ssid}...`, 'succes'); + const r = await apiPost('/api/wifi/connect', { ssid }); + if (r.succes) { + afficherStatut(r.message, 'succes'); + await chargerWifi(); + await scannerWifi(); + } else { + afficherStatut(r.message || 'Échec connexion', 'erreur'); + afficherPopupWifiMdp(ssid); + } +} + +function afficherPopupWifiMdp(ssid) { + _wifiSsidCible = ssid; + document.getElementById('wifi-mdp-titre').textContent = `WiFi : ${ssid}`; + document.getElementById('wifi-mdp-input').value = ''; + document.getElementById('wifi-mdp-erreur').classList.add('cache'); + document.getElementById('popup-wifi-mdp').classList.remove('cache'); + + apiGet(`/api/wifi/password/${encodeURIComponent(ssid)}`).then(data => { + if (data.password) { + document.getElementById('wifi-mdp-input').value = data.password; + } + }).catch(() => {}); +} + +function fermerPopupWifi() { + document.getElementById('popup-wifi-mdp').classList.add('cache'); + _wifiSsidCible = null; +} + +let _wifiMaj = false; +function wifiTape(c) { + const input = document.getElementById('wifi-mdp-input'); + if (_wifiMaj && c.length === 1 && c.match(/[a-z]/)) c = c.toUpperCase(); + input.value += c; + if (_wifiMaj) { _wifiMaj = false; _wifiMajUI(); } +} + +function wifiEffacer() { + const input = document.getElementById('wifi-mdp-input'); + input.value = input.value.slice(0, -1); +} + +function wifiToggleMaj() { + _wifiMaj = !_wifiMaj; + _wifiMajUI(); +} + +function _wifiMajUI() { + const btn = document.getElementById('wifi-btn-maj'); + btn.style.background = _wifiMaj ? 'var(--primaire)' : ''; + btn.style.color = _wifiMaj ? '#fff' : ''; + ['wifi-kb-row1', 'wifi-kb-row2', 'wifi-kb-row3'].forEach(id => { + document.getElementById(id).querySelectorAll('button').forEach(b => { + const c = b.textContent; + if (c.length === 1 && c.match(/[a-zA-Z]/)) { + b.textContent = _wifiMaj ? c.toUpperCase() : c.toLowerCase(); + } + }); + }); +} + +async function validerWifiMdp() { + const mdp = document.getElementById('wifi-mdp-input').value; + if (!mdp) return; + const btn = document.getElementById('wifi-btn-connecter'); + btn.textContent = 'Connexion...'; + btn.disabled = true; + const r = await apiPost('/api/wifi/connect', { ssid: _wifiSsidCible, password: mdp }); + btn.textContent = 'Connecter'; + btn.disabled = false; + if (r.succes) { + fermerPopupWifi(); + afficherStatut(r.message, 'succes'); + await chargerWifi(); + await scannerWifi(); + } else { + const err = document.getElementById('wifi-mdp-erreur'); + err.textContent = r.message || 'Échec connexion'; + err.classList.remove('cache'); + setTimeout(() => err.classList.add('cache'), 4000); + } +} + +async function chargerWifiEnregistres() { + const liste = document.getElementById('wifi-liste-enregistres'); + const saved = await apiGet('/api/wifi/saved'); + liste.innerHTML = ''; + if (saved.length === 0) { + liste.innerHTML = '

Aucun réseau enregistré

'; + return; + } + for (const s of saved) { + const div = document.createElement('div'); + div.className = 'wifi-enregistre-item'; + div.innerHTML = ` + ${_escHtml(s.ssid)} + ${s.mdp_connu ? `👁 mdp` : ''} + + + `; + liste.appendChild(div); + } +} + +async function afficherMdpWifi(ssid, el) { + const data = await apiGet(`/api/wifi/password/${encodeURIComponent(ssid)}`); + if (data.password) { + el.textContent = data.password; + setTimeout(() => { el.innerHTML = '👁 mdp'; }, 5000); + } +} + +async function oublierWifi(ssid) { + await fetch(`/api/wifi/saved/${encodeURIComponent(ssid)}`, { method: 'DELETE' }); + afficherStatut(`${ssid} oublié`, 'succes'); + await chargerWifiEnregistres(); +} + +function _escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } +function _escAttr(s) { return s.replace(/'/g, "\\'").replace(/"/g, '"'); } + async function chargerDiagCamera() { const el = document.getElementById('diag-camera-contenu'); el.textContent = 'Test en cours...';