`,
}
diff --git a/starpilot/system/the_galaxy/assets/mobile/js/views/SystemTools.js b/starpilot/system/the_galaxy/assets/mobile/js/views/SystemTools.js
index 669cfcceee..59a4aa0522 100644
--- a/starpilot/system/the_galaxy/assets/mobile/js/views/SystemTools.js
+++ b/starpilot/system/the_galaxy/assets/mobile/js/views/SystemTools.js
@@ -17,6 +17,27 @@ function toPercent(value) {
}
const CORE_UPDATE_BRANCHES = ["StarPilot", "Dom"]
+const REBOOT_PENDING_STORAGE_KEY = "galaxy-update-reboot-pending"
+
+function readRebootMarker() {
+ try {
+ const raw = localStorage.getItem(REBOOT_PENDING_STORAGE_KEY)
+ if (!raw) return null
+ const parsed = JSON.parse(raw)
+ const startedAt = Number(parsed?.startedAt)
+ return Number.isFinite(startedAt) && startedAt > 0 ? startedAt : null
+ } catch (e) {
+ return null
+ }
+}
+
+function writeRebootMarker(startedAt) {
+ try { localStorage.setItem(REBOOT_PENDING_STORAGE_KEY, JSON.stringify({ startedAt })) } catch (e) {}
+}
+
+function clearRebootMarker() {
+ try { localStorage.removeItem(REBOOT_PENDING_STORAGE_KEY) } catch (e) {}
+}
export const SystemTools = {
name: "SystemTools",
@@ -45,6 +66,11 @@ export const SystemTools = {
isOnroad: false,
fastStatus: null,
+ statusUnavailable: false,
+ rebootPending: !!readRebootMarker(),
+ rebootStartedAt: readRebootMarker() || 0,
+ rebootOfflineSeen: false,
+ reconnectedNotice: false,
checkedForUpdates: false,
busy: "",
autoUpdateBusy: false,
@@ -80,8 +106,8 @@ export const SystemTools = {
return this.branchLoading || this.isOnroad || !!this.fastStatus?.isOnroad || this.updateInProgress || !!this.busy
},
statusRebooting() { return String(this.fastStatus?.stage || "").trim().toLowerCase() === "rebooting" },
- updateInProgress() { return !!this.fastStatus?.running || this.statusRebooting },
- statusPollingNeeded() { return !this.fastStatus || this.updateInProgress },
+ updateInProgress() { return !!this.fastStatus?.running || this.statusRebooting || this.rebootPending },
+ statusPollingNeeded() { return !this.fastStatus || this.updateInProgress || this.rebootPending },
versionChoices() { return this.targetBranch === "StarPilot" ? releaseVersions(this.versionCommits) : this.versionCommits },
installVersionBlocked() {
return this.branchSwitchBlocked || this.branchBusy || !this.branches.includes(this.targetBranch) ||
@@ -145,13 +171,42 @@ export const SystemTools = {
try {
const status = await api.getUpdateFastStatus()
if (!status) throw new Error("Update status unavailable")
+ const stage = String(status.stage || "").trim().toLowerCase()
+ if (stage === "rebooting" && !this.rebootPending) {
+ this.rebootPending = true
+ this.rebootStartedAt = Date.now()
+ writeRebootMarker(this.rebootStartedAt)
+ }
+ const pendingAge = this.rebootStartedAt ? Date.now() - this.rebootStartedAt : 0
+ const deviceReturned = this.rebootPending && !status.running && stage !== "rebooting" &&
+ (this.statusUnavailable || pendingAge >= 30_000)
+ const updateFailed = this.rebootPending && stage === "error"
this.fastStatus = status
+ this.statusUnavailable = false
this.isOnroad = !!status.isOnroad
+ if (deviceReturned) this.clearRebootPending()
+ else if (updateFailed) this.clearRebootPending(false)
} catch (e) {
- this.fastStatus = null
+ this.statusUnavailable = true
+ if (this.rebootPending) this.rebootOfflineSeen = true
+ else this.fastStatus = null
if (throwOnError) throw e
}
},
+ markRebootPending() {
+ this.rebootPending = true
+ this.rebootStartedAt = Date.now()
+ this.rebootOfflineSeen = false
+ this.reconnectedNotice = false
+ writeRebootMarker(this.rebootStartedAt)
+ },
+ clearRebootPending(showNotice = true) {
+ this.rebootPending = false
+ this.rebootStartedAt = 0
+ this.rebootOfflineSeen = false
+ clearRebootMarker()
+ if (showNotice) this.reconnectedNotice = true
+ },
async backupToggles() {
try {
const blob = await api.backupToggles()
@@ -228,6 +283,7 @@ export const SystemTools = {
if (!(await GalaxyConfirm({ title: "Reset toggles to default?", message: "This resets all toggles to their default values and reboots.", confirmLabel: "Reset", danger: true }))) return
try {
await api.resetTogglesDefault()
+ this.markRebootPending()
showSnackbar("Resetting toggles to default... rebooting.")
} catch (e) {
showSnackbar("Reset failed.", "error")
@@ -347,6 +403,7 @@ export const SystemTools = {
return
}
const result = await api.installUpdateVersion(branch, commit)
+ this.markRebootPending()
showSnackbar(result?.message || `Installing ${version} on ${branch}...`)
await this.loadFastStatus()
} catch (e) {
@@ -420,6 +477,7 @@ export const SystemTools = {
}
const fn = action === "fast" ? api.updateFast : action === "recover" ? api.updateRecover : api.updateRollback
const payload = await fn()
+ this.markRebootPending()
showSnackbar(payload?.message || "Update started.")
await this.loadFastStatus()
} catch (e) {
@@ -432,6 +490,7 @@ export const SystemTools = {
if (!(await GalaxyConfirm({ title: "Factory reset (SAVE ME)?", message: "This wipes params, backups, themes, models, maps, and route data, then reboots. This cannot be undone.", confirmLabel: "Factory Reset", danger: true }))) return
try {
await api.factoryReset()
+ this.markRebootPending()
showSnackbar("SAVE ME initiated — factory resetting...")
await this.loadFastStatus()
} catch (e) {
@@ -502,22 +561,26 @@ export const SystemTools = {
Loading update info...
+
+
Installed branch{{ fastStatus.branch || currentBranch || '—' }}
-
Stage{{ fastStatus.stage }} · {{ fastStatus.progressLabel }}
+
Stage{{ fastStatus.stage }} · {{ fastStatus.progressLabel }}
Local{{ shortCommit(fastStatus.localCommit) }}
Remote{{ shortCommit(fastStatus.remoteCommit) }}
-
{{ fastStatus.progressDetail }}
{{ fastStatus.message }}
+
Waiting for the device to reconnect. The last update status is being kept on screen.
{{ fastStatus.warning }}
{{ w }}
diff --git a/starpilot/system/the_galaxy/tests/test_ui_vue_frontend.py b/starpilot/system/the_galaxy/tests/test_ui_vue_frontend.py
index 66dc505462..710187dea2 100644
--- a/starpilot/system/the_galaxy/tests/test_ui_vue_frontend.py
+++ b/starpilot/system/the_galaxy/tests/test_ui_vue_frontend.py
@@ -68,9 +68,11 @@ def test_ui_device_picker_uses_gateway_directory_and_preserves_local_galaxy():
shell = _read("js/components/AppShell.js")
assert 'fetch("/_gateway/devices"' in picker
- assert "hasMultipleDevices" in picker
+ assert "localStorage" in picker
+ assert "Rename comma" in picker
+ assert "hasDevices" in picker
assert "window.location.assign(device.path)" in picker
- assert '
' in shell
+ assert shell.index('
') > shell.index('v-for="(links, section) in NAV"')
assert "Local Galaxy instances do not have the gateway directory endpoint." in picker