Admin WiFi : scan réseaux, connexion, mots de passe mémorisés
- Module backend/wifi.py : scan nmcli, connexion, mots de passe persistés dans data/wifi_passwords.json
- Gère la locale FR (oui/non), déduplique les AP multiples, prend le meilleur signal
- Onglet WiFi dans le menu admin avec statut, scan, clavier virtuel AZERTY complet
- Réseaux enregistrés avec bouton connecter/oublier et affichage temporaire du mdp
- API: /api/wifi/status, /api/wifi/scan, /api/wifi/connect, /api/wifi/saved, /api/wifi/password/{ssid}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ from backend.evenements import (
|
|||||||
)
|
)
|
||||||
from backend.evenements import DOSSIER_EVENEMENTS
|
from backend.evenements import DOSSIER_EVENEMENTS
|
||||||
from backend.eclairage import analyser_frame as analyser_eclairage, dernier_resultat as dernier_eclairage
|
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")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
|
||||||
log = logging.getLogger("photobooth")
|
log = logging.getLogger("photobooth")
|
||||||
@@ -1402,6 +1403,48 @@ async def api_quitter_navigateur():
|
|||||||
return {"succes": True}
|
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 ---
|
# --- WebSocket ---
|
||||||
|
|
||||||
@app.websocket("/ws")
|
@app.websocket("/ws")
|
||||||
|
|||||||
203
backend/wifi.py
Normal file
203
backend/wifi.py
Normal file
@@ -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)
|
||||||
@@ -2036,6 +2036,94 @@ h3 {
|
|||||||
to { opacity: 1; transform: translateY(0); }
|
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 */
|
/* Responsive tactile */
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.accueil-contenu h1 { font-size: 2.5rem; }
|
.accueil-contenu h1 { font-size: 2.5rem; }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
<meta name="google" content="notranslate">
|
<meta name="google" content="notranslate">
|
||||||
<meta http-equiv="Content-Language" content="fr">
|
<meta http-equiv="Content-Language" content="fr">
|
||||||
<link rel="stylesheet" href="/css/style.css?v=9">
|
<link rel="stylesheet" href="/css/style.css?v=10">
|
||||||
<link rel="stylesheet" href="/css/themes.css?v=2">
|
<link rel="stylesheet" href="/css/themes.css?v=2">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -374,6 +374,7 @@
|
|||||||
<button class="onglet" data-onglet="evenement">Evenement</button>
|
<button class="onglet" data-onglet="evenement">Evenement</button>
|
||||||
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
||||||
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive()">Eclairage</button>
|
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive()">Eclairage</button>
|
||||||
|
<button class="onglet" data-onglet="wifi" onclick="chargerWifi()">WiFi</button>
|
||||||
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
|
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -691,6 +692,112 @@
|
|||||||
<button class="btn-danger" onclick="confirmerExtinction()">Eteindre le systeme</button>
|
<button class="btn-danger" onclick="confirmerExtinction()">Eteindre le systeme</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Panneau WiFi -->
|
||||||
|
<div class="admin-panneau" id="panneau-wifi">
|
||||||
|
<div class="wifi-statut" id="wifi-statut">
|
||||||
|
<div class="wifi-statut-icon" id="wifi-statut-icon">📶</div>
|
||||||
|
<div class="wifi-statut-info">
|
||||||
|
<div id="wifi-ssid" class="wifi-ssid">--</div>
|
||||||
|
<div id="wifi-detail" class="wifi-detail">Non connecté</div>
|
||||||
|
</div>
|
||||||
|
<div id="wifi-signal-bars" class="wifi-signal-bars"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="champ-row" style="margin:1rem 0">
|
||||||
|
<button class="btn-action" onclick="scannerWifi()">🔍 Scanner les réseaux</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wifi-scan-loading" class="cache" style="text-align:center;padding:1rem;color:#888">
|
||||||
|
<div class="capture-spinner" style="display:inline-block;width:24px;height:24px"></div>
|
||||||
|
Recherche en cours...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wifi-liste-reseaux" class="wifi-liste"></div>
|
||||||
|
|
||||||
|
<h3 style="margin-top:1.5rem">Réseaux enregistrés</h3>
|
||||||
|
<div id="wifi-liste-enregistres" class="wifi-liste"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Popup saisie mot de passe WiFi -->
|
||||||
|
<div id="popup-wifi-mdp" class="popup-overlay cache">
|
||||||
|
<div class="popup-box popup-box-pave" style="max-width:600px">
|
||||||
|
<h3 id="wifi-mdp-titre">Mot de passe WiFi</h3>
|
||||||
|
<input type="text" id="wifi-mdp-input" placeholder="Mot de passe" autocomplete="off" readonly style="font-size:1.1rem;padding:.8rem;width:100%;border-radius:8px;border:1px solid rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:#fff;margin-bottom:.5rem">
|
||||||
|
<div id="wifi-mdp-erreur" class="mdp-erreur cache">Echec connexion</div>
|
||||||
|
<div class="clavier-virtuel" id="clavier-wifi">
|
||||||
|
<div class="clavier-ligne">
|
||||||
|
<button onclick="wifiTape('1')">1</button>
|
||||||
|
<button onclick="wifiTape('2')">2</button>
|
||||||
|
<button onclick="wifiTape('3')">3</button>
|
||||||
|
<button onclick="wifiTape('4')">4</button>
|
||||||
|
<button onclick="wifiTape('5')">5</button>
|
||||||
|
<button onclick="wifiTape('6')">6</button>
|
||||||
|
<button onclick="wifiTape('7')">7</button>
|
||||||
|
<button onclick="wifiTape('8')">8</button>
|
||||||
|
<button onclick="wifiTape('9')">9</button>
|
||||||
|
<button onclick="wifiTape('0')">0</button>
|
||||||
|
</div>
|
||||||
|
<div class="clavier-ligne" id="wifi-kb-row1">
|
||||||
|
<button onclick="wifiTape('a')">a</button>
|
||||||
|
<button onclick="wifiTape('z')">z</button>
|
||||||
|
<button onclick="wifiTape('e')">e</button>
|
||||||
|
<button onclick="wifiTape('r')">r</button>
|
||||||
|
<button onclick="wifiTape('t')">t</button>
|
||||||
|
<button onclick="wifiTape('y')">y</button>
|
||||||
|
<button onclick="wifiTape('u')">u</button>
|
||||||
|
<button onclick="wifiTape('i')">i</button>
|
||||||
|
<button onclick="wifiTape('o')">o</button>
|
||||||
|
<button onclick="wifiTape('p')">p</button>
|
||||||
|
</div>
|
||||||
|
<div class="clavier-ligne" id="wifi-kb-row2">
|
||||||
|
<button onclick="wifiTape('q')">q</button>
|
||||||
|
<button onclick="wifiTape('s')">s</button>
|
||||||
|
<button onclick="wifiTape('d')">d</button>
|
||||||
|
<button onclick="wifiTape('f')">f</button>
|
||||||
|
<button onclick="wifiTape('g')">g</button>
|
||||||
|
<button onclick="wifiTape('h')">h</button>
|
||||||
|
<button onclick="wifiTape('j')">j</button>
|
||||||
|
<button onclick="wifiTape('k')">k</button>
|
||||||
|
<button onclick="wifiTape('l')">l</button>
|
||||||
|
<button onclick="wifiTape('m')">m</button>
|
||||||
|
</div>
|
||||||
|
<div class="clavier-ligne" id="wifi-kb-row3">
|
||||||
|
<button onclick="wifiTape('w')">w</button>
|
||||||
|
<button onclick="wifiTape('x')">x</button>
|
||||||
|
<button onclick="wifiTape('c')">c</button>
|
||||||
|
<button onclick="wifiTape('v')">v</button>
|
||||||
|
<button onclick="wifiTape('b')">b</button>
|
||||||
|
<button onclick="wifiTape('n')">n</button>
|
||||||
|
<button onclick="wifiTape('!')">!</button>
|
||||||
|
<button onclick="wifiTape('.')">.</button>
|
||||||
|
<button onclick="wifiTape('-')">-</button>
|
||||||
|
<button onclick="wifiTape('_')">_</button>
|
||||||
|
</div>
|
||||||
|
<div class="clavier-ligne clavier-speciales">
|
||||||
|
<button class="clavier-wide" onclick="wifiToggleMaj()" id="wifi-btn-maj">⇧ MAJ</button>
|
||||||
|
<button class="clavier-wide clavier-espace" onclick="wifiTape(' ')">Espace</button>
|
||||||
|
<button class="clavier-wide" onclick="wifiEffacer()">⌫</button>
|
||||||
|
</div>
|
||||||
|
<div class="clavier-ligne clavier-speciales">
|
||||||
|
<button onclick="wifiTape('@')">@</button>
|
||||||
|
<button onclick="wifiTape('#')">#</button>
|
||||||
|
<button onclick="wifiTape('$')">$</button>
|
||||||
|
<button onclick="wifiTape('%')">%</button>
|
||||||
|
<button onclick="wifiTape('&')">&</button>
|
||||||
|
<button onclick="wifiTape('*')">*</button>
|
||||||
|
<button onclick="wifiTape('+')" >+</button>
|
||||||
|
<button onclick="wifiTape('=')">=</button>
|
||||||
|
<button onclick="wifiTape('/')">/</button>
|
||||||
|
<button onclick="wifiTape('?')">?</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:1rem;margin-top:.8rem">
|
||||||
|
<button class="btn-action" onclick="validerWifiMdp()" id="wifi-btn-connecter">Connecter</button>
|
||||||
|
<button class="btn-secondaire" onclick="fermerPopupWifi()">Annuler</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Panneau Infos systeme -->
|
<!-- Panneau Infos systeme -->
|
||||||
<div class="admin-panneau" id="panneau-eclairage">
|
<div class="admin-panneau" id="panneau-eclairage">
|
||||||
<div style="display:flex;gap:1rem;align-items:flex-start;flex-wrap:wrap">
|
<div style="display:flex;gap:1rem;align-items:flex-start;flex-wrap:wrap">
|
||||||
@@ -809,6 +916,6 @@
|
|||||||
<script src="/js/effects.js?v=4"></script>
|
<script src="/js/effects.js?v=4"></script>
|
||||||
<script src="/js/gallery.js?v=3"></script>
|
<script src="/js/gallery.js?v=3"></script>
|
||||||
<script src="/js/share.js?v=7"></script>
|
<script src="/js/share.js?v=7"></script>
|
||||||
<script src="/js/admin.js?v=8"></script>
|
<script src="/js/admin.js?v=9"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
`<div class="bar${signal >= s ? ' actif' : ''}"></div>`
|
||||||
|
).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 = '<p style="color:#888;padding:.5rem">Aucun réseau détecté</p>';
|
||||||
|
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 = `
|
||||||
|
<div class="wifi-reseau-icons">
|
||||||
|
${r.securise ? '🔒' : '🔓'}
|
||||||
|
</div>
|
||||||
|
<div class="wifi-reseau-ssid">${_escHtml(r.ssid)}</div>
|
||||||
|
<div class="wifi-reseau-meta">${r.securite || 'Ouvert'}</div>
|
||||||
|
${r.actif ? '<span class="wifi-badge-connecte">Connecté</span>' : ''}
|
||||||
|
${!r.actif && r.enregistre ? '<span class="wifi-badge-enregistre">Enregistré</span>' : ''}
|
||||||
|
<div class="wifi-signal-bars">${_signalBars(r.signal)}</div>
|
||||||
|
`;
|
||||||
|
liste.appendChild(div);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loading.classList.add('cache');
|
||||||
|
liste.innerHTML = '<p style="color:#f44;padding:.5rem">Erreur scan WiFi</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<p style="color:#888;padding:.5rem;font-size:.85rem">Aucun réseau enregistré</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const s of saved) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'wifi-enregistre-item';
|
||||||
|
div.innerHTML = `
|
||||||
|
<span class="wifi-reseau-ssid">${_escHtml(s.ssid)}</span>
|
||||||
|
${s.mdp_connu ? `<span class="wifi-enregistre-mdp" onclick="afficherMdpWifi('${_escAttr(s.ssid)}', this)" title="Voir le mot de passe">👁 mdp</span>` : ''}
|
||||||
|
<button class="btn-secondaire btn-petit" onclick="connecterWifiDirect('${_escAttr(s.ssid)}')">Connecter</button>
|
||||||
|
<button class="btn-danger btn-petit" onclick="oublierWifi('${_escAttr(s.ssid)}')">Oublier</button>
|
||||||
|
`;
|
||||||
|
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() {
|
async function chargerDiagCamera() {
|
||||||
const el = document.getElementById('diag-camera-contenu');
|
const el = document.getElementById('diag-camera-contenu');
|
||||||
el.textContent = 'Test en cours...';
|
el.textContent = 'Test en cours...';
|
||||||
|
|||||||
Reference in New Issue
Block a user