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 ? '
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.' +
'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.' +
'Create a new isolated wallet connection for your app.
' + - 'An isolated wallet you pair to a single app, with its own Lightning Address.
' + + 'Lightning Address domain: ' + escHtml(state.domain) + '
'; - } else { - html += 'No wallet connections yet. Create your first wallet connection to generate a one-time pairing secret.
'; + html += 'Create your first wallet to get a one-time pairing secret for an app, plus a reusable Lightning Address.
' + + '' + escHtml(data.description) + '
' + - 'How NWC & Lightning Addresses work on new nodes
' + + '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.' +
- '
Required so apps connected via NWC can pay invoices and send funds.
' + + 'Loading wallets…
' +
+ '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.' +
+ '
Required so connected apps can pay invoices and send funds.
' + + '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.
' + + '' + '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…
Wallet Connections is disabled. Enable this feature below, rebuild, then return here to create and manage NWC wallet connections.
'; + html += 'Enable it on the Service & Setup tab and rebuild, then come back here to create and manage wallets.
' + + '' + + '' + escHtml(addonDesc) + '
' + conflictsHtml + @@ -741,19 +862,35 @@ async function openServiceDetailModal(unit, name, icon) { '' + addonStatusLabel + '' + '' + 'If you\'re experiencing issues with this service, try restarting it.
' + '' + '' + - '