Dashboard: boutons restart services, JH-Photomaton et reboot Pi
This commit is contained in:
@@ -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
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user