Rework Update System dialog to the approved design; match rebuild dialog

The update dialog kept the old bare title + spinner layout from the
previous theme. Rework it (and the rebuild dialog, for consistency) to
the approved The Hub dialog anatomy:

- Header: green chip icon, title, version chip, status pill
  (Checking… / Up to date / Updating… / Restart required / Update
  failed / Status unknown) and a header close button. A spinner appears
  in the header while an update is starting or running.
- System Details card: current version, release channel, and last
  checked (relative time, refreshed on every check).
- The log renders as a console with green "ok" and dim hint lines; the
  up-to-date result shows as a single green console line exactly once
  (the redundant status line is hidden in that state).
- Footer: Close plus a "Check again" primary action that re-runs the
  update check. Close and Check again are disabled while a check or
  update is in flight; all existing recovery actions (Save Error
  Report, Retry Status, Retry Update, Restart Entire System) keep their
  exact semantics.
- Opening the dialog now shows a "Checking…" state immediately while
  the existing reattach-then-check logic runs; reattaching to an
  in-progress update (page reload, RDP reconnect) is unchanged.

Sidebar Update System status tints now use the theme palette (red /
amber / blue / green), and the periodic background check refreshes the
dialog's "last checked" value.

Rebuild dialog gets the same header (icon, version chip, Applying… /
Done / Restart required / Failed pill, header close disabled while a
rebuild runs); its log stays hidden as before.

