Add boot splash; separate Systems Operational and Security icons
Loading indicator: - Branded boot splash (Hub logo inside an accent spinner ring, "Starting The Hub") covers the shell while the first services data loads, then fades out once the welcome dashboard has rendered; never blocks longer than 25s and reassures the user after 8s (message about post-reboot delays) - Fire the network and update checks before the first services render so the dashboard cards are current at first paint - Sidebar Update button now adopts the last known update state when built (order-independent), and the welcome dashboard re-renders when the update state changes Icons: - New monochrome g-pulse glyph (activity line) for Systems Operational: welcome card, dialog header, and System Status section — the shield no longer doubles as Security - Tech Support / Security dialog header gets a standard chip; Security shows the shield chip with a plain "Security" title (no emoji), and the shared dialog title now resets correctly when reopening Tech Support after Security
This commit is contained in:
@@ -259,6 +259,67 @@
|
|||||||
.welcome-inner { animation: none; }
|
.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 {
|
.welcome-inner {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 1040px;
|
max-width: 1040px;
|
||||||
@@ -387,6 +448,10 @@
|
|||||||
background: linear-gradient(160deg, #42f39a, #1aa45d);
|
background: linear-gradient(160deg, #42f39a, #1aa45d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.widget-chip.chip-neutral {
|
||||||
|
background: linear-gradient(160deg, #2e333a, #23272c);
|
||||||
|
}
|
||||||
|
|
||||||
.w-body { min-width: 0; }
|
.w-body { min-width: 0; }
|
||||||
|
|
||||||
.widget h3 {
|
.widget h3 {
|
||||||
|
|||||||
@@ -230,7 +230,7 @@
|
|||||||
var attentionTitle = c.attention ? "Systems need attention" : "Systems operational";
|
var attentionTitle = c.attention ? "Systems need attention" : "Systems operational";
|
||||||
$wcSystems.innerHTML =
|
$wcSystems.innerHTML =
|
||||||
'<div class="widget clickable" id="w-systems" role="button" tabindex="0" title="System status and router setup">' +
|
'<div class="widget clickable" id="w-systems" role="button" tabindex="0" title="System status and router setup">' +
|
||||||
'<div class="widget-chip chip-green">' + icon("g-shield-check") + '</div>' +
|
'<div class="widget-chip chip-green">' + icon("g-pulse") + '</div>' +
|
||||||
'<div class="w-body"><h3>' + attentionTitle + '</h3><div class="sub">' + sub + '</div></div>' +
|
'<div class="w-body"><h3>' + attentionTitle + '</h3><div class="sub">' + sub + '</div></div>' +
|
||||||
'<span class="w-chev">' + icon("g-chev") + '</span>' +
|
'<span class="w-chev">' + icon("g-chev") + '</span>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
|
|
||||||
/* System status */
|
/* System status */
|
||||||
html += '<div class="sysmodal-card">' +
|
html += '<div class="sysmodal-card">' +
|
||||||
'<div class="sysmodal-card-title">' + icon("g-shield-check") + 'System Status</div>' +
|
'<div class="sysmodal-card-title">' + icon("g-pulse") + 'System Status</div>' +
|
||||||
step(1, "Services running", "", String(c.running)) +
|
step(1, "Services running", "", String(c.running)) +
|
||||||
step(2, "Needs attention", "", c.attention ? escHtml(c.attentionNames.join(", ")) : "None") +
|
step(2, "Needs attention", "", c.attention ? escHtml(c.attentionNames.join(", ")) : "None") +
|
||||||
step(3, "Turned off", "", c.off ? escHtml(c.offNames.join(", ")) : "None") +
|
step(3, "Turned off", "", c.off ? escHtml(c.offNames.join(", ")) : "None") +
|
||||||
|
|||||||
@@ -244,9 +244,9 @@ async function init() {
|
|||||||
if (badge && cfg.role_label) badge.textContent = cfg.role_label;
|
if (badge && cfg.role_label) badge.textContent = cfg.role_label;
|
||||||
window._roleLabel = cfg.role_label || "";
|
window._roleLabel = cfg.role_label || "";
|
||||||
|
|
||||||
await refreshServices();
|
|
||||||
loadNetwork();
|
loadNetwork();
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
|
await refreshServices();
|
||||||
|
|
||||||
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
||||||
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
||||||
@@ -256,9 +256,9 @@ async function init() {
|
|||||||
}
|
}
|
||||||
loadAutolaunchToggle();
|
loadAutolaunchToggle();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
await refreshServices();
|
|
||||||
loadNetwork();
|
loadNetwork();
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
|
await refreshServices();
|
||||||
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
||||||
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
||||||
loadAutolaunchToggle();
|
loadAutolaunchToggle();
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
function openSecurityModal() {
|
function openSecurityModal() {
|
||||||
if ($supportModal) $supportModal.classList.add("open");
|
if ($supportModal) $supportModal.classList.add("open");
|
||||||
var title = document.getElementById("support-modal-title");
|
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) {
|
if ($supportBody) {
|
||||||
$supportBody.innerHTML =
|
$supportBody.innerHTML =
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
async function openSupportModal() {
|
async function openSupportModal() {
|
||||||
if (!$supportModal) return;
|
if (!$supportModal) return;
|
||||||
$supportModal.classList.add("open");
|
$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 = '<p class="creds-loading">Checking support status…</p>';
|
$supportBody.innerHTML = '<p class="creds-loading">Checking support status…</p>';
|
||||||
try {
|
try {
|
||||||
var status = await apiFetch("/api/support/status");
|
var status = await apiFetch("/api/support/status");
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ function renderSidebarSupport(supportServices) {
|
|||||||
'</span>';
|
'</span>';
|
||||||
sidebarUpdateBtn.addEventListener("click", function() { openUpdateModal(); });
|
sidebarUpdateBtn.addEventListener("click", function() { openUpdateModal(); });
|
||||||
$sidebarSupport.appendChild(sidebarUpdateBtn);
|
$sidebarSupport.appendChild(sidebarUpdateBtn);
|
||||||
|
// checkUpdates may already have run before the sidebar was built
|
||||||
|
applyUpdateSidebarState(window._lastUpdateCheck);
|
||||||
|
|
||||||
for (var i = 0; i < supportServices.length; i++) {
|
for (var i = 0; i < supportServices.length; i++) {
|
||||||
var svc = supportServices[i];
|
var svc = supportServices[i];
|
||||||
@@ -251,6 +253,10 @@ async function refreshServices() {
|
|||||||
var services = await apiFetch("/api/services");
|
var services = await apiFetch("/api/services");
|
||||||
if (_firstLoad) { buildTiles(services, _categoryLabels); _firstLoad = false; }
|
if (_firstLoad) { buildTiles(services, _categoryLabels); _firstLoad = false; }
|
||||||
else { updateTiles(services); }
|
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); }
|
} catch (err) { console.warn("Failed to fetch services:", err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,16 +276,16 @@ async function loadNetwork() {
|
|||||||
|
|
||||||
// ── Update check ──────────────────────────────────────────────────
|
// ── Update check ──────────────────────────────────────────────────
|
||||||
|
|
||||||
async function checkUpdates() {
|
// Paint the sidebar Update button from the last known update state.
|
||||||
try {
|
// Called after each check AND when the button is (re)built, so the hint
|
||||||
var data = await apiFetch("/api/updates/check");
|
// is correct regardless of which finishes first at startup.
|
||||||
window._lastUpdateCheck = data;
|
function applyUpdateSidebarState(data) {
|
||||||
if (typeof markUpdateChecked === "function") markUpdateChecked();
|
if (!data) return;
|
||||||
var hasUpdates = !!data.available;
|
|
||||||
var updateStatus = data.status || "idle";
|
|
||||||
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
|
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
|
||||||
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
|
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
|
||||||
if (sidebarUpdateBtn) {
|
if (!sidebarUpdateBtn) return;
|
||||||
|
var hasUpdates = !!data.available;
|
||||||
|
var updateStatus = data.status || "idle";
|
||||||
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.
|
||||||
@@ -304,5 +310,14 @@ async function checkUpdates() {
|
|||||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "System is up to date";
|
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();
|
||||||
|
applyUpdateSidebarState(data);
|
||||||
|
// The welcome dashboard's updates card reads this state
|
||||||
|
if (typeof window.dashboardServicesUpdated === "function") window.dashboardServicesUpdated();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,11 @@
|
|||||||
<path d="M8.6 12l2.4 2.4 4.4-4.6"/>
|
<path d="M8.6 12l2.4 2.4 4.4-4.6"/>
|
||||||
</g>
|
</g>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<!-- Systems Operational: activity pulse (system health) — distinct from
|
||||||
|
the Security shield above -->
|
||||||
|
<symbol id="g-pulse" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M3 12h4l3-7 4 14 3-7h4"/>
|
||||||
|
</symbol>
|
||||||
<symbol id="g-lifebuoy" viewBox="0 0 24 24">
|
<symbol id="g-lifebuoy" viewBox="0 0 24 24">
|
||||||
<g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
<g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||||
<circle cx="12" cy="12" r="8.6"/><circle cx="12" cy="12" r="3.6"/>
|
<circle cx="12" cy="12" r="8.6"/><circle cx="12" cy="12" r="3.6"/>
|
||||||
@@ -165,6 +170,41 @@
|
|||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
<!-- ═══ BOOT SPLASH ═══ covers the shell while the first data loads;
|
||||||
|
dashboard.js lifts it once the welcome cards are rendered -->
|
||||||
|
<div id="app-splash" role="status" aria-live="polite">
|
||||||
|
<div class="splash-card">
|
||||||
|
<div class="splash-ring">
|
||||||
|
<img src="/static/sovran-hub-icon.svg" alt="" width="64" height="64" />
|
||||||
|
<span class="splash-ring-arc" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="splash-title">Starting The Hub</div>
|
||||||
|
<div class="splash-sub" id="splash-sub">Gathering your services…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var splash = document.getElementById("app-splash");
|
||||||
|
var sub = document.getElementById("splash-sub");
|
||||||
|
var lifted = false;
|
||||||
|
function lift() {
|
||||||
|
if (lifted || !splash) return;
|
||||||
|
lifted = true;
|
||||||
|
splash.classList.add("done");
|
||||||
|
setTimeout(function () {
|
||||||
|
if (splash && splash.parentNode) splash.parentNode.removeChild(splash);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
window.__liftAppSplash = lift;
|
||||||
|
// Reassure the user if the backend is slow (e.g. right after a reboot)
|
||||||
|
setTimeout(function () {
|
||||||
|
if (!lifted && sub) sub.textContent = "Still starting\u2026 this can take a moment after a reboot.";
|
||||||
|
}, 8000);
|
||||||
|
// Never trap the user behind the splash
|
||||||
|
setTimeout(lift, 25000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="app">
|
<div class="app">
|
||||||
|
|
||||||
<!-- ═══ SIDEBAR ═══ -->
|
<!-- ═══ SIDEBAR ═══ -->
|
||||||
@@ -250,7 +290,7 @@
|
|||||||
<div class="creds-dialog">
|
<div class="creds-dialog">
|
||||||
<div class="creds-header">
|
<div class="creds-header">
|
||||||
<span class="creds-title" id="systems-modal-title">
|
<span class="creds-title" id="systems-modal-title">
|
||||||
<span class="widget-chip chip-green" style="width:54px;height:54px;border-radius:16px"><svg style="width:27px;height:27px"><use href="#g-shield-check"/></svg></span>
|
<span class="widget-chip chip-green" style="width:54px;height:54px;border-radius:16px"><svg style="width:27px;height:27px"><use href="#g-pulse"/></svg></span>
|
||||||
<span>Systems Operational</span>
|
<span>Systems Operational</span>
|
||||||
</span>
|
</span>
|
||||||
<button class="creds-close-btn" id="systems-close-btn" title="Close">✕</button>
|
<button class="creds-close-btn" id="systems-close-btn" title="Close">✕</button>
|
||||||
@@ -317,7 +357,10 @@
|
|||||||
<div class="modal-overlay" id="support-modal" role="dialog" aria-modal="true" aria-labelledby="support-modal-title">
|
<div class="modal-overlay" id="support-modal" role="dialog" aria-modal="true" aria-labelledby="support-modal-title">
|
||||||
<div class="creds-dialog">
|
<div class="creds-dialog">
|
||||||
<div class="creds-header">
|
<div class="creds-header">
|
||||||
<span class="creds-title" id="support-modal-title">Tech Support</span>
|
<span class="creds-title">
|
||||||
|
<span class="widget-chip chip-neutral" id="support-modal-chip" style="width:54px;height:54px;border-radius:16px"><svg style="width:27px;height:27px"><use href="#g-lifebuoy"/></svg></span>
|
||||||
|
<span id="support-modal-title">Tech Support</span>
|
||||||
|
</span>
|
||||||
<button class="creds-close-btn" id="support-close-btn" title="Close">✕</button>
|
<button class="creds-close-btn" id="support-close-btn" title="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="creds-body" id="support-body">
|
<div class="creds-body" id="support-body">
|
||||||
|
|||||||
Reference in New Issue
Block a user