Compare commits
8
Commits
7ac3775a96
..
v1.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b8ee5ff26 | ||
|
|
b4990d70ef | ||
|
|
59b734995b | ||
|
|
599405ce18 | ||
|
|
4b31255f13 | ||
|
|
6b79c212a8 | ||
|
|
c3950547b0 | ||
|
|
b28d6dd32c |
@@ -91,3 +91,30 @@
|
||||
border-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;
|
||||
}
|
||||
|
||||
/* 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,28 @@ 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(); });
|
||||
|
||||
// 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 ───────────────────────────────────────
|
||||
|
||||
function openUpgradeModal() {
|
||||
@@ -54,6 +76,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 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() {
|
||||
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,14 @@ 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");
|
||||
|
||||
// Header reboot button
|
||||
const $headerRebootBtn = document.getElementById("btn-header-reboot");
|
||||
|
||||
// System status banner
|
||||
// (removed — health is now shown per-tile via the composite health field)
|
||||
@@ -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);
|
||||
if (!_serverWentDown) {
|
||||
_serverWentDown = true;
|
||||
_setRebootStatus("The computer is restarting\u2026");
|
||||
}
|
||||
setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<span class="title">Sovran_SystemsOS Hub</span>
|
||||
<div class="header-buttons">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
@@ -61,7 +62,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 +165,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 +241,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>
|
||||
|
||||
|
||||
+1
-1
@@ -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"]))
|
||||
|
||||
Reference in New Issue
Block a user