No API or state-machine changes: same endpoints, same polling, same
reattach and recovery behavior.
This commit is contained in:
2026-09-08 16:40:06 -05:00
parent 2013df55bd
commit d164d423ad
7 changed files with 274 additions and 45 deletions
+87 -9
View File
@@ -48,26 +48,97 @@
/* ── Update / Rebuild modal internals ───────────────────────────── */ /* ── Update / Rebuild modal internals ───────────────────────────── */
.modal-header { .upd-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 14px;
padding: 20px 24px 0; padding: 22px 24px 0;
} }
.modal-title { .upd-chip { width: 54px; height: 54px; border-radius: 16px; }
.upd-chip-svg { width: 27px; height: 27px; }
.upd-title-wrap {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 7px;
}
.upd-title {
font-size: 1.13rem; font-size: 1.13rem;
font-weight: 800; font-weight: 800;
letter-spacing: -0.01em; letter-spacing: -0.01em;
line-height: 1.2;
} }
.modal-status { .upd-sub {
margin-left: auto; display: flex;
font-size: 0.78rem; align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.ver-chip {
display: inline-flex;
align-items: center;
white-space: nowrap;
background: rgba(255, 255, 255, 0.05);
color: var(--text-2);
font-family: var(--mono);
font-size: 0.66rem;
font-weight: 600;
padding: 2px 9px;
border-radius: 99px;
border: 1px solid var(--border);
letter-spacing: 0.02em;
min-height: 23px;
}
.upd-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.69rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 99px;
letter-spacing: 0.01em;
white-space: nowrap;
background: rgba(255, 255, 255, 0.05);
color: var(--text-2);
border: 1px solid var(--border);
min-height: 23px;
}
.upd-pill.st-active { background: var(--accent-dim); color: var(--accent); border-color: rgba(66, 243, 154, 0.3); }
.upd-pill.st-loading { background: rgba(120, 174, 237, 0.10); color: var(--blue); border-color: rgba(120, 174, 237, 0.3); }
.upd-pill.st-needs-attention { background: rgba(233, 182, 74, 0.10); color: var(--amber); border-color: rgba(233, 182, 74, 0.3); }
.upd-pill.st-failed { background: rgba(246, 97, 81, 0.10); color: var(--red); border-color: rgba(246, 97, 81, 0.3); }
.upd-pill .status-dot.pulse { animation: tile-dot-pulse 1.4s ease-in-out infinite; }
.modal-body-scroll {
padding: 19px 24px 0;
overflow-y: auto;
flex: 1;
min-height: 0;
}
.update-status-msg {
margin-top: 15px;
font-size: 0.85rem;
font-weight: 650; font-weight: 650;
color: var(--text-2); color: var(--text-2);
line-height: 1.55;
} }
.update-status-msg.st-ok { color: var(--accent); }
.update-status-msg.st-warn { color: var(--amber); }
.update-status-msg.st-err { color: var(--red); }
.modal-footer-spacer { flex: 1; }
.modal-spinner { .modal-spinner {
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -75,9 +146,13 @@
border: 2.5px solid rgba(255, 255, 255, 0.14); border: 2.5px solid rgba(255, 255, 255, 0.14);
border-top-color: var(--accent); border-top-color: var(--accent);
flex-shrink: 0; flex-shrink: 0;
display: none;
} }
.modal-spinner.spinning { animation: spin 0.8s linear infinite; } .modal-spinner.spinning {
display: inline-block;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } } @keyframes spin { to { transform: rotate(360deg); } }
@@ -86,7 +161,7 @@
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 14px; border-radius: 14px;
padding: 13px 16px; padding: 13px 16px;
margin: 16px 24px 0; margin: 15px 0 0;
font-family: var(--mono); font-family: var(--mono);
font-size: 0.72rem; font-size: 0.72rem;
line-height: 1.85; line-height: 1.85;
@@ -98,6 +173,9 @@
word-break: break-word; word-break: break-word;
} }
.modal-log .ok { color: var(--accent); }
.modal-log .dim { color: var(--text-3); }
.modal-footer { .modal-footer {
display: flex; display: flex;
gap: 10px; gap: 10px;
@@ -4,6 +4,8 @@
// if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js // if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js
if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal); if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal);
if ($btnCheckAgain) $btnCheckAgain.addEventListener("click", function() { openUpdateModal(); });
if ($updateCloseBtn) $updateCloseBtn.addEventListener("click", closeUpdateModal);
if ($btnReboot) $btnReboot.addEventListener("click", doReboot); if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport); if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus); if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus);
@@ -28,6 +30,7 @@ if ($logoutBtn) $logoutBtn.addEventListener("click", function () {
// Rebuild modal // Rebuild modal
if ($rebuildClose) $rebuildClose.addEventListener("click", closeRebuildModal); if ($rebuildClose) $rebuildClose.addEventListener("click", closeRebuildModal);
if ($rebuildCloseHdr) $rebuildCloseHdr.addEventListener("click", closeRebuildModal);
if ($rebuildReboot) $rebuildReboot.addEventListener("click", doReboot); if ($rebuildReboot) $rebuildReboot.addEventListener("click", doReboot);
if ($rebuildSave) $rebuildSave.addEventListener("click", saveRebuildErrorReport); if ($rebuildSave) $rebuildSave.addEventListener("click", saveRebuildErrorReport);
if ($rebuildModal) $rebuildModal.addEventListener("click", function(e) { if (e.target === $rebuildModal) closeRebuildModal(); }); if ($rebuildModal) $rebuildModal.addEventListener("click", function(e) { if (e.target === $rebuildModal) closeRebuildModal(); });
+36 -5
View File
@@ -2,6 +2,35 @@
// ── Rebuild modal ───────────────────────────────────────────────── // ── Rebuild modal ─────────────────────────────────────────────────
// Status line + header pill for the rebuild dialog (same presentation
// contract as the update dialog's _setUpdateStatus).
function _setRebuildStatus(text) {
if ($rebuildStatus) $rebuildStatus.textContent = text;
if ($rebuildPill) {
var cls = "upd-pill";
var html;
if (text.charAt(0) === "✓") {
if (text.indexOf("restart required") !== -1) {
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Restart required';
} else {
cls += " st-active"; html = '<span class="status-dot active"></span>Done';
}
} else if (text.charAt(0) === "✗") {
cls += " st-failed"; html = '<span class="status-dot failed"></span>Failed';
} else {
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Applying…';
}
$rebuildPill.className = cls;
$rebuildPill.innerHTML = html;
}
if ($rebuildStatus) {
var msg = "update-status-msg";
if (text.charAt(0) === "✓") $rebuildStatus.className = (text.indexOf("restart required") !== -1) ? msg + " st-warn" : msg + " st-ok";
else if (text.charAt(0) === "✗") $rebuildStatus.className = msg + " st-err";
else $rebuildStatus.className = msg;
}
}
function openRebuildModal() { function openRebuildModal() {
if (!$rebuildModal) return; if (!$rebuildModal) return;
_rebuildLog = ""; _rebuildLog = "";
@@ -13,11 +42,12 @@ function openRebuildModal() {
if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; } if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; }
var action = _rebuildIsEnabling ? "Enabling" : "Disabling"; var action = _rebuildIsEnabling ? "Enabling" : "Disabling";
var label = _rebuildFeatureName || "feature"; var label = _rebuildFeatureName || "feature";
if ($rebuildStatus) $rebuildStatus.textContent = action + " " + label + "…"; _setRebuildStatus(action + " " + label + "…");
if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning"); if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning");
if ($rebuildReboot) $rebuildReboot.style.display = "none"; if ($rebuildReboot) $rebuildReboot.style.display = "none";
if ($rebuildSave) $rebuildSave.style.display = "none"; if ($rebuildSave) $rebuildSave.style.display = "none";
if ($rebuildClose) $rebuildClose.disabled = true; if ($rebuildClose) $rebuildClose.disabled = true;
if ($rebuildCloseHdr) $rebuildCloseHdr.disabled = true;
$rebuildModal.classList.add("open"); $rebuildModal.classList.add("open");
// Delay first poll slightly to let the rebuild service start and clear stale log // Delay first poll slightly to let the rebuild service start and clear stale log
setTimeout(startRebuildPoll, 1500); setTimeout(startRebuildPoll, 1500);
@@ -76,7 +106,7 @@ async function pollRebuildStatus() {
window.location.reload(); window.location.reload();
return; return;
} }
if (!_rebuildServerDown) { _rebuildServerDown = true; if ($rebuildStatus) $rebuildStatus.textContent = "Applying changes…"; } if (!_rebuildServerDown) { _rebuildServerDown = true; _setRebuildStatus("Applying changes…"); }
} finally { } finally {
_rebuildPollInFlight = false; _rebuildPollInFlight = false;
} }
@@ -85,15 +115,16 @@ async function pollRebuildStatus() {
function onRebuildDone(result) { function onRebuildDone(result) {
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning"); if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
if ($rebuildClose) $rebuildClose.disabled = false; if ($rebuildClose) $rebuildClose.disabled = false;
if ($rebuildCloseHdr) $rebuildCloseHdr.disabled = false;
if (result === true) { if (result === true) {
if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done"; _setRebuildStatus("✓ Done");
// 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 — restart required"; _setRebuildStatus("✓ 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"; _setRebuildStatus("✗ Something went wrong");
if ($rebuildSave) $rebuildSave.style.display = "inline-flex"; if ($rebuildSave) $rebuildSave.style.display = "inline-flex";
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex"; if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
} }
@@ -55,6 +55,10 @@ const $btnSave = document.getElementById("btn-save-report");
const $btnRetryUpdate = document.getElementById("btn-retry-update-status"); const $btnRetryUpdate = document.getElementById("btn-retry-update-status");
const $btnRetryRun = document.getElementById("btn-retry-update"); const $btnRetryRun = document.getElementById("btn-retry-update");
const $btnCloseModal = document.getElementById("btn-close-modal"); const $btnCloseModal = document.getElementById("btn-close-modal");
const $btnCheckAgain = document.getElementById("btn-check-again");
const $updateCloseBtn = document.getElementById("update-close-btn");
const $updPill = document.getElementById("upd-pill");
const $updLastChecked = document.getElementById("upd-last-checked");
const $rebootOverlay = document.getElementById("reboot-overlay"); const $rebootOverlay = document.getElementById("reboot-overlay");
const $rebootMainCard = document.getElementById("reboot-main-card"); const $rebootMainCard = document.getElementById("reboot-main-card");
@@ -80,6 +84,8 @@ const $rebuildLog = document.getElementById("rebuild-log");
const $rebuildReboot = document.getElementById("rebuild-reboot-btn"); const $rebuildReboot = document.getElementById("rebuild-reboot-btn");
const $rebuildSave = document.getElementById("rebuild-save-report"); const $rebuildSave = document.getElementById("rebuild-save-report");
const $rebuildClose = document.getElementById("rebuild-close-btn"); const $rebuildClose = document.getElementById("rebuild-close-btn");
const $rebuildCloseHdr = document.getElementById("rebuild-close-hdr");
const $rebuildPill = document.getElementById("rebuild-pill");
// Feature Manager — domain setup modal // Feature Manager — domain setup modal
const $domainSetupModal = document.getElementById("domain-setup-modal"); const $domainSetupModal = document.getElementById("domain-setup-modal");
+9 -8
View File
@@ -272,6 +272,7 @@ async function loadNetwork() {
async function checkUpdates() { async function checkUpdates() {
try { try {
var data = await apiFetch("/api/updates/check"); var data = await apiFetch("/api/updates/check");
if (typeof markUpdateChecked === "function") markUpdateChecked();
var hasUpdates = !!data.available; var hasUpdates = !!data.available;
var updateStatus = data.status || "idle"; var updateStatus = data.status || "idle";
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update"); var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
@@ -280,20 +281,20 @@ async function checkUpdates() {
if (updateStatus === "failed") { if (updateStatus === "failed") {
// Last update errored and did not apply — surface it as a persistent // Last update errored and did not apply — surface it as a persistent
// red banner that re-opens the failed run with a "Retry Update" action. // red banner that re-opens the failed run with a "Retry Update" action.
sidebarUpdateBtn.style.borderColor = "#e01b24"; sidebarUpdateBtn.style.borderColor = "#f66151";
sidebarUpdateBtn.style.backgroundColor = "rgba(224, 27, 36, 0.10)"; sidebarUpdateBtn.style.backgroundColor = "rgba(246, 97, 81, 0.10)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update failed — click to retry"; if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update failed — click to retry";
} else if (updateStatus === "reboot_required") { } else if (updateStatus === "reboot_required") {
sidebarUpdateBtn.style.borderColor = "#e5a50a"; sidebarUpdateBtn.style.borderColor = "#e9b64a";
sidebarUpdateBtn.style.backgroundColor = "rgba(229, 165, 10, 0.10)"; sidebarUpdateBtn.style.backgroundColor = "rgba(233, 182, 74, 0.10)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required"; if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required";
} else if (updateStatus === "running") { } else if (updateStatus === "running") {
sidebarUpdateBtn.style.borderColor = "#3584e4"; sidebarUpdateBtn.style.borderColor = "#78aeed";
sidebarUpdateBtn.style.backgroundColor = "rgba(53, 132, 228, 0.10)"; sidebarUpdateBtn.style.backgroundColor = "rgba(120, 174, 237, 0.10)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update in progress…"; if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update in progress…";
} else if (hasUpdates) { } else if (hasUpdates) {
sidebarUpdateBtn.style.borderColor = "#2ec27e"; sidebarUpdateBtn.style.borderColor = "#3ecf8e";
sidebarUpdateBtn.style.backgroundColor = "rgba(46, 194, 126, 0.08)"; sidebarUpdateBtn.style.backgroundColor = "rgba(62, 207, 142, 0.10)";
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!"; if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!";
} else { } else {
sidebarUpdateBtn.style.borderColor = ""; sidebarUpdateBtn.style.borderColor = "";
+94 -13
View File
@@ -2,9 +2,88 @@
// ── Update modal ────────────────────────────────────────────────── // ── Update modal ──────────────────────────────────────────────────
// ── Update dialog presentation (pill, status line, last-checked) ──
var _updateLastCheckTs = 0;
function markUpdateChecked() {
_updateLastCheckTs = Date.now();
}
function _updateLastCheckedText() {
if (!$updLastChecked) return;
if (!_updateLastCheckTs) { $updLastChecked.textContent = "—"; return; }
var s = Math.floor((Date.now() - _updateLastCheckTs) / 1000);
if (s < 30) { $updLastChecked.textContent = "just now"; return; }
var m = Math.floor(s / 60);
if (m < 60) { $updLastChecked.textContent = m + (m === 1 ? " minute ago" : " minutes ago"); return; }
var h = Math.floor(m / 60);
$updLastChecked.textContent = h + (h === 1 ? " hour ago" : " hours ago");
}
function setUpdatePill(state) {
if (!$updPill) return;
var cls = "upd-pill";
var html;
if (state === "checking") {
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Checking…';
} else if (state === "uptodate") {
cls += " st-active"; html = '<span class="status-dot active"></span>Up to date';
} else if (state === "complete") {
cls += " st-active"; html = '<span class="status-dot active"></span>Update complete';
} else if (state === "reboot") {
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Restart required';
} else if (state === "failed") {
cls += " st-failed"; html = '<span class="status-dot failed"></span>Update failed';
} else if (state === "unavailable") {
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Status unknown';
} else {
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Updating…';
}
$updPill.className = cls;
$updPill.innerHTML = html;
}
// Single place that reflects update state in the dialog: status message
// text + color, header pill, and the Close / Check again availability.
function _setUpdateStatus(text) {
if ($modalStatus) $modalStatus.textContent = text;
var state;
if (text.charAt(0) === "✗") state = "failed";
else if (text.indexOf("restart required") !== -1) state = "reboot";
else if (text.charAt(0) === "✓") state = (text.indexOf("already up to date") !== -1) ? "uptodate" : "complete";
else if (text.indexOf("unavailable") !== -1) state = "unavailable";
else if (text.indexOf("Checking") === 0) state = "checking";
else state = "updating";
if ($modalStatus) {
var msg = "update-status-msg";
if (state === "failed") $modalStatus.className = msg + " st-err";
else if (state === "reboot" || state === "unavailable") $modalStatus.className = msg + " st-warn";
else if (state === "uptodate" || state === "complete") $modalStatus.className = msg + " st-ok";
else $modalStatus.className = msg;
// In the up-to-date state the green console line already says it —
// the mockup shows it exactly once.
$modalStatus.style.display = (state === "uptodate") ? "none" : "";
}
setUpdatePill(state);
var interactive = (state !== "updating");
if ($updateCloseBtn) $updateCloseBtn.disabled = !interactive;
if ($btnCheckAgain) {
$btnCheckAgain.disabled = (state === "checking" || state === "updating");
$btnCheckAgain.style.display = interactive ? "inline-flex" : "none";
}
_updateLastCheckedText();
}
async function openUpdateModal() { async function openUpdateModal() {
if (!$modal) return; if (!$modal) return;
// Open immediately in a checking state; the status/check requests below
// fill in the real result (mockup: auto-check on open).
$modal.classList.add("open");
if ($modalLog) $modalLog.innerHTML = '<span class="dim">Checking for updates…</span>';
_setUpdateStatus("Checking for updates…");
// Reattach before checking for new updates. This makes a browser reload, // Reattach before checking for new updates. This makes a browser reload,
// RDP reconnect, or suspended tab recover the authoritative systemd-backed // RDP reconnect, or suspended tab recover the authoritative systemd-backed
// state instead of starting over or claiming the system is merely up to date. // state instead of starting over or claiming the system is merely up to date.
@@ -31,6 +110,7 @@ async function openUpdateModal() {
STATUS_POLL_FETCH_TIMEOUT STATUS_POLL_FETCH_TIMEOUT
) )
.then(function(data) { .then(function(data) {
markUpdateChecked();
if (!data.available) { if (!data.available) {
stopUpdatePoll(); stopUpdatePoll();
_updateLog = ""; _updateLog = "";
@@ -38,8 +118,8 @@ async function openUpdateModal() {
_updateVisibleLogChars = 0; _updateVisibleLogChars = 0;
_updateFinished = true; _updateFinished = true;
_updateStatusUnavailable = false; _updateStatusUnavailable = false;
if ($modalLog) $modalLog.textContent = ""; if ($modalLog) $modalLog.innerHTML = '<span class="ok">✓ System is already up to date</span>';
if ($modalStatus) $modalStatus.textContent = "✓ System is already up to date"; _setUpdateStatus("✓ System is already up to date");
if ($modalSpinner) $modalSpinner.classList.remove("spinning"); if ($modalSpinner) $modalSpinner.classList.remove("spinning");
if ($btnReboot) $btnReboot.style.display = "none"; if ($btnReboot) $btnReboot.style.display = "none";
if ($btnSave) $btnSave.style.display = "none"; if ($btnSave) $btnSave.style.display = "none";
@@ -68,7 +148,7 @@ function prepareUpdateModal() {
_updateStatusUnavailable = false; _updateStatusUnavailable = false;
_updatePollFailures = 0; _updatePollFailures = 0;
if ($modalLog) $modalLog.textContent = ""; if ($modalLog) $modalLog.textContent = "";
if ($modalStatus) $modalStatus.textContent = "Starting update…"; _setUpdateStatus("Starting update…");
if ($modalSpinner) $modalSpinner.classList.add("spinning"); if ($modalSpinner) $modalSpinner.classList.add("spinning");
if ($btnReboot) $btnReboot.style.display = "none"; if ($btnReboot) $btnReboot.style.display = "none";
if ($btnSave) $btnSave.style.display = "none"; if ($btnSave) $btnSave.style.display = "none";
@@ -89,7 +169,7 @@ function showExistingUpdate(data) {
_updateLogOffset = Number(data.offset) || 0; _updateLogOffset = Number(data.offset) || 0;
if (data.running) { if (data.running) {
if ($modalStatus) $modalStatus.textContent = "Updating…"; _setUpdateStatus("Updating…");
startUpdatePoll(); startUpdatePoll();
return; return;
} }
@@ -154,7 +234,8 @@ function startUpdate() {
) )
.then(function(data) { .then(function(data) {
if (data.status === "no_updates") { if (data.status === "no_updates") {
if ($modalStatus) $modalStatus.textContent = "✓ System is already up to date"; if ($modalLog) $modalLog.innerHTML = '<span class="ok">✓ System is already up to date</span>';
_setUpdateStatus("✓ System is already up to date");
if ($modalSpinner) $modalSpinner.classList.remove("spinning"); if ($modalSpinner) $modalSpinner.classList.remove("spinning");
if ($btnReboot) $btnReboot.style.display = "none"; if ($btnReboot) $btnReboot.style.display = "none";
if ($btnSave) $btnSave.style.display = "none"; if ($btnSave) $btnSave.style.display = "none";
@@ -165,7 +246,7 @@ function startUpdate() {
return; return;
} }
if (data.status === "already_running") appendLog("[Update already in progress, attaching…]\n\n"); if (data.status === "already_running") appendLog("[Update already in progress, attaching…]\n\n");
if ($modalStatus) $modalStatus.textContent = "Updating…"; _setUpdateStatus("Updating…");
startUpdatePoll(); startUpdatePoll();
}) })
.catch(function(err) { .catch(function(err) {
@@ -235,7 +316,7 @@ async function pollUpdateStatus() {
return; return;
} }
appendLog("[Update status reconnected]\n"); appendLog("[Update status reconnected]\n");
if ($modalStatus) $modalStatus.textContent = "Updating…"; _setUpdateStatus("Updating…");
} }
if (data.log) appendLog(data.log); if (data.log) appendLog(data.log);
_updateLogOffset = data.offset; _updateLogOffset = data.offset;
@@ -258,7 +339,7 @@ async function pollUpdateStatus() {
if (!_serverWasDown) { if (!_serverWasDown) {
_serverWasDown = true; _serverWasDown = true;
appendLog("\n[Update status connection interrupted — retrying…]\n"); appendLog("\n[Update status connection interrupted — retrying…]\n");
if ($modalStatus) $modalStatus.textContent = "Reconnecting to update…"; _setUpdateStatus("Reconnecting to update…");
} }
} finally { } finally {
_updatePollInFlight = false; _updatePollInFlight = false;
@@ -270,7 +351,7 @@ function showUpdateStatusUnavailable() {
_updateStatusUnavailable = true; _updateStatusUnavailable = true;
stopUpdatePoll(); stopUpdatePoll();
if ($modalSpinner) $modalSpinner.classList.remove("spinning"); if ($modalSpinner) $modalSpinner.classList.remove("spinning");
if ($modalStatus) $modalStatus.textContent = "Update status unavailable — update may still be running"; _setUpdateStatus("Update status unavailable — update may still be running");
appendLog("\n[The Hub could not confirm update status. The background update was not stopped. Select Retry Status after reconnecting.]\n"); appendLog("\n[The Hub could not confirm update status. The background update was not stopped. Select Retry Status after reconnecting.]\n");
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "inline-flex"; if ($btnRetryUpdate) $btnRetryUpdate.style.display = "inline-flex";
if ($btnCloseModal) $btnCloseModal.disabled = false; if ($btnCloseModal) $btnCloseModal.disabled = false;
@@ -283,7 +364,7 @@ function retryUpdateStatus() {
_updatePollFailures = 0; _updatePollFailures = 0;
_serverWasDown = true; _serverWasDown = true;
if ($modalSpinner) $modalSpinner.classList.add("spinning"); if ($modalSpinner) $modalSpinner.classList.add("spinning");
if ($modalStatus) $modalStatus.textContent = "Reconnecting to update…"; _setUpdateStatus("Reconnecting to update…");
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none"; if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
if ($btnCloseModal) $btnCloseModal.disabled = true; if ($btnCloseModal) $btnCloseModal.disabled = true;
startUpdatePoll(); startUpdatePoll();
@@ -314,13 +395,13 @@ function onUpdateDone(result) {
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none"; if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
if ($btnCloseModal) $btnCloseModal.disabled = false; if ($btnCloseModal) $btnCloseModal.disabled = false;
if (result === true) { if (result === true) {
if ($modalStatus) $modalStatus.textContent = "✓ Update complete"; _setUpdateStatus("✓ 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 — restart required"; _setUpdateStatus("✓ 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 — your system was not changed. Run the update again or save the error report for support."; _setUpdateStatus("✗ Update failed — your system was not changed. Run the update again or save the error report for support.");
if ($btnRetryRun) $btnRetryRun.style.display = "inline-flex"; if ($btnRetryRun) $btnRetryRun.style.display = "inline-flex";
if ($btnSave) $btnSave.style.display = "inline-flex"; if ($btnSave) $btnSave.style.display = "inline-flex";
if ($btnReboot) $btnReboot.style.display = "none"; if ($btnReboot) $btnReboot.style.display = "none";
+39 -10
View File
@@ -257,18 +257,36 @@
<!-- Update modal --> <!-- Update modal -->
<div class="modal-overlay" id="update-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title-text"> <div class="modal-overlay" id="update-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title-text">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-header"> <div class="upd-header">
<span class="modal-title" id="modal-title-text">Sovran_SystemsOS Update</span> <span class="widget-chip chip-sovran upd-chip"><svg class="upd-chip-svg"><use href="#g-refresh"/></svg></span>
<div class="modal-spinner" id="modal-spinner"></div> <span class="upd-title-wrap">
<span class="modal-status" id="modal-status">Updating…</span> <span class="upd-title" id="modal-title-text">Sovran_SystemsOS Update</span>
<span class="upd-sub">
<span class="ver-chip">Sovran_SystemsOS v{{ sovran_version }}</span>
<span class="upd-pill" id="upd-pill"></span>
<span class="modal-spinner" id="modal-spinner"></span>
</span>
</span>
<button class="creds-close-btn" id="update-close-btn" title="Close"></button>
</div>
<div class="modal-body-scroll">
<div class="sysmodal-card">
<div class="sysmodal-card-title"><svg><use href="#g-server"/></svg>System Details</div>
<div class="sysstep"><div class="sysnum">1</div><div class="sysstep-x"><div class="sysstep-t">Current version</div><div class="sysval"><span class="sysval-text">{{ sovran_version }}</span></div></div></div>
<div class="sysstep"><div class="sysnum">2</div><div class="sysstep-x"><div class="sysstep-t">Release channel</div><div class="sysval"><span class="sysval-text">stable</span></div></div></div>
<div class="sysstep"><div class="sysnum">3</div><div class="sysstep-x"><div class="sysstep-t">Last checked</div><div class="sysval"><span class="sysval-text" id="upd-last-checked"></span></div></div></div>
</div>
<div class="update-status-msg" id="modal-status">Checking for updates…</div>
<div class="modal-log" id="modal-log" aria-live="polite"></div>
</div> </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-save" id="btn-retry-update-status" style="display:none">Retry Status</button> <button class="btn btn-save" id="btn-retry-update-status" style="display:none">Retry Status</button>
<button class="btn btn-reboot" id="btn-retry-update" style="display:none">Retry Update</button> <button class="btn btn-reboot" id="btn-retry-update" style="display:none">Retry Update</button>
<button class="btn btn-reboot" id="btn-reboot" style="display:none">Restart Entire System</button> <button class="btn btn-reboot" id="btn-reboot" style="display:none">Restart Entire System</button>
<span class="modal-footer-spacer"></span>
<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>
<button class="btn btn-primary" id="btn-check-again"><svg><use href="#g-refresh"/></svg>Check again</button>
</div> </div>
</div> </div>
</div> </div>
@@ -362,15 +380,26 @@
<!-- Rebuild Modal --> <!-- Rebuild Modal -->
<div class="modal-overlay" id="rebuild-modal" role="dialog" aria-modal="true" aria-labelledby="rebuild-modal-title"> <div class="modal-overlay" id="rebuild-modal" role="dialog" aria-modal="true" aria-labelledby="rebuild-modal-title">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-header"> <div class="upd-header">
<span class="modal-title" id="rebuild-modal-title">Sovran_SystemsOS Rebuild</span> <span class="widget-chip chip-sovran upd-chip"><svg class="upd-chip-svg"><use href="#g-server"/></svg></span>
<div class="modal-spinner" id="rebuild-spinner"></div> <span class="upd-title-wrap">
<span class="modal-status" id="rebuild-status">Rebuilding…</span> <span class="upd-title" id="rebuild-modal-title">Sovran_SystemsOS Rebuild</span>
<span class="upd-sub">
<span class="ver-chip">Sovran_SystemsOS v{{ sovran_version }}</span>
<span class="upd-pill" id="rebuild-pill"></span>
<span class="modal-spinner" id="rebuild-spinner"></span>
</span>
</span>
<button class="creds-close-btn" id="rebuild-close-hdr" title="Close"></button>
</div>
<div class="modal-body-scroll">
<div class="update-status-msg" id="rebuild-status">Rebuilding…</div>
<div class="modal-log" id="rebuild-log" aria-live="polite" style="display:none"></div>
</div> </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">Restart Entire System</button> <button class="btn btn-reboot" id="rebuild-reboot-btn" style="display:none">Restart Entire System</button>
<span class="modal-footer-spacer"></span>
<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>