Initial release 1.2.2

This commit is contained in:
2026-05-24 13:05:18 +02:00
parent 87d33b41c7
commit b075d04706
28 changed files with 2022 additions and 124 deletions
+270 -12
View File
@@ -223,7 +223,6 @@ input.fi:focus{border-color:var(--accent)}
/* ── 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}
@@ -330,6 +329,54 @@ input.fi:focus{border-color:var(--accent)}
}
.btn-scan:hover{background:var(--bg-3);color:var(--ink-1)}
.btn-scan:disabled{cursor:not-allowed;color:var(--ink-4)}
/* ── Modal paramètres ── */
.settings-modal-backdrop{
display:none;position:fixed;inset:0;
background:rgba(0,0,0,.72);z-index:180;
align-items:flex-start;justify-content:center;
padding:48px 12px;overflow-y:auto;
}
.settings-modal-backdrop.open{display:flex}
.settings-modal{
background:var(--bg-2);border:1px solid var(--border-2);
border-radius:12px;width:min(96vw,620px);
box-shadow:0 8px 40px rgba(0,0,0,.62);
padding:22px;display:flex;flex-direction:column;gap:16px;
}
.settings-header{display:flex;align-items:center;justify-content:space-between;gap:12px}
.settings-title{font-size:15px;font-weight:700;color:var(--ink-1);letter-spacing:.04em}
.settings-section{
background:var(--bg-3);border:1px solid var(--border-1);
border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:10px;
}
.settings-section-title{
font-size:11px;font-weight:600;text-transform:uppercase;
letter-spacing:.08em;color:var(--ink-3);
}
.settings-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.ota-row{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:end}
.ota-file{
width:100%;background:var(--bg-4);border:1px solid var(--border-2);
color:var(--ink-1);border-radius:8px;padding:7px;
font-family:var(--font-mono);font-size:12px;
}
.ota-progress{
height:8px;background:var(--bg-4);border:1px solid var(--border-1);
border-radius:6px;overflow:hidden;
}
.ota-progress-bar{height:100%;width:0;background:var(--accent);transition:width .15s}
.ota-feedback{font-size:12px;font-family:var(--font-terminal);min-height:18px;color:var(--ink-3)}
.btn-ota{
background:var(--accent);color:#1a1a1a;font-weight:700;
border:none;border-radius:9px;padding:9px 14px;cursor:pointer;
font-size:12px;font-family:var(--font-ui);white-space:nowrap;
}
.btn-ota:hover{background:var(--accent-soft);color:#fff}
.btn-ota:disabled{background:var(--bg-4);color:var(--ink-4);cursor:not-allowed}
@media(max-width:700px){
.settings-grid{grid-template-columns:1fr}
.ota-row{grid-template-columns:1fr}
}
</style>
</head>
<body>
@@ -340,6 +387,8 @@ input.fi:focus{border-color:var(--accent)}
<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" id="btnRestart" title="Redémarrer">&#10227;</button>
<button class="btn-icon" id="btnSettings" title="Paramètres">&#9881;</button>
<button class="btn-icon btn-cfg" id="btnCfgMobile" title="Configuration">&#9881;</button>
<button class="btn-icon" id="btnWifi" title="Gestion WiFi">&#128246;</button>
<button class="btn-icon" id="btnTheme" title="Basculer thème">&#9790;</button>
@@ -460,6 +509,9 @@ input.fi:focus{border-color:var(--accent)}
<div class="sb-seg">
<span id="sbSondes">Sondes actives : —</span>
</div>
<div class="sb-seg">
<span id="versionBadge">FW — / UI —</span>
</div>
<div class="sb-gap"></div>
<div class="sb-clock" id="sbClock"></div>
</div>
@@ -488,6 +540,56 @@ input.fi:focus{border-color:var(--accent)}
</div>
</div>
<!-- ══ MODAL PARAMÈTRES ══════════════════════════════════════════════ -->
<div class="settings-modal-backdrop" id="settingsModalBackdrop">
<div class="settings-modal">
<div class="settings-header">
<span class="settings-title">&#9881; Paramètres</span>
<button class="btn-close" id="btnCloseSettings">&#10005;</button>
</div>
<div class="settings-section">
<div class="settings-section-title">Configuration</div>
<div class="settings-grid">
<div class="form-group">
<label class="fl" for="sCfgInterval">Intervalle (ms)</label>
<input class="fi" id="sCfgInterval" type="number" value="10000" min="1000" step="1000">
</div>
<div class="form-group">
<label class="fl" for="sCfgBroker">Broker MQTT</label>
<input class="fi" id="sCfgBroker" type="text" value="10.0.0.3" placeholder="IP du broker">
</div>
<div class="form-group">
<label class="fl" for="sCfgPort">Port MQTT</label>
<input class="fi" id="sCfgPort" type="number" value="1883" min="1" max="65535">
</div>
</div>
<button class="btn-apply" id="sBtnApply">Appliquer</button>
<div class="config-feedback" id="sCfgFeedback"></div>
</div>
<div class="settings-section">
<div class="settings-section-title">Mise à jour OTA firmware</div>
<div class="ota-row">
<input class="ota-file" id="otaFirmwareFile" type="file" accept=".bin,application/octet-stream">
<button class="btn-ota" id="btnOtaFirmware">Envoyer firmware</button>
</div>
<div class="ota-progress"><div class="ota-progress-bar" id="otaFirmwareBar"></div></div>
<div class="ota-feedback" id="otaFirmwareFeedback">Fichier attendu : firmware.bin</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Mise à jour OTA filesystem</div>
<div class="ota-row">
<input class="ota-file" id="otaFilesystemFile" type="file" accept=".bin,application/octet-stream">
<button class="btn-ota" id="btnOtaFilesystem">Envoyer LittleFS</button>
</div>
<div class="ota-progress"><div class="ota-progress-bar" id="otaFilesystemBar"></div></div>
<div class="ota-feedback" id="otaFilesystemFeedback">Fichier attendu : littlefs.bin</div>
</div>
</div>
</div>
<!-- ══ MODAL WIFI ═════════════════════════════════════════════════════ -->
<div class="wifi-modal-backdrop" id="wifiModalBackdrop">
<div class="wifi-modal">
@@ -564,10 +666,12 @@ input.fi:focus{border-color:var(--accent)}
CONSTANTES
────────────────────────────────────────────────────────── */
var MAX_POINTS = 288;
var UI_VERSION = '1.2.2';
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;
var HIST_REFRESH_MS = 60000;
/* ──────────────────────────────────────────────────────────
ÉTAT GLOBAL
@@ -578,6 +682,7 @@ var chartInst = null;
var nomsReels = NOMS_DEFAUT.slice();
var premierMessage = true;
var statusTimer = null;
var dernierHistSeq = 0;
/* ──────────────────────────────────────────────────────────
UTILITAIRES
@@ -595,6 +700,16 @@ function fmtHeure(ts){
return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');
}
function fmtPointHistorique(pt){
var d;
if(pt.age_s!==undefined){
d = new Date(Date.now() - (parseInt(pt.age_s)||0)*1000);
} else {
d = new Date((parseInt(pt.ts)||0)*1000);
}
return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');
}
function fmtRam(b){
b = b || 0;
if(b>=1024)return (b/1024).toFixed(0)+' ko';
@@ -732,16 +847,18 @@ function ajouterPointChart(label,valeurs){
}
function chargerHistorique(){
fetch('/api/history')
fetch('/api/v1/history')
.then(function(r){return r.json();})
.then(function(hist){
.then(function(res){
if(!chartInst) return;
var hist = Array.isArray(res) ? res : (res.points || []);
if(!hist||!hist.length)return;
if(res.sequence!==undefined) dernierHistSeq = res.sequence;
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));
d.labels.push(fmtPointHistorique(pt));
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);
@@ -752,6 +869,23 @@ function chargerHistorique(){
.catch(function(){/* pas d'historique disponible */});
}
function chargerDernieresValeurs(){
fetch('/api/v1/readings/latest')
.then(function(r){return r.json();})
.then(function(res){
var readings = res.readings || [];
var actives = 0;
readings.forEach(function(s){
var i = parseInt(s.index);
if(isNaN(i) || i<0 || i>=3) return;
majSonde(i, s.name||nomsReels[i], s.temperature, s.error);
if(!s.error) actives++;
});
document.getElementById('sbSondes').textContent='Sondes actives : '+actives+'/3';
})
.catch(function(){});
}
/* ──────────────────────────────────────────────────────────
CARTES SONDES
────────────────────────────────────────────────────────── */
@@ -793,6 +927,20 @@ function chargerStatus(){
}
function majStatus(s){
var versionBadge = document.getElementById('versionBadge');
if(versionBadge) versionBadge.textContent = 'FW ' + (s.version || '—') + ' / UI ' + UI_VERSION;
if(s.mqttBroker){
['cfgBroker','mCfgBroker','sCfgBroker'].forEach(function(id){
var el = document.getElementById(id);
if(el && !el.matches(':focus')) el.value = s.mqttBroker;
});
}
if(s.mqttPort){
['cfgPort','mCfgPort','sCfgPort'].forEach(function(id){
var el = document.getElementById(id);
if(el && !el.matches(':focus')) el.value = s.mqttPort;
});
}
// Panneau droit
setWifi(s.rssi,s.modeAP);
setMqtt(s.mqttConnecte);
@@ -907,14 +1055,10 @@ function traiterMessageWS(data){
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);
// Le graphique utilise uniquement les moyennes 5 min.
if(data.histSeq!==undefined && data.histSeq!==dernierHistSeq){
chargerHistorique();
}
}
/* ──────────────────────────────────────────────────────────
@@ -947,6 +1091,9 @@ function envoyerConfig(intervalEl,brokerEl,portEl,feedbackEl){
document.getElementById('mCfgInterval').value=payload.intervalleMs;
document.getElementById('mCfgBroker').value=payload.mqttBroker;
document.getElementById('mCfgPort').value=payload.mqttPort;
document.getElementById('sCfgInterval').value=payload.intervalleMs;
document.getElementById('sCfgBroker').value=payload.mqttBroker;
document.getElementById('sCfgPort').value=payload.mqttPort;
}
document.getElementById('btnApply').addEventListener('click',function(){
@@ -967,6 +1114,23 @@ document.getElementById('mBtnApply').addEventListener('click',function(){
);
});
function redemarrerEsp(){
if(!confirm('Redémarrer esp_jardin maintenant ?')) return;
fetch('/api/restart',{method:'POST'})
.catch(function(){});
}
document.getElementById('btnRestart').addEventListener('click', redemarrerEsp);
document.getElementById('sBtnApply').addEventListener('click',function(){
envoyerConfig(
document.getElementById('sCfgInterval'),
document.getElementById('sCfgBroker'),
document.getElementById('sCfgPort'),
document.getElementById('sCfgFeedback')
);
});
/* ──────────────────────────────────────────────────────────
MODAL MOBILE
────────────────────────────────────────────────────────── */
@@ -981,6 +1145,98 @@ modal.addEventListener('click',function(e){
if(e.target===modal) modal.classList.remove('open');
});
/* ──────────────────────────────────────────────────────────
PARAMÈTRES ET OTA HTTP
────────────────────────────────────────────────────────── */
var settingsModal = document.getElementById('settingsModalBackdrop');
function ouvrirSettings(){
document.getElementById('sCfgInterval').value = document.getElementById('cfgInterval').value;
document.getElementById('sCfgBroker').value = document.getElementById('cfgBroker').value;
document.getElementById('sCfgPort').value = document.getElementById('cfgPort').value;
settingsModal.classList.add('open');
}
function fermerSettings(){
settingsModal.classList.remove('open');
}
function envoyerOta(type){
var isFirmware = type === 'firmware';
var fileEl = document.getElementById(isFirmware ? 'otaFirmwareFile' : 'otaFilesystemFile');
var bar = document.getElementById(isFirmware ? 'otaFirmwareBar' : 'otaFilesystemBar');
var feedback = document.getElementById(isFirmware ? 'otaFirmwareFeedback' : 'otaFilesystemFeedback');
var btn = document.getElementById(isFirmware ? 'btnOtaFirmware' : 'btnOtaFilesystem');
var file = fileEl.files && fileEl.files[0];
if(!file){
feedback.style.color = 'var(--err)';
feedback.textContent = 'Sélectionner un fichier .bin.';
return;
}
if(!/\.bin$/i.test(file.name)){
feedback.style.color = 'var(--err)';
feedback.textContent = 'Le fichier doit être un .bin.';
return;
}
var form = new FormData();
form.append('update', file, file.name);
var xhr = new XMLHttpRequest();
xhr.open('POST', isFirmware ? '/api/ota/firmware' : '/api/ota/filesystem');
btn.disabled = true;
bar.style.width = '0%';
feedback.style.color = 'var(--warn)';
feedback.textContent = 'Upload en cours…';
xhr.upload.onprogress = function(evt){
if(evt.lengthComputable){
var pct = Math.round((evt.loaded / evt.total) * 100);
bar.style.width = pct + '%';
feedback.textContent = 'Upload en cours… ' + pct + '%';
}
};
xhr.onload = function(){
if(xhr.status >= 200 && xhr.status < 300){
bar.style.width = '100%';
feedback.style.color = 'var(--ok)';
feedback.textContent = 'Mise à jour envoyée. Redémarrage de lESP…';
} else {
var msg = 'Erreur OTA HTTP ' + xhr.status + '.';
try{
var res = JSON.parse(xhr.responseText || '{}');
if(res.erreur) msg = 'Erreur OTA : ' + res.erreur;
}catch(e){}
feedback.style.color = 'var(--err)';
feedback.textContent = msg;
btn.disabled = false;
}
};
xhr.onerror = function(){
feedback.style.color = 'var(--err)';
feedback.textContent = 'Connexion interrompue pendant lupload.';
btn.disabled = false;
};
xhr.send(form);
}
document.getElementById('btnSettings').addEventListener('click', ouvrirSettings);
document.getElementById('btnCloseSettings').addEventListener('click', fermerSettings);
settingsModal.addEventListener('click',function(e){
if(e.target===settingsModal) fermerSettings();
});
document.getElementById('btnOtaFirmware').addEventListener('click',function(){
envoyerOta('firmware');
});
document.getElementById('btnOtaFilesystem').addEventListener('click',function(){
envoyerOta('filesystem');
});
/* ──────────────────────────────────────────────────────────
HORLOGE STATUS BAR
────────────────────────────────────────────────────────── */
@@ -1182,9 +1438,11 @@ document.getElementById('btnTogglePass').addEventListener('click', function() {
────────────────────────────────────────────────────────── */
initChart(NOMS_DEFAUT);
chargerHistorique();
chargerDernieresValeurs();
chargerStatus();
connecterWS();
setInterval(chargerStatus, STATUS_REFRESH_MS);
setInterval(chargerHistorique, HIST_REFRESH_MS);
setInterval(majHorloge, 1000);
majHorloge();