"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) { qrBlock = '
QR Code for ' + escHtml(cred.label) + '
Scan with Zeus app on your phone
'; } // 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 _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 _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 new isolated wallet connection for your app.

' + '
' + '
' + '
' + '' + '
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 for Wallet Connections pairing secret
Scan now in Zeus or copy the URI below.
'; } html += '
Pairing URI
' + '
' + escHtml(created.pairing_uri || "Unavailable") + '
'; if (created.wallet && created.wallet.lightning_address) { html += '
Lightning Address
' + '
' + escHtml(created.wallet.lightning_address) + '
'; } 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; } html += '
' + '' + '' + '
'; if (state.domain) { html += '

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

'; } else { html += '

Lightning Address domain is not configured yet. Configure your domain first, then create wallet connections.

'; } if (!state.wallets || state.wallets.length === 0) { html += '

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

'; host.innerHTML = html; var openCreateBtn = document.getElementById("nwc-open-create-btn"); if (openCreateBtn) { openCreateBtn.addEventListener("click", function() { if (state.busy) return; _nwcClearMessage(); state.view = "create"; _nwcRenderWalletState(); }); } var refreshBtnEmpty = document.getElementById("nwc-refresh-btn"); if (refreshBtnEmpty) refreshBtnEmpty.addEventListener("click", _nwcRefreshWallets); return; } html += '
'; state.wallets.forEach(function(wallet) { var id = wallet.id || wallet.pubkey || ""; var addressId = "nwc-wallet-addr-" + Math.random().toString(36).substring(2, 8); html += '
' + '
' + escHtml(wallet.name || "Wallet") + '
' + '
Alias
' + escHtml(wallet.alias || "") + '
' + (wallet.lightning_address ? '
Lightning Address
' + escHtml(wallet.lightning_address) + '
' : "") + '
Balance
' + escHtml(String(wallet.balance_sats || 0)) + ' sats
' + '
Pending TX
' + escHtml(String(wallet.pending_transactions || 0)) + '
' + '
' + '' + '' + '' + '
' + '
'; }); html += "
"; host.innerHTML = html; _attachCopyHandlers(host); 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 === "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")); }); }); } 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 = _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 Connection 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 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, createForm: { name: "", alias: "", access_preset: "receive_only", spending_limit_sats: "" } }; await _nwcRefreshWallets(); } async function openServiceDetailModal(unit, name, icon) { if (!$credsModal) return; 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); var html = ""; // Section A: Description if (data.description) { html += '
' + '

