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:
@@ -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 |
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}$")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ function openDomainSetupModal(feat, onSaved) {
|
||||
|
||||
var nwcWarning = isWalletConnections
|
||||
? '<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. ' +
|
||||
'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' +
|
||||
'</div>'
|
||||
@@ -194,7 +194,7 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
||||
|
||||
var nwcWarning = isWalletConnections
|
||||
? '<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. ' +
|
||||
'Do not reuse a domain already assigned to Matrix, Nextcloud, WordPress, BTCPay Server, Vaultwarden, Haven, or another Caddy site.' +
|
||||
'</div>'
|
||||
|
||||
@@ -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
|
||||
? '<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 & 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() {
|
||||
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 +=
|
||||
'<p class="svc-detail-desc">Create a new isolated wallet connection for your app.</p>' +
|
||||
'<div class="matrix-form-group"><label class="matrix-form-label" for="nwc-wallet-name">Wallet Connection Name</label>' +
|
||||
'<div class="nwc-tab-intro-title">Create a Wallet</div>' +
|
||||
'<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>' +
|
||||
'<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">' +
|
||||
@@ -143,7 +218,7 @@ function _nwcRenderWalletState() {
|
||||
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>';
|
||||
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>' +
|
||||
'<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;
|
||||
html += '<div class="matrix-actions-row">' +
|
||||
'<button class="matrix-action-btn" id="nwc-open-create-btn"' + (canCreate ? "" : " disabled") + '>' + (state.domain ? '➕ Create Wallet Connection' : 'Configure Domain First') + '</button>' +
|
||||
'<button class="matrix-form-back" id="nwc-refresh-btn"' + (state.busy ? " disabled" : "") + '>Refresh</button>' +
|
||||
var walletCount = (state.wallets || []).length;
|
||||
html += '<div class="nwc-toolbar">' +
|
||||
'<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 & Setup</button>' +
|
||||
'</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) {
|
||||
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;
|
||||
_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 += '<div class="nwc-wallet-card">' +
|
||||
'<div class="nwc-wallet-card-head">' +
|
||||
'<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
|
||||
? '<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="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-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>' +
|
||||
? '<div class="nwc-wallet-address">' +
|
||||
'<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">Alias</div>' +
|
||||
'<div class="creds-value-wrap"><div class="creds-value">' + escHtml(wallet.alias || "") + '</div></div></div>') +
|
||||
'<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-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="drain" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Drain</button>' +
|
||||
'<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" data-action="test" data-wallet-alias="' + escHtml(wallet.alias || "") + '"' + (state.busy ? " disabled" : "") + '>Verify</button>' +
|
||||
'<span class="nwc-wallet-actions-spacer"></span>' +
|
||||
'<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>';
|
||||
});
|
||||
html += "</div>";
|
||||
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 += '<div class="svc-detail-section">' +
|
||||
addSetup('<div class="svc-detail-section">' +
|
||||
'<p class="svc-detail-desc">' + escHtml(data.description) + '</p>' +
|
||||
'</div>';
|
||||
'</div>');
|
||||
}
|
||||
|
||||
// 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 += '<div class="svc-detail-section">' +
|
||||
addSetup('<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">Status</div>' +
|
||||
'<div class="svc-detail-status">' +
|
||||
'<span class="status-dot ' + sc + '"></span>' +
|
||||
'<span>' + escHtml(st) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
'</div>');
|
||||
|
||||
// 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 = '<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>' +
|
||||
stepsHtml +
|
||||
domainActionHtml +
|
||||
'</div>';
|
||||
'</div>');
|
||||
|
||||
if (data.router_ports && data.router_ports.length > 0) {
|
||||
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>' +
|
||||
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",
|
||||
}) +
|
||||
'</div>';
|
||||
'</div>');
|
||||
}
|
||||
} 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 += '<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">Wallet Connections & Node Liquidity</div>';
|
||||
if (isNwc) {
|
||||
if (effectiveEnabled || data.enabled) {
|
||||
html += '<div class="nwc-liquidity-guide-card">' +
|
||||
'<div class="nwc-liquidity-header">' +
|
||||
'<span class="nwc-liquidity-icon">⚡</span>' +
|
||||
html += '<div class="nwc-tab-intro">' +
|
||||
'<div>' +
|
||||
'<h4 style="margin:0 0 4px 0; font-size:15px; font-weight:600;">Lightning Channel & Liquidity Guide</h4>' +
|
||||
'<p class="nwc-liquidity-subtitle" style="margin:0; font-size:12px; color:var(--text-secondary);">How NWC & Lightning Addresses work on new nodes</p>' +
|
||||
'<div class="nwc-tab-intro-title">Your Lightning Wallets</div>' +
|
||||
'<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>' +
|
||||
'<p style="font-size:13px; line-height:1.5; margin:0 0 12px 0;">' +
|
||||
'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.' +
|
||||
'<div id="nwc-wallets-body"><p class="creds-loading">Loading wallets…</p></div>' +
|
||||
'<details class="nwc-liquidity-details">' +
|
||||
'<summary class="nwc-liquidity-summary">' +
|
||||
'<span class="nwc-liquidity-icon">⚡</span>' +
|
||||
'<span>' +
|
||||
'<strong>Lightning Channel & 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>' +
|
||||
'<div class="nwc-liquidity-steps">' +
|
||||
'<div class="nwc-liquidity-step">' +
|
||||
'<div class="step-num">1</div>' +
|
||||
'<div>' +
|
||||
'<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 class="nwc-liquidity-step">' +
|
||||
'<div class="step-num">2</div>' +
|
||||
'<div>' +
|
||||
'<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 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>' +
|
||||
'<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>' +
|
||||
'<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 nwc-rtl-btn" id="nwc-open-rtl-btn">🚀 Open Ride The Lightning (RTL) to Manage Channels</button>' +
|
||||
'</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>' +
|
||||
'<div id="nwc-wallets-body"><p class="creds-loading">Loading wallet connections…</p></div>';
|
||||
'</details>';
|
||||
} 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 & 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 & Setup</button>' +
|
||||
'</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
} else if (data.has_credentials && data.credentials && data.credentials.length > 0) {
|
||||
html += '<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">Credentials & 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>';
|
||||
}
|
||||
|
||||
html += '<div class="svc-detail-section">' +
|
||||
addSetup('<div class="svc-detail-section">' +
|
||||
'<div class="svc-detail-section-title">' + addonSectionTitle + '</div>' +
|
||||
'<p class="svc-detail-desc">' + escHtml(addonDesc) + '</p>' +
|
||||
conflictsHtml +
|
||||
@@ -741,19 +862,35 @@ async function openServiceDetailModal(unit, name, icon) {
|
||||
'<span class="svc-detail-addon-status ' + addonStatusCls + '">' + addonStatusLabel + '</span>' +
|
||||
'<button class="' + addonBtnCls + '" id="svc-detail-addon-btn">' + escHtml(addonBtnLabel) + '</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
'</div>');
|
||||
}
|
||||
|
||||
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>' +
|
||||
'<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>' +
|
||||
'<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;
|
||||
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) {
|
||||
|
||||
@@ -188,7 +188,7 @@ $HAVEN {
|
||||
EOF
|
||||
fi
|
||||
|
||||
# ── Wallet Connections LNURL ──────────────────────
|
||||
# ── Lightning Wallet Connections LNURL ──────────────────────
|
||||
if [ -n "$LIGHTNING" ]; then
|
||||
cat >> /run/caddy/Caddyfile <<EOF
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
|
||||
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
|
||||
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";
|
||||
sshd = lib.mkEnableOption "SSH remote access";
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ let
|
||||
{ label = "Server"; value = "tcp://127.0.0.1:50001 (Electrs)"; }
|
||||
{ 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"; }
|
||||
]; }
|
||||
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
|
||||
|
||||
@@ -43,11 +43,11 @@ lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
|
||||
assertions = [
|
||||
{
|
||||
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);
|
||||
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;
|
||||
@@ -134,7 +134,7 @@ lib.mkIf config.sovran_systemsOS.features."nwc-wallets" {
|
||||
};
|
||||
|
||||
systemd.services.nwc-lnurl = {
|
||||
description = "Wallet Connections public LNURL service";
|
||||
description = "Lightning Wallet Connections public LNURL service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "albyhub.service" "sovran-hub-web.service" ];
|
||||
wants = [ "albyhub.service" ];
|
||||
|
||||
Reference in New Issue
Block a user