fix(desktop): close the backend update overlay on success; error on no-return

Three rough edges in the remote backend apply flow:
- On success the overlay dropped to IDLE, briefly re-rendering the pre-install
  'update available' view and then the generic 'you're all set' before settling.
  Close the overlay outright once the backend is confirmed back instead of
  bouncing through the idle view.
- If the backend never came back (a failed restart), the flow still reported
  success. waitForBackendReturn now returns whether the backend answered;
  finishBackendApply surfaces an error when it didn't.
- The up-to-date copy said 'you're running the latest version', conflating
  client and backend. Backend target now reads 'the backend is running the
  latest version' — the client's own version is a separate pill.
This commit is contained in:
yoniebans
2026-06-08 08:58:26 -07:00
committed by Teknium
parent 81647458c7
commit cd030f5f40
8 changed files with 45 additions and 11 deletions
+13
View File
@@ -182,5 +182,18 @@ describe('applyBackendUpdate recovery', () => {
expect($backendUpdateApply.get().stage).toBe('idle')
expect($backendUpdateApply.get().applying).toBe(false)
})
it('surfaces an error when the backend never comes back after the restart', async () => {
updateHermesSpy.mockResolvedValue({ ok: true, name: 'update', pid: 1 })
getActionStatusSpy.mockRejectedValue(new Error('ECONNREFUSED'))
checkHermesUpdateSpy.mockRejectedValue(new Error('ECONNREFUSED'))
const promise = applyBackendUpdate()
await vi.advanceTimersByTimeAsync(70000)
const result = await promise
expect(result.ok).toBe(false)
expect($backendUpdateApply.get().stage).toBe('error')
})
})
+26 -10
View File
@@ -307,17 +307,39 @@ export async function applyUpdates(opts: DesktopUpdateApplyOptions = {}): Promis
const BACKEND_RETURN_POLL_MS = 1500
const BACKEND_RETURN_MAX_ATTEMPTS = 40
async function waitForBackendReturn(): Promise<void> {
async function waitForBackendReturn(): Promise<boolean> {
for (let attempt = 0; attempt < BACKEND_RETURN_MAX_ATTEMPTS; attempt += 1) {
await new Promise(resolve => globalThis.setTimeout(resolve, BACKEND_RETURN_POLL_MS))
try {
await checkHermesUpdate()
return
return true
} catch {
continue
}
}
return false
}
function finishBackendApply(returned: boolean): DesktopUpdateApplyResult {
if (returned) {
$backendUpdateApply.set(IDLE)
setUpdateOverlayOpen(false)
void checkBackendUpdates()
return { ok: true, message: 'Backend update applied.' }
}
$backendUpdateApply.set({
...$backendUpdateApply.get(),
applying: false,
stage: 'error',
error: 'apply-failed',
message: 'Backend updated but did not come back online. Check the backend host.'
})
return { ok: false, error: 'apply-failed', message: 'Backend did not come back online.' }
}
export async function applyBackendUpdate(): Promise<DesktopUpdateApplyResult> {
@@ -350,11 +372,8 @@ export async function applyBackendUpdate(): Promise<DesktopUpdateApplyResult> {
stage: 'restart',
message: 'Backend restarting to load the update…'
})
await waitForBackendReturn()
$backendUpdateApply.set(IDLE)
void checkBackendUpdates()
return { ok: true, message: 'Backend update applied; backend is back online.' }
return finishBackendApply(await waitForBackendReturn())
}
if (last && !last.running) {
@@ -365,11 +384,8 @@ export async function applyBackendUpdate(): Promise<DesktopUpdateApplyResult> {
const ok = !!last && (last.exit_code ?? 1) === 0
if (ok) {
$backendUpdateApply.set({ ...$backendUpdateApply.get(), applying: true, stage: 'restart', message: 'Backend restarting to load the update…' })
await waitForBackendReturn()
$backendUpdateApply.set(IDLE)
void checkBackendUpdates()
return { ok: true, message: 'Backend update applied.' }
return finishBackendApply(await waitForBackendReturn())
}
$backendUpdateApply.set({