'use strict';
const REFRESH_MS = (parseInt(localStorage.getItem('refresh_ms') || '1000', 10));
// Noms configurables — chargés depuis /api/names au démarrage
const noms = { relay1: 'Relais 1', relay2: 'Relais 2', di1: 'Entrée 1', di2: 'Entrée 2' };
async function chargerNoms() {
try {
const d = await (await fetch('/api/names')).json();
if (d.relay1) noms.relay1 = d.relay1;
if (d.relay2) noms.relay2 = d.relay2;
if (d.di1) noms.di1 = d.di1;
if (d.di2) noms.di2 = d.di2;
} catch {}
appliquerNoms();
}
function appliquerNoms() {
setText('label-relay1', noms.relay1);
setText('label-relay2', noms.relay2);
setText('label-di1', noms.di1);
setText('label-di2', noms.di2);
setText('cmd-label-r1', noms.relay1);
setText('cmd-label-r2', noms.relay2);
const r1 = document.getElementById('c-n-relay1'); if (r1) r1.value = noms.relay1;
const r2 = document.getElementById('c-n-relay2'); if (r2) r2.value = noms.relay2;
const d1 = document.getElementById('c-n-di1'); if (d1) d1.value = noms.di1;
const d2 = document.getElementById('c-n-di2'); if (d2) d2.value = noms.di2;
}
async function sauvegarderNoms() {
const payload = {
relay1: document.getElementById('c-n-relay1').value.trim() || 'Relais 1',
relay2: document.getElementById('c-n-relay2').value.trim() || 'Relais 2',
di1: document.getElementById('c-n-di1').value.trim() || 'Entrée 1',
di2: document.getElementById('c-n-di2').value.trim() || 'Entrée 2',
};
await fetch('/api/names', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
location.reload();
}
// --- Navigation onglets ---
function afficherOnglet(nom, bouton) {
document.querySelectorAll('.onglet').forEach(s => s.classList.remove('actif'));
document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
document.getElementById(nom).classList.add('actif');
bouton.classList.add('active');
if (nom === 'regles') chargerRegles();
if (nom === 'config') { chargerSleep(); chargerWifi(); chargerPrefsUI(); chargerModbus(); }
if (nom === 'historique') chargerHistorique();
if (nom === 'debug') chargerDebug();
}
// --- Rafraîchissement de l'état ---
async function rafraichir() {
try {
const res = await fetch('/api/state');
if (!res.ok) throw new Error('HTTP ' + res.status);
const d = await res.json();
mettreAJourUI(d);
} catch {
const b = document.getElementById('rs485-badge');
b.textContent = 'Hors ligne';
b.className = 'badge badge-err';
}
}
function mettreAJourUI(d) {
// PV & batterie
setText('battery', d.battery.toFixed(2) + ' V');
setText('pv', d.pv.toFixed(2) + ' V');
setText('pvCurrent', d.pvCurrent.toFixed(2) + ' A');
setText('sun', d.sun ? '☀ Jour' : '🌙 Nuit');
setText('epeverTime', d.epeverTime || '--');
setText('epeverClockOk', d.epeverClockOk ? 'OK' : 'ERR');
setText('header-clock', d.espClockOk ? d.espTime : (d.epeverTime || '--'));
// Batterie détaillée
setText('batSOC', d.batSOC + ' %');
setText('batTemp', d.batTemperature.toFixed(1) + ' °C');
setText('batStatut', ['Arrêt','Float','Boost','Égalisation'][d.batStatut] || '--');
// Load & énergie
setText('loadVoltage', d.loadVoltage.toFixed(2) + ' V');
setText('loadCurrent', d.loadCurrent.toFixed(2) + ' A');
setText('loadPower', d.loadPower.toFixed(1) + ' W');
setText('energieGenJour', d.energieGenJour.toFixed(2) + ' kWh');
setText('energieConJour', d.energieConJour.toFixed(2) + ' kWh');
setText('energieGenTotal', d.energieGenTotal.toFixed(2) + ' kWh');
setText('energieConTotal', d.energieConTotal.toFixed(2) + ' kWh');
// Relais & boutons
setRelais('relay1-etat', 'carte-relay1', 'led-r1', 'btn-r1-on', 'btn-r1-off', d.relay1);
setRelais('relay2-etat', 'carte-relay2', 'led-r2', 'btn-r2-on', 'btn-r2-off', d.relay2);
setBouton('di1-etat', d.di1);
setBouton('di2-etat', d.di2);
const badge = document.getElementById('rs485-badge');
badge.textContent = d.rs485_ok ? 'RS485 OK' : 'RS485 ERR';
badge.className = 'badge ' + (d.rs485_ok ? 'badge-ok' : 'badge-err');
document.getElementById('pied-page').textContent =
'Mise à jour : ' + new Date().toLocaleTimeString('fr-FR');
}
function setBouton(id, etat) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = etat ? '● Appuyé' : '○ Relâché';
el.className = 'valeur ' + (etat ? 'val-on' : 'val-off');
}
function setRelais(id, carteId, ledId, btnOnId, btnOffId, etat) {
const el = document.getElementById(id);
if (el) { el.textContent = etat ? '● ON' : '○ OFF'; el.className = 'valeur ' + (etat ? 'val-on' : 'val-off'); }
const carte = document.getElementById(carteId);
if (carte) carte.classList.toggle('carte-on', etat);
const led = document.getElementById(ledId);
if (led) led.className = 'led ' + (etat ? 'led-on' : 'led-off');
const btnOn = document.getElementById(btnOnId);
const btnOff = document.getElementById(btnOffId);
if (btnOn) btnOn.className = 'btn btn-vert' + (etat ? ' btn-glow-vert' : ' btn-dim');
if (btnOff) btnOff.className = 'btn btn-rouge' + (!etat ? ' btn-glow-rouge' : ' btn-dim');
}
function setText(id, val) {
const el = document.getElementById(id);
if (el) el.textContent = val;
}
// --- Relais / Mode ---
async function relay(n, cmd) {
const res = await fetch('/api/relay/' + n + '/' + cmd, { method: 'POST' });
if (!res.ok) {
const d = await res.json().catch(() => ({}));
afficherToast(d.err || 'Commande refusée');
}
rafraichir();
}
function afficherToast(msg) {
let t = document.getElementById('toast');
if (!t) {
t = document.createElement('div');
t.id = 'toast';
document.body.appendChild(t);
}
t.textContent = msg;
t.classList.add('visible');
clearTimeout(t._timer);
t._timer = setTimeout(() => t.classList.remove('visible'), 2800);
}
async function rebootESP() {
if (!confirm('Redémarrer l\'ESP32 ?')) return;
await fetch('/api/reboot', { method: 'POST' });
afficherToast('Redémarrage en cours…');
setTimeout(() => location.reload(), 5000);
}
// --- Règles ---
async function chargerRegles() {
try {
const res = await fetch('/api/rules');
const data = await res.json();
afficherRegles(data);
} catch {
document.getElementById('liste-regles').innerHTML =
'
Erreur chargement
';
}
}
function afficherRegles(regles) {
const el = document.getElementById('liste-regles');
if (!regles.length) {
el.innerHTML = 'Aucune règle
';
return;
}
el.innerHTML = regles.map(r => {
const cond = construireDescription(r);
const cls = r.enabled ? '' : ' regle-desactivee';
return `
`;
}).join('');
}
function construireDescription(r) {
const dec = [];
if (r.sun !== undefined) dec.push(r.sun ? '☀ Jour' : '🌙 Nuit');
if (r.di1 !== undefined) dec.push('DI1 ' + (r.di1 ? 'fermé' : 'ouvert'));
if (r.di2 !== undefined) dec.push('DI2 ' + (r.di2 ? 'fermé' : 'ouvert'));
const cond = [];
if (r.battery_min) cond.push('Bat ≥ ' + r.battery_min + 'V');
if (r.battery_max) cond.push('Bat ≤ ' + r.battery_max + 'V');
if (r.pv_min) cond.push('PV ≥ ' + r.pv_min + 'V');
if (r.pv_max) cond.push('PV ≤ ' + r.pv_max + 'V');
const extras = [];
if (r.delay) extras.push('délai ' + r.delay + 's');
if (r.hysteresis) extras.push('hyst. ±' + r.hysteresis + 'V');
const c = '▶';
const lines = [];
if (dec.length) lines.push(c + ' Si ' + dec.join(' + '));
if (cond.length) lines.push(c + ' Et ' + cond.join(' + '));
lines.push(c + ' → Relais ' + r.relay + ' ' + (r.state ? 'ON' : 'OFF') + ''
+ (extras.length ? ' (' + extras.join(', ') + ')' : ''));
return lines.join('
');
}
async function toggleRegle(id) {
await fetch('/api/rules/toggle?id=' + id, { method: 'POST' });
chargerRegles();
}
async function supprimerRegle(id) {
await fetch('/api/rules/delete?id=' + id, { method: 'POST' });
chargerRegles();
}
async function ajouterRegle() {
const sun = document.getElementById('f-sun').value;
const di1 = document.getElementById('f-di1').value;
const di2 = document.getElementById('f-di2').value;
const batMin = parseFloat(document.getElementById('f-batmin').value) || 0;
const batMax = parseFloat(document.getElementById('f-batmax').value) || 0;
const pvMin = parseFloat(document.getElementById('f-pvmin').value) || 0;
const pvMax = parseFloat(document.getElementById('f-pvmax').value) || 0;
const relay = parseInt(document.getElementById('f-relay').value);
const state = document.getElementById('f-state').value === 'true';
const delay = parseInt(document.getElementById('f-delay').value) || 0;
const hyst = parseFloat(document.getElementById('f-hysteresis').value) || 0;
const regle = { enabled: true, relay, state, delay };
if (sun !== '') regle.sun = sun === 'true';
if (di1 !== '') regle.di1 = di1 === 'true';
if (di2 !== '') regle.di2 = di2 === 'true';
if (batMin > 0) regle.battery_min = batMin;
if (batMax > 0) regle.battery_max = batMax;
if (pvMin > 0) regle.pv_min = pvMin;
if (pvMax > 0) regle.pv_max = pvMax;
if (hyst > 0) regle.hysteresis = hyst;
const res = await fetch('/api/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(regle)
});
if (res.ok) chargerRegles();
}
// --- Préférences UI (stockées en localStorage) ---
function getRefreshMs() {
return parseInt(localStorage.getItem('refresh_ms') || '1000', 10);
}
function chargerPrefsUI() {
const r = document.getElementById('c-refresh');
if (r) r.value = Math.round(getRefreshMs() / 1000);
const lp = document.getElementById('c-longpress2');
if (lp) lp.value = getLongPressMs();
}
function sauvegarderInterface() {
const s = parseInt(document.getElementById('c-refresh').value) || 1;
const lp = parseInt(document.getElementById('c-longpress2').value) || 500;
localStorage.setItem('refresh_ms', Math.min(60000, Math.max(1000, s * 1000)));
localStorage.setItem('longpress_ms', Math.min(3000, Math.max(200, lp)));
location.reload();
}
// --- WiFi info ---
async function chargerWifi() {
try {
const d = await (await fetch('/api/wifi')).json();
setText('wifi-ssid', d.ssid || '--');
setText('wifi-pwd', d.password || '(vide)');
} catch { /* silencieux */ }
}
// --- Sleep / Config ---
async function chargerSleep() {
try {
const d = await (await fetch('/api/sleep')).json();
document.getElementById('c-sleep-actif').value = String(d.actif);
document.getElementById('c-sleep-intervalle').value = Math.round(d.intervalle / 60);
document.getElementById('c-sleep-seuil').value = d.seuil;
} catch { /* silencieux */ }
}
async function sauvegarderSleep() {
const actif = document.getElementById('c-sleep-actif').value === 'true';
const intervalle = parseInt(document.getElementById('c-sleep-intervalle').value) * 60;
const seuil = parseFloat(document.getElementById('c-sleep-seuil').value);
await fetch('/api/sleep', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actif, intervalle, seuil })
});
}
// --- Intervalles Modbus ---
async function chargerModbus() {
try {
const d = await (await fetch('/api/modbus')).json();
const j = document.getElementById('c-mb-jour'); if (j) j.value = Math.round(d.jour / 1000);
const n = document.getElementById('c-mb-nuit'); if (n) n.value = Math.round(d.nuit / 1000);
} catch { /* silencieux */ }
}
async function sauvegarderModbus() {
const jour = (parseInt(document.getElementById('c-mb-jour').value) || 5) * 1000;
const nuit = (parseInt(document.getElementById('c-mb-nuit').value) || 30) * 1000;
await fetch('/api/modbus', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jour, nuit })
});
afficherToast('Intervalles Modbus sauvegardés');
}
// --- Horloge EPEVER ---
function toDatetimeLocalValue(date) {
const pad = n => String(n).padStart(2, '0');
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) +
'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
}
function remplirHeureNavigateur() {
const input = document.getElementById('c-epever-time');
if (input) input.value = toDatetimeLocalValue(new Date());
}
async function sauvegarderHeureEpever() {
const input = document.getElementById('c-epever-time');
if (!input || !input.value) {
afficherToast('Date/heure manquante');
return;
}
const d = new Date(input.value);
if (Number.isNaN(d.getTime())) {
afficherToast('Date/heure invalide');
return;
}
const payload = {
year: d.getFullYear(),
month: d.getMonth() + 1,
day: d.getDate(),
hour: d.getHours(),
minute: d.getMinutes(),
second: d.getSeconds()
};
const res = await fetch('/api/epever/time', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
afficherToast(res.ok ? 'Horloge EPEVER réglée' : 'Réglage refusé, réessaie dans quelques secondes');
rafraichir();
}
// --- Debug série / RS485 ---
async function chargerDebug() {
const consoleEl = document.getElementById('debug-console');
const metaEl = document.getElementById('debug-meta');
if (!consoleEl || !metaEl) return;
try {
const d = await (await fetch('/api/debug/logs')).json();
metaEl.textContent = d.lines.length + ' lignes en mémoire, compteur ' + d.count;
consoleEl.textContent = d.lines.map(l => {
const s = Math.floor((l.t || 0) / 1000);
return '[' + s.toString().padStart(6, ' ') + 's] ' + l.m;
}).join('\n') || 'Aucun message.';
consoleEl.scrollTop = consoleEl.scrollHeight;
} catch {
metaEl.textContent = 'Erreur lecture journal';
consoleEl.textContent = 'Impossible de lire /api/debug/logs';
}
}
async function viderDebug() {
await fetch('/api/debug/clear', { method: 'POST' });
chargerDebug();
}
// --- Historique ---
let histMode = 'hires';
function setHistMode(mode) {
histMode = mode;
document.getElementById('btn-hires').classList.toggle('active-mode', mode === 'hires');
document.getElementById('btn-lores').classList.toggle('active-mode', mode === 'lores');
chargerHistorique();
}
async function chargerHistorique() {
try {
const url = histMode === 'hires' ? '/api/history/hires' : '/api/history';
const res = await fetch(url);
if (!res.ok) throw new Error('HTTP ' + res.status);
const d = await res.json();
await chargerHistoriqueStatus(d);
const ids = [
{ id: 'chart-battery', data: d.b, couleur: '#00b894', unite: 'V' },
{ id: 'chart-pv', data: d.p, couleur: '#e94560', unite: 'V' },
{ id: 'chart-load', data: d.l, couleur: '#fdcb6e', unite: 'W' },
{ id: 'chart-soc', data: d.s, couleur: '#74b9ff', unite: '%' },
];
ids.forEach(({ id, data, couleur, unite }) => {
const canvas = document.getElementById(id);
if (canvas) dessinerGraphe(canvas, data, couleur, unite, d.step);
});
afficherDerniersPoints(d);
} catch (e) {
setText('hist-debug', 'Erreur chargement historique: ' + e.message);
}
}
async function chargerHistoriqueStatus(hist) {
try {
const s = await (await fetch('/api/history/status')).json();
hist = hist || {};
const mode = hist.mode || histMode;
const n = hist.n !== undefined ? hist.n : 0;
const nbBat = hist.b ? hist.b.length : 0;
const nbPv = hist.p ? hist.p.length : 0;
const nbLoad = hist.l ? hist.l.length : 0;
const nbSoc = hist.s ? hist.s.length : 0;
const nextH = Math.ceil((s.next_hires_ms || 0) / 1000);
const nextL = Math.ceil((s.next_lores_ms || 0) / 1000);
setText('hist-debug',
'Mode ' + mode +
' | points affichés: ' + n +
' | séries b/p/l/s: ' + nbBat + '/' + nbPv + '/' + nbLoad + '/' + nbSoc +
' | hires: ' + s.hires_n + '/' + s.hires_max +
' | lores: ' + s.lores_n + '/' + s.lores_max +
' | acc: ' + s.acc_n +
' | RS485: ' + (s.rs485_ok ? 'OK' : 'ERR') +
' | prochain 1min: ' + nextH + 's' +
' | prochain 5min: ' + nextL + 's'
);
} catch {
setText('hist-debug', 'Debug historique indisponible');
}
}
function afficherDerniersPoints(hist) {
const el = document.getElementById('hist-last');
if (!el) return;
const b = (hist.b || []).map(Number);
const p = (hist.p || []).map(Number);
const l = (hist.l || []).map(Number);
const s = (hist.s || []).map(Number);
const n = Math.min(b.length, p.length, l.length, s.length);
if (!n) {
el.textContent = 'Aucun point historique reçu depuis l’API.';
return;
}
const debut = Math.max(0, n - 5);
const lignes = [];
for (let i = debut; i < n; i++) {
lignes.push(
'#' + (i + 1) +
' bat=' + b[i].toFixed(2) + 'V' +
' pv=' + p[i].toFixed(2) + 'V' +
' load=' + l[i].toFixed(1) + 'W' +
' soc=' + s[i].toFixed(0) + '%'
);
}
el.textContent = 'Derniers points: ' + lignes.join(' | ');
}
function dessinerGraphe(canvas, data, couleur, unite, stepSec) {
stepSec = stepSec || 300;
data = (data || []).map(Number).filter(Number.isFinite);
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const pad = { top: 12, right: 8, bottom: 28, left: 38 };
const w = W - pad.left - pad.right;
const h = H - pad.top - pad.bottom;
ctx.clearRect(0, 0, W, H);
if (!data.length) {
ctx.fillStyle = '#a0aec0';
ctx.font = '14px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Pas encore de données', W / 2, H / 2);
return;
}
const min = Math.min(...data);
const max = Math.max(...data);
const marge = Math.max((max - min) * 0.15, unite === '%' ? 2 : 0.2);
const yMin = min === max ? min - marge : min - marge;
const yMax = min === max ? max + marge : max + marge;
const range = yMax - yMin || 1;
// grille horizontale
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = pad.top + (h / 4) * i;
ctx.strokeStyle = '#0f3460';
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(pad.left + w, y); ctx.stroke();
const val = yMax - (range / 4) * i;
ctx.fillStyle = '#a0aec0';
ctx.font = '10px system-ui';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(val.toFixed(1) + unite, pad.left - 4, y);
}
if (data.length === 1) {
const x = pad.left + w / 2;
const y = pad.top + h - ((data[0] - yMin) / range) * h;
ctx.fillStyle = couleur;
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#a0aec0';
ctx.font = '11px system-ui';
ctx.textAlign = 'center';
ctx.fillText(data[0].toFixed(1) + unite, x, Math.max(12, y - 8));
return;
}
// courbe
ctx.strokeStyle = couleur;
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.beginPath();
data.forEach((v, i) => {
const x = pad.left + (i / (data.length - 1)) * w;
const y = pad.top + h - ((v - yMin) / range) * h;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
// remplissage sous la courbe
ctx.lineTo(pad.left + w, pad.top + h);
ctx.lineTo(pad.left, pad.top + h);
ctx.closePath();
ctx.fillStyle = couleur + '22';
ctx.fill();
// labels temps (axe X — ~5 labels)
const now = Date.now();
const step = Math.max(1, Math.floor(data.length / 5));
ctx.fillStyle = '#a0aec0';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic';
for (let i = 0; i < data.length; i += step) {
const x = pad.left + (i / (data.length - 1)) * w;
const t = new Date(now - (data.length - 1 - i) * stepSec * 1000);
ctx.fillText(
t.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
x, H - 5
);
}
}
// --- Long press relais dashboard ---
function getLongPressMs() {
return parseInt(localStorage.getItem('longpress_ms') || '500', 10);
}
function setupLongPress(carteId, relayNum) {
const el = document.getElementById(carteId);
if (!el) return;
let timer = null;
const start = (e) => {
e.preventDefault();
el.classList.add('press-hold');
timer = setTimeout(async () => {
timer = null;
el.classList.remove('press-hold');
el.classList.add('press-done');
setTimeout(() => el.classList.remove('press-done'), 500);
const res = await fetch('/api/relay/' + relayNum + '/toggle', { method: 'POST' });
if (!res.ok) {
const d = await res.json().catch(() => ({}));
afficherToast(d.err || 'Commande refusée');
} else {
afficherToast((relayNum === 1 ? noms.relay1 : noms.relay2) + ' basculé et sauvegardé');
}
rafraichir();
}, getLongPressMs());
};
const cancel = () => {
clearTimeout(timer);
timer = null;
el.classList.remove('press-hold');
};
el.addEventListener('pointerdown', start);
el.addEventListener('pointerup', cancel);
el.addEventListener('pointerleave', cancel);
el.addEventListener('contextmenu', e => e.preventDefault());
}
function setupSunLongPress() {
const el = document.getElementById('carte-sun');
if (!el) return;
let timer = null;
const start = (e) => {
e.preventDefault();
el.classList.add('press-hold');
timer = setTimeout(() => {
timer = null;
el.classList.remove('press-hold');
ouvrirSunPopup();
}, 2000);
};
const cancel = () => {
clearTimeout(timer);
timer = null;
el.classList.remove('press-hold');
};
el.addEventListener('pointerdown', start);
el.addEventListener('pointerup', cancel);
el.addEventListener('pointerleave', cancel);
el.addEventListener('contextmenu', e => e.preventDefault());
}
function formatSunHistoryTime(value) {
if (!value || value.indexOf('uptime') === 0) return value || '--';
const parts = value.split(' ');
if (parts.length !== 2) return value;
const d = parts[0].split('-');
const t = parts[1].split(':');
if (d.length !== 3 || t.length < 2) return value;
return t[0] + ':' + t[1] + ' ' + d[2] + '/' + d[1];
}
async function ouvrirSunPopup() {
const modal = document.getElementById('sun-modal');
const list = document.getElementById('sun-history-list');
if (!modal || !list) return;
modal.classList.remove('hidden');
list.textContent = 'Chargement...';
try {
const d = await (await fetch('/api/sun/history')).json();
const changes = d.changes || [];
if (!changes.length) {
list.innerHTML = 'Aucun changement enregistré depuis le boot.
';
return;
}
list.innerHTML = changes.slice().reverse().map(c =>
'' + formatSunHistoryTime(c.time) + '' + c.label + '
'
).join('');
} catch {
list.innerHTML = 'Erreur lecture historique jour/nuit.
';
}
}
function fermerSunPopup() {
const modal = document.getElementById('sun-modal');
if (modal) modal.classList.add('hidden');
}
// --- Démarrage ---
chargerNoms();
rafraichir();
setInterval(rafraichir, REFRESH_MS);
setInterval(() => {
const debugEl = document.getElementById('debug');
const debugActif = debugEl && debugEl.classList.contains('actif');
if (debugActif) chargerDebug();
}, 2000);
setInterval(() => {
const histEl = document.getElementById('historique');
const histActif = histEl && histEl.classList.contains('actif');
if (histActif) chargerHistorique();
}, 5000);
setupLongPress('carte-relay1', 1);
setupLongPress('carte-relay2', 2);
setupSunLongPress();