Add welcome dashboard as default view

Replace the All Services grid as the landing view with a
Nextcloud-style welcome dashboard:

- Greeting (time-of-day), "Welcome to Your Sovereign Digital &
  Financial Life" headline, version + role meta line
- Status cards: Systems Operational, Network (LAN/WAN/hostname),
  Bitcoin sync progress with ETA, and available-updates card
- Calm drifting orb background animation (transform-only,
  disabled under prefers-reduced-motion)
- Services grid stays mounted but hidden until the user browses
  (Browse button, search, or a nav category); Dashboard nav item
  returns to the welcome view
- Move role badge and autolaunch preference out of the sidebar;
  autolaunch toggle now lives in the Systems Operational modal
- Remove LAN/WAN chip from the topbar; IPs live on the Network card
This commit is contained in:
2026-09-08 18:46:43 -05:00
parent 61ef286420
commit 78ae9e78ed
8 changed files with 335 additions and 158 deletions
@@ -56,49 +56,6 @@
.search-box input::placeholder { color: var(--text-3); } .search-box input::placeholder { color: var(--text-3); }
/* ── Network chip — LAN + WAN always on one line ────────────────── */
.net-chip {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 17px;
border-radius: 99px;
background: var(--card);
border: 1px solid var(--border);
font-family: var(--mono);
font-size: 0.73rem;
color: var(--text-3);
white-space: nowrap;
order: 4;
}
.net-chip svg { width: 15px; height: 15px; color: var(--text-3); flex-shrink: 0; }
.net-chip .int-wrap, .net-chip .ext-wrap { display: inline-flex; align-items: center; gap: 7px; }
.net-chip .ip-label {
font-size: 0.6rem;
font-weight: 800;
letter-spacing: 0.08em;
color: var(--text-3);
margin-right: 7px;
}
.net-chip .ip-value {
color: var(--text);
font-weight: 600;
font-family: var(--mono);
}
.net-separator {
width: 1px;
height: 15px;
background: var(--border-strong);
margin: 0 3px;
flex-shrink: 0;
color: transparent;
}
.top-actions { .top-actions {
display: flex; display: flex;
gap: 9px; gap: 9px;
@@ -106,17 +63,6 @@
order: 5; order: 5;
} }
/* Below 1160px the chip wraps to its own full-width topbar row —
it never stacks LAN over WAN and never hides the external IP. */
@media (max-width: 1160px) {
.topbar { flex-wrap: wrap; row-gap: 10px; }
.net-chip {
order: 6;
flex-basis: 100%;
justify-content: center;
}
}
/* ── Legacy badges (still used inside dialogs) ──────────────────── */ /* ── Legacy badges (still used inside dialogs) ──────────────────── */
.role-badge { .role-badge {
+118 -13
View File
@@ -204,30 +204,131 @@
.sidebar-spacer { flex: 1; min-height: 18px; } .sidebar-spacer { flex: 1; min-height: 18px; }
.sidebar-footer { /* ── Welcome dashboard (default view) ───────────────────────────── */
border-top: 1px solid var(--border);
padding-top: 14px; .welcome {
position: relative;
max-width: 1040px;
margin: 0 auto;
padding: 44px 8px 24px;
overflow: hidden;
min-height: 100%;
} }
.host-note { /* Soft drifting glow — calm, GPU-cheap (transforms only) */
text-align: center; .welcome-bg {
font-size: 0.66rem; position: absolute;
inset: 0;
pointer-events: none;
}
.orb {
position: absolute;
border-radius: 50%;
will-change: transform;
}
.orb-a {
width: 560px; height: 560px;
left: -140px; top: -180px;
background: radial-gradient(circle, rgba(62, 207, 142, 0.10), transparent 70%);
animation: orb-drift-a 26s ease-in-out infinite alternate;
}
.orb-b {
width: 680px; height: 680px;
right: -200px; top: 20%;
background: radial-gradient(circle, rgba(120, 174, 237, 0.07), transparent 70%);
animation: orb-drift-b 34s ease-in-out infinite alternate;
}
.orb-c {
width: 460px; height: 460px;
left: 34%; bottom: -200px;
background: radial-gradient(circle, rgba(66, 243, 154, 0.06), transparent 70%);
animation: orb-drift-c 42s ease-in-out infinite alternate;
}
@keyframes orb-drift-a { to { transform: translate(70px, 50px) scale(1.14); } }
@keyframes orb-drift-b { to { transform: translate(-60px, -70px) scale(1.10); } }
@keyframes orb-drift-c { to { transform: translate(50px, -40px) scale(1.18); } }
@media (prefers-reduced-motion: reduce) {
.orb { animation: none; }
.welcome-inner { animation: none; }
}
.welcome-inner {
position: relative;
animation: welcome-in 0.5s ease both;
}
@keyframes welcome-in {
from { opacity: 0; transform: translateY(14px); }
}
.welcome-greeting {
font-size: 0.98rem;
font-weight: 650;
color: var(--text-2);
}
.welcome-title {
margin-top: 6px;
font-size: clamp(1.7rem, 2.8vw, 2.25rem);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.18;
}
.welcome-title em { font-style: normal; color: var(--accent); }
.welcome-meta {
margin-top: 12px;
font-size: 0.8rem;
color: var(--text-3); color: var(--text-3);
font-family: var(--mono); font-family: var(--mono);
margin-top: 9px;
} }
/* ── Widgets row (dashboard.js) ─────────────────────────────────── */ .welcome-meta-sep { margin: 0 8px; opacity: 0.6; }
.widgets { .welcome-cards {
display: grid; display: grid;
grid-template-columns: 1.1fr 1.45fr; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px; gap: 16px;
margin-bottom: 32px; margin-top: 34px;
} }
@media (max-width: 900px) { /* wrappers whose children become grid items */
.widgets { grid-template-columns: 1fr; } .welcome-dyn { display: contents; }
.w-network .net-row {
display: flex;
align-items: baseline;
gap: 10px;
margin-top: 6px;
font-family: var(--mono);
}
.w-network .net-k {
font-size: 0.6rem;
font-weight: 800;
letter-spacing: 0.08em;
color: var(--text-3);
width: 30px;
flex-shrink: 0;
}
.w-network .ip-value {
font-size: 0.82rem;
font-weight: 600;
color: var(--text);
}
.w-network .sub { margin-top: 8px; }
.welcome-browse {
margin-top: 26px;
} }
.widget { .widget {
@@ -275,6 +376,10 @@
background: linear-gradient(160deg, #f7931a, #b96a0a); background: linear-gradient(160deg, #f7931a, #b96a0a);
} }
.widget-chip.chip-amber {
background: linear-gradient(160deg, #b98426, #8a5f16);
}
.widget-chip.chip-sovran { .widget-chip.chip-sovran {
background: linear-gradient(160deg, #42f39a, #1aa45d); background: linear-gradient(160deg, #42f39a, #1aa45d);
} }
@@ -1505,6 +1505,15 @@
margin-top: 5px; margin-top: 5px;
} }
.sysprefs {
padding: 0 24px 20px;
border-top: 1px solid var(--border);
margin-top: 16px;
padding-top: 14px;
}
.sysprefs .autolaunch-section .feature-card { padding: 2px 0 0; }
.sysfineprint { .sysfineprint {
font-size: 0.72rem; font-size: 0.72rem;
color: var(--text-3); color: var(--text-3);
+163 -68
View File
@@ -1,23 +1,29 @@
"use strict"; "use strict";
/* ── The Hub dashboard chrome ────────────────────────────────────── /* ── The Hub dashboard chrome ──────────────────────────────────────
Category navigation, service search, status widgets, and the Welcome dashboard (default view), category navigation, service
Systems Operational modal (router ports 80/443 + the live domain search, status cards, and the Systems Operational modal.
diagnostics checklist).
Everything lives in an IIFE so no globals leak into the other Everything lives in an IIFE so no globals leak into the other Hub
Hub scripts; tiles.js calls window.dashboardServicesUpdated() scripts; tiles.js calls window.dashboardServicesUpdated() whenever
whenever the service list refreshes. */ the service list refreshes. */
(function () { (function () {
var $nav = document.getElementById("sidebar-nav"); var $nav = document.getElementById("sidebar-nav");
var $pageTitle = document.getElementById("page-title"); var $pageTitle = document.getElementById("page-title");
var $widgets = document.getElementById("widgets");
var $search = document.getElementById("search-input"); var $search = document.getElementById("search-input");
var $sysModal = document.getElementById("systems-modal"); var $sysModal = document.getElementById("systems-modal");
var $sysBody = document.getElementById("systems-body"); var $sysBody = document.getElementById("systems-body");
var $welcome = document.getElementById("welcome-view");
var $tilesArea = document.getElementById("tiles-area");
var $wcSystems = document.getElementById("wc-systems");
var $wcMore = document.getElementById("wc-more");
var $greeting = document.getElementById("welcome-greeting");
var $welcomeRole = document.getElementById("welcome-role");
var $browseBtn = document.getElementById("welcome-browse-btn");
var _view = "dashboard"; // "dashboard" | "services"
var _cat = "all"; var _cat = "all";
var _query = ""; var _query = "";
@@ -65,7 +71,42 @@
}); });
} }
/* ── Category navigation ─────────────────────────────────────── */ /* ── Views ────────────────────────────────────────────────────── */
function setTitle(t) {
if ($pageTitle) $pageTitle.textContent = t;
}
function syncNav() {
if (!$nav) return;
$nav.querySelectorAll(".nav-item").forEach(function (b) {
var isActive;
if (b.dataset.cat === "__dash") isActive = (_view === "dashboard");
else isActive = (_view === "services" && b.dataset.cat === _cat);
b.classList.toggle("active", isActive);
});
}
function showDashboard() {
_view = "dashboard";
_cat = "all";
if ($welcome) $welcome.style.display = "";
if ($tilesArea) $tilesArea.style.display = "none";
setTitle("Dashboard");
syncNav();
}
function showServices(cat) {
_view = "services";
if (cat) _cat = cat;
if ($welcome) $welcome.style.display = "none";
if ($tilesArea) $tilesArea.style.display = "";
setTitle(_cat === "all" ? "All services" : catLabel(_cat));
syncNav();
applyFilter();
}
/* ── Category navigation ──────────────────────────────────────── */
function renderNav() { function renderNav() {
if (!$nav) return; if (!$nav) return;
@@ -79,7 +120,12 @@
}); });
var total = visibleServices().length; var total = visibleServices().length;
var html = '<div class="nav-label">Services</div>'; var html = '<div class="nav-label">Menu</div>';
html += '<button class="nav-item' + (_view === "dashboard" ? " active" : "") + '" data-cat="__dash" type="button">' +
icon("g-home") +
'<span class="nav-text">Dashboard</span>' +
'</button>';
html += '<div class="nav-label">Services</div>';
html += navItem("all", "All services", total); html += navItem("all", "All services", total);
order.forEach(function (cat) { order.forEach(function (cat) {
html += navItem(cat, catLabel(cat), counts[cat]); html += navItem(cat, catLabel(cat), counts[cat]);
@@ -88,20 +134,14 @@
$nav.querySelectorAll(".nav-item").forEach(function (btn) { $nav.querySelectorAll(".nav-item").forEach(function (btn) {
btn.addEventListener("click", function () { btn.addEventListener("click", function () {
_cat = btn.dataset.cat; if (btn.dataset.cat === "__dash") showDashboard();
$nav.querySelectorAll(".nav-item").forEach(function (b) { else showServices(btn.dataset.cat);
b.classList.toggle("active", b === btn);
});
if ($pageTitle) {
$pageTitle.textContent = _cat === "all" ? "Dashboard" : catLabel(_cat);
}
applyFilter();
}); });
}); });
} }
function navItem(cat, label, count) { function navItem(cat, label, count) {
return '<button class="nav-item' + (cat === _cat ? " active" : "") + '" data-cat="' + escHtml(cat) + '" type="button">' + return '<button class="nav-item' + (_view === "services" && cat === _cat ? " active" : "") + '" data-cat="' + escHtml(cat) + '" type="button">' +
icon(CAT_ICONS[cat] || "g-dots") + icon(CAT_ICONS[cat] || "g-dots") +
'<span class="nav-text">' + escHtml(label) + '</span>' + '<span class="nav-text">' + escHtml(label) + '</span>' +
'<span class="nav-count">' + count + '</span>' + '<span class="nav-count">' + count + '</span>' +
@@ -113,10 +153,11 @@
return CAT_FALLBACK_LABELS[cat] || cat; return CAT_FALLBACK_LABELS[cat] || cat;
} }
/* ── Filtering (category + search, applied together) ─────────── */ /* ── Filtering (applies in the services view) ─────────────────── */
function applyFilter() { function applyFilter() {
var area = document.getElementById("tiles-area"); if (_view !== "services") return;
var area = $tilesArea;
if (!area) return; if (!area) return;
var q = _query.trim().toLowerCase(); var q = _query.trim().toLowerCase();
area.querySelectorAll(".category-section").forEach(function (section) { area.querySelectorAll(".category-section").forEach(function (section) {
@@ -135,50 +176,84 @@
if ($search) { if ($search) {
$search.addEventListener("input", function () { $search.addEventListener("input", function () {
// Searching implies browsing services — leave the welcome view.
if (_view === "dashboard" && $search.value) showServices("all");
_query = $search.value; _query = $search.value;
applyFilter(); applyFilter();
}); });
} }
/* ── Status widgets ──────────────────────────────────────────── */ if ($browseBtn) {
$browseBtn.addEventListener("click", function () { showServices("all"); });
}
function renderWidgets() { /* ── Welcome header ───────────────────────────────────────────── */
if (!$widgets) return;
function updateWelcomeMeta() {
if ($greeting) {
var h = new Date().getHours();
var g;
if (h >= 5 && h < 12) g = "Good morning";
else if (h >= 12 && h < 17) g = "Good afternoon";
else g = "Good evening";
$greeting.textContent = g;
}
if ($welcomeRole) {
$welcomeRole.textContent = (typeof window._roleLabel !== "undefined" && window._roleLabel) ? window._roleLabel : "";
}
}
/* ── Status cards ─────────────────────────────────────────────── */
function serviceCounts() {
var services = visibleServices(); var services = visibleServices();
if (!services.length) { $widgets.style.display = "none"; return; }
var running = 0, attention = 0, off = 0; var running = 0, attention = 0, off = 0;
var attentionNames = [], offNames = [];
services.forEach(function (s) { services.forEach(function (s) {
if (!s.enabled) { off++; return; } if (!s.enabled) { off++; offNames.push(s.name); return; }
var h = s.health || s.status; var h = s.health || s.status;
if (h === "needs_attention" || h === "failed") attention++; if (h === "needs_attention" || h === "failed") { attention++; attentionNames.push(s.name); }
else running++; else running++;
}); });
return { services: services, running: running, attention: attention, off: off, attentionNames: attentionNames, offNames: offNames };
}
var html = ""; function renderWidgets() {
if (!$wcSystems) return;
var c = serviceCounts();
if (!c.services.length) { $wcSystems.innerHTML = ""; if ($wcMore) $wcMore.innerHTML = ""; return; }
/* Systems operational */ /* Systems operational */
var sub = '<b>' + running + '</b> running'; var sub = '<b>' + c.running + '</b> running';
if (attention) sub += ' · <span class="warn">' + attention + ' needs attention</span>'; if (c.attention) sub += ' · <span class="warn">' + c.attention + ' needs attention</span>';
if (off) sub += ' · ' + off + ' off'; if (c.off) sub += ' · ' + c.off + ' off';
var attentionTitle = attention ? "Systems need attention" : "Systems operational"; var attentionTitle = c.attention ? "Systems need attention" : "Systems operational";
html += $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-shield-check") + '</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>';
var wSys = document.getElementById("w-systems");
if (wSys) {
wSys.addEventListener("click", openSystemsModal);
wSys.addEventListener("keydown", function (e) { if (e.key === "Enter") openSystemsModal(); });
}
if (!$wcMore) return;
var more = "";
/* Bitcoin Core sync */ /* Bitcoin Core sync */
var btc = null; var btc = null;
services.forEach(function (s) { c.services.forEach(function (s) {
if (s.sync_ibd && s.enabled) btc = s; if (s.sync_ibd && s.enabled) btc = s;
}); });
if (btc) { if (btc) {
var pct = Math.round((btc.sync_progress || 0) * 100); var pct = Math.round((btc.sync_progress || 0) * 100);
var blocks = btc.sync_blocks ? btc.sync_blocks.toLocaleString() : "—"; var blocks = btc.sync_blocks ? btc.sync_blocks.toLocaleString() : "—";
var eta = (typeof _calcBtcEta === "function") ? _calcBtcEta(btc.unit + "::" + btc.name, btc.sync_progress || 0) : ""; var eta = (typeof _calcBtcEta === "function") ? _calcBtcEta(btc.unit + "::" + btc.name, btc.sync_progress || 0) : "";
html += more +=
'<div class="widget clickable" id="w-btc" role="button" tabindex="0" title="' + escHtml(btc.name) + ' details">' + '<div class="widget clickable" id="w-btc" role="button" tabindex="0" title="' + escHtml(btc.name) + ' details">' +
'<div class="widget-chip chip-btc"><img src="/static/icons/' + escHtml(btc.icon) + '.svg" alt="" style="width:46px;height:46px;display:block;object-fit:contain"/></div>' + '<div class="widget-chip chip-btc"><img src="/static/icons/' + escHtml(btc.icon) + '.svg" alt="" style="width:46px;height:46px;display:block;object-fit:contain"/></div>' +
'<div class="w-body"><h3>' + escHtml(btc.name) + ' — syncing timechain</h3>' + '<div class="w-body"><h3>' + escHtml(btc.name) + ' — syncing timechain</h3>' +
@@ -186,33 +261,49 @@
'<div class="sub">Block <b>' + blocks + '</b> · ' + pct + '% · <span class="warn">' + escHtml(eta) + '</span></div></div>' + '<div class="sub">Block <b>' + blocks + '</b> · ' + pct + '% · <span class="warn">' + escHtml(eta) + '</span></div></div>' +
'<span class="w-chev">' + icon("g-chev") + '</span>' + '<span class="w-chev">' + icon("g-chev") + '</span>' +
'</div>'; '</div>';
} else {
var btcDone = null;
c.services.forEach(function (s) { if (s.unit === "bitcoind.service" && s.enabled) btcDone = s; });
if (btcDone) {
var blk = btcDone.sync_blocks ? btcDone.sync_blocks.toLocaleString() : "";
more +=
'<div class="widget clickable" id="w-btc" role="button" tabindex="0" title="' + escHtml(btcDone.name) + ' details">' +
'<div class="widget-chip chip-btc"><img src="/static/icons/' + escHtml(btcDone.icon) + '.svg" alt="" style="width:46px;height:46px;display:block;object-fit:contain"/></div>' +
'<div class="w-body"><h3>' + escHtml(btcDone.name) + '</h3><div class="sub"><span class="good">Fully synced</span>' + (blk ? ' · Block <b>' + blk + '</b>' : '') + '</div></div>' +
'<span class="w-chev">' + icon("g-chev") + '</span>' +
'</div>';
}
} }
$widgets.innerHTML = html; /* Updates */
$widgets.style.display = ""; var upd = (typeof window._lastUpdateCheck === "object" && window._lastUpdateCheck) ? window._lastUpdateCheck : null;
var hasUpdates = !!(upd && upd.available);
more +=
'<div class="widget clickable" id="w-updates" role="button" tabindex="0" title="Check for system updates">' +
'<div class="widget-chip ' + (hasUpdates ? "chip-amber" : "chip-green") + '">' + icon("g-update") + '</div>' +
'<div class="w-body"><h3>' + (hasUpdates ? "Updates available" : "System is up to date") + '</h3>' +
'<div class="sub">' + (hasUpdates ? 'Click to review and update' : 'Sovran_SystemsOS keeps itself current') + '</div></div>' +
'<span class="w-chev">' + icon("g-chev") + '</span>' +
'</div>';
$wcMore.innerHTML = more;
var wSys = document.getElementById("w-systems");
if (wSys) {
wSys.addEventListener("click", openSystemsModal);
wSys.addEventListener("keydown", function (e) { if (e.key === "Enter") openSystemsModal(); });
}
var wBtc = document.getElementById("w-btc"); var wBtc = document.getElementById("w-btc");
if (wBtc && btc) { if (wBtc) {
(function (svc) { var svc = btc || btcDone;
if (svc) {
wBtc.addEventListener("click", function () { openServiceDetailModal(svc.unit, svc.name, svc.icon); }); wBtc.addEventListener("click", function () { openServiceDetailModal(svc.unit, svc.name, svc.icon); });
wBtc.addEventListener("keydown", function (e) { if (e.key === "Enter") openServiceDetailModal(svc.unit, svc.name, svc.icon); }); wBtc.addEventListener("keydown", function (e) { if (e.key === "Enter") openServiceDetailModal(svc.unit, svc.name, svc.icon); });
})(btc); }
}
var wUpd = document.getElementById("w-updates");
if (wUpd) {
wUpd.addEventListener("click", function () { openUpdateModal(); });
wUpd.addEventListener("keydown", function (e) { if (e.key === "Enter") openUpdateModal(); });
} }
} }
/* ── Systems Operational modal ───────────────────────────────── */ /* ── Systems Operational modal ───────────────────────────────── */
function step(n, title, sub, value) {
return '<div class="sysstep"><div class="sysnum">' + n + '</div><div class="sysstep-x">' +
'<div class="sysstep-t">' + title + (sub ? ' <span class="sysstep-sub">· ' + sub + '</span>' : '') + '</div>' +
(value ? '<div class="sysval"><span class="sysval-text">' + value + '</span></div>' : '') +
'</div></div>';
}
function isNodeRole() { function isNodeRole() {
return (typeof _currentRole !== "undefined" && _currentRole === "node"); return (typeof _currentRole !== "undefined" && _currentRole === "node");
@@ -231,33 +322,32 @@
return 'All your domain services share ports 80 and 443 — Matrix, BTCPay Server, VaultWarden, Nextcloud, WordPress, Haven Relay, Lightning Wallet Connections, and Element Calling.'; return 'All your domain services share ports 80 and 443 — Matrix, BTCPay Server, VaultWarden, Nextcloud, WordPress, Haven Relay, Lightning Wallet Connections, and Element Calling.';
} }
function step(n, title, sub, value) {
return '<div class="sysstep"><div class="sysnum">' + n + '</div><div class="sysstep-x">' +
'<div class="sysstep-t">' + title + (sub ? ' <span class="sysstep-sub">· ' + sub + '</span>' : '') + '</div>' +
(value ? '<div class="sysval"><span class="sysval-text">' + value + '</span></div>' : '') +
'</div></div>';
}
function openSystemsModal() { function openSystemsModal() {
if (!$sysModal || !$sysBody) return; if (!$sysModal || !$sysBody) return;
var services = visibleServices(); var c = serviceCounts();
var running = 0, attention = 0, off = 0;
var attentionNames = [], offNames = [];
services.forEach(function (s) {
if (!s.enabled) { off++; offNames.push(s.name); return; }
var h = s.health || s.status;
if (h === "needs_attention" || h === "failed") { attention++; attentionNames.push(s.name); }
else running++;
});
var html = ""; var html = "";
/* 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-shield-check") + 'System Status</div>' +
step(1, "Services running", "", String(running)) + step(1, "Services running", "", String(c.running)) +
step(2, "Needs attention", "", attention ? escHtml(attentionNames.join(", ")) : "None") + step(2, "Needs attention", "", c.attention ? escHtml(c.attentionNames.join(", ")) : "None") +
step(3, "Turned off", "", off ? escHtml(offNames.join(", ")) : "None") + step(3, "Turned off", "", c.off ? escHtml(c.offNames.join(", ")) : "None") +
'</div>'; '</div>';
/* Router — a simple open / not-open verdict. How to open the ports is /* Router — a simple open / not-open verdict. How to open the ports is
covered during onboarding, so the modal does not repeat instructions. covered during onboarding, so the modal does not repeat instructions.
Node-only role: ports only matter once BTCPay Server or Lightning Node-only role: ports only matter once BTCPay Server or Lightning
Wallet Connections (LNURL) is turned on. */ Wallet Connections (LNURL) is turned on. */
if (isNodeRole() && !hasEnabledDomainService(services)) { if (isNodeRole() && !hasEnabledDomainService(c.services)) {
html += '<div class="sysmodal-card">' + html += '<div class="sysmodal-card">' +
'<div class="sysmodal-card-title">' + icon("g-wifi") + 'Router</div>' + '<div class="sysmodal-card-title">' + icon("g-wifi") + 'Router</div>' +
'<div class="sysnote"><div class="sysnote-title">' + icon("g-check") + 'No router setup needed yet</div>' + '<div class="sysnote"><div class="sysnote-title">' + icon("g-check") + 'No router setup needed yet</div>' +
@@ -272,7 +362,7 @@
/* Who uses these ports (redundant on a Node install with no domain /* Who uses these ports (redundant on a Node install with no domain
services on — the router note above already covers it) */ services on — the router note above already covers it) */
if (!isNodeRole() || hasEnabledDomainService(services)) { if (!isNodeRole() || hasEnabledDomainService(c.services)) {
html += '<div class="sysnote" style="margin-top:14px">' + html += '<div class="sysnote" style="margin-top:14px">' +
'<div class="sysnote-title">' + icon("g-antenna") + 'Who uses these ports</div>' + '<div class="sysnote-title">' + icon("g-antenna") + 'Who uses these ports</div>' +
'<div class="sysnote-desc">' + whoUsesPorts() + '</div></div>'; '<div class="sysnote-desc">' + whoUsesPorts() + '</div></div>';
@@ -286,7 +376,7 @@
var portsCard = document.getElementById("sys-ports-card"); var portsCard = document.getElementById("sys-ports-card");
var portsEl = document.getElementById("sys-ports-status"); var portsEl = document.getElementById("sys-ports-status");
var units = DOMAIN_UNITS.filter(function (u) { var units = DOMAIN_UNITS.filter(function (u) {
return services.some(function (s) { return s.unit === u && s.enabled; }); return c.services.some(function (s) { return s.unit === u && s.enabled; });
}); });
if (!units.length || !portsCard || !portsEl) return; if (!units.length || !portsCard || !portsEl) return;
portsCard.style.display = ""; portsCard.style.display = "";
@@ -329,8 +419,13 @@
window.dashboardServicesUpdated = function () { window.dashboardServicesUpdated = function () {
renderNav(); renderNav();
updateWelcomeMeta();
renderWidgets(); renderWidgets();
applyFilter(); applyFilter();
}; };
// Initial view
showDashboard();
updateWelcomeMeta();
})(); })();
@@ -242,6 +242,7 @@ async function init() {
} }
var badge = document.getElementById("role-badge"); var badge = document.getElementById("role-badge");
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 || "";
await refreshServices(); await refreshServices();
loadNetwork(); loadNetwork();
@@ -628,16 +628,16 @@ async function loadAutolaunchToggle() {
} }
function renderAutolaunchToggle(enabled) { function renderAutolaunchToggle(enabled) {
// Remove existing section if any // The preference lives in the Systems Operational modal (moved from the sidebar)
var old = $sidebarFeatures.querySelector(".autolaunch-section"); var slot = document.getElementById("autolaunch-slot");
if (!slot) return;
var old = slot.querySelector(".autolaunch-section");
if (old) old.parentNode.removeChild(old); if (old) old.parentNode.removeChild(old);
var section = document.createElement("div"); var section = document.createElement("div");
section.className = "category-section autolaunch-section"; section.className = "category-section autolaunch-section";
section.innerHTML = section.innerHTML =
'<div class="section-header">Preferences</div>' +
'<hr class="section-divider" />' +
'<div class="feature-card">' + '<div class="feature-card">' +
'<div class="feature-card-top">' + '<div class="feature-card-top">' +
'<div class="feature-card-info">' + '<div class="feature-card-info">' +
@@ -651,7 +651,7 @@ function renderAutolaunchToggle(enabled) {
'</div>' + '</div>' +
'</div>'; '</div>';
$sidebarFeatures.appendChild(section); slot.appendChild(section);
var input = document.getElementById("autolaunch-toggle-input"); var input = document.getElementById("autolaunch-toggle-input");
var label = document.getElementById("autolaunch-toggle-label"); var label = document.getElementById("autolaunch-toggle-label");
@@ -273,6 +273,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");
window._lastUpdateCheck = data;
if (typeof markUpdateChecked === "function") markUpdateChecked(); if (typeof markUpdateChecked === "function") markUpdateChecked();
var hasUpdates = !!data.available; var hasUpdates = !!data.available;
var updateStatus = data.status || "idle"; var updateStatus = data.status || "idle";
+38 -18
View File
@@ -58,6 +58,13 @@
</g> </g>
</symbol> </symbol>
<!-- nav + ui symbolic icons --> <!-- nav + ui symbolic icons -->
<symbol id="g-home" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 11.2L12 4.5l8 6.7"/>
<path d="M6 10.5v8a1.2 1.2 0 0 0 1.2 1.2h9.6a1.2 1.2 0 0 0 1.2-1.2v-8"/>
<path d="M10 19.7v-5.4h4v5.4"/>
</g>
</symbol>
<symbol id="g-grid" viewBox="0 0 24 24"> <symbol id="g-grid" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"> <g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round">
<rect x="3.5" y="3.5" width="7.2" height="7.2" rx="2"/><rect x="13.3" y="3.5" width="7.2" height="7.2" rx="2"/> <rect x="3.5" y="3.5" width="7.2" height="7.2" rx="2"/><rect x="13.3" y="3.5" width="7.2" height="7.2" rx="2"/>
@@ -179,14 +186,6 @@
<div id="sidebar-features"></div> <div id="sidebar-features"></div>
<div class="sidebar-spacer"></div> <div class="sidebar-spacer"></div>
<div class="sidebar-footer">
<span class="role-badge" id="role-badge-wrap" title="Sovran_SystemsOS role">
<svg width="13" height="13"><use href="#g-shield-check"/></svg>
<span id="role-badge">Loading…</span>
</span>
<div class="host-note">sovransystemsos.local</div>
</div>
</aside> </aside>
<!-- ═══ MAIN ═══ --> <!-- ═══ MAIN ═══ -->
@@ -199,13 +198,6 @@
<input id="search-input" type="text" placeholder="Search services…" autocomplete="off" /> <input id="search-input" type="text" placeholder="Search services…" autocomplete="off" />
</div> </div>
<div class="net-chip" title="Internal IP · External IP">
<svg><use href="#g-wifi"/></svg>
<span class="int-wrap"><span class="ip-label">LAN</span><span class="ip-value" id="ip-internal"></span></span>
<span class="net-separator">|</span>
<span class="ext-wrap"><span class="ip-label">WAN</span><span class="ip-value" id="ip-external"></span></span>
</div>
<div class="top-actions"> <div class="top-actions">
<button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button> <button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button>
<button class="btn btn-logout" id="btn-logout" title="Sign out">Sign Out</button> <button class="btn btn-logout" id="btn-logout" title="Sign out">Sign Out</button>
@@ -213,10 +205,36 @@
</header> </header>
<main class="content" id="content"> <main class="content" id="content">
<!-- Widgets row (dashboard.js) --> <!-- ═══ WELCOME DASHBOARD (default view) ═══ -->
<section class="widgets" id="widgets" style="display:none"></section> <section class="welcome" id="welcome-view">
<div class="welcome-bg" aria-hidden="true">
<div class="orb orb-a"></div>
<div class="orb orb-b"></div>
<div class="orb orb-c"></div>
</div>
<div class="welcome-inner">
<div class="welcome-greeting" id="welcome-greeting">Hello</div>
<h1 class="welcome-title">Welcome to Your <em>Sovereign</em> Digital &amp; Financial Life</h1>
<div class="welcome-meta">Sovran_SystemsOS v{{ sovran_version }}<span class="welcome-meta-sep">·</span><span id="welcome-role"></span></div>
<div class="welcome-cards" id="welcome-cards">
<div class="welcome-dyn" id="wc-systems"></div>
<div class="widget w-network" id="w-network">
<div class="widget-chip chip-green"><svg><use href="#g-wifi"/></svg></div>
<div class="w-body">
<h3>Network</h3>
<div class="net-row"><span class="net-k">LAN</span><span class="ip-value" id="ip-internal"></span></div>
<div class="net-row"><span class="net-k">WAN</span><span class="ip-value" id="ip-external"></span></div>
<div class="sub">sovransystemsos.local</div>
</div>
</div>
<div class="welcome-dyn" id="wc-more"></div>
</div>
<button class="btn btn-ghost welcome-browse" id="welcome-browse-btn">Browse all services →</button>
</div>
</section>
<div id="tiles-area"> <!-- ═══ SERVICES GRID ═══ -->
<div id="tiles-area" style="display:none">
<div class="dashboard-loading" role="status" aria-live="polite"> <div class="dashboard-loading" role="status" aria-live="polite">
<span class="dashboard-loading-spinner" aria-hidden="true"></span> <span class="dashboard-loading-spinner" aria-hidden="true"></span>
<span>Loading service status…</span> <span>Loading service status…</span>
@@ -240,6 +258,8 @@
<div class="creds-body" id="systems-body"> <div class="creds-body" id="systems-body">
<p class="creds-loading">Loading…</p> <p class="creds-loading">Loading…</p>
</div> </div>
<!-- Auto-launch preference (features.js) — moved here from the sidebar -->
<div class="sysprefs" id="autolaunch-slot"></div>
</div> </div>
</div> </div>