Add domain health status to hub tiles and Feature Manager

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/52147672-b757-4524-971a-9e0dab981354

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-04 14:49:30 +00:00
committed by GitHub
parent a3c75462c9
commit 8002b180b1
3 changed files with 294 additions and 6 deletions

View File

@@ -269,7 +269,16 @@ function buildTile(svc) {
portsHtml = '<div class="tile-ports" title="Click to view required router ports"><span class="tile-ports-icon">🔌</span><span class="tile-ports-label tile-ports-label--loading">Ports: ' + ports.length + ' required</span></div>';
}
tile.innerHTML = infoBtn + '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div><div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>' + portsHtml;
// Domain badge — ONLY for services that require a domain
var domainHtml = "";
if (svc.needs_domain) {
domainHtml = '<div class="tile-domain" title="Click to check domain status">'
+ '<span class="tile-domain-icon">🌐</span>'
+ '<span class="tile-domain-label tile-domain-label--checking">' + (svc.domain ? escHtml(svc.domain) : 'Not set') + '</span>'
+ '</div>';
}
tile.innerHTML = infoBtn + '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div><div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>' + portsHtml + domainHtml;
var infoBtnEl = tile.querySelector(".tile-info-btn");
if (infoBtnEl) {
@@ -322,6 +331,51 @@ function buildTile(svc) {
}
}
// Domain badge async check
var domainEl = tile.querySelector(".tile-domain");
if (domainEl && svc.needs_domain) {
domainEl.style.cursor = "pointer";
domainEl.addEventListener("click", function(e) {
e.stopPropagation();
});
if (svc.domain) {
fetch("/api/domains/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ domains: [svc.domain] }),
})
.then(function(r) { return r.json(); })
.then(function(data) {
var d = (data.domains || [])[0];
var lbl = domainEl.querySelector(".tile-domain-label");
if (!lbl || !d) return;
lbl.classList.remove("tile-domain-label--checking");
if (d.status === "connected") {
lbl.className = "tile-domain-label tile-domain-label--ok";
lbl.textContent = svc.domain + " ✓";
} else if (d.status === "dns_mismatch") {
lbl.className = "tile-domain-label tile-domain-label--warn";
lbl.textContent = svc.domain + " (IP mismatch)";
} else if (d.status === "unresolvable") {
lbl.className = "tile-domain-label tile-domain-label--error";
lbl.textContent = svc.domain + " (DNS error)";
} else {
lbl.className = "tile-domain-label tile-domain-label--warn";
lbl.textContent = svc.domain + " (unknown)";
}
})
.catch(function() {});
} else {
var lbl = domainEl.querySelector(".tile-domain-label");
if (lbl) {
lbl.classList.remove("tile-domain-label--checking");
lbl.className = "tile-domain-label tile-domain-label--warn";
lbl.textContent = "Domain: Not set";
}
}
}
return tile;
}
@@ -1398,11 +1452,94 @@ async function loadFeatureManager() {
var data = await apiFetch("/api/features");
_featuresData = data;
renderFeatureManager(data);
// After rendering, do a batch domain check for all features that have a configured domain
_checkFeatureManagerDomains(data);
} catch (err) {
console.warn("Failed to load features:", err);
}
}
function _checkFeatureManagerDomains(data) {
// Collect all features with a configured domain
var featsWithDomain = (data.features || []).filter(function(f) {
return f.needs_domain && f.domain_configured;
});
if (!featsWithDomain.length) return;
// Get the actual domain values from /api/domains/status, then check them
fetch("/api/domains/status")
.then(function(r) { return r.json(); })
.then(function(statusData) {
var domainFileMap = statusData.domains || {};
// Build list of domains to check and a map from domain value → feature id
var domainsToCheck = [];
var domainToFeatIds = {};
featsWithDomain.forEach(function(feat) {
var domainName = feat.domain_name;
var domainVal = domainName ? domainFileMap[domainName] : null;
if (domainVal) {
domainsToCheck.push(domainVal);
if (!domainToFeatIds[domainVal]) domainToFeatIds[domainVal] = [];
domainToFeatIds[domainVal].push(feat.id);
} else {
// Domain file missing — update badge to warn
_updateFeatureDomainBadge(feat.id, null, "unresolvable");
}
});
if (!domainsToCheck.length) return;
return fetch("/api/domains/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ domains: domainsToCheck }),
})
.then(function(r) { return r.json(); })
.then(function(checkData) {
(checkData.domains || []).forEach(function(d) {
var featIds = domainToFeatIds[d.domain] || [];
featIds.forEach(function(featId) {
_updateFeatureDomainBadge(featId, d.domain, d.status);
});
});
});
})
.catch(function() {});
}
function _updateFeatureDomainBadge(featId, domainVal, status) {
var section = $sidebarFeatures.querySelector(".feature-manager-section");
if (!section) return;
// Find the card — cards don't have a data-feat-id, so find via name match
var badges = section.querySelectorAll(".feature-domain-badge.configured");
badges.forEach(function(badge) {
var domainNameAttr = badge.getAttribute("data-domain-name");
// Match by domain_name attribute — we need to look up the feat's domain_name
var feat = _featuresData && _featuresData.features
? _featuresData.features.find(function(f) { return f.id === featId; })
: null;
if (!feat) return;
if (domainNameAttr !== (feat.domain_name || "")) return;
var lbl = badge.querySelector(".feature-domain-label");
if (!lbl) return;
lbl.classList.remove("feature-domain-label--checking");
if (status === "connected") {
lbl.className = "feature-domain-label feature-domain-label--ok";
lbl.textContent = (domainVal || "Domain") + " ✓";
} else if (status === "dns_mismatch") {
lbl.className = "feature-domain-label feature-domain-label--warn";
lbl.textContent = (domainVal || "Domain") + " (IP mismatch)";
} else if (status === "unresolvable") {
lbl.className = "feature-domain-label feature-domain-label--error";
lbl.textContent = (domainVal || "Domain") + " (DNS error)";
} else {
lbl.className = "feature-domain-label feature-domain-label--warn";
lbl.textContent = (domainVal || "Domain") + " (unknown)";
}
});
}
function renderFeatureManager(data) {
// Remove old feature manager section if it exists
var old = $sidebarFeatures.querySelector(".feature-manager-section");
@@ -1467,9 +1604,15 @@ function buildFeatureCard(feat) {
var domainHtml = "";
if (feat.needs_domain) {
if (feat.domain_configured) {
domainHtml = '<div class="feature-domain-badge configured">🌐 Domain: Configured</div>';
domainHtml = '<div class="feature-domain-badge configured" data-domain-name="' + escHtml(feat.domain_name || '') + '">'
+ '<span class="feature-domain-icon">🌐</span>'
+ '<span class="feature-domain-label feature-domain-label--checking">Domain: Checking\u2026</span>'
+ '</div>';
} else {
domainHtml = '<div class="feature-domain-badge not-configured">🌐 Domain: Not configured</div>';
domainHtml = '<div class="feature-domain-badge not-configured">'
+ '<span class="feature-domain-icon">🌐</span>'
+ '<span class="feature-domain-label feature-domain-label--warn">Domain: Not configured</span>'
+ '</div>';
}
}