Resilience : ecran pause, reconnexion WS auto, watchdog systemd, WiFi monitor

- WebSocket : reconnexion avec backoff exponentiel au lieu de reload page
- Ecran pause avec tasse de cafe quand le backend est injoignable
- Endpoint /api/health pour monitoring externe
- Watchdog systemd (sd_notify READY=1 + WATCHDOG=1 toutes les 10s)
- Service systemd durci (Type=notify, WatchdogSec=30, KillMode, limites)
- WiFi watchdog : script + timer systemd, verifie la gateway toutes les 60s
- Destinations : fallback url_tunnel (WireGuard) en priorite

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 19:16:39 +02:00
parent 48f54ab70a
commit 4e431507fc
10 changed files with 273 additions and 99 deletions

View File

@@ -373,6 +373,7 @@
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
<button class="onglet" data-onglet="evenement">Evenement</button>
<button class="onglet" data-onglet="fonctions">Fonctions</button>
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive()">Eclairage</button>
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
</div>
@@ -691,6 +692,19 @@
</div>
<!-- Panneau Infos systeme -->
<div class="admin-panneau" id="panneau-eclairage">
<div style="display:flex;gap:1rem;align-items:flex-start;flex-wrap:wrap">
<div style="flex:1;min-width:280px">
<img id="eclairage-preview" style="width:100%;border-radius:8px;background:#111" alt="Preview">
</div>
<div style="flex:0 0 200px;text-align:center">
<div id="eclairage-score-box" style="font-size:3rem;font-weight:800;padding:1.5rem;border-radius:16px;background:#222;margin-bottom:1rem">--</div>
<div id="eclairage-action" style="font-size:1.2rem;font-weight:600;margin-bottom:.5rem">--</div>
<div id="eclairage-visages" style="font-size:.9rem;color:#aaa">--</div>
</div>
</div>
</div>
<div class="admin-panneau" id="panneau-infos">
<div class="infos-systeme-header">
<h3>Informations systeme</h3>
@@ -772,9 +786,26 @@
</div>
<script src="/js/websocket.js?v=3"></script>
<script src="/js/app.js?v=6"></script>
<script src="/js/camera.js?v=13"></script>
<!-- Ecran de pause (deconnexion backend) -->
<div id="ecran-pause" style="display:none;position:fixed;inset:0;z-index:9999;background:#0a0a0f;flex-direction:column;align-items:center;justify-content:center;text-align:center">
<div style="font-size:6rem;animation:pause-bob 2s ease-in-out infinite">&#9749;</div>
<h1 style="color:#fff;font-size:2.2rem;margin:1rem 0 .5rem;font-weight:700">Petite pause...</h1>
<p style="color:#888;font-size:1.2rem;max-width:400px;line-height:1.5">Le photobooth revient dans un instant.<br>Pas de panique, on s'occupe de tout !</p>
<div style="margin-top:2rem;display:flex;gap:8px">
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out infinite"></span>
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out .2s infinite"></span>
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out .4s infinite"></span>
</div>
<p id="pause-status" style="color:#555;font-size:.85rem;margin-top:2rem">Reconnexion en cours...</p>
</div>
<style>
@keyframes pause-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-20px)} }
@keyframes pause-dot { 0%,100%{opacity:.2;transform:scale(.8)} 50%{opacity:1;transform:scale(1.2)} }
</style>
<script src="/js/websocket.js?v=16"></script>
<script src="/js/app.js?v=15"></script>
<script src="/js/camera.js?v=15"></script>
<script src="/js/effects.js?v=4"></script>
<script src="/js/gallery.js?v=3"></script>
<script src="/js/share.js?v=7"></script>

View File

