Add backend disconnect detection and button type attributes to self-update modal

- Add observedBackendUnavailable flag to track backend state transitions - Update restart flow to wait for backend disconnect before reload - Add intermediate restart states: "Restarting backend" and "Update in progress" - Change health check logic to require backend unavailability before reload - Handle restart request errors with proper HTTP status validation - Add type="button" to all modal footer buttons to prevent form submission

frdel committed Mar 24, 2026 at 21:15 UTC f7d68d341095de8ebbef55b40368a06eafffcdb9
3 files changed +33 -5
tests/test_self_update_tag_filter.py
+5
@@ -54,6 +54,10 @@ def test_self_update_frontend_filters_old_tag_suggestions():
54 assert "this.info?.defaults?.branch ||" in content
55 assert "Version ${tag} does not exist on branch" in content
56 assert "this.selectedTagExistsOnBranch" in content
57 + assert 'const response = await fetch("/api/health"' in content
58 + assert "if (response.ok && observedBackendUnavailable)" in content
59 + assert "Waiting for Agent Zero to disconnect before reloading the page." in content
60 + assert "/api/csrf_token" not in content
61
62
63 def test_self_update_modal_validates_exact_tag_on_blur():
@@ -70,6 +74,7 @@ def test_self_update_modal_validates_exact_tag_on_blur():
74 assert '@blur="$store.selfUpdateStore.onTagBlur()"' in content
75 assert '@mousedown.prevent="$store.selfUpdateStore.selectTag(tag)"' in content
76 assert "$store.selfUpdateStore.tagExistenceWarning" in content
77 + assert 'type="button"' in content
78
79
80 def test_self_update_schedule_rejects_missing_tag_on_branch(monkeypatch, tmp_path):
webui/components/settings/external/self-update-modal.html
+3
@@ -296,6 +296,7 @@
296
297 <div class="modal-footer" data-modal-footer>
298 <button
299 + type="button"
300 class="btn btn-ok"
301 @click="$store.selfUpdateStore.scheduleUpdate()"
302 :disabled="!$store.selfUpdateStore.canScheduleUpdate"
@@ -303,6 +304,7 @@
304 Schedule Update And Restart
305 </button>
306 <button
307 + type="button"
308 class="btn btn-field"
309 @click="$store.selfUpdateStore.refresh()"
310 :disabled="$store.selfUpdateStore.isBusy"
@@ -310,6 +312,7 @@
312 Refresh Status
313 </button>
314 <button
315 + type="button"
316 class="btn btn-cancel"
317 @click="$store.selfUpdateStore.close()"
318 :disabled="$store.selfUpdateStore.restarting"
webui/components/settings/external/self-update-store.js
+25 -5
@@ -492,6 +492,7 @@ const model = {
492 async restartAndReload() {
493 this.restarting = true;
494 this.clearReconnectTimer();
495 + let observedBackendUnavailable = false;
496 this.setRestartState(
497 "Starting self-update",
498 "The request was saved. Agent Zero is about to restart and apply the requested branch and tag."
@@ -500,7 +501,7 @@ const model = {
501
502 try {
503 const token = await API.getCsrfToken();
503 - void fetch("/api/restart", {
504 + const restartResponse = await fetch("/api/restart", {
505 method: "POST",
506 credentials: "same-origin",
507 keepalive: true,
@@ -509,9 +510,10 @@ const model = {
510 "X-CSRF-Token": token,
511 },
512 body: JSON.stringify({}),
512 - }).catch(() => {
513 - // The restart request usually terminates the backend mid-flight.
513 });
514 + if (restartResponse && !restartResponse.ok) {
515 + throw new Error(`Restart request failed with HTTP ${restartResponse.status}.`);
516 + }
517 } catch (_error) {
518 // The restart request often terminates the backend mid-flight.
519 }
@@ -536,14 +538,32 @@ const model = {
538 credentials: "same-origin",
539 cache: "no-store",
540 });
539 - if (response.ok) {
541 + if (response.ok && observedBackendUnavailable) {
542 const returnUrl = this.getSavedReturnUrl() || window.location.href;
543 this.saveReturnUrl("");
544 window.location.replace(returnUrl);
545 return;
546 }
545 - lastError = `Health check returned HTTP ${response.status}.`;
547 + if (response.ok) {
548 + this.setRestartState(
549 + "Restarting backend",
550 + "Waiting for Agent Zero to disconnect before reloading the page."
551 + );
552 + lastError = "Health check is still responding before the restart has completed.";
553 + } else {
554 + observedBackendUnavailable = true;
555 + this.setRestartState(
556 + "Update in progress",
557 + "Agent Zero is restarting and the updater is running. This page will reload automatically when the health check becomes healthy again."
558 + );
559 + lastError = `Health check returned HTTP ${response.status}.`;
560 + }
561 } catch (error) {
562 + observedBackendUnavailable = true;
563 + this.setRestartState(
564 + "Update in progress",
565 + "Agent Zero is temporarily unavailable while it restarts. Waiting for the new runtime to become healthy."
566 + );
567 lastError = error?.message || String(error);
568 }
569