diff --git a/app/sovran_systemsos_web/static/css/layout.css b/app/sovran_systemsos_web/static/css/layout.css index c401fc6..64e3947 100644 --- a/app/sovran_systemsos_web/static/css/layout.css +++ b/app/sovran_systemsos_web/static/css/layout.css @@ -259,6 +259,67 @@ .welcome-inner { animation: none; } } +/* ── Boot splash ────────────────────────────────────────────────── */ +/* Covers the shell while the first services/config data loads; the + inline script in index.html lifts it (window.__liftAppSplash). */ +#app-splash { + position: fixed; + inset: 0; + z-index: 2000; + background: var(--bg); + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.45s ease; +} + +#app-splash.done { opacity: 0; pointer-events: none; } + +.splash-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 24px; +} + +.splash-ring { + position: relative; + width: 96px; + height: 96px; + display: grid; + place-items: center; + margin-bottom: 8px; +} + +.splash-ring img { width: 60px; height: 60px; opacity: 0.95; } + +.splash-ring-arc { + position: absolute; + inset: 0; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, 0.10); + border-top-color: var(--accent); + animation: splash-spin 1s linear infinite; +} + +@keyframes splash-spin { to { transform: rotate(360deg); } } + +.splash-title { + font-size: 1.02rem; + font-weight: 700; + color: var(--text); +} + +.splash-sub { + font-size: 0.84rem; + color: var(--text-3); +} + +@media (prefers-reduced-motion: reduce) { + .splash-ring-arc { animation-duration: 2.5s; } +} + .welcome-inner { position: relative; max-width: 1040px; @@ -387,6 +448,10 @@ background: linear-gradient(160deg, #42f39a, #1aa45d); } +.widget-chip.chip-neutral { + background: linear-gradient(160deg, #2e333a, #23272c); +} + .w-body { min-width: 0; } .widget h3 { diff --git a/app/sovran_systemsos_web/static/js/dashboard.js b/app/sovran_systemsos_web/static/js/dashboard.js index ad7448d..02624cc 100644 --- a/app/sovran_systemsos_web/static/js/dashboard.js +++ b/app/sovran_systemsos_web/static/js/dashboard.js @@ -230,7 +230,7 @@ var attentionTitle = c.attention ? "Systems need attention" : "Systems operational"; $wcSystems.innerHTML = '
'; @@ -337,7 +337,7 @@ /* System status */ html += 'Checking support status…
'; try { var status = await apiFetch("/api/support/status"); diff --git a/app/sovran_systemsos_web/static/js/tiles.js b/app/sovran_systemsos_web/static/js/tiles.js index 787659b..3cd6a01 100644 --- a/app/sovran_systemsos_web/static/js/tiles.js +++ b/app/sovran_systemsos_web/static/js/tiles.js @@ -71,6 +71,8 @@ function renderSidebarSupport(supportServices) { ''; sidebarUpdateBtn.addEventListener("click", function() { openUpdateModal(); }); $sidebarSupport.appendChild(sidebarUpdateBtn); + // checkUpdates may already have run before the sidebar was built + applyUpdateSidebarState(window._lastUpdateCheck); for (var i = 0; i < supportServices.length; i++) { var svc = supportServices[i]; @@ -251,6 +253,10 @@ async function refreshServices() { var services = await apiFetch("/api/services"); if (_firstLoad) { buildTiles(services, _categoryLabels); _firstLoad = false; } else { updateTiles(services); } + // Service data has rendered — the dashboard is ready; lift the boot + // splash (no-op once lifted). Note: dashboardServicesUpdated may also + // fire from checkUpdates before this resolves, so the lift lives here. + if (typeof window.__liftAppSplash === "function") window.__liftAppSplash(); } catch (err) { console.warn("Failed to fetch services:", err); } } @@ -270,39 +276,48 @@ async function loadNetwork() { // ── Update check ────────────────────────────────────────────────── +// Paint the sidebar Update button from the last known update state. +// Called after each check AND when the button is (re)built, so the hint +// is correct regardless of which finishes first at startup. +function applyUpdateSidebarState(data) { + if (!data) return; + var sidebarUpdateBtn = document.getElementById("sidebar-btn-update"); + var sidebarUpdateHint = document.getElementById("sidebar-update-hint"); + if (!sidebarUpdateBtn) return; + var hasUpdates = !!data.available; + var updateStatus = data.status || "idle"; + 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 = "#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 = "#e9b64a"; + sidebarUpdateBtn.style.backgroundColor = "rgba(233, 182, 74, 0.10)"; + if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required"; + } else if (updateStatus === "running") { + 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 = "#3ecf8e"; + sidebarUpdateBtn.style.backgroundColor = "rgba(62, 207, 142, 0.10)"; + if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!"; + } else { + sidebarUpdateBtn.style.borderColor = ""; + sidebarUpdateBtn.style.backgroundColor = ""; + if (sidebarUpdateHint) sidebarUpdateHint.textContent = "System is up to date"; + } +} + async function checkUpdates() { try { var data = await apiFetch("/api/updates/check"); window._lastUpdateCheck = data; if (typeof markUpdateChecked === "function") markUpdateChecked(); - var hasUpdates = !!data.available; - var updateStatus = data.status || "idle"; - var sidebarUpdateBtn = document.getElementById("sidebar-btn-update"); - var sidebarUpdateHint = document.getElementById("sidebar-update-hint"); - if (sidebarUpdateBtn) { - 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 = "#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 = "#e9b64a"; - sidebarUpdateBtn.style.backgroundColor = "rgba(233, 182, 74, 0.10)"; - if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required"; - } else if (updateStatus === "running") { - 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 = "#3ecf8e"; - sidebarUpdateBtn.style.backgroundColor = "rgba(62, 207, 142, 0.10)"; - if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!"; - } else { - sidebarUpdateBtn.style.borderColor = ""; - sidebarUpdateBtn.style.backgroundColor = ""; - if (sidebarUpdateHint) sidebarUpdateHint.textContent = "System is up to date"; - } - } + applyUpdateSidebarState(data); + // The welcome dashboard's updates card reads this state + if (typeof window.dashboardServicesUpdated === "function") window.dashboardServicesUpdated(); } catch (_) {} } diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index e9b0e0b..4aa6e24 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -111,6 +111,11 @@