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
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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…"; }
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
@@ -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 =
|
||||
'<span class="sidebar-support-icon sidebar-restart-icon" aria-hidden="true">\u21BB</span>' +
|
||||
'<span class="sidebar-support-text">' +
|
||||
'<span class="sidebar-support-title">Restart Entire System</span>' +
|
||||
'<span class="sidebar-support-hint sidebar-restart-hint">Restarts the computer, desktop, and all services</span>' +
|
||||
'</span>';
|
||||
restartBtn.addEventListener("click", function() { openRestartConfirmDialog(); });
|
||||
$sidebarSupport.appendChild(restartBtn);
|
||||
}
|
||||
|
||||
function buildTile(svc) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<div class="modal-log" id="modal-log" aria-live="polite"></div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +164,7 @@
|
||||
<div class="modal-log" id="rebuild-log" aria-live="polite"></div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,26 +240,61 @@
|
||||
You will need it to log in to your computer<br />and the Sovran Hub at <em>sovransystemsos.local</em>.
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reboot overlay -->
|
||||
<div class="reboot-overlay" id="reboot-overlay">
|
||||
<div class="reboot-card">
|
||||
<div class="reboot-icon">↻</div>
|
||||
<h2 class="reboot-title">System Rebooting</h2>
|
||||
<!-- Normal restarting card -->
|
||||
<div class="reboot-card" id="reboot-main-card">
|
||||
<div class="reboot-icon" aria-hidden="true">↻</div>
|
||||
<h2 class="reboot-title">Restarting Entire System</h2>
|
||||
<p class="reboot-message">
|
||||
Sovran_SystemsOS is now restarting.<br />
|
||||
This page will automatically reconnect once the system is back online.
|
||||
The entire computer is restarting, including the desktop and all hosted services.<br />
|
||||
This page will reconnect automatically when Sovran_SystemsOS is back online.
|
||||
</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>
|
||||
</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 1–3 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user