"use strict";
// ── Service detail modal ──────────────────────────────────────────
function _renderCredsHtml(credentials, unit) {
var html = "";
for (var i = 0; i < credentials.length; i++) {
var cred = credentials[i];
var id = "cred-" + Math.random().toString(36).substring(2, 8);
var qrBlock = "";
if (cred.qrcode) {
var qrHint = (unit === "zeus-connect-setup.service")
? "In Zeus: Settings → Connect a node → Scan LN node QR. This is an LND REST QR for direct node access."
: "In Zeus: Wallets → + → scan icon. This is an LND REST QR for direct node access.";
qrBlock = '
' + qrHint + '
';
}
// If qronly, render the label + QR block only — skip value and copy button
if (cred.qronly) {
html += '
' + escHtml(cred.label) + '
' + qrBlock + '
';
continue;
}
var displayValue = linkify(cred.value);
html += '
' + escHtml(cred.label) + '
' + qrBlock + '
' + displayValue + '
';
}
return html;
}
function _attachCopyHandlers(container) {
container.querySelectorAll(".creds-copy-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
var target = document.getElementById(btn.dataset.target);
if (!target) return;
var text = target.textContent;
function onSuccess() {
btn.textContent = "Copied!";
btn.classList.add("copied");
setTimeout(function() { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 1500);
}
function fallbackCopy() {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
onSuccess();
} catch (e) {}
document.body.removeChild(ta);
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(onSuccess).catch(fallbackCopy);
} else {
fallbackCopy();
}
});
});
}
var _nwcModalState = null;
function _isNwcServiceUnit(unit) {
// The dashboard tile is backed by Alby Hub, while older drafts referred to
// the logical feature as nwc-wallets.service. Support both so the Wallet
// Connections manager appears from the actual tile and any legacy configs.
return unit === "albyhub.service" || unit === "nwc-wallets.service";
}
function _nwcStateMessageHtml() {
if (!_nwcModalState || !_nwcModalState.message) return "";
var msgClass = _nwcModalState.messageKind === "success" ? "success" : "error";
return '
' + escHtml(_nwcModalState.message) + '
';
}
function _nwcSetMessage(kind, text) {
if (!_nwcModalState) return;
_nwcModalState.messageKind = kind || "error";
_nwcModalState.message = text || "";
}
function _nwcClearMessage() {
if (!_nwcModalState) return;
_nwcModalState.messageKind = "";
_nwcModalState.message = "";
}
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 '
';
}
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;
var state = _nwcModalState;
var html = "";
html += _nwcStateMessageHtml();
if (state.view === "create") {
var selectedLimited = state.createForm.access_preset === "send_receive_limited";
html +=
'
Create a Wallet
' +
'
Create a secure, sandboxed wallet for a specific app. Each wallet gets its own Lightning Address and optional spending limit.
' +
'
' +
'
' +
'
' +
'' +
'
Lowercase letters, numbers, "_" and "-" only.
' +
'
' +
'
' +
'
' +
'
" +
'
' +
'' +
'' +
'
';
host.innerHTML = html;
var presetSel = document.getElementById("nwc-wallet-preset");
if (presetSel) {
presetSel.addEventListener("change", function() {
state.createForm.access_preset = presetSel.value;
_nwcRenderWalletState();
});
}
var cancelBtn = document.getElementById("nwc-create-cancel-btn");
if (cancelBtn) {
cancelBtn.addEventListener("click", function() {
if (state.busy) return;
state.view = state.wallets.length > 0 ? "list" : "empty";
_nwcClearMessage();
_nwcRenderWalletState();
});
}
var submitBtn = document.getElementById("nwc-create-submit-btn");
if (submitBtn) submitBtn.addEventListener("click", _nwcCreateWallet);
return;
}
if (state.view === "created" && state.lastCreated) {
var created = state.lastCreated;
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
' +
'
This is an NWC pairing QR — in Zeus, add a wallet and use the scan icon (see steps below).
';
if (created.wallet && created.wallet.lightning_address) {
html += '
Lightning Address
' +
'
' + escHtml(created.wallet.lightning_address) + '
';
}
html += '
' +
'
📱 Connect to Zeus
' +
'
This pairing URI is an NWC (Nostr Wallet Connect) connection — the modern, mobile-friendly way to use Zeus with your node. It connects directly through your Lightning domain, so no Tor or port forwarding is needed on your phone.
' +
'
' +
'
1
Download Zeus from the App Store or Google Play.
' +
'
2
Open Zeus and open the Wallets screen.
' +
'
3
Tap the + (Add Wallet) button in the top-right corner.
' +
'
4
On Wallet Configuration, tap the scan icon in the top-right corner, then scan the QR code above.
' +
'
5
Zeus detects the NWC QR and fills in Nostr Wallet Connect. Review it, then tap Save Wallet Config.
' +
'
' +
'
💡 Note: This is not the same as the LND REST / Tor QR shown on your LND tile — that connects Zeus directly to your Lightning node for full admin control. NWC gives your wallet sandboxed, limited access for everyday spending.
' +
'
';
html += '
' +
'' +
'' +
'
';
host.innerHTML = html;
_attachCopyHandlers(host);
var continueBtn = document.getElementById("nwc-created-continue-btn");
if (continueBtn) {
continueBtn.addEventListener("click", async function() {
if (state.busy) return;
state.lastCreated = null;
state.view = "list";
await _nwcRefreshWallets();
});
}
var anotherBtn = document.getElementById("nwc-created-another-btn");
if (anotherBtn) {
anotherBtn.addEventListener("click", function() {
if (state.busy) return;
state.view = "create";
_nwcClearMessage();
_nwcRenderWalletState();
});
}
return;
}
if (state.view === "share" && state.shareWallet) {
var sw = state.shareWallet;
var swId = encodeURIComponent(String(sw.id || sw.pubkey || ""));
var swAddress = sw.lightning_address || ((sw.alias && state.domain) ? sw.alias + "@" + state.domain : "");
var pngUrl = "/api/nwc/wallets/" + swId + "/lnurl-qr.png";
var shareAddrId = "nwc-share-addr-" + Math.random().toString(36).substring(2, 8);
var shareLnurlId = "nwc-share-lnurl-" + Math.random().toString(36).substring(2, 8);
html += '
Share the Lightning Address for ' + escHtml(sw.name || "Wallet") + '. This QR never expires — anyone can scan it with any Lightning wallet to send sats, as many times as they like.
' +
'
' +
'' +
'
Scan with any Lightning wallet to pay ' + escHtml(swAddress) + '
' +
'
' +
(swAddress
? '
Lightning Address
' + escHtml(swAddress) + '
'
: "") +
'
LNURL
Loading…
' +
'
' +
'' +
'' +
'' +
'' +
'
' +
'
PNG is ideal for websites and social posts. SVG stays sharp at any print size.
';
host.innerHTML = html;
_attachCopyHandlers(host);
apiFetch("/api/nwc/wallets/" + swId + "/lnurl").then(function(payload) {
var el = document.getElementById(shareLnurlId);
if (el) el.textContent = payload.lnurl || "Unavailable";
}).catch(function() {
var el = document.getElementById(shareLnurlId);
if (el) el.textContent = "Unavailable";
});
function _nwcShareDownload(url) {
var a = document.createElement("a");
a.href = url;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
a.remove();
}
var dlPngBtn = document.getElementById("nwc-share-dl-png-btn");
if (dlPngBtn) dlPngBtn.addEventListener("click", function() { _nwcShareDownload(pngUrl + "?download=1"); });
var dlSvgBtn = document.getElementById("nwc-share-dl-svg-btn");
if (dlSvgBtn) dlSvgBtn.addEventListener("click", function() { _nwcShareDownload("/api/nwc/wallets/" + swId + "/lnurl-qr.svg?download=1"); });
var printBtn = document.getElementById("nwc-share-print-btn");
if (printBtn) printBtn.addEventListener("click", function() { window.open("/api/nwc/wallets/" + swId + "/lnurl-qr/print", "_blank", "noopener"); });
var shareBackBtn = document.getElementById("nwc-share-back-btn");
if (shareBackBtn) {
shareBackBtn.addEventListener("click", function() {
state.view = "list";
state.shareWallet = null;
_nwcClearMessage();
_nwcRenderWalletState();
});
}
return;
}
var canCreate = !!state.domain && !state.busy;
var walletCount = (state.wallets || []).length;
html += '
";
host.innerHTML = html;
_attachCopyHandlers(host);
_nwcWireDomainLink();
var createBtn = document.getElementById("nwc-open-create-btn");
if (createBtn) {
createBtn.addEventListener("click", function() {
if (state.busy) return;
_nwcClearMessage();
state.view = "create";
_nwcRenderWalletState();
});
}
var refreshBtn = document.getElementById("nwc-refresh-btn");
if (refreshBtn) refreshBtn.addEventListener("click", _nwcRefreshWallets);
host.querySelectorAll(".nwc-wallet-action-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
var action = btn.getAttribute("data-action");
if (action === "share") _nwcOpenShare(btn.getAttribute("data-wallet-id"));
else if (action === "test") _nwcVerifyWallet(btn.getAttribute("data-wallet-alias"));
else if (action === "drain") _nwcDrainWallet(btn.getAttribute("data-wallet-id"));
else if (action === "delete") _nwcDeleteWallet(btn.getAttribute("data-wallet-id"));
});
});
}
function _nwcOpenShare(walletId) {
if (!_nwcModalState || _nwcModalState.busy || !walletId) return;
var wallet = null;
for (var i = 0; i < _nwcModalState.wallets.length; i++) {
var w = _nwcModalState.wallets[i];
if (String(w.id || w.pubkey || "") === String(walletId)) { wallet = w; break; }
}
if (!wallet) {
_nwcSetMessage("error", "Wallet connection not found.");
_nwcRenderWalletState();
return;
}
_nwcClearMessage();
_nwcModalState.shareWallet = wallet;
_nwcModalState.view = "share";
_nwcRenderWalletState();
}
async function _nwcRefreshWallets() {
if (!_nwcModalState) return;
var host = _nwcWalletSectionEl();
if (host) host.innerHTML = '
Loading wallet connections…
';
try {
var payload = await apiFetch("/api/nwc/wallets");
_nwcModalState.wallets = Array.isArray(payload.wallets) ? payload.wallets : [];
_nwcModalState.domain = payload.domain || null;
if (_nwcModalState.view !== "create" && _nwcModalState.view !== "created" && _nwcModalState.view !== "share") {
_nwcModalState.view = _nwcModalState.wallets.length > 0 ? "list" : "empty";
}
_nwcRenderWalletState();
} catch (err) {
_nwcSetMessage("error", (err && err.message) ? err.message : "Could not load wallet connections.");
_nwcRenderWalletState();
}
}
function _nwcBusy(on) {
if (!_nwcModalState) return;
_nwcModalState.busy = !!on;
}
async function _nwcCreateWallet() {
if (!_nwcModalState || _nwcModalState.busy) return;
var nameEl = document.getElementById("nwc-wallet-name");
var aliasEl = document.getElementById("nwc-wallet-alias");
var presetEl = document.getElementById("nwc-wallet-preset");
var limitEl = document.getElementById("nwc-wallet-limit");
if (!nameEl || !aliasEl || !presetEl || !limitEl) return;
var name = (nameEl.value || "").trim();
var alias = (aliasEl.value || "").trim().toLowerCase();
var preset = presetEl.value || "receive_only";
var limitRaw = (limitEl.value || "").trim();
var limit = null;
_nwcModalState.createForm = {
name: name,
alias: alias,
access_preset: preset,
spending_limit_sats: limitRaw
};
if (!name) {
_nwcSetMessage("error", "Wallet name is required.");
_nwcRenderWalletState();
return;
}
if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(alias)) {
_nwcSetMessage("error", 'Alias must start with a letter or number and use only lowercase letters, numbers, "_" or "-".');
_nwcRenderWalletState();
return;
}
if (preset === "send_receive_limited") {
limit = parseInt(limitRaw, 10);
if (!Number.isFinite(limit) || limit <= 0) {
_nwcSetMessage("error", "A positive spending limit is required for limited send access.");
_nwcRenderWalletState();
return;
}
}
_nwcBusy(true);
_nwcClearMessage();
_nwcRenderWalletState();
try {
var payload = await apiFetch("/api/nwc/wallets", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name,
alias: alias,
access_preset: preset,
spending_limit_sats: preset === "send_receive_limited" ? limit : null
})
});
_nwcModalState.lastCreated = {
wallet: payload.wallet || null,
pairing_uri: payload.pairing_uri || "",
pairing_qrcode: payload.pairing_qrcode || "",
lightning_address: payload.lightning_address || null
};
_nwcModalState.view = "created";
_nwcSetMessage("success", "Wallet created. Save the one-time secret before you continue.");
_nwcBusy(false);
_nwcRenderWalletState();
} catch (err) {
_nwcBusy(false);
_nwcSetMessage("error", (err && err.message) ? err.message : "Failed to create wallet connection.");
_nwcRenderWalletState();
}
}
async function _nwcVerifyWallet(alias) {
if (!_nwcModalState || _nwcModalState.busy || !alias) return;
_nwcBusy(true);
_nwcClearMessage();
_nwcRenderWalletState();
try {
await apiFetch("/api/nwc/addresses/" + encodeURIComponent(alias) + "/test", { method: "POST" });
_nwcSetMessage("success", "Lightning Address verification succeeded for " + alias + ".");
} catch (err) {
_nwcSetMessage("error", (err && err.message) ? err.message : "Lightning Address verification failed.");
}
_nwcBusy(false);
_nwcRenderWalletState();
}
async function _nwcDrainWallet(walletId) {
if (!_nwcModalState || _nwcModalState.busy || !walletId) return;
if (!window.confirm("Drain this wallet connection now? This cannot be undone.")) return;
_nwcBusy(true);
_nwcClearMessage();
_nwcRenderWalletState();
try {
var resp = await apiFetch("/api/nwc/wallets/" + encodeURIComponent(walletId) + "/drain", { method: "POST" });
_nwcSetMessage("success", "Wallet drained (" + String(resp.drained_sats || 0) + " sats).");
_nwcBusy(false);
await _nwcRefreshWallets();
} catch (err) {
_nwcBusy(false);
_nwcSetMessage("error", (err && err.message) ? err.message : "Failed to drain wallet.");
_nwcRenderWalletState();
}
}
async function _nwcDeleteWallet(walletId) {
if (!_nwcModalState || _nwcModalState.busy || !walletId) return;
if (!window.confirm("Delete this wallet connection? This removes the wallet alias from Lightning Wallet Connections.")) return;
_nwcBusy(true);
_nwcClearMessage();
_nwcRenderWalletState();
try {
await apiFetch("/api/nwc/wallets/" + encodeURIComponent(walletId), { method: "DELETE" });
_nwcSetMessage("success", "Wallet connection deleted.");
_nwcBusy(false);
await _nwcRefreshWallets();
} catch (err) {
_nwcBusy(false);
_nwcSetMessage("error", (err && err.message) ? err.message : "Failed to delete wallet connection.");
_nwcRenderWalletState();
}
}
async function _nwcInitWalletFlow(unit, name, icon) {
_nwcModalState = {
unit: unit,
name: name,
icon: icon,
view: "empty",
wallets: [],
domain: null,
busy: false,
message: "",
messageKind: "",
lastCreated: null,
shareWallet: null,
createForm: {
name: "",
alias: "",
access_preset: "receive_only",
spending_limit_sats: ""
}
};
await _nwcRefreshWallets();
}
async function openServiceDetailModal(unit, name, icon) {
if (!$credsModal) return;
var isNwc = _isNwcServiceUnit(unit);
_setCredsDialogWide(isNwc);
if ($credsTitle) {
$credsTitle.innerHTML = '';
if (icon) {
var iconImg = document.createElement("img");
iconImg.className = "creds-title-icon";
iconImg.src = "/static/icons/" + escHtml(icon) + ".svg";
iconImg.alt = name;
iconImg.onerror = function() { this.style.display = "none"; };
$credsTitle.appendChild(iconImg);
}
var nameSpan = document.createElement("span");
nameSpan.textContent = name;
$credsTitle.appendChild(nameSpan);
}
if ($credsBody) $credsBody.innerHTML = '
Loading…
';
$credsModal.classList.add("open");
try {
var url = "/api/service-detail/" + encodeURIComponent(unit);
if (icon) url += "?icon=" + encodeURIComponent(icon);
var data = await apiFetch(url);
// Append version badge next to title if version is detected
var serviceVersion = data.version || data.bitcoin_version || '';
if (serviceVersion && $credsTitle) {
var existingBadge = $credsTitle.querySelector(".creds-title-version-badge");
if (existingBadge) {
existingBadge.textContent = serviceVersion;
} else {
var badge = document.createElement("span");
badge.className = "creds-title-version-badge";
badge.textContent = serviceVersion;
$credsTitle.appendChild(badge);
}
}
// 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) {
addSetup('
' +
'
' + escHtml(data.description) + '
' +
'
');
}
// Section B: Status
// When a feature override is present, use the feature's enabled state so the
// modal matches what the dashboard tile shows (feature toggle is authoritative).
var effectiveEnabled = data.feature ? data.feature.enabled : data.enabled;
var effectiveHealth = data.feature && !data.feature.enabled
? "disabled"
: (data.health || data.status);
var sc = statusClass(effectiveHealth);
var st = statusText(effectiveHealth, effectiveEnabled);
addSetup('
' +
'
Status
' +
'
' +
'' +
'' + escHtml(st) + '' +
'
' +
'
');
// Section B2: BIP-110 live status (bip110 tile only)
if (icon === 'bip110' && data.bip110) {
var bip110 = data.bip110;
var bip110State = bip110.state || 'unknown';
var bip110Cfg = BIP110_BADGE_CONFIG[bip110State] || BIP110_BADGE_CONFIG.unknown;
var bip110Source = bip110.source ? ' (source: ' + escHtml(bip110.source) + ')' : '';
html += '
');
}
} else if (data.port_statuses && data.port_statuses.length > 0) {
// Non-domain services (e.g. SSH): show what to forward, plus a short
// note if the service isn't actually listening on this computer yet.
var localInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
var notListening = data.port_statuses.filter(function(p) {
return p.status === "closed";
});
var localNote = notListening.length
? '
' +
'⚠ Port ' + escHtml(notListening.map(function(p) { return p.port; }).join(", ")) +
' is not open on this computer yet — enable the service below first, then forward it in your router.' +
'
'
: "";
html += '
' +
'
Ports to Forward in Your Router
' +
'
Only needed if you want to reach this service from outside your home network. On your local network it already works without any router changes.
' +
'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.
' +
'' +
'
' +
'
' +
'';
} else {
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.
' +
'' +
'
';
}
} else if (data.has_credentials && data.credentials && data.credentials.length > 0) {
html += '
');
}
if ((effectiveEnabled || data.enabled) && unit !== "phpfpm-nextcloud.service" && unit !== "phpfpm-wordpress.service") {
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);
var nwcRtlBtn = document.getElementById("nwc-open-rtl-btn");
if (nwcRtlBtn) {
nwcRtlBtn.addEventListener("click", function() {
openServiceDetailModal("rtl.service", "Ride The Lightning", "rtl");
});
}
}
if (unit === "matrix-synapse.service") {
var addBtn = document.getElementById("matrix-add-user-btn");
var changePwBtn = document.getElementById("matrix-change-pw-btn");
if (addBtn) addBtn.addEventListener("click", function() { openMatrixCreateUserModal(unit, name, icon); });
if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(unit, name, icon); });
}
if (unit === "root-password-setup.service") {
var sysPwBtn = document.getElementById("sys-change-pw-btn");
if (sysPwBtn) sysPwBtn.addEventListener("click", function() { openSystemChangePasswordModal(unit, name, icon); });
}
if (data.feature) {
var addonBtn = document.getElementById("svc-detail-addon-btn");
if (addonBtn) {
var addonFeat = data.feature;
addonBtn.addEventListener("click", function() {
closeCredsModal();
handleFeatureToggle(addonFeat, !addonFeat.enabled);
});
}
}
var restartBtn = document.getElementById("svc-detail-restart-btn");
var restartResult = document.getElementById("svc-detail-restart-result");
if (restartBtn && restartResult) {
var RESTART_REFRESH_DELAY_MS = 3000;
restartBtn.addEventListener("click", async function() {
restartBtn.disabled = true;
restartBtn.textContent = "Restarting…";
restartResult.className = "svc-detail-restart-result";
restartResult.textContent = "";
try {
await apiFetch("/api/service/" + encodeURIComponent(unit) + "/restart", { method: "POST" });
restartResult.classList.add("success");
restartResult.textContent = "✅ Service restarted successfully.";
restartBtn.disabled = false;
restartBtn.textContent = "🔄 Restart Service";
setTimeout(function() {
openServiceDetailModal(unit, name, icon);
}, RESTART_REFRESH_DELAY_MS);
} catch (e) {
restartResult.classList.add("error");
restartResult.textContent = e && e.message ? e.message : "Failed to restart service. Please check service logs and try again.";
restartBtn.disabled = false;
restartBtn.textContent = "🔄 Restart Service";
}
});
}
// Configure / Reconfigure Domain buttons (for non-feature services that need a domain)
var configDomainBtn = document.getElementById("svc-detail-config-domain-btn");
var reconfigDomainBtn = document.getElementById("svc-detail-reconfig-domain-btn");
if ((configDomainBtn || reconfigDomainBtn) && data.needs_domain && data.domain_name) {
var pseudoFeat = {
id: data.domain_name,
name: name,
domain_name: data.domain_name,
needs_ddns: true,
extra_fields: []
};
if (configDomainBtn) configDomainBtn.addEventListener("click", function() {
closeCredsModal();
openDomainSetupModal(pseudoFeat, function() {
openServiceDetailModal(unit, name, icon);
});
});
if (reconfigDomainBtn) reconfigDomainBtn.addEventListener("click", function() {
closeCredsModal();
openDomainReconfigureModal(pseudoFeat, data.domain || "", function() {
openServiceDetailModal(unit, name, icon);
});
});
}
} catch (err) {
if ($credsBody) $credsBody.innerHTML = '
Could not load service details.
';
}
}
// ── Credentials info modal ────────────────────────────────────────
async function openCredsModal(unit, name, icon) {
if (!$credsModal) return;
_setCredsDialogWide(false);
if ($credsTitle) {
$credsTitle.innerHTML = '';
if (icon) {
var iconImg = document.createElement("img");
iconImg.className = "creds-title-icon";
iconImg.src = "/static/icons/" + escHtml(icon) + ".svg";
iconImg.alt = name;
iconImg.onerror = function() { this.style.display = "none"; };
$credsTitle.appendChild(iconImg);
}
var nameSpan = document.createElement("span");
nameSpan.textContent = name + " — Connection Info";
$credsTitle.appendChild(nameSpan);
}
if ($credsBody) $credsBody.innerHTML = '
Loading…
';
$credsModal.classList.add("open");
try {
var data = await apiFetch("/api/credentials/" + encodeURIComponent(unit));
if (!data.credentials || data.credentials.length === 0) {
$credsBody.innerHTML = '
No connection info available yet.
';
return;
}
var html = _renderCredsHtml(data.credentials, unit);
if (unit === "matrix-synapse.service") {
html += '
' +
'' +
'' +
'
';
}
if (unit === "zeus-connect-setup.service") {
html += '
' +
'
📱 Connect to Zeus (Direct Node Access)
' +
'
This QR is an LND REST connection — the direct way to use Zeus with your Lightning node for full admin control. It lets you manage channels, balances, and payments from your phone.
' +
'
' +
'
1
Download Zeus from the App Store or Google Play.
' +
'
2
Open Zeus and go to Settings → Connect a node (or Scan Node Config).
' +
'
3
Tap Scan LN node QR and scan the QR code above.
' +
'
4
Review the connection details, then tap Save Node Config.
' +
'
' +
'
💡 Note: This is not the same as the NWC pairing QR shown in Lightning Wallet Connections — that gives your wallet sandboxed, limited access for everyday spending. LND REST connects Zeus directly to your node for full admin control.
' +
'
';
}
$credsBody.innerHTML = html;
_attachCopyHandlers($credsBody);
if (unit === "matrix-synapse.service") {
var addBtn = document.getElementById("matrix-add-user-btn");
var changePwBtn = document.getElementById("matrix-change-pw-btn");
if (addBtn) addBtn.addEventListener("click", function() { openMatrixCreateUserModal(unit, name); });
if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(unit, name); });
}
} catch (err) {
$credsBody.innerHTML = '
Could not load credentials.
';
}
}
function openMatrixCreateUserModal(unit, name, icon) {
if (!$credsBody) return;
$credsBody.innerHTML =
'
This updates the password for the free user account. This is also your Sovran Hub login password — both will change.
' +
'
' +
'
' +
'
' +
'' +
'' +
'
' +
'
Password must be at least 8 characters.
' +
'
' +
'
' +
'' +
'' +
'
' +
'
⚠ This will change both your desktop login and Hub login password. After changing, your updated password will appear in the System Passwords credentials tile.