From aa4ffaa803bb051925c71c38085f6bbfc33b0c36 Mon Sep 17 00:00:00 2001 From: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> Date: Wed, 29 Jul 2026 01:59:28 +0000 Subject: [PATCH] Rename NWC feature to Lightning Wallet Connections and split modal into tabs Naming: user-facing 'Wallet Connections' -> 'Lightning Wallet Connections' across the Hub, feature registry, tile, and NixOS modules. Internal ids (nwc-wallets, albyhub.service, /api/nwc/*) are unchanged. UX: the service-detail modal put status, domain diagnostics, router ports, the enable/disable toggle, restart, the liquidity guide and the whole wallet manager in one cramped scrolling column. For this feature the modal is now 980px wide and split into two tabs: - Wallets: wallet grid, create/share/verify flows, collapsible liquidity guide - Service & Setup: description, status, domain checklist, ports, enable, restart A status dot and domain chip sit in the tab bar so state is visible from both tabs, and the modal opens on Setup when the service is off or the Lightning Address domain is unconfigured. Wallet cards gain a balance chip, pending badge, a prominent address row, and separated destructive actions. Non-NWC services keep the original single-column layout and width. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- README.md | 2 +- app/sovran_systemsos_web/nwc_hub_manager.py | 2 +- app/sovran_systemsos_web/nwc_lnurl_service.py | 2 +- app/sovran_systemsos_web/server.py | 8 +- .../static/css/modals.css | 433 +++++++++++++++++- .../static/js/features.js | 4 +- .../static/js/service-detail.js | 274 ++++++++--- modules/core/caddy.nix | 2 +- modules/core/roles.nix | 2 +- modules/core/sovran-hub.nix | 2 +- modules/nwc-wallets.nix | 6 +- 11 files changed, 636 insertions(+), 101 deletions(-) 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 <