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
|
Écrit userdata/private.css puis redémarre photobooth-app pour que le CSS
|
||||||
soit pris en compte.
|
soit pris en compte.
|
||||||
"""
|
"""
|
||||||
import asyncio, subprocess
|
|
||||||
pb = request.app.state.photobooth_service
|
pb = request.app.state.photobooth_service
|
||||||
config_svc = request.app.state.config_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():
|
async def _restart_photobooth():
|
||||||
import asyncio
|
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"sudo", "systemctl", "restart", "photobooth-app.service",
|
"sudo", "systemctl", "restart", "photobooth-app.service",
|
||||||
@@ -107,11 +105,71 @@ async def _restart_photobooth():
|
|||||||
@router.post("/system/restart-photobooth")
|
@router.post("/system/restart-photobooth")
|
||||||
async def restart_photobooth(request: Request):
|
async def restart_photobooth(request: Request):
|
||||||
"""Redémarre photobooth-app.service pour recharger la config (actions, etc.)."""
|
"""Redémarre photobooth-app.service pour recharger la config (actions, etc.)."""
|
||||||
import asyncio
|
|
||||||
asyncio.create_task(_restart_photobooth())
|
asyncio.create_task(_restart_photobooth())
|
||||||
return {"ok": True, "message": "Redémarrage de photobooth-app en cours…"}
|
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")
|
@router.post("/system/screen/refresh")
|
||||||
async def screen_refresh(request: Request):
|
async def screen_refresh(request: Request):
|
||||||
"""Envoie F5 à l'écran HDMI connecté (Chromium kiosque Wayland/X11)."""
|
"""Envoie F5 à l'écran HDMI connecté (Chromium kiosque Wayland/X11)."""
|
||||||
|
|||||||
@@ -142,12 +142,19 @@
|
|||||||
<div class="card-title">Services</div>
|
<div class="card-title">Services</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<table class="table">
|
<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">
|
<tbody id="services-tbody">
|
||||||
{% for svc in services %}
|
{% for svc in services %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ svc.label }}</td>
|
<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><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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -155,7 +162,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
@@ -324,6 +348,50 @@ async function saveEvent(isNew) {
|
|||||||
} catch(err) { showToast('Erreur sauvegarde événement', 'error'); }
|
} 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 ──────────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
loadQueue();
|
loadQueue();
|
||||||
loadEvent();
|
loadEvent();
|
||||||
|
|||||||
Reference in New Issue
Block a user