diff --git a/README.md b/README.md index 395f865..3c3d17e 100644 --- a/README.md +++ b/README.md @@ -786,7 +786,7 @@ sudo nixos-rebuild switch --rollback | `modules/` | Core modules, Bitcoin services, self-hosted services, and optional features | | `modules/core/` | Roles, Hub integration, Caddy, desktop, support, and other core behavior | | `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 | | `assets/` | Repository documentation images | | `custom.template.nix` | Template for local features and service overrides | diff --git a/app/sovran_systemsos_web/nwc_hub_manager.py b/app/sovran_systemsos_web/nwc_hub_manager.py index 5ef708d..e5c9ca9 100644 --- a/app/sovran_systemsos_web/nwc_hub_manager.py +++ b/app/sovran_systemsos_web/nwc_hub_manager.py @@ -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 http://127.0.0.1:18080 by default (override with NWC_ALBY_HUB_API_BASE). diff --git a/app/sovran_systemsos_web/nwc_lnurl_service.py b/app/sovran_systemsos_web/nwc_lnurl_service.py index f6d358d..fcb19e5 100644 --- a/app/sovran_systemsos_web/nwc_lnurl_service.py +++ b/app/sovran_systemsos_web/nwc_lnurl_service.py @@ -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). Caddy proxies the public Lightning Address domain's LNURL routes to this port. diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index a8552b7..44a8dc6 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -227,7 +227,7 @@ FEATURE_REGISTRY = [ }, { "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.", "category": "bitcoin", "needs_domain": True, @@ -461,7 +461,7 @@ SERVICE_DESCRIPTIONS: dict[str, str] = { "Sovran_SystemsOS integrates Tor natively across your entire stack." ), "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." ), "gnome-remote-desktop.service": ( @@ -4323,7 +4323,7 @@ async def api_domains_set(req: DomainSetRequest): "error": "domain_conflict", "conflicting_domain_key": conflicting_key, "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. " 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)} -# ── Wallet Connections (NWC) endpoints ──────────────────────────── +# ── Lightning Wallet Connections (NWC) endpoints ──────────────────────────── NWC_DOMAIN_FILE = "/var/lib/domains/lightning" NWC_ALIAS_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,31}$") diff --git a/app/sovran_systemsos_web/static/css/modals.css b/app/sovran_systemsos_web/static/css/modals.css index e5ba26e..2161741 100644 --- a/app/sovran_systemsos_web/static/css/modals.css +++ b/app/sovran_systemsos_web/static/css/modals.css @@ -223,6 +223,10 @@ button.btn-reboot:hover:not(:disabled) { color: var(--text-primary); } +.creds-dialog--wide { + max-width: 980px; +} + .creds-body { padding: 24px 28px; overflow-y: auto; @@ -489,38 +493,325 @@ button.btn-reboot:hover:not(:disabled) { font-size: 0.86rem; } -.nwc-wallet-list { +/* ── Lightning Wallet Connections: tabbed shell ─────────────────── */ + +.nwc-tabs-bar { 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; margin-top: 12px; } .nwc-wallet-card { border: 1px solid var(--border-color); - border-radius: 10px; - padding: 12px; + border-radius: 12px; + padding: 14px 16px; 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 { font-weight: 700; - margin-bottom: 8px; + font-size: 0.95rem; 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 { display: flex; + align-items: center; gap: 8px; 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 .btn-close-modal, .nwc-wallet-actions .matrix-action-btn { - padding: 8px 12px; - font-size: 0.82rem; + padding: 7px 12px; + 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 ───────────────────────────── */ @@ -547,26 +838,68 @@ button.btn-reboot:hover:not(:disabled) { text-align: center; } -/* ── NWC Liquidity Guide Card ────────────────────────────────────── */ +/* ── NWC Liquidity Guide (collapsible) ──────────────────────────── */ -.nwc-liquidity-guide-card { - background: rgba(255, 255, 255, 0.02); +.nwc-liquidity-details { + margin-top: 20px; border: 1px solid var(--border-color); border-radius: 12px; - padding: 16px; - margin-bottom: 16px; - color: var(--text-primary); + background: rgba(255, 255, 255, 0.015); } -.nwc-liquidity-header { +.nwc-liquidity-summary { display: flex; + align-items: center; gap: 12px; - align-items: flex-start; - margin-bottom: 12px; + padding: 14px 16px; + 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 { - 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 { @@ -583,6 +916,13 @@ button.btn-reboot:hover:not(:disabled) { 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 { background: var(--accent-color); color: #0A1A10; @@ -605,3 +945,60 @@ button.btn-reboot:hover:not(:disabled) { border-radius: 0 8px 8px 0; 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; + } +} diff --git a/app/sovran_systemsos_web/static/js/features.js b/app/sovran_systemsos_web/static/js/features.js index b212869..2863316 100644 --- a/app/sovran_systemsos_web/static/js/features.js +++ b/app/sovran_systemsos_web/static/js/features.js @@ -77,7 +77,7 @@ function openDomainSetupModal(feat, onSaved) { var nwcWarning = isWalletConnections ? '
' + - '⚠ Wallet Connections requires its own unique hostname. ' + + '⚠ Lightning Wallet Connections requires its own unique hostname. ' + 'Use a new subdomain such as lightning.yourdomain.com, or a separate domain. ' + 'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' + '
' @@ -194,7 +194,7 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) { var nwcWarning = isWalletConnections ? '
' + - '⚠ Wallet Connections requires its own unique hostname. ' + + '⚠ Lightning Wallet Connections requires its own unique hostname. ' + 'Use a new subdomain such as lightning.yourdomain.com, or a separate domain. ' + 'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' + '
' diff --git a/app/sovran_systemsos_web/static/js/service-detail.js b/app/sovran_systemsos_web/static/js/service-detail.js index 7acee5d..060574a 100644 --- a/app/sovran_systemsos_web/static/js/service-detail.js +++ b/app/sovran_systemsos_web/static/js/service-detail.js @@ -89,6 +89,80 @@ function _nwcWalletSectionEl() { 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 + ? '🌐 ' + escHtml(opts.domain) + '' + : '🌐 Domain not set'; + + return '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + escHtml(opts.statusLabel) + '' + + domainChip + + '
' + + '
' + + '
' + opts.walletsHtml + '
' + + '
' + opts.setupHtml + '
' + + '
'; +} + +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() { var host = _nwcWalletSectionEl(); if (!host || !_nwcModalState) return; @@ -99,8 +173,9 @@ function _nwcRenderWalletState() { if (state.view === "create") { var selectedLimited = state.createForm.access_preset === "send_receive_limited"; html += - '

Create a new isolated wallet connection for your app.

' + - '
' + + '
Create a Wallet
' + + '

An isolated wallet you pair to a single app, with its own Lightning Address.

' + + '
' + '
' + '
' + '' + @@ -143,7 +218,7 @@ function _nwcRenderWalletState() { var pairId = "nwc-pairing-uri-" + Math.random().toString(36).substring(2, 8); html += '
⚠ One-time pairing secret. Save it now — it will not be shown again.
'; if (created.pairing_qrcode) { - html += '
QR code for Wallet Connections pairing secret
Scan now in Zeus or copy the URI below.
'; + html += '
QR code for Lightning Wallet Connections pairing secret
Scan now in Zeus or copy the URI below.
'; } html += '
Pairing URI
' + '
' + escHtml(created.pairing_uri || "Unavailable") + '
'; @@ -237,19 +312,30 @@ function _nwcRenderWalletState() { } var canCreate = !!state.domain && !state.busy; - html += '
' + - '' + - '' + + var walletCount = (state.wallets || []).length; + html += '
' + + '
' + (walletCount === 1 ? '1 wallet' : walletCount + ' wallets') + '
' + + '
' + + '' + + '' + + '
' + '
'; - if (state.domain) { - html += '

Lightning Address domain: ' + escHtml(state.domain) + '

'; - } else { - html += '
Domain required
Configure the Lightning Address domain in the Domain Diagnostic Checklist above before creating Wallet Connections. Use a unique hostname such as lightning.yourdomain.com.
'; + if (!state.domain) { + html += '
' + + '⚠ Lightning Address domain required' + + '
Set up a unique hostname such as lightning.yourdomain.com before creating wallets.
' + + '' + + '
'; } if (!state.wallets || state.wallets.length === 0) { - html += '

No wallet connections yet. Create your first wallet connection to generate a one-time pairing secret.

'; + html += '
' + + '
👛
' + + '
No wallets yet
' + + '

Create your first wallet to get a one-time pairing secret for an app, plus a reusable Lightning Address.

' + + '
'; host.innerHTML = html; + _nwcWireDomainLink(); var openCreateBtn = document.getElementById("nwc-open-create-btn"); if (openCreateBtn) { openCreateBtn.addEventListener("click", function() { @@ -268,25 +354,38 @@ function _nwcRenderWalletState() { state.wallets.forEach(function(wallet) { var id = wallet.id || wallet.pubkey || ""; var addressId = "nwc-wallet-addr-" + Math.random().toString(36).substring(2, 8); + var pending = Number(wallet.pending_transactions || 0); html += '
' + - '
' + escHtml(wallet.name || "Wallet") + '
' + - '
Alias
' + escHtml(wallet.alias || "") + '
' + + '
' + + '
' + escHtml(wallet.name || "Wallet") + '
' + + '
' + + '' + escHtml(_nwcFormatSats(wallet.balance_sats)) + ' sats' + + (pending > 0 + ? '⏳ ' + escHtml(String(pending)) + ' pending' + : '') + + '
' + + '
' + (wallet.lightning_address - ? '
Lightning Address
' + escHtml(wallet.lightning_address) + '
' - : "") + - '
Balance
' + escHtml(String(wallet.balance_sats || 0)) + ' sats
' + - '
Pending TX
' + escHtml(String(wallet.pending_transactions || 0)) + '
' + + ? '
' + + '
Lightning Address
' + + '
' + escHtml(wallet.lightning_address) + '
' + + '
' + + '
' + : '
Alias
' + + '
' + escHtml(wallet.alias || "") + '
') + '
' + '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + '
' + '
'; }); html += "
"; host.innerHTML = html; _attachCopyHandlers(host); + _nwcWireDomainLink(); var createBtn = document.getElementById("nwc-open-create-btn"); if (createBtn) { @@ -375,7 +474,7 @@ async function _nwcCreateWallet() { }; if (!name) { - _nwcSetMessage("error", "Wallet Connection name is required."); + _nwcSetMessage("error", "Wallet name is required."); _nwcRenderWalletState(); return; } @@ -459,7 +558,7 @@ async function _nwcDrainWallet(walletId) { async function _nwcDeleteWallet(walletId) { 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); _nwcClearMessage(); _nwcRenderWalletState(); @@ -500,6 +599,8 @@ async function _nwcInitWalletFlow(unit, name, icon) { async function openServiceDetailModal(unit, name, icon) { if (!$credsModal) return; + var isNwc = _isNwcServiceUnit(unit); + _setCredsDialogWide(isNwc); if ($credsTitle) { $credsTitle.innerHTML = ''; if (icon) { @@ -521,13 +622,24 @@ async function openServiceDetailModal(unit, name, icon) { var url = "/api/service-detail/" + encodeURIComponent(unit); if (icon) url += "?icon=" + encodeURIComponent(icon); 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 setupHtml = ""; + function addSetup(chunk) { + if (isNwc) setupHtml += chunk; + else html += chunk; + } // Section A: Description if (data.description) { - html += '
' + + addSetup('
' + '

' + escHtml(data.description) + '

' + - '
'; + '
'); } // Section B: Status @@ -539,13 +651,13 @@ async function openServiceDetailModal(unit, name, icon) { : (data.health || data.status); var sc = statusClass(effectiveHealth); var st = statusText(effectiveHealth, effectiveEnabled); - html += '
' + + addSetup('
' + '
Status
' + '
' + '' + '' + escHtml(st) + '' + '
' + - '
'; + '
'); // Section B2: BIP-110 live status (bip110 tile only) if (icon === 'bip110' && data.bip110) { @@ -587,15 +699,15 @@ async function openServiceDetailModal(unit, name, icon) { domainActionHtml = ''; } - html += '
' + + addSetup('
' + '
Domain Diagnostic Checklist
' + stepsHtml + domainActionHtml + - '
'; + '
'); if (data.router_ports && data.router_ports.length > 0) { var trimmedInternalIp = data.internal_ip ? String(data.internal_ip).trim() : ""; - html += '
' + + addSetup('
' + '
Ports to Forward in Your Router
' + renderPortForwardGuideHtml(data.router_ports, { internalIp: trimmedInternalIp || null, @@ -603,7 +715,7 @@ async function openServiceDetailModal(unit, name, icon) { introClass: "svc-detail-port-note", noteClass: "svc-detail-port-note", }) + - '
'; + '
'); } } else if (data.port_statuses && data.port_statuses.length > 0) { // 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 - if (_isNwcServiceUnit(unit)) { - html += '
' + - '
Wallet Connections & Node Liquidity
'; + if (isNwc) { if (effectiveEnabled || data.enabled) { - html += '
' + - '
' + - '' + + html += '
' + '
' + - '

Lightning Channel & Liquidity Guide

' + - '

How NWC & Lightning Addresses work on new nodes

' + + '
Your Lightning Wallets
' + + '

Each wallet is isolated: pair it to one app with a one-time secret, and share its Lightning Address to get paid.

' + '
' + '
' + - '

' + - 'Your Alby Hub, NWC pairing keys, and Lightning Address endpoints (like name@yourdomain.com) are set up and working instantly. However, sending and receiving actual Bitcoin payments requires an operational Lightning node with active channels.' + - '

' + - '
' + - '
' + - '
1
' + - '
' + - 'Outbound Liquidity (Sending)' + - '

Required so apps connected via NWC can pay invoices and send funds.

' + + '

Loading wallets…

' + + '
' + + '' + + '' + + '' + + 'Lightning Channel & Liquidity Guide' + + 'Why a brand-new node can\'t send or receive yet' + + '' + + '' + + '
' + + '

' + + 'Your wallets, pairing keys, and Lightning Address endpoints (like name@yourdomain.com) work the moment you create them. Moving actual bitcoin, though, needs a Lightning node with open channels.' + + '

' + + '
' + + '
' + + '
1
' + + '
' + + 'Outbound Liquidity (Sending)' + + '

Required so connected apps can pay invoices and send funds.

' + + '
' + + '
' + + '
' + + '
2
' + + '
' + + 'Inbound Liquidity (Receiving)' + + '

Required so your Lightning Address can receive payments from anyone.

' + + '
' + + '
' + + '
' + + '
' + + '

💧 The Plumbing Analogy: 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.

' + + '' + '
' + '
' + - '
' + - '
2
' + - '
' + - 'Inbound Liquidity (Receiving)' + - '

Required so your Lightning Address can receive incoming payments from anyone.

' + - '
' + - '
' + - '
' + - '
' + - '

💧 The Plumbing Analogy: 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.

' + - '' + - '
' + - '
' + - '

Create, verify, drain, and delete isolated NWC wallet connections without leaving the Hub.

' + - '

Loading wallet connections…

'; + ''; } else { - html += '

Wallet Connections is disabled. Enable this feature below, rebuild, then return here to create and manage NWC wallet connections.

'; + html += '
' + + '
' + + '
Lightning Wallet Connections is turned off
' + + '

Enable it on the Service & Setup tab and rebuild, then come back here to create and manage wallets.

' + + '' + + '
'; } - html += '
'; } else if (data.has_credentials && data.credentials && data.credentials.length > 0) { html += '
' + '
Credentials & Access
' + @@ -733,7 +854,7 @@ async function openServiceDetailModal(unit, name, icon) { conflictsHtml = '
\u26A0 Mutually exclusive with: ' + escHtml(conflictNames.join(", ")) + '
'; } - html += '
' + + addSetup('
' + '
' + addonSectionTitle + '
' + '

' + escHtml(addonDesc) + '

' + conflictsHtml + @@ -741,19 +862,35 @@ async function openServiceDetailModal(unit, name, icon) { '' + addonStatusLabel + '' + '' + '
' + - '
'; + '
'); } if ((effectiveEnabled || data.enabled) && unit !== "phpfpm-nextcloud.service" && unit !== "phpfpm-wordpress.service") { - html += '
' + + addSetup('
' + '
Troubleshooting
' + '

If you\'re experiencing issues with this service, try restarting it.

' + '' + '
' + - '
'; + '
'); + } + + 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; + if (isNwc) _nwcWireTabs(); _attachCopyHandlers($credsBody); if (_isNwcServiceUnit(unit) && (effectiveEnabled || data.enabled)) { await _nwcInitWalletFlow(unit, name, icon); @@ -849,6 +986,7 @@ async function openServiceDetailModal(unit, name, icon) { async function openCredsModal(unit, name, icon) { if (!$credsModal) return; + _setCredsDialogWide(false); if ($credsTitle) { $credsTitle.innerHTML = ''; if (icon) { diff --git a/modules/core/caddy.nix b/modules/core/caddy.nix index 9b56f89..5f6d9e3 100755 --- a/modules/core/caddy.nix +++ b/modules/core/caddy.nix @@ -188,7 +188,7 @@ $HAVEN { EOF fi - # ── Wallet Connections LNURL ────────────────────── + # ── Lightning Wallet Connections LNURL ────────────────────── if [ -n "$LIGHTNING" ]; then cat >> /run/caddy/Caddyfile <