Files
esp_jardin/data/index.html
T
gilles d9d2db8b5e feat: interface web temps réel complète (WebSocket, Chart.js, thème Gruvbox Seventies)
Page auto-contenue LittleFS (31 Ko) : 3 cartes sondes temps réel, graphique 24h Chart.js,
panneau statut/config, bascule dark/light, status bar, reconnexion WS automatique, responsive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:53:16 +02:00

829 lines
31 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP Jardin — Monitoring</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<style>
/* ── Variables thème dark ── */
:root[data-theme="dark"]{
--accent:#fe8019;--accent-soft:#d65d0e;--accent-tint:rgba(254,128,25,0.12);
--bg-1:#2a231d;--bg-2:#322920;--bg-3:#3c332a;--bg-4:#463c30;
--ink-1:#f2e5c7;--ink-2:#d5c4a1;--ink-3:#a89984;--ink-4:#6e6459;
--ok:#4dbb26;--warn:#fabd2f;--err:#fb4934;--blue:#3db0d1;--purple:#c882c8;
--border-1:rgba(255,255,255,0.06);--border-2:rgba(255,255,255,0.12);
--shadow-tile:0 1px 0 rgba(255,255,255,0.06) inset,0 4px 16px rgba(0,0,0,0.45);
--font-ui:'Inter',system-ui,sans-serif;
--font-mono:'JetBrains Mono',monospace;
--font-terminal:'Share Tech Mono',monospace;
}
/* ── Variables thème light ── */
:root[data-theme="light"]{
--accent:#af3a03;--accent-soft:#8a2d02;--accent-tint:rgba(175,58,3,0.10);
--bg-1:#f9f5f0;--bg-2:#f0ebe4;--bg-3:#e8e2da;--bg-4:#ddd6cc;
--ink-1:#3c3228;--ink-2:#5a4e42;--ink-3:#7c6e60;--ink-4:#a0907e;
--ok:#3c911c;--warn:#b57614;--err:#9d0006;--blue:#2d82a3;--purple:#8c468c;
--border-1:rgba(0,0,0,0.06);--border-2:rgba(0,0,0,0.14);
--shadow-tile:0 1px 0 rgba(255,255,255,0.8) inset,0 2px 8px rgba(0,0,0,0.15);
--font-ui:'Inter',system-ui,sans-serif;
--font-mono:'JetBrains Mono',monospace;
--font-terminal:'Share Tech Mono',monospace;
}
/* ── Reset ── */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-size:14px;line-height:1.5}
/* ── Layout global ── */
body{display:flex;flex-direction:column;min-height:100vh}
/* ── Header ── */
header{
display:flex;align-items:center;gap:12px;
padding:10px 20px;
background:var(--bg-2);
border-bottom:1px solid var(--border-2);
flex-shrink:0;
}
.logo{font-size:18px;font-weight:700;color:var(--accent);letter-spacing:.04em;white-space:nowrap}
.logo span{color:var(--ink-2);font-weight:400}
.header-gap{flex:1}
/* LED état WS */
.ws-led{
width:10px;height:10px;border-radius:50%;
background:var(--ink-4);flex-shrink:0;
transition:background .3s;
}
.ws-led.ok{background:var(--ok)}
.ws-led.err{background:var(--err);animation:pulse-led 1s ease-in-out infinite}
.ws-led.warn{background:var(--warn);animation:pulse-led .7s ease-in-out infinite}
@keyframes pulse-led{0%,100%{opacity:1}50%{opacity:.3}}
.rssi-badge{
font-family:var(--font-mono);font-size:11px;color:var(--ink-3);
background:var(--bg-3);border:1px solid var(--border-1);
padding:2px 8px;border-radius:6px;white-space:nowrap;
}
/* Boutons header */
.btn-icon{
background:var(--bg-3);border:1px solid var(--border-1);
color:var(--ink-2);cursor:pointer;border-radius:8px;
padding:6px 10px;font-size:13px;font-family:var(--font-ui);
transition:background .15s,color .15s;
}
.btn-icon:hover{background:var(--bg-4);color:var(--ink-1)}
/* ── Contenu principal ── */
.main-wrap{display:flex;flex:1;overflow:hidden;min-height:0}
main{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:16px;min-width:0}
/* ── Grille sondes ── */
.sondes-grid{
display:grid;grid-template-columns:repeat(3,1fr);gap:14px;
}
/* ── Carte sonde ── */
.sonde-card{
background:var(--bg-3);border-radius:11px;
border:1px solid var(--border-1);
box-shadow:var(--shadow-tile);
padding:16px 18px;
display:flex;flex-direction:column;gap:6px;
transition:border-color .3s;
}
.sonde-card.erreur{border-color:var(--err)}
.sonde-nom{
font-size:11px;font-weight:600;letter-spacing:.08em;
text-transform:uppercase;color:var(--ink-3);
}
.sonde-temp{
font-family:var(--font-mono);font-size:36px;font-weight:700;
color:var(--accent);line-height:1;
}
.sonde-temp .unite{font-size:18px;color:var(--ink-3);margin-left:2px}
.sonde-erreur-txt{
font-size:13px;color:var(--err);font-weight:500;
}
.sonde-status{
font-size:11px;color:var(--ink-4);margin-top:2px;
}
/* ── Carte graphique ── */
.chart-card{
background:var(--bg-3);border-radius:11px;
border:1px solid var(--border-1);
box-shadow:var(--shadow-tile);
padding:16px 18px;flex:1;
}
.card-titre{
font-size:11px;font-weight:600;letter-spacing:.08em;
text-transform:uppercase;color:var(--ink-3);margin-bottom:12px;
}
.chart-wrap{position:relative;height:220px}
/* ── Panneau droit ── */
.panel-right{
width:280px;flex-shrink:0;
background:var(--bg-2);
border-left:1px solid var(--border-2);
overflow-y:auto;padding:14px;
display:flex;flex-direction:column;gap:14px;
}
/* ── Tuile statut ── */
.stat-tile{
background:var(--bg-3);border-radius:10px;
border:1px solid var(--border-1);
box-shadow:var(--shadow-tile);
padding:12px 14px;
}
.stat-tile .tile-titre{
font-size:11px;font-weight:600;letter-spacing:.08em;
text-transform:uppercase;color:var(--ink-3);margin-bottom:8px;
}
.stat-row{
display:flex;justify-content:space-between;align-items:center;
padding:3px 0;border-bottom:1px solid var(--border-1);
}
.stat-row:last-child{border-bottom:none}
.stat-label{font-size:12px;color:var(--ink-3)}
.stat-val{
font-family:var(--font-mono);font-size:12px;color:var(--ink-1);
}
.badge{
display:inline-flex;align-items:center;gap:4px;
font-size:11px;font-weight:600;border-radius:4px;
padding:1px 7px;
}
.badge.ok{background:rgba(77,187,38,.15);color:var(--ok)}
.badge.err{background:rgba(251,73,52,.15);color:var(--err)}
.badge.warn{background:rgba(250,189,47,.15);color:var(--warn)}
/* ── Formulaire config ── */
.form-tile{
background:var(--bg-3);border-radius:10px;
border:1px solid var(--border-1);
box-shadow:var(--shadow-tile);
padding:12px 14px;
}
.form-tile .tile-titre{
font-size:11px;font-weight:600;letter-spacing:.08em;
text-transform:uppercase;color:var(--ink-3);margin-bottom:10px;
}
.form-group{margin-bottom:8px}
label.fl{display:block;font-size:11px;color:var(--ink-3);margin-bottom:3px;letter-spacing:.04em}
input.fi{
width:100%;background:var(--bg-4);border:1px solid var(--border-2);
color:var(--ink-1);border-radius:6px;padding:5px 9px;
font-family:var(--font-mono);font-size:12px;
transition:border-color .15s;outline:none;
}
input.fi:focus{border-color:var(--accent)}
.btn-apply{
width:100%;background:var(--accent);color:#fff;
border:none;border-radius:8px;padding:7px 0;
font-family:var(--font-ui);font-size:13px;font-weight:600;
cursor:pointer;margin-top:4px;
transition:background .15s;
}
.btn-apply:hover{background:var(--accent-soft)}
.config-feedback{font-size:11px;color:var(--ok);margin-top:4px;min-height:14px}
/* ── Status bar ── */
.statusbar{
display:flex;align-items:center;gap:0;
background:var(--bg-2);border-top:1px solid var(--border-2);
padding:0;height:28px;flex-shrink:0;font-size:11px;
font-family:var(--font-terminal);color:var(--ink-3);overflow:hidden;
}
.sb-seg{
display:flex;align-items:center;gap:6px;
padding:0 14px;height:100%;border-right:1px solid var(--border-2);
white-space:nowrap;
}
.sb-seg:last-child{border-right:none}
.sb-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.sb-gap{flex:1}
.sb-clock{padding:0 14px;height:100%;display:flex;align-items:center;
font-family:var(--font-terminal);font-size:11px;color:var(--ink-3);}
/* ── Modal config (mobile) ── */
.modal-backdrop{
display:none;position:fixed;inset:0;
background:rgba(0,0,0,.65);z-index:100;
align-items:center;justify-content:center;
}
.modal-backdrop.open{display:flex}
.modal{
background:var(--bg-2);border:1px solid var(--border-2);
border-radius:12px;padding:20px;width:min(92vw,360px);
box-shadow:0 8px 40px rgba(0,0,0,.6);
}
.modal-header{
display:flex;justify-content:space-between;align-items:center;
margin-bottom:14px;
}
.modal-titre{font-size:14px;font-weight:600;color:var(--ink-1)}
.btn-close{
background:none;border:none;cursor:pointer;
color:var(--ink-3);font-size:18px;line-height:1;padding:0 4px;
}
.btn-close:hover{color:var(--ink-1)}
/* ── Responsive ── */
@media(max-width:900px){
.panel-right{display:none}
.btn-cfg{display:inline-flex!important}
}
@media(max-width:700px){
.sondes-grid{grid-template-columns:1fr}
header{padding:8px 14px;gap:8px}
.logo{font-size:15px}
main{padding:10px;gap:10px}
}
/* Bouton config visible seulement mobile */
.btn-cfg{display:none}
/* Scrollbar custom dark */
: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}
</style>
</head>
<body>
<!-- ══ HEADER ══════════════════════════════════════════════════════════ -->
<header>
<div class="logo">ESP<span> Jardin</span></div>
<div id="wsLed" class="ws-led" title="WebSocket"></div>
<div id="rssiBadge" class="rssi-badge">RSSI —</div>
<div class="header-gap"></div>
<button class="btn-icon btn-cfg" id="btnCfgMobile" title="Configuration">&#9881;</button>
<button class="btn-icon" id="btnTheme" title="Basculer thème">&#9790;</button>
</header>
<!-- ══ CORPS PRINCIPAL ══════════════════════════════════════════════════ -->
<div class="main-wrap">
<!-- ─ Contenu ─────────────────────────────────────────── -->
<main>
<!-- Grille 3 sondes -->
<div class="sondes-grid" id="sondesGrid">
<!-- Sonde 0 -->
<div class="sonde-card" id="sonde0">
<div class="sonde-nom" id="nom0">SONDE 1</div>
<div id="affTemp0">
<div class="sonde-temp"><span class="unite">°C</span></div>
</div>
<div class="sonde-status" id="status0">En attente…</div>
</div>
<!-- Sonde 1 -->
<div class="sonde-card" id="sonde1">
<div class="sonde-nom" id="nom1">SONDE 2</div>
<div id="affTemp1">
<div class="sonde-temp"><span class="unite">°C</span></div>
</div>
<div class="sonde-status" id="status1">En attente…</div>
</div>
<!-- Sonde 2 -->
<div class="sonde-card" id="sonde2">
<div class="sonde-nom" id="nom2">SONDE 3</div>
<div id="affTemp2">
<div class="sonde-temp"><span class="unite">°C</span></div>
</div>
<div class="sonde-status" id="status2">En attente…</div>
</div>
</div>
<!-- Graphique 24h -->
<div class="chart-card">
<div class="card-titre">Historique 24h — températures</div>
<div class="chart-wrap">
<canvas id="chartTemp"></canvas>
</div>
</div>
</main>
<!-- ─ Panneau droit ────────────────────────────────────── -->
<aside class="panel-right">
<!-- Statut système -->
<div class="stat-tile">
<div class="tile-titre">Statut système</div>
<div class="stat-row">
<span class="stat-label">WiFi</span>
<span id="stWifi" class="badge"></span>
</div>
<div class="stat-row">
<span class="stat-label">Mode</span>
<span id="stMode" class="stat-val"></span>
</div>
<div class="stat-row">
<span class="stat-label">RSSI</span>
<span id="stRssi" class="stat-val"></span>
</div>
<div class="stat-row">
<span class="stat-label">MQTT</span>
<span id="stMqtt" class="badge"></span>
</div>
<div class="stat-row">
<span class="stat-label">RAM libre</span>
<span id="stRam" class="stat-val"></span>
</div>
<div class="stat-row">
<span class="stat-label">Uptime</span>
<span id="stUptime" class="stat-val"></span>
</div>
</div>
<!-- Formulaire config -->
<div class="form-tile">
<div class="tile-titre">Configuration</div>
<div class="form-group">
<label class="fl" for="cfgInterval">Intervalle (ms)</label>
<input class="fi" id="cfgInterval" type="number" value="10000" min="1000" step="1000">
</div>
<div class="form-group">
<label class="fl" for="cfgBroker">Broker MQTT</label>
<input class="fi" id="cfgBroker" type="text" value="10.0.0.3" placeholder="IP du broker">
</div>
<div class="form-group">
<label class="fl" for="cfgPort">Port MQTT</label>
<input class="fi" id="cfgPort" type="number" value="1883" min="1" max="65535">
</div>
<button class="btn-apply" id="btnApply">Appliquer</button>
<div class="config-feedback" id="cfgFeedback"></div>
</div>
</aside>
</div>
<!-- ══ STATUS BAR ════════════════════════════════════════════════════ -->
<div class="statusbar">
<div class="sb-seg">
<div id="sbWsLed" class="sb-dot" style="background:var(--ink-4)"></div>
<span id="sbWsLabel">WS déconnecté</span>
</div>
<div class="sb-seg">
<div id="sbModeLed" class="sb-dot" style="background:var(--blue)"></div>
<span id="sbModeLabel">WiFi —</span>
</div>
<div class="sb-seg">
<div id="sbMqttLed" class="sb-dot" style="background:var(--ink-4)"></div>
<span id="sbMqttLabel">MQTT —</span>
</div>
<div class="sb-seg">
<span id="sbSondes">Sondes actives : —</span>
</div>
<div class="sb-gap"></div>
<div class="sb-clock" id="sbClock"></div>
</div>
<!-- ══ MODAL CONFIG (mobile) ══════════════════════════════════════════ -->
<div class="modal-backdrop" id="modalBackdrop">
<div class="modal">
<div class="modal-header">
<span class="modal-titre">Configuration</span>
<button class="btn-close" id="btnCloseModal">&#10005;</button>
</div>
<div class="form-group">
<label class="fl" for="mCfgInterval">Intervalle (ms)</label>
<input class="fi" id="mCfgInterval" type="number" value="10000" min="1000" step="1000">
</div>
<div class="form-group">
<label class="fl" for="mCfgBroker">Broker MQTT</label>
<input class="fi" id="mCfgBroker" type="text" value="10.0.0.3" placeholder="IP du broker">
</div>
<div class="form-group">
<label class="fl" for="mCfgPort">Port MQTT</label>
<input class="fi" id="mCfgPort" type="number" value="1883" min="1" max="65535">
</div>
<button class="btn-apply" id="mBtnApply">Appliquer</button>
<div class="config-feedback" id="mCfgFeedback"></div>
</div>
</div>
<!-- ══ SCRIPT ═════════════════════════════════════════════════════════ -->
<script>
(function(){
'use strict';
/* ──────────────────────────────────────────────────────────
CONSTANTES
────────────────────────────────────────────────────────── */
var MAX_POINTS = 288;
var COULEURS = ['#fe8019','#3db0d1','#c882c8'];
var NOMS_DEFAUT = ['T°C Ext','T°C Serre','T°C Sol'];
var WS_RETRY_MS = 3000;
var STATUS_REFRESH_MS = 30000;
/* ──────────────────────────────────────────────────────────
ÉTAT GLOBAL
────────────────────────────────────────────────────────── */
var ws = null;
var wsReady = false;
var chartInst = null;
var nomsReels = NOMS_DEFAUT.slice();
var premierMessage = true;
var statusTimer = null;
/* ──────────────────────────────────────────────────────────
UTILITAIRES
────────────────────────────────────────────────────────── */
function fmtUptime(sec){
var h = Math.floor(sec/3600);
var m = Math.floor((sec%3600)/60);
var s = sec%60;
return (h>0?h+'h ':'')+m+'m '+s+'s';
}
function fmtHeure(ts){
var d = new Date(ts*1000);
return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');
}
function fmtRam(b){
if(b>=1024)return (b/1024).toFixed(0)+' ko';
return b+' o';
}
function cssVar(name){
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
/* ──────────────────────────────────────────────────────────
THÈME
────────────────────────────────────────────────────────── */
var btnTheme = document.getElementById('btnTheme');
btnTheme.addEventListener('click',function(){
var html = document.documentElement;
var th = html.getAttribute('data-theme')==='dark'?'light':'dark';
html.setAttribute('data-theme',th);
btnTheme.textContent = th==='dark'?'☾':'☀️';
if(chartInst) mettreAJourCouleursFond();
localStorage.setItem('theme',th);
});
// Restaurer thème
(function(){
var th = localStorage.getItem('theme');
if(th){
document.documentElement.setAttribute('data-theme',th);
btnTheme.textContent = th==='dark'?'☾':'☀️';
}
})();
function mettreAJourCouleursFond(){
var bg = cssVar('--bg-3');
var ink3 = cssVar('--ink-3');
var border1 = cssVar('--border-1');
chartInst.options.scales.x.grid.color = border1;
chartInst.options.scales.x.ticks.color = ink3;
chartInst.options.scales.y.grid.color = border1;
chartInst.options.scales.y.ticks.color = ink3;
chartInst.options.plugins.legend.labels.color = ink3;
chartInst.update('none');
}
/* ──────────────────────────────────────────────────────────
CHART.JS
────────────────────────────────────────────────────────── */
function initChart(noms){
var ctx = document.getElementById('chartTemp').getContext('2d');
var datasets = noms.map(function(n,i){
return {
label:n,
data:[],
borderColor:COULEURS[i],
backgroundColor:'transparent',
borderWidth:1.5,
pointRadius:0,
spanGaps:false,
tension:0.3
};
});
chartInst = new Chart(ctx,{
type:'line',
data:{labels:[],datasets:datasets},
options:{
animation:false,
responsive:true,
maintainAspectRatio:false,
interaction:{mode:'index',intersect:false},
plugins:{
legend:{
display:true,
position:'top',
labels:{
color:cssVar('--ink-3'),
font:{family:"'JetBrains Mono',monospace",size:11},
boxWidth:16,padding:12
}
},
tooltip:{
backgroundColor:'rgba(42,35,29,.95)',
titleColor:cssVar('--ink-2'),
bodyColor:cssVar('--ink-1'),
borderColor:cssVar('--border-2'),
borderWidth:1,
callbacks:{
label:function(ctx){
var v = ctx.parsed.y;
return v==null?'—':ctx.dataset.label+': '+v.toFixed(1)+'°C';
}
}
}
},
scales:{
x:{
grid:{color:cssVar('--border-1')},
ticks:{
color:cssVar('--ink-3'),
font:{family:"'Share Tech Mono',monospace",size:10},
maxTicksLimit:12,
maxRotation:0
}
},
y:{
grid:{color:cssVar('--border-1')},
ticks:{
color:cssVar('--ink-3'),
font:{family:"'JetBrains Mono',monospace",size:10},
callback:function(v){return v+'°C'}
}
}
}
}
});
}
function ajouterPointChart(label,valeurs){
/* valeurs = [v0, v1, v2] — null si erreur */
var d = chartInst.data;
d.labels.push(label);
for(var i=0;i<3;i++){
d.datasets[i].data.push(valeurs[i]);
}
if(d.labels.length>MAX_POINTS){
d.labels.shift();
for(var j=0;j<3;j++) d.datasets[j].data.shift();
}
chartInst.update('none');
}
function chargerHistorique(){
fetch('/api/history')
.then(function(r){return r.json();})
.then(function(hist){
if(!hist||!hist.length)return;
var d = chartInst.data;
d.labels=[];
for(var i=0;i<3;i++) d.datasets[i].data=[];
hist.forEach(function(pt){
d.labels.push(fmtHeure(pt.ts));
for(var i=0;i<3;i++){
var v = pt.t&&pt.t[i]!=null?parseFloat(pt.t[i]):null;
d.datasets[i].data.push(isNaN(v)?null:v);
}
});
chartInst.update('none');
})
.catch(function(){/* pas d'historique disponible */});
}
/* ──────────────────────────────────────────────────────────
CARTES SONDES
────────────────────────────────────────────────────────── */
function majSonde(i,nom,temp,erreur){
var card = document.getElementById('sonde'+i);
var nomEl = document.getElementById('nom'+i);
var affEl = document.getElementById('affTemp'+i);
var stEl = document.getElementById('status'+i);
nomEl.textContent = nom.toUpperCase();
if(erreur){
card.classList.add('erreur');
affEl.innerHTML = '<div class="sonde-erreur-txt">Sonde déconnectée</div>';
stEl.textContent = 'Erreur de lecture';
stEl.style.color = 'var(--err)';
} else {
card.classList.remove('erreur');
var v = parseFloat(temp).toFixed(1);
affEl.innerHTML = '<div class="sonde-temp">'+v+'<span class="unite">°C</span></div>';
stEl.textContent = 'Mis à jour à '+heureMaintenant();
stEl.style.color = 'var(--ink-4)';
}
}
function heureMaintenant(){
var d = new Date();
return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0');
}
/* ──────────────────────────────────────────────────────────
STATUT SYSTÈME
────────────────────────────────────────────────────────── */
function chargerStatus(){
fetch('/api/status')
.then(function(r){return r.json();})
.then(function(s){
majStatus(s);
})
.catch(function(){});
}
function majStatus(s){
// Panneau droit
setWifi(s.rssi,s.modeAP);
setMqtt(s.mqttConnecte);
var stRssi = document.getElementById('stRssi');
if(stRssi) stRssi.textContent = s.rssi+' dBm';
var stRam = document.getElementById('stRam');
if(stRam) stRam.textContent = fmtRam(s.ramLibre);
var stUp = document.getElementById('stUptime');
if(stUp) stUp.textContent = fmtUptime(s.uptime);
// Header RSSI
document.getElementById('rssiBadge').textContent = s.rssi+' dBm';
// Status bar mode
var modeTxt = s.modeAP?'AP: ESP_CHEF_JARDIN':'STA';
document.getElementById('sbModeLabel').textContent = 'WiFi '+modeTxt;
document.getElementById('sbModeLed').style.background = s.modeAP?cssVar('--warn'):cssVar('--blue');
// Status bar MQTT
document.getElementById('sbMqttLabel').textContent = 'MQTT '+(s.mqttConnecte?'OK':'—');
document.getElementById('sbMqttLed').style.background = s.mqttConnecte?cssVar('--ok'):cssVar('--ink-4');
}
function setWifi(rssi,modeAP){
var el = document.getElementById('stWifi');
var modeEl = document.getElementById('stMode');
if(el){
el.className='badge ok';
el.textContent='Connecté';
}
if(modeEl) modeEl.textContent = modeAP?'AP (Hotspot)':'Station (STA)';
}
function setMqtt(ok){
var el = document.getElementById('stMqtt');
if(!el) return;
if(ok){ el.className='badge ok'; el.textContent='Connecté'; }
else { el.className='badge err'; el.textContent='Hors ligne'; }
}
/* ──────────────────────────────────────────────────────────
WEBSOCKET
────────────────────────────────────────────────────────── */
function setLedWs(etat){
/* etat: 'ok'|'err'|'warn' */
var led = document.getElementById('wsLed');
var sbLed = document.getElementById('sbWsLed');
var sbLabel = document.getElementById('sbWsLabel');
led.className='ws-led '+etat;
sbLed.style.background = etat==='ok'?cssVar('--ok'):etat==='warn'?cssVar('--warn'):cssVar('--err');
sbLabel.textContent = etat==='ok'?'WS connecté':etat==='warn'?'WS reconnexion…':'WS déconnecté';
}
function connecterWS(){
var proto = location.protocol==='https:'?'wss':'ws';
var url = proto+'://'+location.host+'/ws';
setLedWs('warn');
try{ ws = new WebSocket(url); }
catch(e){ setLedWs('err'); return; }
ws.onopen = function(){
wsReady=true;
setLedWs('ok');
};
ws.onmessage = function(evt){
var data;
try{ data=JSON.parse(evt.data); }catch(e){ return; }
traiterMessageWS(data);
};
ws.onclose = function(){
wsReady=false;
setLedWs('err');
setTimeout(connecterWS, WS_RETRY_MS);
};
ws.onerror = function(){
setLedWs('err');
};
}
function traiterMessageWS(data){
if(!data||!data.sondes) return;
// Mise à jour des vrais noms au premier message
if(premierMessage){
premierMessage=false;
data.sondes.forEach(function(s,i){
if(s.nom){ nomsReels[i]=s.nom; chartInst.data.datasets[i].label=s.nom; }
});
chartInst.update('none');
}
// Sondes
var actives=0;
data.sondes.forEach(function(s,i){
majSonde(i, s.nom||nomsReels[i], s.temp, s.erreur);
if(!s.erreur) actives++;
});
document.getElementById('sbSondes').textContent='Sondes actives : '+actives+'/3';
// RSSI du message WS si présent
if(data.rssi!==undefined){
document.getElementById('rssiBadge').textContent=data.rssi+' dBm';
}
// Graphique — timestamp = maintenant si absent du payload
var ts = data.ts?data.ts:Math.floor(Date.now()/1000);
var valeurs = data.sondes.map(function(s){
if(s.erreur||s.temp==null) return null;
var v=parseFloat(s.temp);
return isNaN(v)?null:v;
});
ajouterPointChart(fmtHeure(ts), valeurs);
}
/* ──────────────────────────────────────────────────────────
CONFIGURATION
────────────────────────────────────────────────────────── */
function envoyerConfig(intervalEl,brokerEl,portEl,feedbackEl){
var payload={
intervalleMs:parseInt(intervalEl.value)||10000,
mqttBroker:brokerEl.value.trim(),
mqttPort:parseInt(portEl.value)||1883
};
fetch('/api/config',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload)
})
.then(function(r){
feedbackEl.style.color='var(--ok)';
feedbackEl.textContent=r.ok?'Configuration appliquée.':'Erreur (code '+r.status+')';
if(!r.ok) feedbackEl.style.color='var(--err)';
})
.catch(function(){
feedbackEl.style.color='var(--err)';
feedbackEl.textContent='Erreur réseau.';
});
// Synchroniser les deux formulaires
document.getElementById('cfgInterval').value=payload.intervalleMs;
document.getElementById('cfgBroker').value=payload.mqttBroker;
document.getElementById('cfgPort').value=payload.mqttPort;
document.getElementById('mCfgInterval').value=payload.intervalleMs;
document.getElementById('mCfgBroker').value=payload.mqttBroker;
document.getElementById('mCfgPort').value=payload.mqttPort;
}
document.getElementById('btnApply').addEventListener('click',function(){
envoyerConfig(
document.getElementById('cfgInterval'),
document.getElementById('cfgBroker'),
document.getElementById('cfgPort'),
document.getElementById('cfgFeedback')
);
});
document.getElementById('mBtnApply').addEventListener('click',function(){
envoyerConfig(
document.getElementById('mCfgInterval'),
document.getElementById('mCfgBroker'),
document.getElementById('mCfgPort'),
document.getElementById('mCfgFeedback')
);
});
/* ──────────────────────────────────────────────────────────
MODAL MOBILE
────────────────────────────────────────────────────────── */
var modal = document.getElementById('modalBackdrop');
document.getElementById('btnCfgMobile').addEventListener('click',function(){
modal.classList.add('open');
});
document.getElementById('btnCloseModal').addEventListener('click',function(){
modal.classList.remove('open');
});
modal.addEventListener('click',function(e){
if(e.target===modal) modal.classList.remove('open');
});
/* ──────────────────────────────────────────────────────────
HORLOGE STATUS BAR
────────────────────────────────────────────────────────── */
function majHorloge(){
var d = new Date();
var h = String(d.getHours()).padStart(2,'0');
var m = String(d.getMinutes()).padStart(2,'0');
var s = String(d.getSeconds()).padStart(2,'0');
document.getElementById('sbClock').textContent='esp_jardin | '+h+':'+m+':'+s;
}
/* ──────────────────────────────────────────────────────────
INITIALISATION
────────────────────────────────────────────────────────── */
initChart(NOMS_DEFAUT);
chargerHistorique();
chargerStatus();
connecterWS();
setInterval(chargerStatus, STATUS_REFRESH_MS);
setInterval(majHorloge, 1000);
majHorloge();
})();
</script>
</body>
</html>