' + 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); html += '
' + '
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 += '
' + '
BIP-110 Deployment Status
' + '
' + '' + escHtml(bip110Cfg.label) + '' + bip110Source + '
' + '
'; } // Section C: Domain diagnostics (domain services) if (data.needs_domain) { var steps = data.domain_check_steps || []; var stepsHtml = ""; steps.forEach(function(step) { var iconLabel = "—"; if (step.status === "ok") iconLabel = "✅"; else if (step.status === "error") iconLabel = "❌"; else if (step.status === "warning") iconLabel = "⚠️"; else if (step.status === "skipped") iconLabel = "⏭️"; var detail = escHtml(step.detail || "").replace(/\n/g, "
"); stepsHtml += '
' + '' + iconLabel + ' Step ' + escHtml(String(step.step)) + ': ' + escHtml(step.label || "") + '' + (detail ? '
' + detail + '
' : '') + '
'; }); var domainActionHtml = ""; var ds = data.domain_status || {}; if (!data.domain && data.domain_name) { domainActionHtml = ''; } else if (data.domain && (ds.status === "dns_mismatch" || ds.status === "unresolvable")) { domainActionHtml = ''; } html += '
' + '
Domain Diagnostic Checklist
' + stepsHtml + domainActionHtml + '
'; if (unit === "livekit.service" && data.extra_ports && data.extra_ports.length > 0) { var trimmedInternalIp = data.internal_ip ? String(data.internal_ip).trim() : ""; var internalIp = trimmedInternalIp || ""; var internalIpHtml = internalIp ? escHtml(internalIp) : "Could not detect"; var routerIpHelp = internalIp ? "Use this IP address as the destination/internal IP when creating each router forwarding rule." : "Use this computer’s internal IP as the destination/internal IP when creating each router forwarding rule."; var routerNextStep = internalIp ? 'Next step: Log in to your router and create forwarding rules for the ports above. Set the destination/internal IP to ' + internalIpHtml + '.' : 'Next step: Log in to your router and create forwarding rules for the ports above. Use this computer’s internal IP as the destination/internal IP.'; var domainConfigured = !!(data.domain && String(data.domain).trim()); var extraRows = ""; data.extra_ports.forEach(function(p) { var statusIcon, statusClass2; if (!effectiveEnabled) { statusIcon = "⚠ Configure Element Call first"; statusClass2 = "port-status-open"; } else if (!domainConfigured) { statusIcon = "⚠ Configure domain first"; statusClass2 = "port-status-open"; } else if (p.status === "listening") { statusIcon = "✅ Ready"; statusClass2 = "port-status-listening"; } else if (p.status === "firewall_open") { statusIcon = "✅ Ready"; statusClass2 = "port-status-open"; } else if (p.status === "closed") { statusIcon = "❌ Not ready yet"; statusClass2 = "port-status-closed"; } else { statusIcon = "— Could not check"; statusClass2 = "port-status-unknown"; } extraRows += '' + '' + escHtml(p.port) + '' + '' + escHtml(p.protocol) + '' + '' + escHtml(p.description || "") + '' + '' + statusIcon + '' + ''; }); html += '
' + '
Ports to Forward in Your Router
' + '
Forward these ports in your router to this Sovran_SystemsOS computer.
' + '
Router Forward-To IP: ' + internalIpHtml + '
' + '
' + routerIpHelp + '
' + '' + '' + '' + extraRows + '' + '
PortProtocolUsed ForSovran_SystemsOS Status
' + '
The Hub can check whether Sovran_SystemsOS is ready on this computer, but full public port verification requires an outside internet check.
' + '
' + routerNextStep + '
' + '
'; } } else if (data.port_statuses && data.port_statuses.length > 0) { // Non-domain services (SSH) keep local single-port checks. var portTableRows = ""; data.port_statuses.forEach(function(p) { var statusIcon, statusClass2; if (p.status === "listening") { statusIcon = "✅ Ready"; statusClass2 = "port-status-listening"; } else if (p.status === "firewall_open") { statusIcon = "✅ Ready"; statusClass2 = "port-status-open"; } else if (p.status === "closed") { statusIcon = "❌ Not ready"; statusClass2 = "port-status-closed"; } else { statusIcon = "— Could not check"; statusClass2 = "port-status-unknown"; } portTableRows += '' + '' + escHtml(p.port) + '' + '' + escHtml(p.protocol) + '' + '' + escHtml(p.description || "") + '' + '' + statusIcon + '' + ''; }); html += '
' + '
Port Requirements
' + '
This shows whether Sovran_SystemsOS is ready to use this port on this computer. If you need access from outside your home network, forward this port in your router.
' + '' + '' + '' + portTableRows + '' + '
PortProtocolUsed ForSovran_SystemsOS Status
' + '
'; } // Section E: Credentials & Links if (unit === "nwc-wallets.service") { html += '
' + '
Wallet Connections
' + '

Loading wallet connections…

' + '
'; } else if (data.has_credentials && data.credentials && data.credentials.length > 0) { html += '
' + '
Credentials & Access
' + _renderCredsHtml(data.credentials, unit) + (unit === "matrix-synapse.service" ? '
' + '' + '' + '
' : "") + (unit === "root-password-setup.service" ? '
' + '' + '
' : "") + '
'; } else if (!data.enabled && !data.feature) { html += '
' + '

