diff --git a/core/archipelago/src/api/rpc/mod.rs b/core/archipelago/src/api/rpc/mod.rs index 07779577..2a38a2e0 100644 --- a/core/archipelago/src/api/rpc/mod.rs +++ b/core/archipelago/src/api/rpc/mod.rs @@ -705,6 +705,7 @@ impl RpcHandler { "system.detect-usb-devices" => self.handle_system_detect_usb_devices().await, "system.disk-status" => self.handle_system_disk_status().await, "system.disk-cleanup" => self.handle_system_disk_cleanup().await, + "system.reboot" => self.handle_system_reboot(params).await, "system.factory-reset" => self.handle_system_factory_reset(params).await, // Opt-in anonymous analytics diff --git a/core/archipelago/src/api/rpc/system.rs b/core/archipelago/src/api/rpc/system.rs index 81168226..9b818370 100644 --- a/core/archipelago/src/api/rpc/system.rs +++ b/core/archipelago/src/api/rpc/system.rs @@ -642,6 +642,38 @@ async fn read_temperatures() -> Result> { impl RpcHandler { /// system.factory-reset — Wipe all user data, remove containers, and restart. /// Only preserves the data_dir itself (recreated empty on restart). + /// system.reboot — Reboot the machine. Requires password re-verification. + pub(super) async fn handle_system_reboot( + &self, + params: Option, + ) -> Result { + let password = params + .as_ref() + .and_then(|p| p.get("password")) + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing password — re-authentication required"))?; + + let valid = self.auth_manager.verify_password(password).await?; + if !valid { + return Err(anyhow::anyhow!("Password incorrect")); + } + + info!("System reboot initiated by user"); + + // Schedule reboot in 2 seconds (gives time for the RPC response to reach the client) + // Uses the tor-helper path unit pattern (writes action file, systemd triggers root service) + tokio::spawn(async { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let action = serde_json::json!({"action": "reboot"}); + let _ = tokio::fs::write( + "/var/lib/archipelago/tor-config/tor-action", + serde_json::to_string(&action).unwrap_or_default(), + ).await; + }); + + Ok(serde_json::json!({ "rebooting": true })) + } + pub(super) async fn handle_system_factory_reset( &self, params: Option, diff --git a/neode-ui/src/views/Settings.vue b/neode-ui/src/views/Settings.vue index 2eaf35fe..331b843d 100644 --- a/neode-ui/src/views/Settings.vue +++ b/neode-ui/src/views/Settings.vue @@ -991,6 +991,51 @@ + +
+
+
+

Reboot

+

Restart the machine. All containers will restart automatically.

+
+ +
+
+ + + +
+
+

Reboot Node

+

Enter your password to confirm reboot. The node will be temporarily unavailable.

+ +

{{ rebootError }}

+
+ + +
+
+
+
+

Factory Reset

@@ -1048,6 +1093,25 @@ const router = useRouter() const { t, locale } = useI18n() const store = useAppStore() +// Reboot +const showRebootConfirm = ref(false) +const rebooting = ref(false) +const rebootPassword = ref('') +const rebootError = ref('') +async function performReboot() { + if (!rebootPassword.value) return + rebooting.value = true + rebootError.value = '' + try { + await rpcClient.call({ method: 'system.reboot', params: { password: rebootPassword.value } }) + showRebootConfirm.value = false + rebootPassword.value = '' + } catch (e) { + rebootError.value = e instanceof Error ? e.message : 'Reboot failed' + rebooting.value = false + } +} + // Factory Reset const showFactoryResetConfirm = ref(false) const factoryResetLoading = ref(false) diff --git a/scripts/tor-helper.sh b/scripts/tor-helper.sh index 66db9fef..d0a5e608 100755 --- a/scripts/tor-helper.sh +++ b/scripts/tor-helper.sh @@ -109,6 +109,13 @@ case "$ACTION_TYPE" in write_result '{"ok":true}' ;; + reboot) + write_result '{"ok":true}' + log "System reboot initiated" + sleep 1 + systemctl reboot + ;; + *) log "Unknown action: $ACTION_TYPE" write_result '{"ok":false,"error":"Unknown action"}'