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:
@@ -56,49 +56,6 @@
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
@@ -106,17 +63,6 @@
|
||||
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) ──────────────────── */
|
||||
|
||||
.role-badge {
|
||||
|
||||
@@ -204,30 +204,131 @@
|
||||
|
||||
.sidebar-spacer { flex: 1; min-height: 18px; }
|
||||
|
||||
.sidebar-footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 14px;
|
||||
/* ── Welcome dashboard (default view) ───────────────────────────── */
|
||||
|
||||
.welcome {
|
||||
position: relative;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
padding: 44px 8px 24px;
|
||||
overflow: hidden;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.host-note {
|
||||
text-align: center;
|
||||
font-size: 0.66rem;
|
||||
/* Soft drifting glow — calm, GPU-cheap (transforms only) */
|
||||
.welcome-bg {
|
||||
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);
|
||||
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;
|
||||
grid-template-columns: 1.1fr 1.45fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.widgets { grid-template-columns: 1fr; }
|
||||
/* wrappers whose children become grid items */
|
||||
.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 {
|
||||
@@ -275,6 +376,10 @@
|
||||
background: linear-gradient(160deg, #f7931a, #b96a0a);
|
||||
}
|
||||
|
||||
.widget-chip.chip-amber {
|
||||
background: linear-gradient(160deg, #b98426, #8a5f16);
|
||||
}
|
||||
|
||||
.widget-chip.chip-sovran {
|
||||
background: linear-gradient(160deg, #42f39a, #1aa45d);
|
||||
}
|
||||
|
||||
@@ -1505,6 +1505,15 @@
|
||||
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 {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-3);
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
/* ── The Hub dashboard chrome ──────────────────────────────────────
|
||||
Category navigation, service search, status widgets, and the
|
||||
Systems Operational modal (router ports 80/443 + the live domain
|
||||
diagnostics checklist).
|
||||
Welcome dashboard (default view), category navigation, service
|
||||
search, status cards, and the Systems Operational modal.
|
||||
|
||||
Everything lives in an IIFE so no globals leak into the other
|
||||
Hub scripts; tiles.js calls window.dashboardServicesUpdated()
|
||||
whenever the service list refreshes. */
|
||||
Everything lives in an IIFE so no globals leak into the other Hub
|
||||
scripts; tiles.js calls window.dashboardServicesUpdated() whenever
|
||||
the service list refreshes. */
|
||||
|
||||
(function () {
|
||||
|
||||
var $nav = document.getElementById("sidebar-nav");
|
||||
var $pageTitle = document.getElementById("page-title");
|
||||
var $widgets = document.getElementById("widgets");
|
||||
var $search = document.getElementById("search-input");
|
||||
var $sysModal = document.getElementById("systems-modal");
|
||||
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 _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() {
|
||||
if (!$nav) return;
|
||||
@@ -79,7 +120,12 @@
|
||||
});
|
||||
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);
|
||||
order.forEach(function (cat) {
|
||||
html += navItem(cat, catLabel(cat), counts[cat]);
|
||||
@@ -88,20 +134,14 @@
|
||||
|
||||
$nav.querySelectorAll(".nav-item").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
_cat = btn.dataset.cat;
|
||||
$nav.querySelectorAll(".nav-item").forEach(function (b) {
|
||||
b.classList.toggle("active", b === btn);
|
||||
});
|
||||
if ($pageTitle) {
|
||||
$pageTitle.textContent = _cat === "all" ? "Dashboard" : catLabel(_cat);
|
||||
}
|
||||
applyFilter();
|
||||
if (btn.dataset.cat === "__dash") showDashboard();
|
||||
else showServices(btn.dataset.cat);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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") +
|
||||
'<span class="nav-text">' + escHtml(label) + '</span>' +
|
||||
'<span class="nav-count">' + count + '</span>' +
|
||||
@@ -113,10 +153,11 @@
|
||||
return CAT_FALLBACK_LABELS[cat] || cat;
|
||||
}
|
||||
|
||||
/* ── Filtering (category + search, applied together) ─────────── */
|
||||
/* ── Filtering (applies in the services view) ─────────────────── */
|
||||
|
||||
function applyFilter() {
|
||||
var area = document.getElementById("tiles-area");
|
||||
if (_view !== "services") return;
|
||||
var area = $tilesArea;
|
||||
if (!area) return;
|
||||
var q = _query.trim().toLowerCase();
|
||||
area.querySelectorAll(".category-section").forEach(function (section) {
|
||||
@@ -135,50 +176,84 @@
|
||||
|
||||
if ($search) {
|
||||
$search.addEventListener("input", function () {
|
||||
// Searching implies browsing services — leave the welcome view.
|
||||
if (_view === "dashboard" && $search.value) showServices("all");
|
||||
_query = $search.value;
|
||||
applyFilter();
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Status widgets ──────────────────────────────────────────── */
|
||||
if ($browseBtn) {
|
||||
$browseBtn.addEventListener("click", function () { showServices("all"); });
|
||||
}
|
||||
|
||||
function renderWidgets() {
|
||||
if (!$widgets) return;
|
||||
/* ── Welcome header ───────────────────────────────────────────── */
|
||||
|
||||
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();
|
||||
if (!services.length) { $widgets.style.display = "none"; return; }
|
||||
|
||||
var running = 0, attention = 0, off = 0;
|
||||
var attentionNames = [], offNames = [];
|
||||
services.forEach(function (s) {
|
||||
if (!s.enabled) { off++; return; }
|
||||
if (!s.enabled) { off++; offNames.push(s.name); return; }
|
||||
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++;
|
||||
});
|
||||
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 */
|
||||
var sub = '<b>' + running + '</b> running';
|
||||
if (attention) sub += ' · <span class="warn">' + attention + ' needs attention</span>';
|
||||
if (off) sub += ' · ' + off + ' off';
|
||||
var attentionTitle = attention ? "Systems need attention" : "Systems operational";
|
||||
html +=
|
||||
var sub = '<b>' + c.running + '</b> running';
|
||||
if (c.attention) sub += ' · <span class="warn">' + c.attention + ' needs attention</span>';
|
||||
if (c.off) sub += ' · ' + c.off + ' off';
|
||||
var attentionTitle = c.attention ? "Systems need attention" : "Systems operational";
|
||||
$wcSystems.innerHTML =
|
||||
'<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="w-body"><h3>' + attentionTitle + '</h3><div class="sub">' + sub + '</div></div>' +
|
||||
'<span class="w-chev">' + icon("g-chev") + '</span>' +
|
||||
'</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 */
|
||||
var btc = null;
|
||||
services.forEach(function (s) {
|
||||
c.services.forEach(function (s) {
|
||||
if (s.sync_ibd && s.enabled) btc = s;
|
||||
});
|
||||
if (btc) {
|
||||
var pct = Math.round((btc.sync_progress || 0) * 100);
|
||||
var blocks = btc.sync_blocks ? btc.sync_blocks.toLocaleString() : "—";
|
||||
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-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>' +
|
||||
@@ -186,33 +261,49 @@
|
||||
'<div class="sub">Block <b>' + blocks + '</b> · ' + pct + '% · <span class="warn">' + escHtml(eta) + '</span></div></div>' +
|
||||
'<span class="w-chev">' + icon("g-chev") + '</span>' +
|
||||
'</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;
|
||||
$widgets.style.display = "";
|
||||
/* Updates */
|
||||
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");
|
||||
if (wBtc && btc) {
|
||||
(function (svc) {
|
||||
if (wBtc) {
|
||||
var svc = btc || btcDone;
|
||||
if (svc) {
|
||||
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); });
|
||||
})(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 ───────────────────────────────── */
|
||||
|
||||
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>';
|
||||
}
|
||||
/* ── Systems Operational modal ────────────────────────────────── */
|
||||
|
||||
function isNodeRole() {
|
||||
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.';
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!$sysModal || !$sysBody) return;
|
||||
var services = visibleServices();
|
||||
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 c = serviceCounts();
|
||||
|
||||
var html = "";
|
||||
|
||||
/* System status */
|
||||
html += '<div class="sysmodal-card">' +
|
||||
'<div class="sysmodal-card-title">' + icon("g-shield-check") + 'System Status</div>' +
|
||||
step(1, "Services running", "", String(running)) +
|
||||
step(2, "Needs attention", "", attention ? escHtml(attentionNames.join(", ")) : "None") +
|
||||
step(3, "Turned off", "", off ? escHtml(offNames.join(", ")) : "None") +
|
||||
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") +
|
||||
'</div>';
|
||||
|
||||
/* Router — a simple open / not-open verdict. How to open the ports is
|
||||
covered during onboarding, so the modal does not repeat instructions.
|
||||
Node-only role: ports only matter once BTCPay Server or Lightning
|
||||
Wallet Connections (LNURL) is turned on. */
|
||||
if (isNodeRole() && !hasEnabledDomainService(services)) {
|
||||
if (isNodeRole() && !hasEnabledDomainService(c.services)) {
|
||||
html += '<div class="sysmodal-card">' +
|
||||
'<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>' +
|
||||
@@ -272,7 +362,7 @@
|
||||
|
||||
/* Who uses these ports (redundant on a Node install with no domain
|
||||
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">' +
|
||||
'<div class="sysnote-title">' + icon("g-antenna") + 'Who uses these ports</div>' +
|
||||
'<div class="sysnote-desc">' + whoUsesPorts() + '</div></div>';
|
||||
@@ -286,7 +376,7 @@
|
||||
var portsCard = document.getElementById("sys-ports-card");
|
||||
var portsEl = document.getElementById("sys-ports-status");
|
||||
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;
|
||||
portsCard.style.display = "";
|
||||
@@ -329,8 +419,13 @@
|
||||
|
||||
window.dashboardServicesUpdated = function () {
|
||||
renderNav();
|
||||
updateWelcomeMeta();
|
||||
renderWidgets();
|
||||
applyFilter();
|
||||
};
|
||||
|
||||
// Initial view
|
||||
showDashboard();
|
||||
updateWelcomeMeta();
|
||||
|
||||
})();
|
||||
|
||||
@@ -242,6 +242,7 @@ async function init() {
|
||||
}
|
||||
var badge = document.getElementById("role-badge");
|
||||
if (badge && cfg.role_label) badge.textContent = cfg.role_label;
|
||||
window._roleLabel = cfg.role_label || "";
|
||||
|
||||
await refreshServices();
|
||||
loadNetwork();
|
||||
|
||||
@@ -628,16 +628,16 @@ async function loadAutolaunchToggle() {
|
||||
}
|
||||
|
||||
function renderAutolaunchToggle(enabled) {
|
||||
// Remove existing section if any
|
||||
var old = $sidebarFeatures.querySelector(".autolaunch-section");
|
||||
// The preference lives in the Systems Operational modal (moved from the sidebar)
|
||||
var slot = document.getElementById("autolaunch-slot");
|
||||
if (!slot) return;
|
||||
var old = slot.querySelector(".autolaunch-section");
|
||||
if (old) old.parentNode.removeChild(old);
|
||||
|
||||
var section = document.createElement("div");
|
||||
section.className = "category-section autolaunch-section";
|
||||
|
||||
section.innerHTML =
|
||||
'<div class="section-header">Preferences</div>' +
|
||||
'<hr class="section-divider" />' +
|
||||
'<div class="feature-card">' +
|
||||
'<div class="feature-card-top">' +
|
||||
'<div class="feature-card-info">' +
|
||||
@@ -651,7 +651,7 @@ function renderAutolaunchToggle(enabled) {
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
$sidebarFeatures.appendChild(section);
|
||||
slot.appendChild(section);
|
||||
|
||||
var input = document.getElementById("autolaunch-toggle-input");
|
||||
var label = document.getElementById("autolaunch-toggle-label");
|
||||
|
||||
@@ -273,6 +273,7 @@ async function loadNetwork() {
|
||||
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";
|
||||
|
||||
@@ -58,6 +58,13 @@
|
||||
</g>
|
||||
</symbol>
|
||||
<!-- 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">
|
||||
<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"/>
|
||||
@@ -179,14 +186,6 @@
|
||||
<div id="sidebar-features"></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>
|
||||
|
||||
<!-- ═══ MAIN ═══ -->
|
||||
@@ -199,13 +198,6 @@
|
||||
<input id="search-input" type="text" placeholder="Search services…" autocomplete="off" />
|
||||
</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">
|
||||
<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>
|
||||
@@ -213,10 +205,36 @@
|
||||
</header>
|
||||
|
||||
<main class="content" id="content">
|
||||
<!-- Widgets row (dashboard.js) -->
|
||||
<section class="widgets" id="widgets" style="display:none"></section>
|
||||
<!-- ═══ WELCOME DASHBOARD (default view) ═══ -->
|
||||
<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 & 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">
|
||||
<span class="dashboard-loading-spinner" aria-hidden="true"></span>
|
||||
<span>Loading service status…</span>
|
||||
@@ -240,6 +258,8 @@
|
||||
<div class="creds-body" id="systems-body">
|
||||
<p class="creds-loading">Loading…</p>
|
||||
</div>
|
||||
<!-- Auto-launch preference (features.js) — moved here from the sidebar -->
|
||||
<div class="sysprefs" id="autolaunch-slot"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user