Polish pass: sysops wording, QR size, NWC toolbar, logo, diagnostics placement

Eight fixes from live testing:

Systems Operational modal:
- Drop the "test from your phone on mobile data" (hairpin NAT) sentence
  from the router note — too technical for the intended audience.
- Node-only role: when BTCPay Server and Lightning Wallet Connections
  (LNURL) are both off, the router card becomes a simple "No router
  setup needed yet" note explaining that ports 80/443 only matter if
  one of those services is turned on. When one is enabled, the card
  shows the same port steps and live domain/port diagnostics as the
  Desktop + Server role (diagnostics now poll only enabled services).

Service modals:
- The Domain Diagnostic Checklist now shows only the domain-active
  step (Domain Configured). DNS and port diagnostics live in Systems
  Operational, which shows the full checklist.
- Node-only role: BTCPay Server and Lightning Wallet Connections
  modals gain a "Ports to Forward in Your Router" section with the
  standard 80/443 wording and this computer's LAN address.
- Domain setup and reconfigure dialogs no longer contain router
  port-forwarding instructions (already handled during Desktop +
  Server onboarding and shown in Systems Operational).

Lightning Wallet Connections:
- Refresh and New Wallet toolbar buttons now share one height and
  baseline (a leftover 12px top margin on Refresh was offsetting it).
- The header status chip has a proper gap between the status dot and
  its label.

Zeus Connect / QR codes:
- QR codes render at the original 240px with the white frame and
  pixelated upscaling, restoring scannability.

Brand:
- The sidebar logo loads via /static/sovran-hub-icon.svg (same as the
  login page) instead of an inline <use> symbol — the gradient-heavy
  symbol did not render reliably. The icon sprite is hidden with the
  browser-safe zero-size pattern instead of display:none.
