From 112ab8c30ac3e56d870f9e860134f08e3f90bbe1 Mon Sep 17 00:00:00 2001 From: grabowski Date: Wed, 29 Oct 2025 17:30:44 +0700 Subject: [PATCH] Add system shutdown/restart functionality via web interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the ability to safely shutdown or restart the Raspberry Pi from the web interface, with proper sudo permissions and user confirmations. Features: - API endpoints for /api/system/shutdown and /api/system/restart - System Control card in UI with shutdown and restart buttons - JavaScript confirmation dialogs to prevent accidental shutdowns - Shutdown has 1-minute delay, restart is immediate - Auto-reload after restart (30 second delay) - Sudoers file for passwordless sudo commands Technical details: - Uses subprocess.Popen() for non-blocking command execution - Shutdown: 'sudo shutdown -h +1' (1 minute delay) - Restart: 'sudo reboot' (immediate) - Created wedding-phone-shutdown sudoers file - Template version updated to 1.9.0 Installation required: sudo cp wedding-phone-shutdown /etc/sudoers.d/wedding-phone-shutdown sudo chmod 0440 /etc/sudoers.d/wedding-phone-shutdown 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- rotary_phone_web.py | 95 +++++++++++++++++++++++++++++++++++++++++- wedding-phone-shutdown | 8 ++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 wedding-phone-shutdown diff --git a/rotary_phone_web.py b/rotary_phone_web.py index 7eb65be..34c312c 100644 --- a/rotary_phone_web.py +++ b/rotary_phone_web.py @@ -151,7 +151,7 @@ BACKUP_ON_WRITE = BACKUP_CONFIG.get('backup_on_write', True) WEB_PORT = SYS_CONFIG['web']['port'] # Template version - increment this when HTML template changes -TEMPLATE_VERSION = "1.8.0" # Updated: Added recording sorting by date, name, duration, size +TEMPLATE_VERSION = "1.9.0" # Updated: Added system shutdown/restart controls # Flask app app = Flask(__name__) @@ -975,6 +975,28 @@ def api_backup_test(): except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 +@app.route('/api/system/shutdown', methods=['POST']) +def api_system_shutdown(): + """Shutdown the Raspberry Pi""" + try: + import subprocess + # Schedule shutdown in 1 minute to allow response to be sent + subprocess.Popen(['sudo', 'shutdown', '-h', '+1']) + return jsonify({"success": True, "message": "System will shutdown in 1 minute"}) + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/system/restart', methods=['POST']) +def api_system_restart(): + """Restart the Raspberry Pi""" + try: + import subprocess + # Schedule restart to allow response to be sent + subprocess.Popen(['sudo', 'reboot']) + return jsonify({"success": True, "message": "System will restart shortly"}) + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/upload_greeting', methods=['POST']) def upload_greeting(): """Upload a new greeting message""" @@ -1869,6 +1891,30 @@ def main(): + +
+

⚙️ System Control

+ +
+

+ Safely shutdown or restart the Raspberry Pi +

+ +
+ + +
+ +

+ ⚠️ System will shutdown/restart after confirmation +

+
+
+

🎵 Sound Assignment

@@ -2473,6 +2519,53 @@ def main(): }, 5000); } + // System Control Functions + function shutdownSystem() { + if (confirm('⚠️ Are you sure you want to SHUTDOWN the system?\n\nThe Raspberry Pi will power off in 1 minute.')) { + fetch('/api/system/shutdown', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert('🔌 System shutting down in 1 minute...', 'warning'); + setTimeout(() => { + showAlert('System is now shutting down. This page will be unavailable.', 'error'); + }, 3000); + } else { + showAlert('Error: ' + data.error, 'error'); + } + }) + .catch(error => showAlert('Error: ' + error, 'error')); + } + } + + function restartSystem() { + if (confirm('⚠️ Are you sure you want to RESTART the system?\n\nThe Raspberry Pi will reboot shortly.')) { + fetch('/api/system/restart', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert('🔄 System restarting...', 'warning'); + setTimeout(() => { + showAlert('System is rebooting. Page will reload automatically.', 'warning'); + // Try to reload page after 30 seconds + setTimeout(() => { + location.reload(); + }, 30000); + }, 3000); + } else { + showAlert('Error: ' + data.error, 'error'); + } + }) + .catch(error => showAlert('Error: ' + error, 'error')); + } + } + // USB Backup Functions function refreshBackupStatus() { fetch('/api/backup/status') diff --git a/wedding-phone-shutdown b/wedding-phone-shutdown new file mode 100644 index 0000000..aa0c141 --- /dev/null +++ b/wedding-phone-shutdown @@ -0,0 +1,8 @@ +# Allow wedding-phone user to shutdown and reboot without password +# Copy this file to /etc/sudoers.d/wedding-phone-shutdown +# Usage: sudo cp wedding-phone-shutdown /etc/sudoers.d/wedding-phone-shutdown +# Then: sudo chmod 0440 /etc/sudoers.d/wedding-phone-shutdown + +# Allow user to run shutdown and reboot commands without password +berwn ALL=(ALL) NOPASSWD: /sbin/shutdown +berwn ALL=(ALL) NOPASSWD: /sbin/reboot