Merge pull request #352 from naturallaw777/arena/019fab62-sovran-systemsos

Rename NWC feature to Lightning Wallet Connections and split modal into tabs
This commit is contained in:
Sovran Systems
2026-07-28 21:02:21 -05:00
committed by GitHub
11 changed files with 636 additions and 101 deletions
+1 -1
View File
@@ -786,7 +786,7 @@ sudo nixos-rebuild switch --rollback
| `modules/` | Core modules, Bitcoin services, self-hosted services, and optional features | | `modules/` | Core modules, Bitcoin services, self-hosted services, and optional features |
| `modules/core/` | Roles, Hub integration, Caddy, desktop, support, and other core behavior | | `modules/core/` | Roles, Hub integration, Caddy, desktop, support, and other core behavior |
| `app/` | Sovran Hub backend, templates, static assets, scripts, and web interface | | `app/` | Sovran Hub backend, templates, static assets, scripts, and web interface |
| `docs/wallet-connections.md` | Wallet Connections architecture, API, CLI, and security/operations notes | | `docs/wallet-connections.md` | Lightning Wallet Connections architecture, API, CLI, and security/operations notes |
| `iso/` | Installer configuration, installer code, and installer assets | | `iso/` | Installer configuration, installer code, and installer assets |
| `assets/` | Repository documentation images | | `assets/` | Repository documentation images |
| `custom.template.nix` | Template for local features and service overrides | | `custom.template.nix` | Template for local features and service overrides |
+1 -1
View File
@@ -1,5 +1,5 @@
""" """
Alby Hub manager — shared backend for Wallet Connections API and recovery CLI. Alby Hub manager — shared backend for Lightning Wallet Connections API and recovery CLI.
Interfaces with the local Alby Hub instance at Interfaces with the local Alby Hub instance at
http://127.0.0.1:18080 by default (override with NWC_ALBY_HUB_API_BASE). http://127.0.0.1:18080 by default (override with NWC_ALBY_HUB_API_BASE).
@@ -1,5 +1,5 @@
""" """
Dedicated LNURL service for Wallet Connections. Dedicated LNURL service for Lightning Wallet Connections.
Runs as ``nwc-lnurl.service`` on 127.0.0.1:8181 (loopback only). Runs as ``nwc-lnurl.service`` on 127.0.0.1:8181 (loopback only).
Caddy proxies the public Lightning Address domain's LNURL routes to this port. Caddy proxies the public Lightning Address domain's LNURL routes to this port.
+4 -4
View File
@@ -227,7 +227,7 @@ FEATURE_REGISTRY = [
}, },
{ {
"id": "nwc-wallets", "id": "nwc-wallets",
"name": "Wallet Connections", "name": "Lightning Wallet Connections",
"description": "Connect apps to isolated wallets on your Lightning node and create reusable Lightning Addresses.", "description": "Connect apps to isolated wallets on your Lightning node and create reusable Lightning Addresses.",
"category": "bitcoin", "category": "bitcoin",
"needs_domain": True, "needs_domain": True,
@@ -461,7 +461,7 @@ SERVICE_DESCRIPTIONS: dict[str, str] = {
"Sovran_SystemsOS integrates Tor natively across your entire stack." "Sovran_SystemsOS integrates Tor natively across your entire stack."
), ),
"albyhub.service": ( "albyhub.service": (
"Create isolated Wallet Connections for Lightning apps and attach reusable Lightning " "Create isolated Lightning wallets for your apps and attach reusable Lightning "
"Addresses on your Sovran_SystemsOS node." "Addresses on your Sovran_SystemsOS node."
), ),
"gnome-remote-desktop.service": ( "gnome-remote-desktop.service": (
@@ -4323,7 +4323,7 @@ async def api_domains_set(req: DomainSetRequest):
"error": "domain_conflict", "error": "domain_conflict",
"conflicting_domain_key": conflicting_key, "conflicting_domain_key": conflicting_key,
"message": ( "message": (
"Wallet Connections requires its own unique hostname. " "Lightning Wallet Connections requires its own unique hostname. "
"Choose a new subdomain such as lightning.yourdomain.com. " "Choose a new subdomain such as lightning.yourdomain.com. "
f"This hostname is already assigned to: {conflicting_key}." f"This hostname is already assigned to: {conflicting_key}."
), ),
@@ -4448,7 +4448,7 @@ async def api_domains_check(req: DomainCheckRequest):
return {"domains": list(check_results)} return {"domains": list(check_results)}
# ── Wallet Connections (NWC) endpoints ──────────────────────────── # ── Lightning Wallet Connections (NWC) endpoints ────────────────────────────
NWC_DOMAIN_FILE = "/var/lib/domains/lightning" NWC_DOMAIN_FILE = "/var/lib/domains/lightning"
NWC_ALIAS_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,31}$") NWC_ALIAS_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,31}$")
+415 -18
View File
@@ -223,6 +223,10 @@ button.btn-reboot:hover:not(:disabled) {
color: var(--text-primary); color: var(--text-primary);
} }
.creds-dialog--wide {
max-width: 980px;
}
.creds-body { .creds-body {
padding: 24px 28px; padding: 24px 28px;
overflow-y: auto; overflow-y: auto;
@@ -489,38 +493,325 @@ button.btn-reboot:hover:not(:disabled) {
font-size: 0.86rem; font-size: 0.86rem;
} }
.nwc-wallet-list { /* ── Lightning Wallet Connections: tabbed shell ─────────────────── */
.nwc-tabs-bar {
display: flex; display: flex;
flex-direction: column; align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin: -6px 0 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.nwc-tabs-btns {
display: flex;
gap: 4px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 4px;
}
.nwc-tab-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 0.86rem;
font-weight: 600;
padding: 8px 16px;
border-radius: 7px;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s ease, color 0.15s ease;
}
.nwc-tab-btn:hover:not(.active) {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.04);
}
.nwc-tab-btn.active {
background: var(--accent-color);
color: #0A1A10;
}
.nwc-header-chips {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.nwc-header-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-color);
border-radius: 999px;
padding: 4px 10px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nwc-header-chip--domain {
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
font-size: 0.72rem;
}
.nwc-header-chip--warn {
border-color: rgba(245, 158, 11, 0.5);
color: #fbbf24;
}
.nwc-tab-panel {
display: none;
}
.nwc-tab-panel.active {
display: block;
animation: nwc-tab-fade 0.16s ease-out;
}
@keyframes nwc-tab-fade {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Setup tab: first section shouldn't double up on top padding */
#nwc-tab-setup .svc-detail-section:first-child {
padding-top: 0;
margin-top: 0;
border-top: none;
}
.nwc-tab-intro {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.nwc-tab-intro-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.nwc-tab-intro-desc {
margin: 0;
font-size: 0.85rem;
line-height: 1.5;
color: var(--text-secondary);
max-width: 62ch;
}
/* ── Wallet toolbar ─────────────────────────────────────────────── */
.nwc-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.nwc-toolbar-count {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-dim);
}
.nwc-toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.nwc-toolbar-actions .matrix-action-btn,
.nwc-toolbar-actions .matrix-form-back {
padding: 8px 14px;
font-size: 0.82rem;
}
/* ── Domain-required callout ────────────────────────────────────── */
.nwc-domain-required {
border: 1px solid rgba(245, 158, 11, 0.4);
background: rgba(245, 158, 11, 0.08);
border-radius: 10px;
padding: 14px 16px;
margin-bottom: 16px;
font-size: 0.86rem;
color: var(--text-primary);
}
.nwc-domain-required-body {
margin-top: 6px;
color: var(--text-secondary);
line-height: 1.5;
}
.nwc-domain-required-btn {
margin-top: 12px;
}
/* ── Empty / disabled states ────────────────────────────────────── */
.nwc-empty-state,
.nwc-disabled-state {
text-align: center;
padding: 36px 24px;
border: 1px dashed var(--border-color);
border-radius: 12px;
background: rgba(255, 255, 255, 0.015);
}
.nwc-empty-icon,
.nwc-disabled-icon {
font-size: 2rem;
margin-bottom: 10px;
}
.nwc-empty-title,
.nwc-disabled-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 6px;
}
.nwc-empty-desc,
.nwc-disabled-desc {
margin: 0 auto 14px;
max-width: 46ch;
font-size: 0.86rem;
line-height: 1.55;
color: var(--text-secondary);
}
/* ── Wallet cards ───────────────────────────────────────────────── */
.nwc-wallet-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 12px; gap: 12px;
margin-top: 12px; margin-top: 12px;
} }
.nwc-wallet-card { .nwc-wallet-card {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 10px; border-radius: 12px;
padding: 12px; padding: 14px 16px;
background: rgba(255, 255, 255, 0.02); background: rgba(255, 255, 255, 0.02);
display: flex;
flex-direction: column;
transition: border-color 0.15s ease;
}
.nwc-wallet-card:hover {
border-color: rgba(255, 255, 255, 0.16);
}
.nwc-wallet-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
} }
.nwc-wallet-card-title { .nwc-wallet-card-title {
font-weight: 700; font-weight: 700;
margin-bottom: 8px; font-size: 0.95rem;
color: var(--text-primary); color: var(--text-primary);
word-break: break-word;
}
.nwc-wallet-chips {
display: flex;
gap: 6px;
flex-wrap: wrap;
justify-content: flex-end;
flex-shrink: 0;
}
.nwc-wallet-chip {
display: inline-flex;
align-items: center;
font-size: 0.72rem;
font-weight: 700;
border-radius: 999px;
padding: 4px 10px;
white-space: nowrap;
}
.nwc-wallet-chip--balance {
background: rgba(109, 191, 139, 0.14);
color: var(--green);
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
}
.nwc-wallet-chip--pending {
background: rgba(245, 158, 11, 0.14);
color: #fbbf24;
}
.nwc-wallet-address {
margin-bottom: 4px;
}
.nwc-wallet-address .creds-value {
font-size: 0.82rem;
padding: 9px 12px;
}
.nwc-wallet-address .creds-copy-btn {
margin-top: 5px;
padding: 7px 12px;
} }
.nwc-wallet-actions { .nwc-wallet-actions {
display: flex; display: flex;
align-items: center;
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 10px; margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.nwc-wallet-actions-spacer {
flex: 1;
min-width: 0;
} }
.nwc-wallet-actions .matrix-form-back, .nwc-wallet-actions .matrix-form-back,
.nwc-wallet-actions .btn-close-modal, .nwc-wallet-actions .btn-close-modal,
.nwc-wallet-actions .matrix-action-btn { .nwc-wallet-actions .matrix-action-btn {
padding: 8px 12px; padding: 7px 12px;
font-size: 0.82rem; font-size: 0.8rem;
}
.nwc-wallet-danger-btn {
color: #f08a8a;
}
.nwc-wallet-danger-btn:hover:not(:disabled) {
background-color: rgba(220, 80, 80, 0.14);
color: #ff9d9d;
} }
/* ── NWC LNURL share / download view ───────────────────────────── */ /* ── NWC LNURL share / download view ───────────────────────────── */
@@ -547,26 +838,68 @@ button.btn-reboot:hover:not(:disabled) {
text-align: center; text-align: center;
} }
/* ── NWC Liquidity Guide Card ────────────────────────────────────── */ /* ── NWC Liquidity Guide (collapsible) ──────────────────────────── */
.nwc-liquidity-guide-card { .nwc-liquidity-details {
background: rgba(255, 255, 255, 0.02); margin-top: 20px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 12px; border-radius: 12px;
padding: 16px; background: rgba(255, 255, 255, 0.015);
margin-bottom: 16px;
color: var(--text-primary);
} }
.nwc-liquidity-header { .nwc-liquidity-summary {
display: flex; display: flex;
align-items: center;
gap: 12px; gap: 12px;
align-items: flex-start; padding: 14px 16px;
margin-bottom: 12px; cursor: pointer;
list-style: none;
color: var(--text-primary);
font-size: 0.9rem;
}
.nwc-liquidity-summary::-webkit-details-marker {
display: none;
}
.nwc-liquidity-summary::after {
content: "▸";
margin-left: auto;
color: var(--text-dim);
transition: transform 0.15s ease;
}
.nwc-liquidity-details[open] .nwc-liquidity-summary::after {
transform: rotate(90deg);
}
.nwc-liquidity-summary:hover {
background: rgba(255, 255, 255, 0.02);
border-radius: 12px;
} }
.nwc-liquidity-icon { .nwc-liquidity-icon {
font-size: 24px; font-size: 22px;
}
.nwc-liquidity-subtitle {
display: block;
margin-top: 2px;
font-size: 0.75rem;
font-weight: 400;
color: var(--text-secondary);
}
.nwc-liquidity-guide-card {
padding: 0 16px 16px;
color: var(--text-primary);
}
.nwc-liquidity-body {
font-size: 0.82rem;
line-height: 1.55;
margin: 0 0 12px;
color: var(--text-secondary);
} }
.nwc-liquidity-steps { .nwc-liquidity-steps {
@@ -583,6 +916,13 @@ button.btn-reboot:hover:not(:disabled) {
font-size: 0.85rem; font-size: 0.85rem;
} }
.nwc-liquidity-step-desc {
margin: 2px 0 0;
color: var(--text-secondary);
font-size: 0.78rem;
line-height: 1.5;
}
.step-num { .step-num {
background: var(--accent-color); background: var(--accent-color);
color: #0A1A10; color: #0A1A10;
@@ -605,3 +945,60 @@ button.btn-reboot:hover:not(:disabled) {
border-radius: 0 8px 8px 0; border-radius: 0 8px 8px 0;
font-size: 0.82rem; font-size: 0.82rem;
} }
.nwc-liquidity-analogy {
margin: 0 0 10px;
font-size: 0.8rem;
line-height: 1.45;
}
.nwc-rtl-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--border-color);
color: var(--text-primary);
border: none;
padding: 8px 14px;
border-radius: 8px;
font-size: 0.8rem;
cursor: pointer;
}
.nwc-rtl-btn:hover {
background: #1c2a24;
}
/* ── Narrow screens ─────────────────────────────────────────────── */
@media (max-width: 720px) {
.nwc-tabs-bar {
align-items: stretch;
flex-direction: column;
}
.nwc-tabs-btns {
justify-content: stretch;
}
.nwc-tab-btn {
flex: 1;
padding: 8px 10px;
}
.nwc-wallet-list {
grid-template-columns: 1fr;
}
.nwc-wallet-card-head {
flex-direction: column;
}
.nwc-wallet-chips {
justify-content: flex-start;
}
.nwc-wallet-actions-spacer {
display: none;
}
}
@@ -77,7 +77,7 @@ function openDomainSetupModal(feat, onSaved) {
var nwcWarning = isWalletConnections var nwcWarning = isWalletConnections
? '<div class="domain-nwc-warning">' + ? '<div class="domain-nwc-warning">' +
'<strong>⚠ Wallet Connections requires its own unique hostname.</strong> ' + '<strong>⚠ Lightning Wallet Connections requires its own unique hostname.</strong> ' +
'Use a new subdomain such as <code>lightning.yourdomain.com</code>, or a separate domain. ' + 'Use a new subdomain such as <code>lightning.yourdomain.com</code>, or a separate domain. ' +
'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' + 'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' +
'</div>' '</div>'
@@ -194,7 +194,7 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
var nwcWarning = isWalletConnections var nwcWarning = isWalletConnections
? '<div class="domain-nwc-warning">' + ? '<div class="domain-nwc-warning">' +
'<strong>⚠ Wallet Connections requires its own unique hostname.</strong> ' + '<strong>⚠ Lightning Wallet Connections requires its own unique hostname.</strong> ' +
'Use a new subdomain such as <code>lightning.yourdomain.com</code>, or a separate domain. ' + 'Use a new subdomain such as <code>lightning.yourdomain.com</code>, or a separate domain. ' +
'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' + 'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' +
'</div>' '</div>'
@@ -89,6 +89,80 @@ function _nwcWalletSectionEl() {
return document.getElementById("nwc-wallets-body"); return document.getElementById("nwc-wallets-body");
} }
function _nwcFormatSats(value) {
var n = Number(value || 0);
if (!Number.isFinite(n)) n = 0;
return n.toLocaleString("en-US");
}
// "Go to Service & Setup" shortcut shown when no Lightning Address domain
// is configured yet — jumps to the sibling tab instead of dead-ending.
function _nwcWireDomainLink() {
var link = document.getElementById("nwc-domain-setup-link");
if (link) link.addEventListener("click", function() { _nwcActivateTab("setup"); });
}
// ── Lightning Wallet Connections: tabbed modal shell ──────────────
// The wallet manager is a small app in its own right, so it gets the full
// width of the dialog on a "Wallets" tab, while status / enable / domain /
// ports / restart live on a "Service & Setup" tab.
function _setCredsDialogWide(on) {
if (!$credsModal) return;
var dialog = $credsModal.querySelector(".creds-dialog");
if (dialog) dialog.classList.toggle("creds-dialog--wide", !!on);
}
function _nwcTabsShellHtml(opts) {
var domainChip = opts.domain
? '<span class="nwc-header-chip nwc-header-chip--domain" title="Lightning Address domain">🌐 ' + escHtml(opts.domain) + '</span>'
: '<span class="nwc-header-chip nwc-header-chip--warn" title="No Lightning Address domain configured yet">🌐 Domain not set</span>';
return '<div class="nwc-tabs" data-active="' + escHtml(opts.activeTab) + '">' +
'<div class="nwc-tabs-bar" role="tablist" aria-label="Lightning Wallet Connections sections">' +
'<div class="nwc-tabs-btns">' +
'<button class="nwc-tab-btn" role="tab" data-tab="wallets" id="nwc-tab-btn-wallets" aria-controls="nwc-tab-wallets">⚡ Wallets</button>' +
'<button class="nwc-tab-btn" role="tab" data-tab="setup" id="nwc-tab-btn-setup" aria-controls="nwc-tab-setup">⚙ Service &amp; Setup</button>' +
'</div>' +
'<div class="nwc-header-chips">' +
'<span class="nwc-header-chip"><span class="status-dot ' + opts.statusDotClass + '"></span>' + escHtml(opts.statusLabel) + '</span>' +
domainChip +
'</div>' +
'</div>' +
'<div class="nwc-tab-panel" role="tabpanel" id="nwc-tab-wallets" aria-labelledby="nwc-tab-btn-wallets" data-tab="wallets">' + opts.walletsHtml + '</div>' +
'<div class="nwc-tab-panel" role="tabpanel" id="nwc-tab-setup" aria-labelledby="nwc-tab-btn-setup" data-tab="setup">' + opts.setupHtml + '</div>' +
'</div>';
}
function _nwcActivateTab(tab) {
var root = document.querySelector(".nwc-tabs");
if (!root) return;
root.setAttribute("data-active", tab);
root.querySelectorAll(".nwc-tab-btn").forEach(function(btn) {
var on = btn.getAttribute("data-tab") === tab;
btn.classList.toggle("active", on);
btn.setAttribute("aria-selected", on ? "true" : "false");
});
root.querySelectorAll(".nwc-tab-panel").forEach(function(panel) {
panel.classList.toggle("active", panel.getAttribute("data-tab") === tab);
});
var body = document.getElementById("creds-body");
if (body) body.scrollTop = 0;
}
function _nwcWireTabs() {
var root = document.querySelector(".nwc-tabs");
if (!root) return;
root.querySelectorAll(".nwc-tab-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
_nwcActivateTab(btn.getAttribute("data-tab"));
});
});
_nwcActivateTab(root.getAttribute("data-active") || "wallets");
var gotoSetup = document.getElementById("nwc-goto-setup-btn");
if (gotoSetup) gotoSetup.addEventListener("click", function() { _nwcActivateTab("setup"); });
}
function _nwcRenderWalletState() { function _nwcRenderWalletState() {
var host = _nwcWalletSectionEl(); var host = _nwcWalletSectionEl();
if (!host || !_nwcModalState) return; if (!host || !_nwcModalState) return;
@@ -99,8 +173,9 @@ function _nwcRenderWalletState() {
if (state.view === "create") { if (state.view === "create") {
var selectedLimited = state.createForm.access_preset === "send_receive_limited"; var selectedLimited = state.createForm.access_preset === "send_receive_limited";
html += html +=
'<p class="svc-detail-desc">Create a new isolated wallet connection for your app.</p>' + '<div class="nwc-tab-intro-title">Create a Wallet</div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-name">Wallet Connection Name</label>' + '<p class="nwc-tab-intro-desc">An isolated wallet you pair to a single app, with its own Lightning Address.</p>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-name">Wallet Name</label>' +
'<input class="matrix-form-input" id="nwc-wallet-name" type="text" placeholder="My Wallet" value="' + escHtml(state.createForm.name || "") + '" autocomplete="off"></div>' + '<input class="matrix-form-input" id="nwc-wallet-name" type="text" placeholder="My Wallet" value="' + escHtml(state.createForm.name || "") + '" autocomplete="off"></div>' +
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-alias">Lightning Address Alias</label>' + '<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-alias">Lightning Address Alias</label>' +
'<input class="matrix-form-input" id="nwc-wallet-alias" type="text" placeholder="my-wallet" value="' + escHtml(state.createForm.alias || "") + '" autocomplete="off">' + '<input class="matrix-form-input" id="nwc-wallet-alias" type="text" placeholder="my-wallet" value="' + escHtml(state.createForm.alias || "") + '" autocomplete="off">' +
@@ -143,7 +218,7 @@ function _nwcRenderWalletState() {
var pairId = "nwc-pairing-uri-" + Math.random().toString(36).substring(2, 8); var pairId = "nwc-pairing-uri-" + Math.random().toString(36).substring(2, 8);
html += '<div class="nwc-secret-warning">⚠ One-time pairing secret. Save it now — it will not be shown again.</div>'; html += '<div class="nwc-secret-warning">⚠ One-time pairing secret. Save it now — it will not be shown again.</div>';
if (created.pairing_qrcode) { if (created.pairing_qrcode) {
html += '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + created.pairing_qrcode + '" alt="QR code for Wallet Connections pairing secret"><div class="creds-qr-hint">Scan now in Zeus or copy the URI below.</div></div>'; html += '<div class="creds-qr-wrap"><img class="creds-qr-img" src="' + created.pairing_qrcode + '" alt="QR code for Lightning Wallet Connections pairing secret"><div class="creds-qr-hint">Scan now in Zeus or copy the URI below.</div></div>';
} }
html += '<div class="creds-row"><div class="creds-label">Pairing URI</div>' + html += '<div class="creds-row"><div class="creds-label">Pairing URI</div>' +
'<div class="creds-value-wrap"><div class="creds-value" id="' + pairId + '">' + escHtml(created.pairing_uri || "Unavailable") + '</div><button class="creds-copy-btn" data-target="' + pairId + '">Copy</button></div></div>'; '<div class="creds-value-wrap"><div class="creds-value" id="' + pairId + '">' + escHtml(created.pairing_uri || "Unavailable") + '</div><button class="creds-copy-btn" data-target="' + pairId + '">Copy</button></div></div>';
@@ -237,19 +312,30 @@ function _nwcRenderWalletState() {
} }
var canCreate = !!state.domain && !state.busy; var canCreate = !!state.domain && !state.busy;
html += '<div class="matrix-actions-row">' + var walletCount = (state.wallets || []).length;
'<button class="matrix-action-btn" id="nwc-open-create-btn"' + (canCreate ? "" : " disabled") + '>' + (state.domain ? ' Create Wallet Connection' : 'Configure Domain First') + '</button>' + html += '<div class="nwc-toolbar">' +
'<button class="matrix-form-back" id="nwc-refresh-btn"' + (state.busy ? " disabled" : "") + '>Refresh</button>' + '<div class="nwc-toolbar-count">' + (walletCount === 1 ? '1 wallet' : walletCount + ' wallets') + '</div>' +
'<div class="nwc-toolbar-actions">' +
'<button class="matrix-form-back nwc-toolbar-refresh" id="nwc-refresh-btn"' + (state.busy ? " disabled" : "") + '>↻ Refresh</button>' +
'<button class="matrix-action-btn" id="nwc-open-create-btn"' + (canCreate ? "" : " disabled") + '> New Wallet</button>' +
'</div>' +
'</div>';
if (!state.domain) {
html += '<div class="nwc-domain-required">' +
'<strong>⚠ Lightning Address domain required</strong>' +
'<div class="nwc-domain-required-body">Set up a unique hostname such as <strong>lightning.yourdomain.com</strong> before creating wallets.</div>' +
'<button class="btn btn-primary nwc-domain-required-btn" id="nwc-domain-setup-link">⚙ Go to Service &amp; Setup</button>' +
'</div>'; '</div>';
if (state.domain) {
html += '<p class="svc-detail-desc">Lightning Address domain: <strong>' + escHtml(state.domain) + '</strong></p>';
} else {
html += '<div class="svc-detail-troubleshoot" style="margin-bottom:10px"><strong>Domain required</strong><div style="margin-top:6px">Configure the Lightning Address domain in the Domain Diagnostic Checklist above before creating Wallet Connections. Use a unique hostname such as <strong>lightning.yourdomain.com</strong>.</div></div>';
} }
if (!state.wallets || state.wallets.length === 0) { if (!state.wallets || state.wallets.length === 0) {
html += '<p class="creds-empty">No wallet connections yet. Create your first wallet connection to generate a one-time pairing secret.</p>'; html += '<div class="nwc-empty-state">' +
'<div class="nwc-empty-icon">👛</div>' +
'<div class="nwc-empty-title">No wallets yet</div>' +
'<p class="nwc-empty-desc">Create your first wallet to get a one-time pairing secret for an app, plus a reusable Lightning Address.</p>' +
'</div>';
host.innerHTML = html; host.innerHTML = html;
_nwcWireDomainLink();
var openCreateBtn = document.getElementById("nwc-open-create-btn"); var openCreateBtn = document.getElementById("nwc-open-create-btn");
if (openCreateBtn) { if (openCreateBtn) {
openCreateBtn.addEventListener("click", function() { openCreateBtn.addEventListener("click", function() {
@@ -268,25 +354,38 @@ function _nwcRenderWalletState() {
state.wallets.forEach(function(wallet) { state.wallets.forEach(function(wallet) {
var id = wallet.id || wallet.pubkey || ""; var id = wallet.id || wallet.pubkey || "";
var addressId = "nwc-wallet-addr-" + Math.random().toString(36).substring(2, 8); var addressId = "nwc-wallet-addr-" + Math.random().toString(36).substring(2, 8);
var pending = Number(wallet.pending_transactions || 0);
html += '<div class="nwc-wallet-card">' + html += '<div class="nwc-wallet-card">' +
'<div class="nwc-wallet-card-head">' +
'<div class="nwc-wallet-card-title">' + escHtml(wallet.name || "Wallet") + '</div>' + '<div class="nwc-wallet-card-title">' + escHtml(wallet.name || "Wallet") + '</div>' +
'<div class="creds-row"><div class="creds-label">Alias</div><div class="creds-value-wrap"><div class="creds-value">' + escHtml(wallet.alias || "") + '</div></div></div>' + '<div class="nwc-wallet-chips">' +
'<span class="nwc-wallet-chip nwc-wallet-chip--balance">' + escHtml(_nwcFormatSats(wallet.balance_sats)) + ' sats</span>' +
(pending > 0
? '<span class="nwc-wallet-chip nwc-wallet-chip--pending" title="Pending transactions">⏳ ' + escHtml(String(pending)) + ' pending</span>'
: '') +
'</div>' +
'</div>' +
(wallet.lightning_address (wallet.lightning_address
? '<div class="creds-row"><div class="creds-label">Lightning Address</div><div class="creds-value-wrap"><div class="creds-value" id="' + addressId + '">' + escHtml(wallet.lightning_address) + '</div><button class="creds-copy-btn" data-target="' + addressId + '">Copy</button></div></div>' ? '<div class="nwc-wallet-address">' +
: "") + '<div class="creds-label">Lightning Address</div>' +
'<div class="creds-row"><div class="creds-label">Balance</div><div class="creds-value-wrap"><div class="creds-value">' + escHtml(String(wallet.balance_sats || 0)) + ' sats</div></div></div>' + '<div class="creds-value-wrap"><div class="creds-value" id="' + addressId + '">' + escHtml(wallet.lightning_address) + '</div>' +
'<div class="creds-row"><div class="creds-label">Pending TX</div><div class="creds-value-wrap"><div class="creds-value">' + escHtml(String(wallet.pending_transactions || 0)) + '</div></div></div>' + '<button class="creds-copy-btn" data-target="' + addressId + '">Copy</button></div>' +
'</div>'
: '<div class="nwc-wallet-address"><div class="creds-label">Alias</div>' +
'<div class="creds-value-wrap"><div class="creds-value">' + escHtml(wallet.alias || "") + '</div></div></div>') +
'<div class="nwc-wallet-actions">' + '<div class="nwc-wallet-actions">' +
'<button class="matrix-action-btn nwc-wallet-action-btn" data-action="share" data-wallet-id="' + escHtml(id) + '"' + ((state.busy || !wallet.lightning_address) ? " disabled" : "") + '>⚡ Share QR</button>' + '<button class="matrix-action-btn nwc-wallet-action-btn" data-action="share" data-wallet-id="' + escHtml(id) + '"' + ((state.busy || !wallet.lightning_address) ? " disabled" : "") + '>⚡ Share QR</button>' +
'<button class="matrix-form-back nwc-wallet-action-btn" data-action="test" data-wallet-alias="' + escHtml(wallet.alias || "") + '"' + (state.busy ? " disabled" : "") + '>Verify Address</button>' + '<button class="matrix-form-back nwc-wallet-action-btn" data-action="test" data-wallet-alias="' + escHtml(wallet.alias || "") + '"' + (state.busy ? " disabled" : "") + '>Verify</button>' +
'<button class="matrix-form-back nwc-wallet-action-btn" data-action="drain" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Drain</button>' + '<span class="nwc-wallet-actions-spacer"></span>' +
'<button class="btn btn-close-modal nwc-wallet-action-btn" data-action="delete" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Delete</button>' + '<button class="matrix-form-back nwc-wallet-action-btn nwc-wallet-danger-btn" data-action="drain" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Drain</button>' +
'<button class="matrix-form-back nwc-wallet-action-btn nwc-wallet-danger-btn" data-action="delete" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Delete</button>' +
'</div>' + '</div>' +
'</div>'; '</div>';
}); });
html += "</div>"; html += "</div>";
host.innerHTML = html; host.innerHTML = html;
_attachCopyHandlers(host); _attachCopyHandlers(host);
_nwcWireDomainLink();
var createBtn = document.getElementById("nwc-open-create-btn"); var createBtn = document.getElementById("nwc-open-create-btn");
if (createBtn) { if (createBtn) {
@@ -375,7 +474,7 @@ async function _nwcCreateWallet() {
}; };
if (!name) { if (!name) {
_nwcSetMessage("error", "Wallet Connection name is required."); _nwcSetMessage("error", "Wallet name is required.");
_nwcRenderWalletState(); _nwcRenderWalletState();
return; return;
} }
@@ -459,7 +558,7 @@ async function _nwcDrainWallet(walletId) {
async function _nwcDeleteWallet(walletId) { async function _nwcDeleteWallet(walletId) {
if (!_nwcModalState || _nwcModalState.busy || !walletId) return; if (!_nwcModalState || _nwcModalState.busy || !walletId) return;
if (!window.confirm("Delete this wallet connection? This removes the wallet alias from Wallet Connections.")) return; if (!window.confirm("Delete this wallet connection? This removes the wallet alias from Lightning Wallet Connections.")) return;
_nwcBusy(true); _nwcBusy(true);
_nwcClearMessage(); _nwcClearMessage();
_nwcRenderWalletState(); _nwcRenderWalletState();
@@ -500,6 +599,8 @@ async function _nwcInitWalletFlow(unit, name, icon) {
async function openServiceDetailModal(unit, name, icon) { async function openServiceDetailModal(unit, name, icon) {
if (!$credsModal) return; if (!$credsModal) return;
var isNwc = _isNwcServiceUnit(unit);
_setCredsDialogWide(isNwc);
if ($credsTitle) { if ($credsTitle) {
$credsTitle.innerHTML = ''; $credsTitle.innerHTML = '';
if (icon) { if (icon) {
@@ -521,13 +622,24 @@ async function openServiceDetailModal(unit, name, icon) {
var url = "/api/service-detail/" + encodeURIComponent(unit); var url = "/api/service-detail/" + encodeURIComponent(unit);
if (icon) url += "?icon=" + encodeURIComponent(icon); if (icon) url += "?icon=" + encodeURIComponent(icon);
var data = await apiFetch(url); var data = await apiFetch(url);
// Two content buckets. For most services everything lands in `html` and is
// rendered as one column, exactly as before. For Lightning Wallet
// Connections the day-to-day wallet manager (html) is split away from the
// service plumbing — status, enable/disable, domain, ports, restart
// (setupHtml) — and the two are rendered as tabs so neither feels cramped.
var html = ""; var html = "";
var setupHtml = "";
function addSetup(chunk) {
if (isNwc) setupHtml += chunk;
else html += chunk;
}
// Section A: Description // Section A: Description
if (data.description) { if (data.description) {
html += '<div class="svc-detail-section">' + addSetup('<div class="svc-detail-section">' +
'<p class="svc-detail-desc">' + escHtml(data.description) + '</p>' + '<p class="svc-detail-desc">' + escHtml(data.description) + '</p>' +
'</div>'; '</div>');
} }
// Section B: Status // Section B: Status
@@ -539,13 +651,13 @@ async function openServiceDetailModal(unit, name, icon) {
: (data.health || data.status); : (data.health || data.status);
var sc = statusClass(effectiveHealth); var sc = statusClass(effectiveHealth);
var st = statusText(effectiveHealth, effectiveEnabled); var st = statusText(effectiveHealth, effectiveEnabled);
html += '<div class="svc-detail-section">' + addSetup('<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Status</div>' + '<div class="svc-detail-section-title">Status</div>' +
'<div class="svc-detail-status">' + '<div class="svc-detail-status">' +
'<span class="status-dot ' + sc + '"></span>' + '<span class="status-dot ' + sc + '"></span>' +
'<span>' + escHtml(st) + '</span>' + '<span>' + escHtml(st) + '</span>' +
'</div>' + '</div>' +
'</div>'; '</div>');
// Section B2: BIP-110 live status (bip110 tile only) // Section B2: BIP-110 live status (bip110 tile only)
if (icon === 'bip110' && data.bip110) { if (icon === 'bip110' && data.bip110) {
@@ -587,15 +699,15 @@ async function openServiceDetailModal(unit, name, icon) {
domainActionHtml = '<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-reconfig-domain-btn">🔄 Reconfigure Domain</button>'; domainActionHtml = '<button class="btn btn-primary svc-detail-domain-btn" id="svc-detail-reconfig-domain-btn">🔄 Reconfigure Domain</button>';
} }
html += '<div class="svc-detail-section">' + addSetup('<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Domain Diagnostic Checklist</div>' + '<div class="svc-detail-section-title">Domain Diagnostic Checklist</div>' +
stepsHtml + stepsHtml +
domainActionHtml + domainActionHtml +
'</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() : "";
html += '<div class="svc-detail-section">' + addSetup('<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' + '<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
renderPortForwardGuideHtml(data.router_ports, { renderPortForwardGuideHtml(data.router_ports, {
internalIp: trimmedInternalIp || null, internalIp: trimmedInternalIp || null,
@@ -603,7 +715,7 @@ async function openServiceDetailModal(unit, name, icon) {
introClass: "svc-detail-port-note", introClass: "svc-detail-port-note",
noteClass: "svc-detail-port-note", noteClass: "svc-detail-port-note",
}) + }) +
'</div>'; '</div>');
} }
} else if (data.port_statuses && data.port_statuses.length > 0) { } else if (data.port_statuses && data.port_statuses.length > 0) {
// Non-domain services (e.g. SSH): show what to forward, plus a short // Non-domain services (e.g. SSH): show what to forward, plus a short
@@ -632,48 +744,57 @@ async function openServiceDetailModal(unit, name, icon) {
} }
// Section E: Credentials & Links // Section E: Credentials & Links
if (_isNwcServiceUnit(unit)) { if (isNwc) {
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Wallet Connections &amp; Node Liquidity</div>';
if (effectiveEnabled || data.enabled) { if (effectiveEnabled || data.enabled) {
html += '<div class="nwc-liquidity-guide-card">' + html += '<div class="nwc-tab-intro">' +
'<div class="nwc-liquidity-header">' +
'<span class="nwc-liquidity-icon">⚡</span>' +
'<div>' + '<div>' +
'<h4 style="margin:0 0 4px 0; font-size:15px; font-weight:600;">Lightning Channel &amp; Liquidity Guide</h4>' + '<div class="nwc-tab-intro-title">Your Lightning Wallets</div>' +
'<p class="nwc-liquidity-subtitle" style="margin:0; font-size:12px; color:var(--text-secondary);">How NWC &amp; Lightning Addresses work on new nodes</p>' + '<p class="nwc-tab-intro-desc">Each wallet is isolated: pair it to one app with a one-time secret, and share its Lightning Address to get paid.</p>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'<p style="font-size:13px; line-height:1.5; margin:0 0 12px 0;">' + '<div id="nwc-wallets-body"><p class="creds-loading">Loading wallets…</p></div>' +
'Your Alby Hub, NWC pairing keys, and Lightning Address endpoints (like <code>name@yourdomain.com</code>) are set up and working instantly. However, sending and receiving actual Bitcoin payments requires an operational Lightning node with active channels.' + '<details class="nwc-liquidity-details">' +
'<summary class="nwc-liquidity-summary">' +
'<span class="nwc-liquidity-icon">⚡</span>' +
'<span>' +
'<strong>Lightning Channel &amp; Liquidity Guide</strong>' +
'<span class="nwc-liquidity-subtitle">Why a brand-new node can\'t send or receive yet</span>' +
'</span>' +
'</summary>' +
'<div class="nwc-liquidity-guide-card">' +
'<p class="nwc-liquidity-body">' +
'Your wallets, pairing keys, and Lightning Address endpoints (like <code>name@yourdomain.com</code>) work the moment you create them. Moving actual bitcoin, though, needs a Lightning node with open channels.' +
'</p>' + '</p>' +
'<div class="nwc-liquidity-steps">' + '<div class="nwc-liquidity-steps">' +
'<div class="nwc-liquidity-step">' + '<div class="nwc-liquidity-step">' +
'<div class="step-num">1</div>' + '<div class="step-num">1</div>' +
'<div>' + '<div>' +
'<strong>Outbound Liquidity (Sending)</strong>' + '<strong>Outbound Liquidity (Sending)</strong>' +
'<p style="margin:2px 0 0 0; color:var(--text-secondary); font-size:12px;">Required so apps connected via NWC can pay invoices and send funds.</p>' + '<p class="nwc-liquidity-step-desc">Required so connected apps can pay invoices and send funds.</p>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'<div class="nwc-liquidity-step">' + '<div class="nwc-liquidity-step">' +
'<div class="step-num">2</div>' + '<div class="step-num">2</div>' +
'<div>' + '<div>' +
'<strong>Inbound Liquidity (Receiving)</strong>' + '<strong>Inbound Liquidity (Receiving)</strong>' +
'<p style="margin:2px 0 0 0; color:var(--text-secondary); font-size:12px;">Required so your Lightning Address can receive incoming payments from anyone.</p>' + '<p class="nwc-liquidity-step-desc">Required so your Lightning Address can receive payments from anyone.</p>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'<div class="nwc-liquidity-action">' + '<div class="nwc-liquidity-action">' +
'<p style="margin:0 0 10px 0; font-size:12.55px; line-height:1.4;">💧 <strong>The Plumbing Analogy:</strong> Think of channels like two-way water pipes. To pour water in (receive) or pump water out (send), you need open channels with liquidity on both sides.</p>' + '<p class="nwc-liquidity-analogy">💧 <strong>The Plumbing Analogy:</strong> Channels are two-way water pipes. To pour water in (receive) or pump it out (send), you need open channels with liquidity on the right side.</p>' +
'<button class="matrix-action-btn secondary-btn" id="nwc-open-rtl-btn" style="display:inline-flex; align-items:center; gap:6px; background:var(--border-color); color:var(--text-primary); border:none; padding:8px 14px; border-radius:8px; font-size:12.55px; cursor:pointer;">🚀 Open Ride The Lightning (RTL) to Manage Channels</button>' + '<button class="matrix-action-btn nwc-rtl-btn" id="nwc-open-rtl-btn">🚀 Open Ride The Lightning (RTL) to Manage Channels</button>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'<p class="svc-detail-desc" style="margin-top:16px;">Create, verify, drain, and delete isolated NWC wallet connections without leaving the Hub.</p>' + '</details>';
'<div id="nwc-wallets-body"><p class="creds-loading">Loading wallet connections…</p></div>';
} else { } else {
html += '<p class="creds-empty">Wallet Connections is disabled. Enable this feature below, rebuild, then return here to create and manage NWC wallet connections.</p>'; html += '<div class="nwc-disabled-state">' +
'<div class="nwc-disabled-icon">⚡</div>' +
'<div class="nwc-disabled-title">Lightning Wallet Connections is turned off</div>' +
'<p class="nwc-disabled-desc">Enable it on the <strong>Service &amp; Setup</strong> tab and rebuild, then come back here to create and manage wallets.</p>' +
'<button class="btn btn-primary" id="nwc-goto-setup-btn">⚙ Go to Service &amp; Setup</button>' +
'</div>';
} }
html += '</div>';
} else if (data.has_credentials && data.credentials && data.credentials.length > 0) { } else if (data.has_credentials && data.credentials && data.credentials.length > 0) {
html += '<div class="svc-detail-section">' + html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">Credentials &amp; Access</div>' + '<div class="svc-detail-section-title">Credentials &amp; Access</div>' +
@@ -733,7 +854,7 @@ async function openServiceDetailModal(unit, name, icon) {
conflictsHtml = '<div class="feature-conflict-warning">\u26A0 Mutually exclusive with: ' + escHtml(conflictNames.join(", ")) + '</div>'; conflictsHtml = '<div class="feature-conflict-warning">\u26A0 Mutually exclusive with: ' + escHtml(conflictNames.join(", ")) + '</div>';
} }
html += '<div class="svc-detail-section">' + addSetup('<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">' + addonSectionTitle + '</div>' + '<div class="svc-detail-section-title">' + addonSectionTitle + '</div>' +
'<p class="svc-detail-desc">' + escHtml(addonDesc) + '</p>' + '<p class="svc-detail-desc">' + escHtml(addonDesc) + '</p>' +
conflictsHtml + conflictsHtml +
@@ -741,19 +862,35 @@ async function openServiceDetailModal(unit, name, icon) {
'<span class="svc-detail-addon-status ' + addonStatusCls + '">' + addonStatusLabel + '</span>' + '<span class="svc-detail-addon-status ' + addonStatusCls + '">' + addonStatusLabel + '</span>' +
'<button class="' + addonBtnCls + '" id="svc-detail-addon-btn">' + escHtml(addonBtnLabel) + '</button>' + '<button class="' + addonBtnCls + '" id="svc-detail-addon-btn">' + escHtml(addonBtnLabel) + '</button>' +
'</div>' + '</div>' +
'</div>'; '</div>');
} }
if ((effectiveEnabled || data.enabled) && unit !== "phpfpm-nextcloud.service" && unit !== "phpfpm-wordpress.service") { if ((effectiveEnabled || data.enabled) && unit !== "phpfpm-nextcloud.service" && unit !== "phpfpm-wordpress.service") {
html += '<div class="svc-detail-section svc-detail-restart-section">' + addSetup('<div class="svc-detail-section svc-detail-restart-section">' +
'<div class="svc-detail-section-title">Troubleshooting</div>' + '<div class="svc-detail-section-title">Troubleshooting</div>' +
'<p class="svc-detail-desc">If you\'re experiencing issues with this service, try restarting it.</p>' + '<p class="svc-detail-desc">If you\'re experiencing issues with this service, try restarting it.</p>' +
'<button class="btn btn-warning svc-detail-restart-btn" id="svc-detail-restart-btn">🔄 Restart Service</button>' + '<button class="btn btn-warning svc-detail-restart-btn" id="svc-detail-restart-btn">🔄 Restart Service</button>' +
'<div class="svc-detail-restart-result" id="svc-detail-restart-result"></div>' + '<div class="svc-detail-restart-result" id="svc-detail-restart-result"></div>' +
'</div>'; '</div>');
}
if (isNwc) {
// Land on Setup when the service is off or the domain still needs
// configuring — otherwise the Wallets tab is the useful default.
var domainReady = !!data.domain;
var startTab = (effectiveEnabled || data.enabled) && domainReady ? "wallets" : "setup";
html = _nwcTabsShellHtml({
walletsHtml: html,
setupHtml: setupHtml,
statusDotClass: sc,
statusLabel: st,
domain: data.domain || null,
activeTab: startTab
});
} }
$credsBody.innerHTML = html; $credsBody.innerHTML = html;
if (isNwc) _nwcWireTabs();
_attachCopyHandlers($credsBody); _attachCopyHandlers($credsBody);
if (_isNwcServiceUnit(unit) && (effectiveEnabled || data.enabled)) { if (_isNwcServiceUnit(unit) && (effectiveEnabled || data.enabled)) {
await _nwcInitWalletFlow(unit, name, icon); await _nwcInitWalletFlow(unit, name, icon);
@@ -849,6 +986,7 @@ async function openServiceDetailModal(unit, name, icon) {
async function openCredsModal(unit, name, icon) { async function openCredsModal(unit, name, icon) {
if (!$credsModal) return; if (!$credsModal) return;
_setCredsDialogWide(false);
if ($credsTitle) { if ($credsTitle) {
$credsTitle.innerHTML = ''; $credsTitle.innerHTML = '';
if (icon) { if (icon) {
+1 -1
View File
@@ -188,7 +188,7 @@ $HAVEN {
EOF EOF
fi fi
# Wallet Connections LNURL # Lightning Wallet Connections LNURL
if [ -n "$LIGHTNING" ]; then if [ -n "$LIGHTNING" ]; then
cat >> /run/caddy/Caddyfile <<EOF cat >> /run/caddy/Caddyfile <<EOF
+1 -1
View File
@@ -46,7 +46,7 @@
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer"; mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
element-calling = lib.mkEnableOption "Element Video and Audio Calling"; element-calling = lib.mkEnableOption "Element Video and Audio Calling";
bitcoin-core = lib.mkEnableOption "Bitcoin Core"; bitcoin-core = lib.mkEnableOption "Bitcoin Core";
"nwc-wallets" = lib.mkEnableOption "Wallet Connections"; "nwc-wallets" = lib.mkEnableOption "Lightning Wallet Connections";
rdp = lib.mkEnableOption "Gnome Remote Desktop"; rdp = lib.mkEnableOption "Gnome Remote Desktop";
sshd = lib.mkEnableOption "SSH remote access"; sshd = lib.mkEnableOption "SSH remote access";
+1 -1
View File
@@ -61,7 +61,7 @@ let
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; } { label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
{ label = "Status"; value = "Auto-configured on first boot"; } { label = "Status"; value = "Auto-configured on first boot"; }
]; } ]; }
{ name = "Wallet Connections"; unit = "albyhub.service"; type = "system"; icon = "nwc"; enabled = cfg.features."nwc-wallets"; category = "bitcoin-apps"; credentials = [ { name = "Lightning Wallet Connections"; unit = "albyhub.service"; type = "system"; icon = "nwc"; enabled = cfg.features."nwc-wallets"; category = "bitcoin-apps"; credentials = [
{ label = "Lightning Address Domain"; file = "/var/lib/domains/lightning"; } { label = "Lightning Address Domain"; file = "/var/lib/domains/lightning"; }
]; } ]; }
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [ { name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
+3 -3
View File
@@ -43,11 +43,11 @@ lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
assertions = [ assertions = [
{ {
assertion = config.services.lnd.enable; assertion = config.services.lnd.enable;
message = "Wallet Connections requires services.lnd.enable = true."; message = "Lightning Wallet Connections requires services.lnd.enable = true.";
} }
{ {
assertion = !(lib.attrByPath [ "nix-bitcoin" "netns-isolation" "enable" ] false config); assertion = !(lib.attrByPath [ "nix-bitcoin" "netns-isolation" "enable" ] false config);
message = "Wallet Connections requires nix-bitcoin.netns-isolation.enable = false."; message = "Lightning Wallet Connections requires nix-bitcoin.netns-isolation.enable = false.";
} }
{ {
assertion = albyHubPort != config.services.lnd.restPort; assertion = albyHubPort != config.services.lnd.restPort;
@@ -134,7 +134,7 @@ lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
}; };
systemd.services.nwc-lnurl = { systemd.services.nwc-lnurl = {
description = "Wallet Connections public LNURL service"; description = "Lightning Wallet Connections public LNURL service";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "albyhub.service" "sovran-hub-web.service" ]; after = [ "albyhub.service" "sovran-hub-web.service" ];
wants = [ "albyhub.service" ]; wants = [ "albyhub.service" ];