"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 = '
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();
}
});
});
}
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 += '
';
});
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 internalIp = (data.internal_ip && String(data.internal_ip).trim()) ? String(data.internal_ip).trim() : "";
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 + '
' +
'
' +
'
Port
Protocol
Used For
Sovran_SystemsOS Status
' +
'' + extraRows + '' +
'
' +
'
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.
' +
'
' +
'
Port
Protocol
Used For
Sovran_SystemsOS Status
' +
'' + portTableRows + '' +
'
' +
'
';
}
// Section E: Credentials & Links
if (data.has_credentials && data.credentials && data.credentials.length > 0) {
html += '
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.