diff --git a/app/sovran_systemsos_web/static/css/modals.css b/app/sovran_systemsos_web/static/css/modals.css index bd7057f..4816df5 100644 --- a/app/sovran_systemsos_web/static/css/modals.css +++ b/app/sovran_systemsos_web/static/css/modals.css @@ -48,26 +48,97 @@ /* ── Update / Rebuild modal internals ───────────────────────────── */ -.modal-header { +.upd-header { display: flex; align-items: center; - gap: 10px; - padding: 20px 24px 0; + gap: 14px; + 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-weight: 800; letter-spacing: -0.01em; + line-height: 1.2; } -.modal-status { - margin-left: auto; - font-size: 0.78rem; +.upd-sub { + display: flex; + 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; 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 { width: 16px; height: 16px; @@ -75,9 +146,13 @@ border: 2.5px solid rgba(255, 255, 255, 0.14); border-top-color: var(--accent); 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); } } @@ -86,7 +161,7 @@ border: 1px solid var(--border); border-radius: 14px; padding: 13px 16px; - margin: 16px 24px 0; + margin: 15px 0 0; font-family: var(--mono); font-size: 0.72rem; line-height: 1.85; @@ -98,6 +173,9 @@ word-break: break-word; } +.modal-log .ok { color: var(--accent); } +.modal-log .dim { color: var(--text-3); } + .modal-footer { display: flex; gap: 10px; diff --git a/app/sovran_systemsos_web/static/js/events.js b/app/sovran_systemsos_web/static/js/events.js index dbb2840..602f1e1 100644 --- a/app/sovran_systemsos_web/static/js/events.js +++ b/app/sovran_systemsos_web/static/js/events.js @@ -4,6 +4,8 @@ // if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js 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 ($btnSave) $btnSave.addEventListener("click", saveErrorReport); if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus); @@ -28,6 +30,7 @@ if ($logoutBtn) $logoutBtn.addEventListener("click", function () { // Rebuild modal if ($rebuildClose) $rebuildClose.addEventListener("click", closeRebuildModal); +if ($rebuildCloseHdr) $rebuildCloseHdr.addEventListener("click", closeRebuildModal); if ($rebuildReboot) $rebuildReboot.addEventListener("click", doReboot); if ($rebuildSave) $rebuildSave.addEventListener("click", saveRebuildErrorReport); if ($rebuildModal) $rebuildModal.addEventListener("click", function(e) { if (e.target === $rebuildModal) closeRebuildModal(); }); diff --git a/app/sovran_systemsos_web/static/js/rebuild.js b/app/sovran_systemsos_web/static/js/rebuild.js index df606f1..03c018f 100644 --- a/app/sovran_systemsos_web/static/js/rebuild.js +++ b/app/sovran_systemsos_web/static/js/rebuild.js @@ -2,6 +2,35 @@ // ── 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 = 'Restart required'; + } else { + cls += " st-active"; html = 'Done'; + } + } else if (text.charAt(0) === "✗") { + cls += " st-failed"; html = 'Failed'; + } else { + cls += " st-loading"; html = '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() { if (!$rebuildModal) return; _rebuildLog = ""; @@ -13,11 +42,12 @@ function openRebuildModal() { if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; } var action = _rebuildIsEnabling ? "Enabling" : "Disabling"; var label = _rebuildFeatureName || "feature"; - if ($rebuildStatus) $rebuildStatus.textContent = action + " " + label + "…"; + _setRebuildStatus(action + " " + label + "…"); if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning"); if ($rebuildReboot) $rebuildReboot.style.display = "none"; if ($rebuildSave) $rebuildSave.style.display = "none"; if ($rebuildClose) $rebuildClose.disabled = true; + if ($rebuildCloseHdr) $rebuildCloseHdr.disabled = true; $rebuildModal.classList.add("open"); // Delay first poll slightly to let the rebuild service start and clear stale log setTimeout(startRebuildPoll, 1500); @@ -76,7 +106,7 @@ async function pollRebuildStatus() { window.location.reload(); return; } - if (!_rebuildServerDown) { _rebuildServerDown = true; if ($rebuildStatus) $rebuildStatus.textContent = "Applying changes…"; } + if (!_rebuildServerDown) { _rebuildServerDown = true; _setRebuildStatus("Applying changes…"); } } finally { _rebuildPollInFlight = false; } @@ -85,15 +115,16 @@ async function pollRebuildStatus() { function onRebuildDone(result) { if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning"); if ($rebuildClose) $rebuildClose.disabled = false; + if ($rebuildCloseHdr) $rebuildCloseHdr.disabled = false; 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 setTimeout(function() { window.location.reload(); }, 1200); } else if (result === "reboot_required") { - if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done — restart required"; + _setRebuildStatus("✓ Done — restart required"); if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex"; } else { - if ($rebuildStatus) $rebuildStatus.textContent = "✗ Something went wrong"; + _setRebuildStatus("✗ Something went wrong"); if ($rebuildSave) $rebuildSave.style.display = "inline-flex"; if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex"; } diff --git a/app/sovran_systemsos_web/static/js/state.js b/app/sovran_systemsos_web/static/js/state.js index 200722f..d05fd50 100644 --- a/app/sovran_systemsos_web/static/js/state.js +++ b/app/sovran_systemsos_web/static/js/state.js @@ -55,6 +55,10 @@ const $btnSave = document.getElementById("btn-save-report"); const $btnRetryUpdate = document.getElementById("btn-retry-update-status"); const $btnRetryRun = document.getElementById("btn-retry-update"); 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 $rebootMainCard = document.getElementById("reboot-main-card"); @@ -80,6 +84,8 @@ const $rebuildLog = document.getElementById("rebuild-log"); const $rebuildReboot = document.getElementById("rebuild-reboot-btn"); const $rebuildSave = document.getElementById("rebuild-save-report"); 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 const $domainSetupModal = document.getElementById("domain-setup-modal"); diff --git a/app/sovran_systemsos_web/static/js/tiles.js b/app/sovran_systemsos_web/static/js/tiles.js index e8b4a30..85ace74 100644 --- a/app/sovran_systemsos_web/static/js/tiles.js +++ b/app/sovran_systemsos_web/static/js/tiles.js @@ -272,6 +272,7 @@ async function loadNetwork() { async function checkUpdates() { try { var data = await apiFetch("/api/updates/check"); + if (typeof markUpdateChecked === "function") markUpdateChecked(); var hasUpdates = !!data.available; var updateStatus = data.status || "idle"; var sidebarUpdateBtn = document.getElementById("sidebar-btn-update"); @@ -280,20 +281,20 @@ async function checkUpdates() { if (updateStatus === "failed") { // 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. - sidebarUpdateBtn.style.borderColor = "#e01b24"; - sidebarUpdateBtn.style.backgroundColor = "rgba(224, 27, 36, 0.10)"; + sidebarUpdateBtn.style.borderColor = "#f66151"; + sidebarUpdateBtn.style.backgroundColor = "rgba(246, 97, 81, 0.10)"; if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update failed — click to retry"; } else if (updateStatus === "reboot_required") { - sidebarUpdateBtn.style.borderColor = "#e5a50a"; - sidebarUpdateBtn.style.backgroundColor = "rgba(229, 165, 10, 0.10)"; + sidebarUpdateBtn.style.borderColor = "#e9b64a"; + sidebarUpdateBtn.style.backgroundColor = "rgba(233, 182, 74, 0.10)"; if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required"; } else if (updateStatus === "running") { - sidebarUpdateBtn.style.borderColor = "#3584e4"; - sidebarUpdateBtn.style.backgroundColor = "rgba(53, 132, 228, 0.10)"; + sidebarUpdateBtn.style.borderColor = "#78aeed"; + sidebarUpdateBtn.style.backgroundColor = "rgba(120, 174, 237, 0.10)"; if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update in progress…"; } else if (hasUpdates) { - sidebarUpdateBtn.style.borderColor = "#2ec27e"; - sidebarUpdateBtn.style.backgroundColor = "rgba(46, 194, 126, 0.08)"; + sidebarUpdateBtn.style.borderColor = "#3ecf8e"; + sidebarUpdateBtn.style.backgroundColor = "rgba(62, 207, 142, 0.10)"; if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!"; } else { sidebarUpdateBtn.style.borderColor = ""; diff --git a/app/sovran_systemsos_web/static/js/update.js b/app/sovran_systemsos_web/static/js/update.js index 7ea98bd..200eba0 100644 --- a/app/sovran_systemsos_web/static/js/update.js +++ b/app/sovran_systemsos_web/static/js/update.js @@ -2,9 +2,88 @@ // ── 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 = 'Checking…'; + } else if (state === "uptodate") { + cls += " st-active"; html = 'Up to date'; + } else if (state === "complete") { + cls += " st-active"; html = 'Update complete'; + } else if (state === "reboot") { + cls += " st-needs-attention"; html = 'Restart required'; + } else if (state === "failed") { + cls += " st-failed"; html = 'Update failed'; + } else if (state === "unavailable") { + cls += " st-needs-attention"; html = 'Status unknown'; + } else { + cls += " st-loading"; html = '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() { 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 = 'Checking for updates…'; + _setUpdateStatus("Checking for updates…"); + // Reattach before checking for new updates. This makes a browser reload, // RDP reconnect, or suspended tab recover the authoritative systemd-backed // 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 ) .then(function(data) { + markUpdateChecked(); if (!data.available) { stopUpdatePoll(); _updateLog = ""; @@ -38,8 +118,8 @@ async function openUpdateModal() { _updateVisibleLogChars = 0; _updateFinished = true; _updateStatusUnavailable = false; - if ($modalLog) $modalLog.textContent = ""; - if ($modalStatus) $modalStatus.textContent = "✓ System is already up to date"; + if ($modalLog) $modalLog.innerHTML = '✓ System is already up to date'; + _setUpdateStatus("✓ System is already up to date"); if ($modalSpinner) $modalSpinner.classList.remove("spinning"); if ($btnReboot) $btnReboot.style.display = "none"; if ($btnSave) $btnSave.style.display = "none"; @@ -68,7 +148,7 @@ function prepareUpdateModal() { _updateStatusUnavailable = false; _updatePollFailures = 0; if ($modalLog) $modalLog.textContent = ""; - if ($modalStatus) $modalStatus.textContent = "Starting update…"; + _setUpdateStatus("Starting update…"); if ($modalSpinner) $modalSpinner.classList.add("spinning"); if ($btnReboot) $btnReboot.style.display = "none"; if ($btnSave) $btnSave.style.display = "none"; @@ -89,7 +169,7 @@ function showExistingUpdate(data) { _updateLogOffset = Number(data.offset) || 0; if (data.running) { - if ($modalStatus) $modalStatus.textContent = "Updating…"; + _setUpdateStatus("Updating…"); startUpdatePoll(); return; } @@ -154,7 +234,8 @@ function startUpdate() { ) .then(function(data) { if (data.status === "no_updates") { - if ($modalStatus) $modalStatus.textContent = "✓ System is already up to date"; + if ($modalLog) $modalLog.innerHTML = '✓ System is already up to date'; + _setUpdateStatus("✓ System is already up to date"); if ($modalSpinner) $modalSpinner.classList.remove("spinning"); if ($btnReboot) $btnReboot.style.display = "none"; if ($btnSave) $btnSave.style.display = "none"; @@ -165,7 +246,7 @@ function startUpdate() { return; } if (data.status === "already_running") appendLog("[Update already in progress, attaching…]\n\n"); - if ($modalStatus) $modalStatus.textContent = "Updating…"; + _setUpdateStatus("Updating…"); startUpdatePoll(); }) .catch(function(err) { @@ -235,7 +316,7 @@ async function pollUpdateStatus() { return; } appendLog("[Update status reconnected]\n"); - if ($modalStatus) $modalStatus.textContent = "Updating…"; + _setUpdateStatus("Updating…"); } if (data.log) appendLog(data.log); _updateLogOffset = data.offset; @@ -258,7 +339,7 @@ async function pollUpdateStatus() { if (!_serverWasDown) { _serverWasDown = true; appendLog("\n[Update status connection interrupted — retrying…]\n"); - if ($modalStatus) $modalStatus.textContent = "Reconnecting to update…"; + _setUpdateStatus("Reconnecting to update…"); } } finally { _updatePollInFlight = false; @@ -270,7 +351,7 @@ function showUpdateStatusUnavailable() { _updateStatusUnavailable = true; stopUpdatePoll(); 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"); if ($btnRetryUpdate) $btnRetryUpdate.style.display = "inline-flex"; if ($btnCloseModal) $btnCloseModal.disabled = false; @@ -283,7 +364,7 @@ function retryUpdateStatus() { _updatePollFailures = 0; _serverWasDown = true; if ($modalSpinner) $modalSpinner.classList.add("spinning"); - if ($modalStatus) $modalStatus.textContent = "Reconnecting to update…"; + _setUpdateStatus("Reconnecting to update…"); if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none"; if ($btnCloseModal) $btnCloseModal.disabled = true; startUpdatePoll(); @@ -314,13 +395,13 @@ function onUpdateDone(result) { if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none"; if ($btnCloseModal) $btnCloseModal.disabled = false; if (result === true) { - if ($modalStatus) $modalStatus.textContent = "✓ Update complete"; + _setUpdateStatus("✓ Update complete"); if ($btnReboot) $btnReboot.style.display = "inline-flex"; } 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"; } 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 ($btnSave) $btnSave.style.display = "inline-flex"; if ($btnReboot) $btnReboot.style.display = "none"; diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index c292324..dc4073a 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -257,18 +257,36 @@