This commit is contained in:
2026-09-08 17:19:26 -05:00
parent d164d423ad
commit ea9d4f21d7
5 changed files with 79 additions and 50 deletions
+20 -6
View File
@@ -471,11 +471,13 @@
} }
.creds-qr-img { .creds-qr-img {
width: 150px; width: 240px;
height: 150px; height: 240px;
padding: 10px; border-radius: 12px;
background: #edf4ef; border: 4px solid #fff;
border-radius: 14px; background-color: #fff;
image-rendering: pixelated;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
} }
.creds-qr-hint { .creds-qr-hint {
@@ -912,6 +914,7 @@
.nwc-header-chip { .nwc-header-chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px;
font-family: var(--mono); font-family: var(--mono);
font-size: 0.66rem; font-size: 0.66rem;
font-weight: 600; font-weight: 600;
@@ -953,10 +956,22 @@
.nwc-toolbar-actions { .nwc-toolbar-actions {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
align-items: center;
gap: 9px; gap: 9px;
flex-wrap: wrap; flex-wrap: wrap;
} }
/* Refresh + New Wallet share one height so the toolbar reads as one row */
.nwc-toolbar-actions .matrix-form-back,
.nwc-toolbar-actions .matrix-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
padding: 7px 16px;
font-size: 0.8rem;
}
.nwc-wallet-list { display: flex; flex-direction: column; } .nwc-wallet-list { display: flex; flex-direction: column; }
.nwc-wallet-card { .nwc-wallet-card {
@@ -1031,7 +1046,6 @@
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.nwc-toolbar-refresh,
.nwc-domain-required-btn { margin-top: 12px; } .nwc-domain-required-btn { margin-top: 12px; }
.nwc-intro-header { margin-bottom: 14px; } .nwc-intro-header { margin-bottom: 14px; }
@@ -221,9 +221,19 @@
'</div></div>'; '</div></div>';
} }
function isNodeRole() {
return (typeof _currentRole !== "undefined" && _currentRole === "node");
}
function hasEnabledDomainService(services) {
return DOMAIN_UNITS.some(function (u) {
return services.some(function (s) { return s.unit === u && s.enabled; });
});
}
function whoUsesPorts() { function whoUsesPorts() {
if (typeof _currentRole !== "undefined" && _currentRole === "node") { if (isNodeRole()) {
return 'On this <strong>Bitcoin Node</strong> install, BTCPay Server and Lightning Wallet Connections (LNURL) are the domain services that use these ports.'; return 'On this <strong>Bitcoin Node</strong> install, <strong>BTCPay Server</strong> and <strong>Lightning Wallet Connections (LNURL)</strong> are the domain services that use these ports.';
} }
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.';
} }
@@ -253,14 +263,24 @@
step(3, "Turned off", "", off ? escHtml(offNames.join(", ")) : "None") + step(3, "Turned off", "", off ? escHtml(offNames.join(", ")) : "None") +
'</div>'; '</div>';
/* Router ports — repo wording (domain-prereqs.js / server.py) */ /* Router ports — repo wording (domain-prereqs.js / server.py).
Node-only role: ports matter only once BTCPay Server or Lightning
Wallet Connections (LNURL) is turned on — until then, no router task. */
if (isNodeRole() && !hasEnabledDomainService(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>' +
'<div class="sysnote-desc">Ports 80 and 443 only need to be forwarded on your router if you turn on <strong>BTCPay Server</strong> or <strong>Lightning Wallet Connections (LNURL)</strong>. If you enable one of them, come back here — this card will show exactly what to do.</div></div>' +
'</div>';
} else {
html += '<div class="sysmodal-card">' + html += '<div class="sysmodal-card">' +
'<div class="sysmodal-card-title">' + icon("g-wifi") + 'Router — Ports to Forward</div>' + '<div class="sysmodal-card-title">' + icon("g-wifi") + 'Router — Ports to Forward</div>' +
step(1, "Port 80", "TCP — HTTP (redirect to HTTPS)", fwd) + step(1, "Port 80", "TCP — HTTP (redirect to HTTPS)", fwd) +
step(2, "Port 443", "TCP — HTTPS", fwd) + step(2, "Port 443", "TCP — HTTPS", fwd) +
'<div class="sysnote"><div class="sysnote-title">' + icon("g-alert") + 'One router task</div>' + '<div class="sysnote"><div class="sysnote-title">' + icon("g-alert") + 'One router task</div>' +
'<div class="sysnote-desc">Set the internal and external port to the <strong>same number</strong>. You only need to do this once — all your services share these two ports. Test from your phone on mobile data (your home network may not support hairpin NAT / loopback).</div></div>' + '<div class="sysnote-desc">Set the internal and external port to the <strong>same number</strong>. You only need to do this once — all your services share these two ports.</div></div>' +
'</div>'; '</div>';
}
/* Live domain diagnostics (sequential checklist from the backend — /* Live domain diagnostics (sequential checklist from the backend —
same data the domain-service modals show) */ same data the domain-service modals show) */
@@ -269,10 +289,13 @@
'<div id="sys-check-steps"><div class="sysfineprint">Checking…</div></div>' + '<div id="sys-check-steps"><div class="sysfineprint">Checking…</div></div>' +
'</div>'; '</div>';
/* Who uses these ports */ /* 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)) {
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>';
}
$sysBody.innerHTML = html; $sysBody.innerHTML = html;
$sysModal.classList.add("open"); $sysModal.classList.add("open");
@@ -281,7 +304,7 @@
var checkCard = document.getElementById("sys-check-card"); var checkCard = document.getElementById("sys-check-card");
var stepsEl = document.getElementById("sys-check-steps"); var stepsEl = document.getElementById("sys-check-steps");
var units = DOMAIN_UNITS.filter(function (u) { var units = DOMAIN_UNITS.filter(function (u) {
return services.some(function (s) { return s.unit === u; }); return services.some(function (s) { return s.unit === u && s.enabled; });
}); });
if (!units.length || !checkCard || !stepsEl) return; if (!units.length || !checkCard || !stepsEl) return;
checkCard.style.display = ""; checkCard.style.display = "";
@@ -97,7 +97,6 @@ function openDomainSetupModal(feat, onSaved) {
nwcWarning + nwcWarning +
renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample, purpose: purpose }) + renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample, purpose: purpose }) +
renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) + renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) +
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
'<p style="margin-top:10px;">Enter the address for this service and paste the update command from Njal.la.</p>' + '<p style="margin-top:10px;">Enter the address for this service and paste the update command from Njal.la.</p>' +
'</div>' + '</div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service address (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" /></div>' + '<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service address (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" /></div>' +
@@ -144,8 +143,6 @@ function openDomainSetupModal(feat, onSaved) {
$domainSetupModal.classList.add("open"); $domainSetupModal.classList.add("open");
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
renderRouterPortsBox("domain-router-box");
} }
function openDomainReconfigureModal(feat, existingDomain, onSaved) { function openDomainReconfigureModal(feat, existingDomain, onSaved) {
@@ -193,9 +190,7 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' + '<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
'<li>If the IP is wrong or the record is missing, update it</li>' + '<li>If the IP is wrong or the record is missing, update it</li>' +
'<li>If you changed the DDNS curl command, paste the updated one below</li>' + '<li>If you changed the DDNS curl command, paste the updated one below</li>' +
'<li>Confirm ports <strong>80</strong> and <strong>443</strong> (TCP) are still forwarded on your router to this computer — see the reminder below:</li>' +
'</ol>' + '</ol>' +
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
'</div>' + '</div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" value="' + escHtml(currentDomain) + '" /></div>' + '<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" value="' + escHtml(currentDomain) + '" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=' + domainPlaceholder + '&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint"> Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' + '<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=' + domainPlaceholder + '&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint"> Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
@@ -241,8 +236,6 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
$domainSetupModal.classList.add("open"); $domainSetupModal.classList.add("open");
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
renderRouterPortsBox("domain-router-box");
} }
function closeDomainSetupModal() { function closeDomainSetupModal() {
@@ -718,7 +718,11 @@ async function openServiceDetailModal(unit, name, icon) {
// Section C: Domain diagnostics (domain services) // Section C: Domain diagnostics (domain services)
if (data.needs_domain) { if (data.needs_domain) {
var steps = data.domain_check_steps || []; // The Hub shows only the domain-active step here; the full DNS and
// port diagnostics live in the Systems Operational modal.
var steps = (data.domain_check_steps || []).filter(function (s) {
return Number(s.step) === 1;
});
var stepsHtml = ""; var stepsHtml = "";
steps.forEach(function(step) { steps.forEach(function(step) {
var iconLabel = "—"; var iconLabel = "—";
@@ -747,6 +751,17 @@ async function openServiceDetailModal(unit, name, icon) {
domainActionHtml + domainActionHtml +
'</div>'); '</div>');
// Node-only role: BTCPay Server and Lightning Wallet Connections are
// the domain services — surface the router task here. (Desktop + Server
// set this up during onboarding; Systems Operational shows it too.)
if (typeof _currentRole !== "undefined" && _currentRole === "node" &&
(unit === "btcpayserver.service" || unit === "albyhub.service")) {
addSetup('<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
'<div class="onboarding-port-warn" id="svc-node-router-box"></div>' +
'</div>');
}
if (data.router_ports && data.router_ports.length > 0) { if (data.router_ports && data.router_ports.length > 0) {
var trimmedInternalIp = data.internal_ip ? String(data.internal_ip).trim() : ""; var trimmedInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
addSetup('<div class="svc-detail-section">' + addSetup('<div class="svc-detail-section">' +
@@ -1004,6 +1019,9 @@ async function openServiceDetailModal(unit, name, icon) {
$credsBody.innerHTML = html; $credsBody.innerHTML = html;
if (isNwc) _nwcWireTabs(); if (isNwc) _nwcWireTabs();
_attachCopyHandlers($credsBody); _attachCopyHandlers($credsBody);
if (document.getElementById("svc-node-router-box")) {
renderRouterPortsBox("svc-node-router-box");
}
if (_isNwcServiceUnit(unit) && (effectiveEnabled || data.enabled)) { if (_isNwcServiceUnit(unit) && (effectiveEnabled || data.enabled)) {
await _nwcInitWalletFlow(unit, name, icon); await _nwcInitWalletFlow(unit, name, icon);
var nwcRtlBtn = document.getElementById("nwc-open-rtl-btn"); var nwcRtlBtn = document.getElementById("nwc-open-rtl-btn");
+2 -21
View File
@@ -18,26 +18,7 @@
</head> </head>
<body> <body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<!-- The Hub logo (verbatim from app/sovran_systemsos_web/static/sovran-hub-icon.svg) -->
<symbol id="g-sovran" viewBox="0 0 256 256">
<linearGradient id="sv-bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#153126"/><stop offset="55%" stop-color="#0F241B"/><stop offset="100%" stop-color="#091C14"/>
</linearGradient>
<linearGradient id="sv-outer" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#42F39A"/><stop offset="45%" stop-color="#28D978"/><stop offset="100%" stop-color="#1AA45D"/>
</linearGradient>
<linearGradient id="sv-inner" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#27C86F"/><stop offset="100%" stop-color="#157E49"/>
</linearGradient>
<rect width="256" height="256" rx="48" ry="48" fill="url(#sv-bg)"/>
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5" fill="none" stroke="rgba(255,255,255,0.08)"/>
<path d="M128 32 A96 96 0 1 1 58 196" fill="none" stroke="url(#sv-outer)" stroke-width="12" stroke-linecap="round"/>
<path d="M128 56 A72 72 0 1 1 76 178" fill="none" stroke="url(#sv-inner)" stroke-width="10" stroke-linecap="round"/>
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
</symbol>
<!-- service glyphs --> <!-- service glyphs -->
<!-- ═══ Real service logos — extracted verbatim from app/icons/*.svg ═══ <!-- ═══ Real service logos — extracted verbatim from app/icons/*.svg ═══
@@ -174,7 +155,7 @@
<!-- ═══ SIDEBAR ═══ --> <!-- ═══ SIDEBAR ═══ -->
<aside class="sidebar" id="sidebar"> <aside class="sidebar" id="sidebar">
<div class="brand"> <div class="brand">
<svg class="brand-logo"><use href="#g-sovran"/></svg> <img src="/static/sovran-hub-icon.svg" class="brand-logo" alt="The Hub" />
<div class="brand-text"> <div class="brand-text">
<div class="brand-title">The <em>Hub</em></div> <div class="brand-title">The <em>Hub</em></div>
<div class="brand-sub">Sovran_SystemsOS v{{ sovran_version }}</div> <div class="brand-sub">Sovran_SystemsOS v{{ sovran_version }}</div>