7 Commits
Author SHA1 Message Date
Sovran SystemsandGitHub b4990d70ef Merge pull request #327 from naturallaw777/copilot/remove-restart-sidebar-action
Move Reboot from sidebar to compact header button
2026-07-15 18:59:25 +00:00
copilot-swe-agent[bot]andGitHub 59b734995b feat: move Reboot button from sidebar to header, between role badge and Sign Out 2026-07-15 18:58:10 +00:00
copilot-swe-agent[bot]andGitHub 599405ce18 Initial plan 2026-07-15 18:55:54 +00:00
Sovran SystemsandGitHub 4b31255f13 Merge pull request #326 from naturallaw777/copilot/add-system-restart-action
feat: implement consistent whole-system restart UX across the Hub
2026-07-15 18:26:26 +00:00
copilot-swe-agent[bot]andGitHub 6b79c212a8 refactor: improve variable name in openRestartConfirmDialog for clarity 2026-07-15 16:51:52 +00:00
copilot-swe-agent[bot]andGitHub c3950547b0 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
2026-07-15 16:50:01 +00:00
copilot-swe-agent[bot]andGitHub b28d6dd32c Initial plan 2026-07-15 16:41:20 +00:00
9 changed files with 225 additions and 26 deletions
@@ -91,3 +91,30 @@
border-color: var(--accent-color); border-color: var(--accent-color);
color: var(--accent-color); color: var(--accent-color);
} }
/* ── Header reboot button ───────────────────────────────────────── */
.btn-header-reboot {
background: transparent;
border: 1px solid rgba(184, 125, 0, 0.35);
color: #c98d08;
font-size: 0.78rem;
font-weight: 600;
padding: 4px 12px;
border-radius: var(--radius-btn);
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background-color 0.15s;
}
.btn-header-reboot:hover {
border-color: #b87d00;
color: #e0a010;
background-color: rgba(184, 125, 0, 0.1);
}
@media (max-width: 480px) {
.btn-header-reboot {
padding: 4px 8px;
font-size: 0.72rem;
}
}
@@ -102,6 +102,48 @@ button.btn-reboot:hover:not(:disabled) {
background-color: #529E7E; 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 { .btn-save {
background-color: var(--yellow); background-color: var(--yellow);
color: #0A1A10; color: #0A1A10;
@@ -44,6 +44,28 @@ if ($upgradeCloseBtn) $upgradeCloseBtn.addEventListener("click", closeUpgradeMod
if ($upgradeCancelBtn) $upgradeCancelBtn.addEventListener("click", closeUpgradeModal); if ($upgradeCancelBtn) $upgradeCancelBtn.addEventListener("click", closeUpgradeModal);
if ($upgradeModal) $upgradeModal.addEventListener("click", function(e) { if (e.target === $upgradeModal) 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(); });
// Header Reboot button
if ($headerRebootBtn) $headerRebootBtn.addEventListener("click", function() { openRestartConfirmDialog(); });
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 ─────────────────────────────────────── // ── Upgrade modal functions ───────────────────────────────────────
function openUpgradeModal() { function openUpgradeModal() {
@@ -54,6 +76,37 @@ function closeUpgradeModal() {
if ($upgradeModal) $upgradeModal.classList.remove("open"); 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 isOperationInProgress = !!_updatePollTimer || !!_rebuildPollTimer;
if ($restartConflictBox) $restartConflictBox.style.display = isOperationInProgress ? "" : "none";
if ($restartConfirmOk) $restartConfirmOk.disabled = isOperationInProgress;
$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() { async function doUpgradeToServer() {
var confirmBtn = $upgradeConfirmBtn; var confirmBtn = $upgradeConfirmBtn;
if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = "Upgrading…"; } if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = "Upgrading…"; }
@@ -69,7 +69,7 @@ function onRebuildDone(result) {
// Auto-reload the page after a short delay so tiles and toggles reflect the new state // Auto-reload the page after a short delay so tiles and toggles reflect the new state
setTimeout(function() { window.location.reload(); }, 1200); setTimeout(function() { window.location.reload(); }, 1200);
} else if (result === "reboot_required") { } 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"; if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
} else { } else {
if ($rebuildStatus) $rebuildStatus.textContent = "✗ Something went wrong"; if ($rebuildStatus) $rebuildStatus.textContent = "✗ Something went wrong";
+8 -11
View File
@@ -145,28 +145,25 @@ function openSecurityModal() {
if (rebootBtn) { if (rebootBtn) {
// Keep button disabled for 5 seconds to prevent accidental clicks // Keep button disabled for 5 seconds to prevent accidental clicks
var countdown = 5; 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() { var timer = setInterval(function() {
countdown--; countdown--;
if (countdown <= 0) { if (countdown <= 0) {
clearInterval(timer); clearInterval(timer);
rebootBtn.disabled = false; 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 { } 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); }, 1000);
rebootBtn.addEventListener("click", function() { rebootBtn.addEventListener("click", function() {
rebootBtn.disabled = true; rebootBtn.disabled = true;
rebootBtn.textContent = "Rebooting\u2026"; rebootBtn.textContent = "Restarting\u2026";
if ($rebootOverlay) $rebootOverlay.classList.add("visible"); // Hide the security reset overlay so the shared reboot overlay is visible
_rebootStartTime = Date.now(); var $secResetOverlay2 = document.getElementById("security-reset-overlay");
_serverWentDown = false; if ($secResetOverlay2) $secResetOverlay2.classList.remove("visible");
setTimeout(waitForServerReboot, REBOOT_INITIAL_DELAY); doReboot();
var rebootCtrl = new AbortController();
setTimeout(function() { rebootCtrl.abort(); }, REBOOT_REQUEST_TIMEOUT);
fetch("/api/reboot", { method: "POST", signal: rebootCtrl.signal }).catch(function() {});
}, { once: true }); }, { once: true });
} }
} catch (err) { } catch (err) {
@@ -49,6 +49,9 @@ const $btnSave = document.getElementById("btn-save-report");
const $btnCloseModal = document.getElementById("btn-close-modal"); const $btnCloseModal = document.getElementById("btn-close-modal");
const $rebootOverlay = document.getElementById("reboot-overlay"); 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 $credsModal = document.getElementById("creds-modal");
const $credsTitle = document.getElementById("creds-modal-title"); const $credsTitle = document.getElementById("creds-modal-title");
@@ -101,5 +104,14 @@ const $upgradeConfirmBtn = document.getElementById("upgrade-confirm-btn");
const $upgradeCancelBtn = document.getElementById("upgrade-cancel-btn"); const $upgradeCancelBtn = document.getElementById("upgrade-cancel-btn");
const $upgradeCloseBtn = document.getElementById("upgrade-close-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");
// Header reboot button
const $headerRebootBtn = document.getElementById("btn-header-reboot");
// System status banner // System status banner
// (removed — health is now shown per-tile via the composite health field) // (removed — health is now shown per-tile via the composite health field)
+34 -2
View File
@@ -154,7 +154,7 @@ function onUpdateDone(result) {
if ($modalStatus) $modalStatus.textContent = "✓ Update complete"; if ($modalStatus) $modalStatus.textContent = "✓ Update complete";
if ($btnReboot) $btnReboot.style.display = "inline-flex"; if ($btnReboot) $btnReboot.style.display = "inline-flex";
} else if (result === "reboot_required") { } 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"; if ($btnReboot) $btnReboot.style.display = "inline-flex";
} else { } else {
if ($modalStatus) $modalStatus.textContent = "✗ Update failed"; if ($modalStatus) $modalStatus.textContent = "✗ Update failed";
@@ -179,23 +179,50 @@ function saveErrorReport() {
var _rebootStartTime = 0; var _rebootStartTime = 0;
var _serverWentDown = false; var _serverWentDown = false;
var _rebootFailed = false;
function _setRebootStatus(msg) {
if ($rebootSubmessage) $rebootSubmessage.textContent = msg;
}
function doReboot() { function doReboot() {
if ($modal) $modal.classList.remove("open"); if ($modal) $modal.classList.remove("open");
if ($rebuildModal) $rebuildModal.classList.remove("open"); if ($rebuildModal) $rebuildModal.classList.remove("open");
stopUpdatePoll(); stopUpdatePoll();
stopRebuildPoll(); 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"); if ($rebootOverlay) $rebootOverlay.classList.add("visible");
_rebootStartTime = Date.now(); _rebootStartTime = Date.now();
_serverWentDown = false; _serverWentDown = false;
_rebootFailed = false;
var rebootCtrl = new AbortController(); var rebootCtrl = new AbortController();
setTimeout(function() { rebootCtrl.abort(); }, REBOOT_REQUEST_TIMEOUT); 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 // Wait before the first check — NixOS shutdown after an update can take 20-40s
setTimeout(waitForServerReboot, REBOOT_INITIAL_DELAY); setTimeout(waitForServerReboot, REBOOT_INITIAL_DELAY);
} }
function waitForServerReboot() { 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 controller = new AbortController();
var timeoutId = setTimeout(function() { controller.abort(); }, REBOOT_FETCH_TIMEOUT); var timeoutId = setTimeout(function() { controller.abort(); }, REBOOT_FETCH_TIMEOUT);
@@ -205,18 +232,23 @@ function waitForServerReboot() {
if (_serverWentDown) { if (_serverWentDown) {
// Server is responding after having been down — reboot is complete. // Server is responding after having been down — reboot is complete.
// Any response (even 401/500) means the server process is back. // Any response (even 401/500) means the server process is back.
_setRebootStatus("System is back online. Reconnecting\u2026");
window.location.reload(); window.location.reload();
} else if ((Date.now() - _rebootStartTime) < 90000) { } else if ((Date.now() - _rebootStartTime) < 90000) {
// Server still responding but hasn't gone down yet — keep waiting // Server still responding but hasn't gone down yet — keep waiting
setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL);
} else { } else {
// Been over 90 seconds and server is responding — just reload // Been over 90 seconds and server is responding — just reload
_setRebootStatus("System is back online. Reconnecting\u2026");
window.location.reload(); window.location.reload();
} }
}) })
.catch(function() { .catch(function() {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (!_serverWentDown) {
_serverWentDown = true; _serverWentDown = true;
_setRebootStatus("The computer is restarting\u2026");
}
setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL);
}); });
} }
+46 -10
View File
@@ -24,6 +24,7 @@
<span class="title">Sovran_SystemsOS Hub</span> <span class="title">Sovran_SystemsOS Hub</span>
<div class="header-buttons"> <div class="header-buttons">
<span class="role-badge" id="role-badge">Loading…</span> <span class="role-badge" id="role-badge">Loading…</span>
<button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button>
<button class="btn btn-logout" id="btn-logout" title="Sign out">Sign Out</button> <button class="btn btn-logout" id="btn-logout" title="Sign out">Sign Out</button>
</div> </div>
</header> </header>
@@ -61,7 +62,7 @@
<div class="modal-log" id="modal-log" aria-live="polite"></div> <div class="modal-log" id="modal-log" aria-live="polite"></div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-save" id="btn-save-report" style="display:none">Save Error Report</button> <button class="btn btn-save" id="btn-save-report" style="display:none">Save Error Report</button>
<button class="btn btn-reboot" id="btn-reboot" style="display:none">Reboot</button> <button class="btn btn-reboot" id="btn-reboot" style="display:none">Restart Entire System</button>
<button class="btn btn-close-modal" id="btn-close-modal" disabled>Close</button> <button class="btn btn-close-modal" id="btn-close-modal" disabled>Close</button>
</div> </div>
</div> </div>
@@ -164,7 +165,7 @@
<div class="modal-log" id="rebuild-log" aria-live="polite"></div> <div class="modal-log" id="rebuild-log" aria-live="polite"></div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-save" id="rebuild-save-report" style="display:none">Save Error Report</button> <button class="btn btn-save" id="rebuild-save-report" style="display:none">Save Error Report</button>
<button class="btn btn-reboot" id="rebuild-reboot-btn" style="display:none">Reboot</button> <button class="btn btn-reboot" id="rebuild-reboot-btn" style="display:none">Restart Entire System</button>
<button class="btn btn-close-modal" id="rebuild-close-btn" disabled>Close</button> <button class="btn btn-close-modal" id="rebuild-close-btn" disabled>Close</button>
</div> </div>
</div> </div>
@@ -240,26 +241,61 @@
You will need it to log in to your computer<br />and the Sovran Hub at <em>sovransystemsos.local</em>. You will need it to log in to your computer<br />and the Sovran Hub at <em>sovransystemsos.local</em>.
</p> </p>
<button class="security-reset-reboot-btn" id="security-reset-reboot-btn" disabled> <button class="security-reset-reboot-btn" id="security-reset-reboot-btn" disabled>
I have written down my new password — Reboot now I have written down my new password — Restart Entire System
</button> </button>
</div> </div>
</div> </div>
<!-- Reboot overlay --> <!-- Reboot overlay -->
<div class="reboot-overlay" id="reboot-overlay"> <div class="reboot-overlay" id="reboot-overlay">
<div class="reboot-card"> <!-- Normal restarting card -->
<div class="reboot-icon"></div> <div class="reboot-card" id="reboot-main-card">
<h2 class="reboot-title">System Rebooting</h2> <div class="reboot-icon" aria-hidden="true"></div>
<h2 class="reboot-title">Restarting Entire System</h2>
<p class="reboot-message"> <p class="reboot-message">
Sovran_SystemsOS is now restarting.<br /> The entire computer is restarting, including the desktop and all hosted services.<br />
This page will automatically reconnect once the system is back online. This page will reconnect automatically when Sovran_SystemsOS is back online.
</p> </p>
<div class="reboot-dots"> <div class="reboot-dots" aria-hidden="true">
<span class="reboot-dot"></span> <span class="reboot-dot"></span>
<span class="reboot-dot"></span> <span class="reboot-dot"></span>
<span class="reboot-dot"></span> <span class="reboot-dot"></span>
</div> </div>
<p class="reboot-submessage">Stay tuned</p> <p class="reboot-submessage" id="reboot-submessage" aria-live="polite">Sending restart request</p>
</div>
<!-- Error card (shown if restart request fails definitively) -->
<div class="reboot-card" id="reboot-error-card" style="display:none">
<div class="reboot-icon" aria-hidden="true"></div>
<h2 class="reboot-title">Restart could not be started</h2>
<p class="reboot-message">
The computer did not begin restarting. No services were intentionally stopped. Please try again.
</p>
<div class="reboot-error-actions">
<button class="btn btn-close-modal" id="reboot-error-close-btn">Close</button>
<button class="btn btn-restart-amber" id="reboot-error-retry-btn">Try Again</button>
</div>
</div>
</div>
<!-- Restart Confirm Dialog -->
<div class="modal-overlay" id="restart-confirm-modal" role="dialog" aria-modal="true" aria-labelledby="restart-confirm-title">
<div class="creds-dialog domain-narrow-dialog">
<div class="creds-header">
<span class="creds-title" id="restart-confirm-title">Restart the entire computer?</span>
</div>
<div class="creds-body">
<div id="restart-conflict-box" class="restart-conflict-box" style="display:none">
<p class="restart-conflict-title">The system cannot restart right now.</p>
<p class="restart-conflict-desc">A system update, rebuild, backup, restore, or security operation is currently running. Wait for it to finish, then try again.</p>
</div>
<p class="support-desc"><strong>This will reboot the physical machine running Sovran_SystemsOS — not just the Hub.</strong></p>
<p class="support-desc">The desktop and all hosted services will stop temporarily and restart with the computer. Anyone currently using these services will be disconnected.</p>
<p class="support-desc">The system usually returns within 13 minutes. This page will reconnect automatically.</p>
<div class="domain-field-actions">
<button class="btn btn-close-modal" id="restart-confirm-cancel-btn">Cancel</button>
<button class="btn btn-restart-amber" id="restart-confirm-ok-btn">Restart Entire System</button>
</div>
</div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1169,7 +1169,7 @@ class InstallerWindow(Adw.ApplicationWindow):
btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
btn_box.set_halign(Gtk.Align.CENTER) btn_box.set_halign(Gtk.Align.CENTER)
btn_box.set_margin_bottom(32) 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("suggested-action")
reboot_btn.add_css_class("pill") reboot_btn.add_css_class("pill")
reboot_btn.connect("clicked", lambda b: subprocess.run(["sudo", "reboot"])) reboot_btn.connect("clicked", lambda b: subprocess.run(["sudo", "reboot"]))