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 = '
' + - '
' + icon("g-shield-check") + '
' + + '
' + icon("g-pulse") + '
' + '

' + attentionTitle + '

' + sub + '
' + '' + icon("g-chev") + '' + '
'; @@ -337,7 +337,7 @@ /* System status */ html += '
' + - '
' + icon("g-shield-check") + 'System Status
' + + '
' + icon("g-pulse") + 'System Status
' + step(1, "Services running", "", String(c.running)) + step(2, "Needs attention", "", c.attention ? escHtml(c.attentionNames.join(", ")) : "None") + step(3, "Turned off", "", c.off ? escHtml(c.offNames.join(", ")) : "None") + diff --git a/app/sovran_systemsos_web/static/js/events.js b/app/sovran_systemsos_web/static/js/events.js index c940130..b03d861 100644 --- a/app/sovran_systemsos_web/static/js/events.js +++ b/app/sovran_systemsos_web/static/js/events.js @@ -244,9 +244,9 @@ async function init() { if (badge && cfg.role_label) badge.textContent = cfg.role_label; window._roleLabel = cfg.role_label || ""; - await refreshServices(); loadNetwork(); checkUpdates(); + await refreshServices(); setInterval(refreshServices, POLL_INTERVAL_SERVICES); setInterval(checkUpdates, POLL_INTERVAL_UPDATES); @@ -256,9 +256,9 @@ async function init() { } loadAutolaunchToggle(); } catch (_) { - await refreshServices(); loadNetwork(); checkUpdates(); + await refreshServices(); setInterval(refreshServices, POLL_INTERVAL_SERVICES); setInterval(checkUpdates, POLL_INTERVAL_UPDATES); loadAutolaunchToggle(); diff --git a/app/sovran_systemsos_web/static/js/security.js b/app/sovran_systemsos_web/static/js/security.js index 78fa6b6..c6df1e3 100644 --- a/app/sovran_systemsos_web/static/js/security.js +++ b/app/sovran_systemsos_web/static/js/security.js @@ -5,7 +5,12 @@ function openSecurityModal() { if ($supportModal) $supportModal.classList.add("open"); var title = document.getElementById("support-modal-title"); - if (title) title.textContent = "\uD83D\uDEE1 Security"; + if (title) title.textContent = "Security"; + var chip = document.getElementById("support-modal-chip"); + if (chip) { + var use = chip.querySelector("use"); + if (use) use.setAttribute("href", "#g-shield-check"); + } if ($supportBody) { $supportBody.innerHTML = diff --git a/app/sovran_systemsos_web/static/js/support.js b/app/sovran_systemsos_web/static/js/support.js index 9560661..88d4832 100644 --- a/app/sovran_systemsos_web/static/js/support.js +++ b/app/sovran_systemsos_web/static/js/support.js @@ -5,6 +5,14 @@ async function openSupportModal() { if (!$supportModal) return; $supportModal.classList.add("open"); + // The dialog is shared with Security — always restore its identity + var title = document.getElementById("support-modal-title"); + if (title) title.textContent = "Tech Support"; + var chip = document.getElementById("support-modal-chip"); + if (chip) { + var use = chip.querySelector("use"); + if (use) use.setAttribute("href", "#g-lifebuoy"); + } $supportBody.innerHTML = '

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 @@ + + + + @@ -165,6 +170,41 @@ + +
+
+
+ + +
+
Starting The Hub
+
Gathering your services…
+
+
+ +
@@ -250,7 +290,7 @@
- + Systems Operational @@ -317,7 +357,10 @@