This service is not enabled in your configuration.

' + '
'; } // Section F: Addon Feature toggle if (data.feature) { var feat = data.feature; // Sync this feature into _featuresData so handleFeatureToggle can look up conflicts / ssl state if (!_featuresData) { _featuresData = { features: [feat], ssl_email_configured: false }; } else { var fidx = _featuresData.features.findIndex(function(f) { return f.id === feat.id; }); if (fidx >= 0) { _featuresData.features[fidx] = feat; } else { _featuresData.features.push(feat); } } var addonStatusLabel = feat.enabled ? "Enabled \u2713" : "Disabled"; var addonStatusCls = feat.enabled ? "addon-status--on" : "addon-status--off"; var addonBtnLabel = feat.enabled ? "Disable Feature" : "Enable Feature"; var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary"; // Section title: use a more specific label for mutually-exclusive Bitcoin node features var addonSectionTitle = (feat.id === "bitcoin-core") ? "\u20BF Bitcoin Node Selection" : "\uD83D\uDD27 Addon Feature"; // Description: prefer the feature's own description over a generic fallback var addonDesc = feat.description ? feat.description : "This is an optional addon feature. You can enable or disable it at any time."; // Conflicts warning: list mutually-exclusive feature names when present var conflictsHtml = ""; if (feat.conflicts_with && feat.conflicts_with.length > 0) { var conflictNames = feat.conflicts_with.map(function(cid) { if (_featuresData && Array.isArray(_featuresData.features)) { var cf = _featuresData.features.find(function(f) { return f.id === cid; }); if (cf) return cf.name; } return cid; }); conflictsHtml = '
\u26A0 Mutually exclusive with: ' + escHtml(conflictNames.join(", ")) + '
'; } html += '
' + '
' + addonSectionTitle + '
' + '

' + escHtml(addonDesc) + '

' + conflictsHtml + '
' + '' + addonStatusLabel + '' + '' + '
' + '
'; } if ((effectiveEnabled || data.enabled) && unit !== "phpfpm-nextcloud.service" && unit !== "phpfpm-wordpress.service") { html += '
' + '
Troubleshooting
' + '

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

