feat: gestion WiFi — NVS credentials + scan + UI Gruvbox
- network.h/.cpp : lecture credentials NVS (Preferences) au boot,
fallback config.h si vide ; network_get_ssid() et
network_save_credentials() exposés
- web_server.cpp : 3 nouvelles routes REST
GET /api/wifi/current → SSID/IP/RSSI/modeAP
GET /api/wifi/networks → scan async + polling état
POST /api/wifi/connect → sauvegarde NVS + ESP.restart()
- index.html : modal WiFi (réseau actuel, liste scannée avec
barres signal ▁▂▃▄▅ + cadenas, formulaire SSID/mdp,
bouton œil, message redémarrage avec lien esp_jardin.local)
design Gruvbox Seventies cohérent avec le reste
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+346
@@ -237,6 +237,99 @@ input.fi:focus{border-color:var(--accent)}
|
||||
:root[data-theme="dark"] *::-webkit-scrollbar{width:6px;height:6px}
|
||||
:root[data-theme="dark"] *::-webkit-scrollbar-track{background:var(--bg-2)}
|
||||
:root[data-theme="dark"] *::-webkit-scrollbar-thumb{background:var(--bg-4);border-radius:3px}
|
||||
/* ── Modal WiFi ── */
|
||||
.wifi-modal-backdrop{
|
||||
display:none;position:fixed;inset:0;
|
||||
background:rgba(0,0,0,.7);z-index:200;
|
||||
align-items:flex-start;justify-content:center;
|
||||
padding-top:60px;overflow-y:auto;
|
||||
}
|
||||
.wifi-modal-backdrop.open{display:flex}
|
||||
.wifi-modal{
|
||||
background:var(--bg-2);border:1px solid var(--border-2);
|
||||
border-radius:12px;padding:24px;width:min(96vw,480px);
|
||||
box-shadow:0 8px 40px rgba(0,0,0,.6);
|
||||
display:flex;flex-direction:column;gap:18px;
|
||||
margin-bottom:60px;
|
||||
}
|
||||
.wifi-modal-header{
|
||||
display:flex;justify-content:space-between;align-items:center;
|
||||
}
|
||||
.wifi-modal-titre{font-size:15px;font-weight:700;color:var(--ink-1);letter-spacing:.04em}
|
||||
/* Bloc réseau actuel */
|
||||
.wifi-current{
|
||||
background:var(--bg-3);border-radius:10px;border:1px solid var(--border-1);
|
||||
padding:14px 16px;display:flex;flex-direction:column;gap:6px;
|
||||
}
|
||||
.wifi-current-titre{
|
||||
font-size:11px;font-weight:600;text-transform:uppercase;
|
||||
letter-spacing:.08em;color:var(--ink-3);margin-bottom:4px;
|
||||
}
|
||||
.wifi-current-row{display:flex;justify-content:space-between;align-items:center;gap:8px;}
|
||||
.wifi-current-label{font-size:12px;color:var(--ink-3);font-family:var(--font-ui);}
|
||||
.wifi-current-val{font-family:var(--font-mono);font-size:13px;color:var(--ink-1);}
|
||||
/* Liste réseaux */
|
||||
.wifi-scan-header{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
||||
.wifi-scan-titre{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3)}
|
||||
.wifi-networks-list{
|
||||
display:flex;flex-direction:column;gap:4px;
|
||||
max-height:220px;overflow-y:auto;
|
||||
background:var(--bg-3);border-radius:10px;border:1px solid var(--border-1);
|
||||
padding:6px;
|
||||
}
|
||||
.wifi-net-item{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
padding:8px 10px;border-radius:8px;cursor:pointer;
|
||||
transition:background .15s;border:1px solid transparent;
|
||||
}
|
||||
.wifi-net-item:hover{background:var(--bg-4);border-color:var(--border-2)}
|
||||
.wifi-net-ssid{flex:1;font-size:13px;color:var(--ink-1);font-family:var(--font-ui);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.wifi-net-signal{font-family:var(--font-terminal);font-size:14px;color:var(--blue);letter-spacing:-.05em;flex-shrink:0}
|
||||
.wifi-net-lock{font-size:12px;color:var(--ink-3);flex-shrink:0}
|
||||
.wifi-scan-msg{
|
||||
font-size:12px;color:var(--ink-3);font-family:var(--font-terminal);
|
||||
padding:12px;text-align:center;
|
||||
}
|
||||
/* Formulaire connexion */
|
||||
.wifi-form-titre{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-3)}
|
||||
.wifi-form{display:flex;flex-direction:column;gap:10px}
|
||||
.wifi-field{display:flex;flex-direction:column;gap:4px}
|
||||
.wifi-label{font-size:11px;color:var(--ink-3);letter-spacing:.04em}
|
||||
.wifi-input{
|
||||
background:var(--bg-3);border:1px solid var(--border-2);border-radius:8px;
|
||||
color:var(--ink-1);font-family:var(--font-mono);font-size:13px;
|
||||
padding:8px 12px;outline:none;width:100%;
|
||||
transition:border-color .15s;
|
||||
}
|
||||
.wifi-input:focus{border-color:var(--accent)}
|
||||
.wifi-pass-wrap{position:relative;display:flex;align-items:center}
|
||||
.wifi-pass-wrap .wifi-input{padding-right:38px}
|
||||
.wifi-pass-toggle{
|
||||
position:absolute;right:8px;background:none;border:none;
|
||||
cursor:pointer;color:var(--ink-3);font-size:16px;padding:2px;
|
||||
line-height:1;transition:color .15s;
|
||||
}
|
||||
.wifi-pass-toggle:hover{color:var(--ink-1)}
|
||||
.btn-wifi-save{
|
||||
background:var(--accent);color:#1a1a1a;font-weight:700;
|
||||
border:none;border-radius:9px;padding:10px 18px;
|
||||
cursor:pointer;font-size:13px;font-family:var(--font-ui);
|
||||
letter-spacing:.04em;transition:background .15s;margin-top:4px;
|
||||
}
|
||||
.btn-wifi-save:hover{background:var(--accent-soft);color:#fff}
|
||||
.btn-wifi-save:disabled{background:var(--bg-4);color:var(--ink-4);cursor:not-allowed}
|
||||
.wifi-feedback{
|
||||
font-size:12px;font-family:var(--font-terminal);min-height:18px;
|
||||
text-align:center;color:var(--ok);
|
||||
}
|
||||
.btn-scan{
|
||||
background:var(--bg-4);border:1px solid var(--border-2);
|
||||
color:var(--ink-2);cursor:pointer;border-radius:7px;
|
||||
padding:5px 12px;font-size:12px;font-family:var(--font-ui);
|
||||
transition:background .15s,color .15s;
|
||||
}
|
||||
.btn-scan:hover{background:var(--bg-3);color:var(--ink-1)}
|
||||
.btn-scan:disabled{cursor:not-allowed;color:var(--ink-4)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -248,6 +341,7 @@ input.fi:focus{border-color:var(--accent)}
|
||||
<div id="rssiBadge" class="rssi-badge">RSSI —</div>
|
||||
<div class="header-gap"></div>
|
||||
<button class="btn-icon btn-cfg" id="btnCfgMobile" title="Configuration">⚙</button>
|
||||
<button class="btn-icon" id="btnWifi" title="Gestion WiFi">📶</button>
|
||||
<button class="btn-icon" id="btnTheme" title="Basculer thème">☾</button>
|
||||
</header>
|
||||
|
||||
@@ -394,6 +488,73 @@ input.fi:focus{border-color:var(--accent)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ MODAL WIFI ═════════════════════════════════════════════════════ -->
|
||||
<div class="wifi-modal-backdrop" id="wifiModalBackdrop">
|
||||
<div class="wifi-modal">
|
||||
|
||||
<!-- En-tête -->
|
||||
<div class="wifi-modal-header">
|
||||
<span class="wifi-modal-titre">📶 Gestion WiFi</span>
|
||||
<button class="btn-close" id="btnCloseWifi">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Réseau actuel -->
|
||||
<div class="wifi-current">
|
||||
<div class="wifi-current-titre">Réseau actuel</div>
|
||||
<div class="wifi-current-row">
|
||||
<span class="wifi-current-label">SSID</span>
|
||||
<span class="wifi-current-val" id="wifiCurSsid">—</span>
|
||||
</div>
|
||||
<div class="wifi-current-row">
|
||||
<span class="wifi-current-label">Adresse IP</span>
|
||||
<span class="wifi-current-val" id="wifiCurIp">—</span>
|
||||
</div>
|
||||
<div class="wifi-current-row">
|
||||
<span class="wifi-current-label">RSSI</span>
|
||||
<span class="wifi-current-val" id="wifiCurRssi">—</span>
|
||||
</div>
|
||||
<div class="wifi-current-row">
|
||||
<span class="wifi-current-label">Mode</span>
|
||||
<span class="wifi-current-val" id="wifiCurMode">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scanner réseaux -->
|
||||
<div>
|
||||
<div class="wifi-scan-header">
|
||||
<span class="wifi-scan-titre">Réseaux disponibles</span>
|
||||
<button class="btn-scan" id="btnScan">🔎 Scanner</button>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<div class="wifi-networks-list" id="wifiNetworksList">
|
||||
<div class="wifi-scan-msg">Appuyer sur "Scanner" pour détecter les réseaux.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire connexion -->
|
||||
<div>
|
||||
<div class="wifi-form-titre" style="margin-bottom:10px">Connexion</div>
|
||||
<div class="wifi-form">
|
||||
<div class="wifi-field">
|
||||
<label class="wifi-label" for="wifiSsidInput">SSID</label>
|
||||
<input class="wifi-input" id="wifiSsidInput" type="text" placeholder="Nom du réseau WiFi" autocomplete="off">
|
||||
</div>
|
||||
<div class="wifi-field">
|
||||
<label class="wifi-label" for="wifiPassInput">Mot de passe</label>
|
||||
<div class="wifi-pass-wrap">
|
||||
<input class="wifi-input" id="wifiPassInput" type="password" placeholder="Mot de passe" autocomplete="new-password">
|
||||
<button class="wifi-pass-toggle" id="btnTogglePass" type="button" title="Afficher/masquer">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-wifi-save" id="btnWifiSave">Sauvegarder et redémarrer</button>
|
||||
<div class="wifi-feedback" id="wifiFeedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ SCRIPT ═════════════════════════════════════════════════════════ -->
|
||||
<script>
|
||||
(function(){
|
||||
@@ -831,6 +992,191 @@ function majHorloge(){
|
||||
document.getElementById('sbClock').textContent='esp_jardin | '+h+':'+m+':'+s;
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────
|
||||
GESTION WIFI
|
||||
────────────────────────────────────────────────────────── */
|
||||
var wifiScanInterval = null;
|
||||
|
||||
// Convertit un RSSI en barres Unicode ▁▂▃▄▅
|
||||
function rssiBarres(rssi) {
|
||||
if (rssi >= -55) return '▅▅▅▅▅';
|
||||
if (rssi >= -65) return '▄▄▄▄░';
|
||||
if (rssi >= -75) return '▃▃▃░░';
|
||||
if (rssi >= -85) return '▂▂░░░';
|
||||
return '▁░░░░';
|
||||
}
|
||||
|
||||
function ouvrirModalWifi() {
|
||||
document.getElementById('wifiModalBackdrop').classList.add('open');
|
||||
chargerWifiCurrent();
|
||||
}
|
||||
|
||||
function fermerModalWifi() {
|
||||
document.getElementById('wifiModalBackdrop').classList.remove('open');
|
||||
stopScanPolling();
|
||||
}
|
||||
|
||||
function chargerWifiCurrent() {
|
||||
fetch('/api/wifi/current')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d) {
|
||||
document.getElementById('wifiCurSsid').textContent = d.ssid || '—';
|
||||
document.getElementById('wifiCurIp').textContent = d.ip || '—';
|
||||
document.getElementById('wifiCurRssi').textContent = d.rssi ? d.rssi + ' dBm' : '—';
|
||||
document.getElementById('wifiCurMode').textContent = d.modeAP ? 'Point d\'accès (AP)' : 'Station (STA)';
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('wifiCurSsid').textContent = 'Erreur réseau';
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanPolling() {
|
||||
if (wifiScanInterval) {
|
||||
clearInterval(wifiScanInterval);
|
||||
wifiScanInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function demarrerScan() {
|
||||
var btn = document.getElementById('btnScan');
|
||||
var liste = document.getElementById('wifiNetworksList');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Scan…';
|
||||
liste.innerHTML = '<div class="wifi-scan-msg">Scan en cours…</div>';
|
||||
stopScanPolling();
|
||||
|
||||
// Premier appel immédiat
|
||||
fetch('/api/wifi/networks')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d) { traiterResultatScan(d); })
|
||||
.catch(function() {
|
||||
liste.innerHTML = '<div class="wifi-scan-msg" style="color:var(--err)">Erreur réseau.</div>';
|
||||
btn.disabled = false;
|
||||
btn.textContent = '⟳ Réessayer';
|
||||
});
|
||||
|
||||
// Polling toutes les 2s
|
||||
wifiScanInterval = setInterval(function() {
|
||||
fetch('/api/wifi/networks')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d) { traiterResultatScan(d); })
|
||||
.catch(function() { stopScanPolling(); });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function traiterResultatScan(d) {
|
||||
var btn = document.getElementById('btnScan');
|
||||
var liste = document.getElementById('wifiNetworksList');
|
||||
|
||||
if (d.etat === 'scan_en_cours') {
|
||||
// Continuer le polling
|
||||
return;
|
||||
}
|
||||
|
||||
// Scan terminé
|
||||
stopScanPolling();
|
||||
btn.disabled = false;
|
||||
btn.textContent = '↻ Rescanner';
|
||||
|
||||
if (!d.reseaux || d.reseaux.length === 0) {
|
||||
liste.innerHTML = '<div class="wifi-scan-msg">Aucun réseau détecté.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Tri par RSSI décroissant
|
||||
d.reseaux.sort(function(a, b) { return b.rssi - a.rssi; });
|
||||
|
||||
liste.innerHTML = '';
|
||||
d.reseaux.forEach(function(r) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'wifi-net-item';
|
||||
item.innerHTML =
|
||||
'<span class="wifi-net-ssid">' + escHtml(r.ssid) + '</span>' +
|
||||
'<span class="wifi-net-signal" title="' + r.rssi + ' dBm">' + rssiBarres(r.rssi) + '</span>' +
|
||||
'<span class="wifi-net-lock" title="' + (r.secure ? 'Sécurisé' : 'Ouvert') + '">' +
|
||||
(r.secure ? '🔒' : '🔓') +
|
||||
'</span>';
|
||||
item.addEventListener('click', function() {
|
||||
document.getElementById('wifiSsidInput').value = r.ssid;
|
||||
document.getElementById('wifiPassInput').value = '';
|
||||
document.getElementById('wifiPassInput').focus();
|
||||
});
|
||||
liste.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g,'&')
|
||||
.replace(/</g,'<')
|
||||
.replace(/>/g,'>')
|
||||
.replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function envoyerWifiConnect() {
|
||||
var ssid = document.getElementById('wifiSsidInput').value.trim();
|
||||
var pass = document.getElementById('wifiPassInput').value;
|
||||
var feedback = document.getElementById('wifiFeedback');
|
||||
var btnSave = document.getElementById('btnWifiSave');
|
||||
|
||||
if (!ssid) {
|
||||
feedback.style.color = 'var(--err)';
|
||||
feedback.textContent = 'Le SSID ne peut pas être vide.';
|
||||
return;
|
||||
}
|
||||
|
||||
btnSave.disabled = true;
|
||||
feedback.style.color = 'var(--warn)';
|
||||
feedback.textContent = 'Envoi en cours…';
|
||||
|
||||
fetch('/api/wifi/connect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ssid: ssid, password: pass })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.ok) {
|
||||
feedback.style.color = 'var(--ok)';
|
||||
feedback.innerHTML =
|
||||
'Redémarrage en cours… (~5s)<br>' +
|
||||
'<small style="color:var(--ink-3)">Reconnectez-vous sur <a href="http://esp_jardin.local" style="color:var(--blue)">esp_jardin.local</a></small>';
|
||||
} else {
|
||||
feedback.style.color = 'var(--err)';
|
||||
feedback.textContent = d.erreur || 'Erreur inconnue.';
|
||||
btnSave.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
// L'ESP redémarre et coupe la connexion — c'est normal
|
||||
feedback.style.color = 'var(--ok)';
|
||||
feedback.innerHTML =
|
||||
'Redémarrage en cours… (~5s)<br>' +
|
||||
'<small style="color:var(--ink-3)">Reconnectez-vous sur <a href="http://esp_jardin.local" style="color:var(--blue)">esp_jardin.local</a></small>';
|
||||
});
|
||||
}
|
||||
|
||||
// Événements modal WiFi
|
||||
document.getElementById('btnWifi').addEventListener('click', ouvrirModalWifi);
|
||||
document.getElementById('btnCloseWifi').addEventListener('click', fermerModalWifi);
|
||||
document.getElementById('wifiModalBackdrop').addEventListener('click', function(e) {
|
||||
if (e.target === this) fermerModalWifi();
|
||||
});
|
||||
document.getElementById('btnScan').addEventListener('click', demarrerScan);
|
||||
document.getElementById('btnWifiSave').addEventListener('click', envoyerWifiConnect);
|
||||
|
||||
// Bouton afficher/masquer mot de passe
|
||||
document.getElementById('btnTogglePass').addEventListener('click', function() {
|
||||
var inp = document.getElementById('wifiPassInput');
|
||||
if (inp.type === 'password') {
|
||||
inp.type = 'text';
|
||||
this.textContent = '🙈';
|
||||
} else {
|
||||
inp.type = 'password';
|
||||
this.textContent = '👁';
|
||||
}
|
||||
});
|
||||
|
||||
/* ──────────────────────────────────────────────────────────
|
||||
INITIALISATION
|
||||
────────────────────────────────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user