diff --git a/public/css/mobile.css b/public/css/mobile.css index 75b7cdc..9ed967b 100644 --- a/public/css/mobile.css +++ b/public/css/mobile.css @@ -35,6 +35,9 @@ button { cursor: pointer; } button:active { filter: brightness(1.3); } button.wide { width: 100%; margin-top: 10px; font-size: 15px; } #audio-src { width: 100%; } +button.danger { border-color: rgba(255,120,120,.5); color: #ffb3b3; background: rgba(255,80,80,.10); } +button.danger.armed { background: #c0392b; color: #fff; border-color: #c0392b; animation: pulse 1s infinite; } +@keyframes pulse { 50% { filter: brightness(1.35); } } input[type=range] { -webkit-appearance: none; appearance: none; background: transparent; padding: 0; height: 34px; border: none; } input[type=range]::-webkit-slider-runnable-track { height: 6px; border-radius: 4px; background: rgba(255,255,255,.14); } input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; border-radius: 50%; background: var(--accent); margin-top: -9px; box-shadow: 0 1px 4px rgba(0,0,0,.5); } diff --git a/public/index.html b/public/index.html index d7076fc..244f623 100644 --- a/public/index.html +++ b/public/index.html @@ -126,6 +126,10 @@
+ + + +
Tocca due volte per confermare. Dopo lo spegnimento, per riaccendere stacca e riattacca l'alimentazione.
diff --git a/public/js/control.js b/public/js/control.js index 0597cf3..dbfd82b 100644 --- a/public/js/control.js +++ b/public/js/control.js @@ -576,6 +576,24 @@ function updateRec() { $('#rec').classList.toggle('recording', recOn); } +// Power: reboot / shutdown the Pi. Two-tap confirm to avoid accidental taps. +function armPower(btnId, action) { + const btn = $('#' + btnId); if (!btn) return; + const orig = btn.textContent; let armed = false, t = null; + btn.addEventListener('click', () => { + if (!armed) { + armed = true; btn.classList.add('armed'); btn.textContent = '⚠️ Tocca per confermare'; + t = setTimeout(() => { armed = false; btn.classList.remove('armed'); btn.textContent = orig; }, 4000); + return; + } + clearTimeout(t); armed = false; btn.classList.remove('armed'); btn.textContent = orig; + $('#pw-status').textContent = action === 'reboot' ? '🔄 Riavvio in corso… il telecomando si riconnette da solo tra ~40s.' : '⏻ Spegnimento in corso… per riaccendere stacca e riattacca l\'alimentazione.'; + fetch('/api/power/' + action, { method: 'POST' }).catch(() => {}); + }); +} +armPower('pw-reboot', 'reboot'); +armPower('pw-shutdown', 'shutdown'); + // ---- Reports --------------------------------------------------------------- function setMeter(id, v) { const e = $('#' + id); if (e) e.style.width = Math.min(100, (v || 0) * 100) + '%'; } djv.onReport((m) => { diff --git a/server.js b/server.js index 2b9442b..9561f15 100644 --- a/server.js +++ b/server.js @@ -69,6 +69,16 @@ app.post('/api/upload', upload.array('files'), (req, res) => { app.get('/api/clients', (_req, res) => { res.json([...wss.clients].map((c) => ({ role: c.role, open: c.readyState === 1 }))); }); +// ---- Power: reboot / shutdown the appliance (sudo is passwordless on the Pi) - +app.post('/api/power/:action', (req, res) => { + const action = req.params.action; + if (action !== 'reboot' && action !== 'shutdown') return res.status(400).json({ ok: false, error: 'azione non valida' }); + res.json({ ok: true, action }); + // Reply first, then act — the command tears down this very process/host. + // Use systemctl explicitly (the service PATH is minimal); sudo is passwordless. + setTimeout(() => { try { execFile('sudo', ['systemctl', action === 'reboot' ? 'reboot' : 'poweroff']); } catch (e) {} }, 700); +}); + app.get('/api/svgs', (_req, res) => { let files = []; try { files = fs.readdirSync(SVG_DIR).filter(f => /\.svg$/i.test(f)); } catch (e) {}