@@ -118,9 +118,34 @@ function allerA(ecran) {
chargerGalerie();
} else if (ecran === 'admin') {
chargerAdmin();
demarrerTimeoutAdmin();
} else {
arreterTimeoutAdmin();
}
}
let _adminTimeoutId = null;
const ADMIN_TIMEOUT_MS = 30000;
function demarrerTimeoutAdmin() {
arreterTimeoutAdmin();
_adminTimeoutId = setTimeout(() => { allerA('accueil'); }, ADMIN_TIMEOUT_MS);
const ecranAdmin = document.getElementById('ecran-admin');
ecranAdmin.removeEventListener('pointerdown', _resetAdminTimeout);
ecranAdmin.addEventListener('pointerdown', _resetAdminTimeout);
}
function _resetAdminTimeout() {
if (_adminTimeoutId) {
clearTimeout(_adminTimeoutId);
_adminTimeoutId = setTimeout(() => { allerA('accueil'); }, ADMIN_TIMEOUT_MS);
}
}
function arreterTimeoutAdmin() {
if (_adminTimeoutId) { clearTimeout(_adminTimeoutId); _adminTimeoutId = null; }
}
function recommencer() {
photosSession = [];
photoFinale = null;

View File

@@ -48,13 +48,19 @@ function lancerPreview() {
if (previewActif) return;
previewActif = true;
afficherErreurPreview(false);
// Flou le temps que la 1ere frame fraiche arrive (le miroir DSLR doit remonter
// apres la capture precedente) : evite l'impression d'image figee sur l'ancienne photo.
document.getElementById('img-preview')?.classList.add('preview-chargement');
wsEnvoyer({ type: 'preview_start' });
resetPreviewTimeout();
}
function demarrerEclairageLive() {
if (!previewActif) {
previewActif = true;
wsEnvoyer({ type: 'preview_start' });
resetPreviewTimeout();
}
}
function arreterPreview() {
if (!previewActif) return;
previewActif = false;
@@ -81,6 +87,10 @@ wsOnMessage('preview', (msg) => {
};
img.src = msg.image;
}
const adminPrev = document.getElementById('eclairage-preview');
if (adminPrev && document.getElementById('panneau-eclairage')?.classList.contains('actif')) {
adminPrev.src = msg.image;
}
afficherErreurPreview(false);
resetPreviewTimeout();
});
@@ -175,33 +185,32 @@ wsOnMessage('camera_ok', () => {
wsOnMessage('eclairage', (msg) => {
if (captureEnCours) return;
let el = document.getElementById('indicateur-eclairage');
if (!el) {
el = document.createElement('div');
el.id = 'indicateur-eclairage';
el.style.cssText = 'position:fixed;top:12px;left:12px;padding:8px 16px;border-radius:12px;font-size:.9rem;font-weight:600;z-index:90;transition:all .3s;pointer-events:none;display:flex;align-items:center;gap:8px;';
document.body.appendChild(el);
// Admin panel eclairage
const scoreBox = document.getElementById('eclairage-score-box');
const actionEl = document.getElementById('eclairage-action');
const visagesEl = document.getElementById('eclairage-visages');
if (scoreBox) {
const score = msg.score >= 0 ? msg.score : '--';
scoreBox.textContent = score;
if (msg.action === 'allumer') {
scoreBox.style.background = '#dc2626'; scoreBox.style.color = '#fff';
actionEl.textContent = 'ALLUMER LA LUMIERE';
actionEl.style.color = '#f87171';
} else if (msg.action === 'attention') {
scoreBox.style.background = '#d97706'; scoreBox.style.color = '#fff';
actionEl.textContent = 'Eclairage faible';
actionEl.style.color = '#fbbf24';
} else if (msg.action === 'ok') {
scoreBox.style.background = '#16a34a'; scoreBox.style.color = '#fff';
actionEl.textContent = 'Eclairage OK';
actionEl.style.color = '#4ade80';
} else {
scoreBox.style.background = '#222'; scoreBox.style.color = '#888';
actionEl.textContent = 'Aucun visage';
actionEl.style.color = '#888';
}
visagesEl.textContent = msg.visages > 0 ? msg.visages + ' visage' + (msg.visages > 1 ? 's' : '') + ' detecte' + (msg.visages > 1 ? 's' : '') : '';
}
const score = msg.score >= 0 ? msg.score : '?';
const faces = msg.visages || 0;
if (msg.action === 'allumer') {
el.style.background = 'rgba(239,68,68,.9)';
el.style.color = '#fff';
el.innerHTML = `☀️ ALLUMER <span style="opacity:.7;font-size:.8rem">${score}/100 · ${faces} visage${faces > 1 ? 's' : ''}</span>`;
} else if (msg.action === 'attention') {
el.style.background = 'rgba(245,158,11,.85)';
el.style.color = '#fff';
el.innerHTML = `⚠️ Faible <span style="opacity:.7;font-size:.8rem">${score}/100 · ${faces} visage${faces > 1 ? 's' : ''}</span>`;
} else if (msg.action === 'ok') {
el.style.background = 'rgba(34,197,94,.7)';
el.style.color = '#fff';
el.innerHTML = `✓ <span style="opacity:.7;font-size:.8rem">${score}/100 · ${faces} visage${faces > 1 ? 's' : ''}</span>`;
} else if (msg.action === 'no_face') {
el.style.background = 'rgba(100,100,120,.6)';
el.style.color = '#ccc';
el.innerHTML = `<span style="font-size:.8rem">Aucun visage</span>`;
}
el.style.display = '';
});
// --- Capture ---

View File

@@ -1,30 +1,38 @@
/* Communication WebSocket avec le backend */
/* Communication WebSocket avec le backend — reconnexion résiliente */
let ws = null;
let wsReconnectTimer = null;
let _wsReconnectDelay = 1000;
let _wsWasConnected = false;
const wsCallbacks = {};
function wsConnecter() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}/ws`);
try { ws = new WebSocket(`${proto}//${location.host}/ws`); } catch(e) { _wsScheduleReconnect(); return; }
ws.onopen = () => {
console.log('WebSocket connecte');
if (wsReconnectTimer) {
clearInterval(wsReconnectTimer);
wsReconnectTimer = null;
_wsReconnectDelay = 1000;
if (_wsWasConnected) {
_masquerPause();
// Recharger la config pour resynchroniser l'état
if (typeof chargerConfig === 'function') chargerConfig();
}
_wsWasConnected = true;
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
const cbs = wsCallbacks[msg.type];
if (cbs) cbs.forEach(cb => cb(msg));
try {
const msg = JSON.parse(event.data);
const cbs = wsCallbacks[msg.type];
if (cbs) cbs.forEach(cb => cb(msg));
} catch(e) {}
};
ws.onclose = () => {
console.log('WebSocket deconnecte, rechargement dans 5s...');
setTimeout(() => { location.reload(); }, 5000);
console.log('WebSocket deconnecte');
ws = null;
_afficherPause();
_wsScheduleReconnect();
};
ws.onerror = () => {
@@ -32,6 +40,26 @@ function wsConnecter() {
};
}
function _wsScheduleReconnect() {
const delay = Math.min(_wsReconnectDelay, 10000);
_wsReconnectDelay = Math.min(_wsReconnectDelay * 1.5, 10000);
setTimeout(wsConnecter, delay);
}
function _afficherPause() {
const el = document.getElementById('ecran-pause');
if (el) el.style.display = 'flex';
}
function _masquerPause() {
const el = document.getElementById('ecran-pause');
if (el) { el.style.display = 'none'; }
// Relancer le preview si on était sur l'accueil
if (typeof lancerPreview === 'function' && document.getElementById('ecran-accueil')?.classList.contains('actif')) {
lancerPreview();
}
}
function wsEnvoyer(msg) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));