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:
@@ -151,7 +151,7 @@ BACKUP_ON_WRITE = BACKUP_CONFIG.get('backup_on_write', True)
|
|||||||
WEB_PORT = SYS_CONFIG['web']['port']
|
WEB_PORT = SYS_CONFIG['web']['port']
|
||||||
|
|
||||||
# Template version - increment this when HTML template changes
|
# 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
|
# Flask app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -975,6 +975,28 @@ def api_backup_test():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
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'])
|
@app.route('/upload_greeting', methods=['POST'])
|
||||||
def upload_greeting():
|
def upload_greeting():
|
||||||
"""Upload a new greeting message"""
|
"""Upload a new greeting message"""
|
||||||
@@ -1869,6 +1891,30 @@ def main():
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Sound Assignment Card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>🎵 Sound Assignment</h2>
|
<h2>🎵 Sound Assignment</h2>
|
||||||
@@ -2473,6 +2519,53 @@ def main():
|
|||||||
}, 5000);
|
}, 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
|
// USB Backup Functions
|
||||||
function refreshBackupStatus() {
|
function refreshBackupStatus() {
|
||||||
fetch('/api/backup/status')
|
fetch('/api/backup/status')
|
||||||
|
|||||||
8
wedding-phone-shutdown
Normal file
8
wedding-phone-shutdown
Normal 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
|
||||||
Reference in New Issue
Block a user