' + '' + '
' + '
'; } $credsBody.innerHTML = html; _attachCopyHandlers($credsBody); if (unit === "nwc-wallets.service") { await _nwcInitWalletFlow(unit, name, icon); } 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; 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 += '
' + '' + '' + '
'; } $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 = '
' + '
' + '
' + '
' + '
' + '
' + '' + '' + '
' + '
'; document.getElementById("matrix-create-back-btn").addEventListener("click", function() { openServiceDetailModal(unit, name, icon); }); document.getElementById("matrix-create-submit-btn").addEventListener("click", async function() { var submitBtn = document.getElementById("matrix-create-submit-btn"); var resultEl = document.getElementById("matrix-create-result"); var username = (document.getElementById("matrix-new-username").value || "").trim(); var password = document.getElementById("matrix-new-password").value || ""; var isAdmin = document.getElementById("matrix-new-admin").checked; if (!username || !password) { resultEl.className = "matrix-form-result error"; resultEl.textContent = "Username and password are required."; return; } submitBtn.disabled = true; submitBtn.textContent = "Creating…"; resultEl.className = "matrix-form-result"; resultEl.textContent = ""; try { var resp = await apiFetch("/api/matrix/create-user", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username, password: password, admin: isAdmin }) }); resultEl.className = "matrix-form-result success"; resultEl.textContent = "✅ User @" + escHtml(resp.username) + " created successfully."; submitBtn.textContent = "Create User"; submitBtn.disabled = false; } catch (err) { resultEl.className = "matrix-form-result error"; resultEl.textContent = "❌ " + (err.message || "Failed to create user."); submitBtn.textContent = "Create User"; submitBtn.disabled = false; } }); } function openMatrixChangePasswordModal(unit, name, icon) { if (!$credsBody) return; $credsBody.innerHTML = '
' + '
' + '
' + '
' + '
' + '' + '' + '
' + '
'; document.getElementById("matrix-chpw-back-btn").addEventListener("click", function() { openServiceDetailModal(unit, name, icon); }); document.getElementById("matrix-chpw-submit-btn").addEventListener("click", async function() { var submitBtn = document.getElementById("matrix-chpw-submit-btn"); var resultEl = document.getElementById("matrix-chpw-result"); var username = (document.getElementById("matrix-chpw-username").value || "").trim(); var newPassword = document.getElementById("matrix-chpw-password").value || ""; if (!username || !newPassword) { resultEl.className = "matrix-form-result error"; resultEl.textContent = "Username and new password are required."; return; } submitBtn.disabled = true; submitBtn.textContent = "Changing…"; resultEl.className = "matrix-form-result"; resultEl.textContent = ""; try { var resp = await apiFetch("/api/matrix/change-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username, new_password: newPassword }) }); resultEl.className = "matrix-form-result success"; resultEl.textContent = "✅ Password for @" + escHtml(resp.username) + " changed successfully."; submitBtn.textContent = "Change Password"; submitBtn.disabled = false; } catch (err) { resultEl.className = "matrix-form-result error"; resultEl.textContent = "❌ " + (err.message || "Failed to change password."); submitBtn.textContent = "Change Password"; submitBtn.disabled = false; } }); } function openSystemChangePasswordModal(unit, name, icon) { if (!$credsBody) return; $credsBody.innerHTML = '
' + '
🔑 Change Free Account & Hub Login Password
' + '
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.
' + '
' + '' + '' + '
' + '
'; document.getElementById("sys-chpw-back-btn").addEventListener("click", function() { openServiceDetailModal(unit, name, icon); }); document.getElementById("sys-chpw-new-toggle").addEventListener("click", function() { var inp = document.getElementById("sys-chpw-new"); var isHidden = inp.type === "password"; inp.type = isHidden ? "text" : "password"; this.textContent = isHidden ? "👁‍🗨" : "👁"; }); document.getElementById("sys-chpw-confirm-toggle").addEventListener("click", function() { var inp = document.getElementById("sys-chpw-confirm"); var isHidden = inp.type === "password"; inp.type = isHidden ? "text" : "password"; this.textContent = isHidden ? "👁‍🗨" : "👁"; }); document.getElementById("sys-chpw-submit-btn").addEventListener("click", async function() { var submitBtn = document.getElementById("sys-chpw-submit-btn"); var resultEl = document.getElementById("sys-chpw-result"); var newPassword = document.getElementById("sys-chpw-new").value || ""; var confirmPassword = document.getElementById("sys-chpw-confirm").value || ""; if (!newPassword || !confirmPassword) { resultEl.className = "matrix-form-result error"; resultEl.textContent = "Both password fields are required."; return; } if (newPassword.length < 8) { resultEl.className = "matrix-form-result error"; resultEl.textContent = "Password must be at least 8 characters."; return; } if (newPassword !== confirmPassword) { resultEl.className = "matrix-form-result error"; resultEl.textContent = "Passwords do not match."; return; } submitBtn.disabled = true; submitBtn.textContent = "Changing…"; resultEl.className = "matrix-form-result"; resultEl.textContent = ""; try { await apiFetch("/api/change-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ new_password: newPassword, confirm_password: confirmPassword }) }); resultEl.className = "matrix-form-result success"; resultEl.textContent = "✅ Free account & Hub login password changed successfully."; submitBtn.textContent = "Change Password"; submitBtn.disabled = false; } catch (err) { resultEl.className = "matrix-form-result error"; resultEl.textContent = "❌ " + (err.message || "Failed to change password."); submitBtn.textContent = "Change Password"; submitBtn.disabled = false; } }); } function closeCredsModal() { if ($credsModal) $credsModal.classList.remove("open"); }