Add system shutdown/restart functionality via web interface

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 <noreply@anthropic.com>
This commit is contained in:
2025-10-29 17:30:44 +07:00
parent 63ce5b1703
commit 112ab8c30a
2 changed files with 102 additions and 1 deletions

View File

@@ -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():
</div>
</div>
<!-- System Control Card -->
<div class="card">
<h2>⚙️ System Control</h2>
<div style="padding: 20px;">
<p style="margin: 0 0 20px 0; color: #6b7280; text-align: center;">
Safely shutdown or restart the Raspberry Pi
</p>
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
<button class="btn btn-danger" onclick="shutdownSystem()" style="min-width: 150px;">
🔌 Shutdown
</button>
<button class="btn btn-warning" onclick="restartSystem()" style="min-width: 150px; background: #f59e0b;">
🔄 Restart
</button>
</div>
<p style="margin-top: 15px; color: #9ca3af; font-size: 0.8em; text-align: center;">
⚠️ System will shutdown/restart after confirmation
</p>
</div>
</div>
<!-- Sound Assignment Card -->
<div class="card">
<h2>🎵 Sound Assignment</h2>
@@ -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')

8
wedding-phone-shutdown Normal file
View File

@@ -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