Dashboard: boutons restart services, JH-Photomaton et reboot Pi
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled

This commit is contained in:
2026-07-18 10:35:43 +02:00
parent 3522d2d996
commit fb35533b37
3 changed files with 148 additions and 4 deletions
+18
View File
@@ -0,0 +1,18 @@
# JH Photomaton — droits sudo sans mot de passe pour la gestion des services
#
# INSTALLATION :
# sudo cp sudoers-jh-photomaton /etc/sudoers.d/jh-photomaton
# sudo chmod 440 /etc/sudoers.d/jh-photomaton
# sudo visudo -cf /etc/sudoers.d/jh-photomaton # vérification syntaxe
#
# Ces règles permettent à l'utilisateur "pi" de redémarrer les services
# nécessaires au photomaton ainsi que de rebooter le Pi, sans mot de passe.
pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart photobooth-app.service
pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart photobooth-kiosk.service
pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart zoraxy.service
pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart hostapd.service
pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart dnsmasq.service
pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart cups.service
pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart jh-photomaton.service
pi ALL=(ALL) NOPASSWD: /usr/sbin/reboot
+61 -3
View File
@@ -79,7 +79,6 @@ async def set_delete_button(request: Request, visible: bool):
Écrit userdata/private.css puis redémarre photobooth-app pour que le CSS
soit pris en compte.
"""
import asyncio, subprocess
pb = request.app.state.photobooth_service
config_svc = request.app.state.config_service
@@ -93,7 +92,6 @@ async def set_delete_button(request: Request, visible: bool):
async def _restart_photobooth():
import asyncio
await asyncio.sleep(0.5)
proc = await asyncio.create_subprocess_exec(
"sudo", "systemctl", "restart", "photobooth-app.service",
@@ -107,11 +105,71 @@ async def _restart_photobooth():
@router.post("/system/restart-photobooth")
async def restart_photobooth(request: Request):
"""Redémarre photobooth-app.service pour recharger la config (actions, etc.)."""
import asyncio
asyncio.create_task(_restart_photobooth())
return {"ok": True, "message": "Redémarrage de photobooth-app en cours…"}
# ── Gestion services & système ────────────────────────────────────────────────
@router.get("/system/ping")
async def ping():
"""Endpoint de health-check — utilisé par le polling de reconnexion côté client."""
return {"ok": True}
@router.post("/system/service/{name}/restart")
async def restart_service(name: str):
"""Redémarre un service systemd autorisé."""
if name not in _ALLOWED_SERVICES:
return JSONResponse({"ok": False, "error": f"Service '{name}' non autorisé"}, status_code=403)
asyncio.create_task(_run_systemctl_restart(name))
return {"ok": True, "message": f"Redémarrage de {name} en cours…"}
async def _run_systemctl_restart(name: str):
proc = await asyncio.create_subprocess_exec(
"sudo", "systemctl", "restart", f"{name}.service",
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await proc.wait()
logger.info("%s redémarré (exit %d)", name, proc.returncode)
@router.post("/system/restart-self")
async def restart_self():
"""Redémarre le service jh-photomaton lui-même (avec délai pour que la réponse parte d'abord)."""
asyncio.create_task(_restart_self_delayed())
return {"ok": True, "message": "Redémarrage de JH-Photomaton en cours…"}
async def _restart_self_delayed():
await asyncio.sleep(1.5)
proc = await asyncio.create_subprocess_exec(
"sudo", "systemctl", "restart", "jh-photomaton.service",
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await proc.wait()
@router.post("/system/reboot")
async def reboot_pi():
"""Redémarre le Raspberry Pi (avec délai pour que la réponse parte d'abord)."""
asyncio.create_task(_reboot_delayed())
return {"ok": True, "message": "Redémarrage du Pi en cours…"}
async def _reboot_delayed():
await asyncio.sleep(2)
proc = await asyncio.create_subprocess_exec(
"sudo", "reboot",
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await proc.wait()
@router.post("/system/screen/refresh")
async def screen_refresh(request: Request):
"""Envoie F5 à l'écran HDMI connecté (Chromium kiosque Wayland/X11)."""
+69 -1
View File
@@ -142,12 +142,19 @@
<div class="card-title">Services</div>
<div class="card">
<table class="table">
<thead><tr><th>Service</th><th>Statut</th></tr></thead>
<thead><tr><th>Service</th><th>Statut</th><th>Action</th></tr></thead>
<tbody id="services-tbody">
{% for svc in services %}
<tr>
<td>{{ svc.label }}</td>
<td><span class="badge {{ 'badge-success' if svc.active else 'badge-error' }}">{{ 'Actif' if svc.active else 'Arrêté' }}</span></td>
<td>
{% if svc.name == 'jh-photomaton' %}
<button class="btn btn-ghost btn-sm" onclick="restartSelf()">🔄 Restart</button>
{% else %}
<button class="btn btn-ghost btn-sm" onclick="restartService('{{ svc.name }}', '{{ svc.label }}')">🔄 Restart</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
@@ -155,7 +162,24 @@
</div>
</div>
<!-- Actions système -->
<div class="section">
<div class="card-title">Actions système</div>
<div class="card flex gap-2" style="flex-wrap:wrap;align-items:center">
<button class="btn btn-primary btn-sm" onclick="restartSelf()">🔄 Redémarrer JH-Photomaton</button>
<button class="btn btn-danger btn-sm" onclick="rebootPi()">⚡ Redémarrer le Raspberry Pi</button>
</div>
</div>
</div>
<!-- Overlay reconnexion -->
<div id="reconnect-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:9999;display:none;align-items:center;justify-content:center;flex-direction:column;gap:1rem">
<div style="color:#fff;font-size:1.2rem;font-weight:600" id="reconnect-msg">Redémarrage en cours…</div>
<div style="color:#aaa;font-size:.9rem">Reconnexion automatique dans quelques secondes…</div>
<div class="spinner" style="width:40px;height:40px;border:4px solid #444;border-top-color:#4f9cf9;border-radius:50%;animation:spin 1s linear infinite"></div>
</div>
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
{% endblock %}
{% block scripts %}
@@ -324,6 +348,50 @@ async function saveEvent(isNew) {
} catch(err) { showToast('Erreur sauvegarde événement', 'error'); }
}
// ── Restart services ─────────────────────────────────────────────────────────
async function restartService(name, label) {
if (!confirm(`Redémarrer ${label} ?\nLe service sera temporairement indisponible.`)) return;
try {
await api('POST', `/api/system/service/${name}/restart`);
showToast(`🔄 ${label} redémarre…`, 'info');
} catch(e) { showToast('Erreur restart ' + name, 'error'); }
}
async function restartSelf() {
if (!confirm('Redémarrer JH-Photomaton ?\nVous serez déconnecté quelques secondes.')) return;
try {
await api('POST', '/api/system/restart-self');
showReconnectOverlay('Redémarrage de JH-Photomaton…');
} catch(e) { showToast('Erreur restart', 'error'); }
}
async function rebootPi() {
if (!confirm('⚡ ATTENTION : Redémarrer le Raspberry Pi ?\n\nTout sera arrêté (photobooth, WiFi, impression).\nLe démarrage prend environ 30 secondes.\n\nConfirmer le redémarrage ?')) return;
try {
await api('POST', '/api/system/reboot');
showReconnectOverlay('Redémarrage du Raspberry Pi…');
} catch(e) { showToast('Erreur reboot', 'error'); }
}
function showReconnectOverlay(msg) {
const overlay = document.getElementById('reconnect-overlay');
document.getElementById('reconnect-msg').textContent = msg;
overlay.style.display = 'flex';
// Attendre 5s puis commencer à sonder /api/system/ping
setTimeout(pollReconnect, 5000);
}
async function pollReconnect() {
try {
const r = await fetch('/api/system/ping');
if (r.ok) {
window.location.reload();
return;
}
} catch(e) { /* serveur pas encore là */ }
setTimeout(pollReconnect, 2000);
}
// ── Init ──────────────────────────────────────────────────────────────────────
loadQueue();
loadEvent();