From b28d6dd32c23767f7856d211616b1289eceeee2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:41:20 +0000 Subject: [PATCH 1/3] Initial plan From c3950547b0eb6927e2ac77fc19c943052402f5ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:50:01 +0000 Subject: [PATCH 2/3] feat: implement consistent restart UX across Sovran_SystemsOS Hub - Add Restart Entire System sidebar action with amber treatment and divider - Add shared restart confirmation dialog with conflict detection - Update reboot overlay: new title, body copy, status progression, error card - Improve doReboot(): failure handling, aria-live status messages - Standardize 'restart required' / 'Restart Entire System' terminology - Update security.js reboot flow to use shared doReboot() - Update installer.py button label - Add .btn-restart-amber, .restart-conflict-box, .sidebar-restart-btn CSS --- .../static/css/layout.css | 15 +++++ .../static/css/modals.css | 42 ++++++++++++++ app/sovran_systemsos_web/static/js/events.js | 50 +++++++++++++++++ app/sovran_systemsos_web/static/js/rebuild.js | 2 +- .../static/js/security.js | 19 +++---- app/sovran_systemsos_web/static/js/state.js | 9 +++ app/sovran_systemsos_web/static/js/tiles.js | 17 ++++++ app/sovran_systemsos_web/static/js/update.js | 38 ++++++++++++- app/sovran_systemsos_web/templates/index.html | 55 +++++++++++++++---- iso/installer.py | 2 +- 10 files changed, 223 insertions(+), 26 deletions(-) diff --git a/app/sovran_systemsos_web/static/css/layout.css b/app/sovran_systemsos_web/static/css/layout.css index aa90940..446b3fd 100644 --- a/app/sovran_systemsos_web/static/css/layout.css +++ b/app/sovran_systemsos_web/static/css/layout.css @@ -84,6 +84,21 @@ margin: 16px 0; } +/* ── Sidebar restart button (amber/muted treatment) ─────────────── */ + +.sidebar-restart-btn { + border-color: rgba(184, 125, 0, 0.25); +} + +.sidebar-restart-btn:hover { + border-color: #b87d00; + background-color: rgba(184, 125, 0, 0.08); +} + +.sidebar-restart-btn .sidebar-restart-hint { + color: #c98d08; +} + /* ── Upgrade modal ──────────────────────────────────────────────── */ .upgrade-dialog { diff --git a/app/sovran_systemsos_web/static/css/modals.css b/app/sovran_systemsos_web/static/css/modals.css index 5afe44b..c08322a 100644 --- a/app/sovran_systemsos_web/static/css/modals.css +++ b/app/sovran_systemsos_web/static/css/modals.css @@ -102,6 +102,48 @@ button.btn-reboot:hover:not(:disabled) { background-color: #529E7E; } +/* Restart = AMBER (manual restart action) */ +.btn-restart-amber { + background-color: #b87d00; + color: #fff; +} + +.btn-restart-amber:hover:not(:disabled) { + background-color: #9a6800; +} + +/* Restart conflict warning box */ +.restart-conflict-box { + background-color: rgba(180, 100, 0, 0.12); + border-left: 3px solid #c97a00; + border-radius: 6px; + padding: 12px 14px; + margin-bottom: 14px; +} + +.restart-conflict-title { + font-size: 0.88rem; + font-weight: 700; + color: #e69000; + margin: 0 0 6px 0; +} + +.restart-conflict-desc { + font-size: 0.83rem; + color: var(--text-secondary); + line-height: 1.5; + margin: 0; +} + +/* Reboot error card actions row */ +.reboot-error-actions { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; + margin-top: 20px; +} + .btn-save { background-color: var(--yellow); color: #0A1A10; diff --git a/app/sovran_systemsos_web/static/js/events.js b/app/sovran_systemsos_web/static/js/events.js index ae413dd..67923ce 100644 --- a/app/sovran_systemsos_web/static/js/events.js +++ b/app/sovran_systemsos_web/static/js/events.js @@ -44,6 +44,25 @@ if ($upgradeCloseBtn) $upgradeCloseBtn.addEventListener("click", closeUpgradeMod if ($upgradeCancelBtn) $upgradeCancelBtn.addEventListener("click", closeUpgradeModal); if ($upgradeModal) $upgradeModal.addEventListener("click", function(e) { if (e.target === $upgradeModal) closeUpgradeModal(); }); +// Restart confirm dialog +if ($restartConfirmCancel) $restartConfirmCancel.addEventListener("click", closeRestartConfirmDialog); +if ($restartConfirmModal) $restartConfirmModal.addEventListener("click", function(e) { if (e.target === $restartConfirmModal) closeRestartConfirmDialog(); }); +if ($restartConfirmModal) $restartConfirmModal.addEventListener("keydown", function(e) { if (e.key === "Escape") closeRestartConfirmDialog(); }); +if ($restartConfirmOk) $restartConfirmOk.addEventListener("click", function() { + if ($restartConfirmOk.disabled) return; + $restartConfirmOk.disabled = true; + closeRestartConfirmDialog(); + doReboot(); +}); + +// Reboot error card buttons +var $rebootErrorCloseBtn = document.getElementById("reboot-error-close-btn"); +var $rebootErrorRetryBtn = document.getElementById("reboot-error-retry-btn"); +if ($rebootErrorCloseBtn) $rebootErrorCloseBtn.addEventListener("click", function() { + if ($rebootOverlay) $rebootOverlay.classList.remove("visible"); +}); +if ($rebootErrorRetryBtn) $rebootErrorRetryBtn.addEventListener("click", doReboot); + // ── Upgrade modal functions ─────────────────────────────────────── function openUpgradeModal() { @@ -54,6 +73,37 @@ function closeUpgradeModal() { if ($upgradeModal) $upgradeModal.classList.remove("open"); } +// ── Restart confirm dialog functions ───────────────────────────── + +var _restartDialogOpener = null; + +function openRestartConfirmDialog() { + if (!$restartConfirmModal) return; + _restartDialogOpener = document.activeElement; + + // Detect conflicting operations + var conflicted = !!_updatePollTimer || !!_rebuildPollTimer; + if ($restartConflictBox) $restartConflictBox.style.display = conflicted ? "" : "none"; + if ($restartConfirmOk) $restartConfirmOk.disabled = conflicted; + + $restartConfirmModal.classList.add("open"); + + // Focus Cancel initially for safety + var cancelBtn = document.getElementById("restart-confirm-cancel-btn"); + if (cancelBtn) setTimeout(function() { cancelBtn.focus(); }, 50); +} + +function closeRestartConfirmDialog() { + if ($restartConfirmModal) $restartConfirmModal.classList.remove("open"); + // Re-enable confirm button for next open + if ($restartConfirmOk) $restartConfirmOk.disabled = false; + // Return focus to the element that opened the dialog + if (_restartDialogOpener && _restartDialogOpener.focus) { + try { _restartDialogOpener.focus(); } catch (_) {} + _restartDialogOpener = null; + } +} + async function doUpgradeToServer() { var confirmBtn = $upgradeConfirmBtn; if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = "Upgrading…"; } diff --git a/app/sovran_systemsos_web/static/js/rebuild.js b/app/sovran_systemsos_web/static/js/rebuild.js index 41b7a03..91189a4 100644 --- a/app/sovran_systemsos_web/static/js/rebuild.js +++ b/app/sovran_systemsos_web/static/js/rebuild.js @@ -69,7 +69,7 @@ function onRebuildDone(result) { // Auto-reload the page after a short delay so tiles and toggles reflect the new state setTimeout(function() { window.location.reload(); }, 1200); } else if (result === "reboot_required") { - if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done — reboot required"; + if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done — restart required"; if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex"; } else { if ($rebuildStatus) $rebuildStatus.textContent = "✗ Something went wrong"; diff --git a/app/sovran_systemsos_web/static/js/security.js b/app/sovran_systemsos_web/static/js/security.js index f17eda1..78fa6b6 100644 --- a/app/sovran_systemsos_web/static/js/security.js +++ b/app/sovran_systemsos_web/static/js/security.js @@ -145,28 +145,25 @@ function openSecurityModal() { if (rebootBtn) { // Keep button disabled for 5 seconds to prevent accidental clicks var countdown = 5; - rebootBtn.textContent = "I have written down my new password \u2014 Reboot now (" + countdown + ")"; + rebootBtn.textContent = "I have written down my new password \u2014 Restart Entire System (" + countdown + ")"; var timer = setInterval(function() { countdown--; if (countdown <= 0) { clearInterval(timer); rebootBtn.disabled = false; - rebootBtn.textContent = "I have written down my new password \u2014 Reboot now"; + rebootBtn.textContent = "I have written down my new password \u2014 Restart Entire System"; } else { - rebootBtn.textContent = "I have written down my new password \u2014 Reboot now (" + countdown + ")"; + rebootBtn.textContent = "I have written down my new password \u2014 Restart Entire System (" + countdown + ")"; } }, 1000); rebootBtn.addEventListener("click", function() { rebootBtn.disabled = true; - rebootBtn.textContent = "Rebooting\u2026"; - if ($rebootOverlay) $rebootOverlay.classList.add("visible"); - _rebootStartTime = Date.now(); - _serverWentDown = false; - setTimeout(waitForServerReboot, REBOOT_INITIAL_DELAY); - var rebootCtrl = new AbortController(); - setTimeout(function() { rebootCtrl.abort(); }, REBOOT_REQUEST_TIMEOUT); - fetch("/api/reboot", { method: "POST", signal: rebootCtrl.signal }).catch(function() {}); + rebootBtn.textContent = "Restarting\u2026"; + // Hide the security reset overlay so the shared reboot overlay is visible + var $secResetOverlay2 = document.getElementById("security-reset-overlay"); + if ($secResetOverlay2) $secResetOverlay2.classList.remove("visible"); + doReboot(); }, { once: true }); } } catch (err) { diff --git a/app/sovran_systemsos_web/static/js/state.js b/app/sovran_systemsos_web/static/js/state.js index 30b4bff..53c57d2 100644 --- a/app/sovran_systemsos_web/static/js/state.js +++ b/app/sovran_systemsos_web/static/js/state.js @@ -49,6 +49,9 @@ const $btnSave = document.getElementById("btn-save-report"); const $btnCloseModal = document.getElementById("btn-close-modal"); const $rebootOverlay = document.getElementById("reboot-overlay"); +const $rebootMainCard = document.getElementById("reboot-main-card"); +const $rebootErrorCard = document.getElementById("reboot-error-card"); +const $rebootSubmessage = document.getElementById("reboot-submessage"); const $credsModal = document.getElementById("creds-modal"); const $credsTitle = document.getElementById("creds-modal-title"); @@ -101,5 +104,11 @@ const $upgradeConfirmBtn = document.getElementById("upgrade-confirm-btn"); const $upgradeCancelBtn = document.getElementById("upgrade-cancel-btn"); const $upgradeCloseBtn = document.getElementById("upgrade-close-btn"); +// Restart confirm dialog +const $restartConfirmModal = document.getElementById("restart-confirm-modal"); +const $restartConfirmOk = document.getElementById("restart-confirm-ok-btn"); +const $restartConfirmCancel = document.getElementById("restart-confirm-cancel-btn"); +const $restartConflictBox = document.getElementById("restart-conflict-box"); + // System status banner // (removed — health is now shown per-tile via the composite health field) \ No newline at end of file diff --git a/app/sovran_systemsos_web/static/js/tiles.js b/app/sovran_systemsos_web/static/js/tiles.js index e07bf04..b2bbfc5 100644 --- a/app/sovran_systemsos_web/static/js/tiles.js +++ b/app/sovran_systemsos_web/static/js/tiles.js @@ -133,6 +133,23 @@ function renderSidebarSupport(supportServices) { var hr = document.createElement("hr"); hr.className = "sidebar-divider"; $sidebarSupport.appendChild(hr); + + // ── Restart Entire System button (bottom, visually separated) + var restartDivider = document.createElement("hr"); + restartDivider.className = "sidebar-divider"; + $sidebarSupport.appendChild(restartDivider); + + var restartBtn = document.createElement("button"); + restartBtn.className = "sidebar-support-btn sidebar-restart-btn"; + restartBtn.id = "sidebar-btn-restart"; + restartBtn.innerHTML = + '' + + '' + + 'Restart Entire System' + + 'Restarts the computer, desktop, and all services' + + ''; + restartBtn.addEventListener("click", function() { openRestartConfirmDialog(); }); + $sidebarSupport.appendChild(restartBtn); } function buildTile(svc) { diff --git a/app/sovran_systemsos_web/static/js/update.js b/app/sovran_systemsos_web/static/js/update.js index 791e8d2..0537f49 100644 --- a/app/sovran_systemsos_web/static/js/update.js +++ b/app/sovran_systemsos_web/static/js/update.js @@ -154,7 +154,7 @@ function onUpdateDone(result) { if ($modalStatus) $modalStatus.textContent = "✓ Update complete"; if ($btnReboot) $btnReboot.style.display = "inline-flex"; } else if (result === "reboot_required") { - if ($modalStatus) $modalStatus.textContent = "✓ Update complete — reboot required"; + if ($modalStatus) $modalStatus.textContent = "✓ Update complete — restart required"; if ($btnReboot) $btnReboot.style.display = "inline-flex"; } else { if ($modalStatus) $modalStatus.textContent = "✗ Update failed"; @@ -179,23 +179,50 @@ function saveErrorReport() { var _rebootStartTime = 0; var _serverWentDown = false; +var _rebootFailed = false; + +function _setRebootStatus(msg) { + if ($rebootSubmessage) $rebootSubmessage.textContent = msg; +} function doReboot() { if ($modal) $modal.classList.remove("open"); if ($rebuildModal) $rebuildModal.classList.remove("open"); stopUpdatePoll(); stopRebuildPoll(); + // Reset overlay to main card + if ($rebootMainCard) $rebootMainCard.style.display = ""; + if ($rebootErrorCard) $rebootErrorCard.style.display = "none"; + _setRebootStatus("Sending restart request\u2026"); if ($rebootOverlay) $rebootOverlay.classList.add("visible"); _rebootStartTime = Date.now(); _serverWentDown = false; + _rebootFailed = false; var rebootCtrl = new AbortController(); setTimeout(function() { rebootCtrl.abort(); }, REBOOT_REQUEST_TIMEOUT); - fetch("/api/reboot", { method: "POST", signal: rebootCtrl.signal }).catch(function() {}); + fetch("/api/reboot", { method: "POST", signal: rebootCtrl.signal }) + .then(function(res) { + if (!res.ok) { + // Definitive HTTP error — server rejected the request before going down + _rebootFailed = true; + if ($rebootMainCard) $rebootMainCard.style.display = "none"; + if ($rebootErrorCard) $rebootErrorCard.style.display = ""; + // Leave overlay visible so the error card is shown + } + // HTTP 2xx: request accepted, proceed with polling + }) + .catch(function() { + // Connection dropped or request aborted — the server is likely already going + // down as part of the restart. Treat as success and continue polling. + }); // Wait before the first check — NixOS shutdown after an update can take 20-40s setTimeout(waitForServerReboot, REBOOT_INITIAL_DELAY); } function waitForServerReboot() { + if (_rebootFailed) return; + // Update status on first check (server hasn't gone down yet) + if (!_serverWentDown) _setRebootStatus("Waiting for the computer to shut down\u2026"); var controller = new AbortController(); var timeoutId = setTimeout(function() { controller.abort(); }, REBOOT_FETCH_TIMEOUT); @@ -205,18 +232,23 @@ function waitForServerReboot() { if (_serverWentDown) { // Server is responding after having been down — reboot is complete. // Any response (even 401/500) means the server process is back. + _setRebootStatus("System is back online. Reconnecting\u2026"); window.location.reload(); } else if ((Date.now() - _rebootStartTime) < 90000) { // Server still responding but hasn't gone down yet — keep waiting setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); } else { // Been over 90 seconds and server is responding — just reload + _setRebootStatus("System is back online. Reconnecting\u2026"); window.location.reload(); } }) .catch(function() { clearTimeout(timeoutId); - _serverWentDown = true; + if (!_serverWentDown) { + _serverWentDown = true; + _setRebootStatus("The computer is restarting\u2026"); + } setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); }); } diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index 3168de4..b29c1b3 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -61,7 +61,7 @@ @@ -164,7 +164,7 @@ @@ -240,26 +240,61 @@ You will need it to log in to your computer
and the Sovran Hub at sovransystemsos.local.

-
-
-

System Rebooting

+ +
+ +

Restarting Entire System

- Sovran_SystemsOS is now restarting.
- This page will automatically reconnect once the system is back online. + The entire computer is restarting, including the desktop and all hosted services.
+ This page will reconnect automatically when Sovran_SystemsOS is back online.

-
+ -

Stay tuned…

+

Sending restart request…

+
+ + +
+ + + diff --git a/iso/installer.py b/iso/installer.py index fcbb730..98c5865 100644 --- a/iso/installer.py +++ b/iso/installer.py @@ -1169,7 +1169,7 @@ class InstallerWindow(Adw.ApplicationWindow): btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) btn_box.set_halign(Gtk.Align.CENTER) btn_box.set_margin_bottom(32) - reboot_btn = Gtk.Button(label="I Have Written Down My Password — Reboot Now") + reboot_btn = Gtk.Button(label="I Have Written Down My Password — Restart Entire System") reboot_btn.add_css_class("suggested-action") reboot_btn.add_css_class("pill") reboot_btn.connect("clicked", lambda b: subprocess.run(["sudo", "reboot"])) From 6b79c212a8ad21da0b8f9a683b319e996453d873 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:51:52 +0000 Subject: [PATCH 3/3] refactor: improve variable name in openRestartConfirmDialog for clarity --- app/sovran_systemsos_web/static/js/events.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/sovran_systemsos_web/static/js/events.js b/app/sovran_systemsos_web/static/js/events.js index 67923ce..4576a4f 100644 --- a/app/sovran_systemsos_web/static/js/events.js +++ b/app/sovran_systemsos_web/static/js/events.js @@ -82,9 +82,9 @@ function openRestartConfirmDialog() { _restartDialogOpener = document.activeElement; // Detect conflicting operations - var conflicted = !!_updatePollTimer || !!_rebuildPollTimer; - if ($restartConflictBox) $restartConflictBox.style.display = conflicted ? "" : "none"; - if ($restartConfirmOk) $restartConfirmOk.disabled = conflicted; + var isOperationInProgress = !!_updatePollTimer || !!_rebuildPollTimer; + if ($restartConflictBox) $restartConflictBox.style.display = isOperationInProgress ? "" : "none"; + if ($restartConfirmOk) $restartConfirmOk.disabled = isOperationInProgress; $restartConfirmModal.classList.add("open");