Initial release 1.2.2
This commit is contained in:
+270
-12
@@ -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">⟳</button>
|
||||
<button class="btn-icon" id="btnSettings" title="Paramètres">⚙</button>
|
||||
<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>
|
||||
@@ -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">⚙ Paramètres</span>
|
||||
<button class="btn-close" id="btnCloseSettings">✕</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 l’ESP…';
|
||||
} 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 l’upload.';
|
||||
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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user