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