Merge pull request #48 from naturallaw777/copilot/research-caddy-and-zeus-tiles

Fix Caddy domain, Zeus emoji, Feature Manager in tiles, header centering, domain dialog parity
This commit is contained in:
Sovran_Systems
2026-04-04 11:29:44 -05:00
committed by GitHub
4 changed files with 135 additions and 12 deletions

View File

@@ -241,7 +241,6 @@ SERVICE_DOMAIN_MAP: dict[str, str] = {
"phpfpm-wordpress.service": "wordpress",
"haven-relay.service": "haven",
"livekit.service": "element-calling",
"caddy.service": "matrix", # Caddy serves the main domain
}
# For features that share a unit, disambiguate by icon field
@@ -1334,7 +1333,7 @@ async def api_service_detail(unit: str):
}
loop = asyncio.get_event_loop()
overrides, _ = await loop.run_in_executor(None, _read_hub_overrides)
overrides, nostr_npub = await loop.run_in_executor(None, _read_hub_overrides)
# Find the service config entry
entry = next((s for s in services if s.get("unit") == unit), None)
@@ -1478,6 +1477,41 @@ async def api_service_detail(unit: str):
else:
health = status # loading states, etc.
# Build feature entry if this service is an addon feature
feature_entry: dict | None = None
if feat_id is not None:
feat_meta = next((f for f in FEATURE_REGISTRY if f["id"] == feat_id), None)
if feat_meta is not None:
domain_name_feat = feat_meta.get("domain_name")
domain_configured = True
if domain_name_feat:
domain_path_feat = os.path.join(DOMAINS_DIR, domain_name_feat)
try:
with open(domain_path_feat, "r") as f:
domain_configured = bool(f.read(256).strip())
except OSError:
domain_configured = False
extra_fields = []
for ef in feat_meta.get("extra_fields", []):
ef_copy = dict(ef)
if ef["id"] == "nostr_npub":
ef_copy["current_value"] = nostr_npub or ""
extra_fields.append(ef_copy)
feature_entry = {
"id": feat_id,
"name": feat_meta["name"],
"description": feat_meta["description"],
"category": feat_meta["category"],
"enabled": enabled,
"needs_domain": feat_meta.get("needs_domain", False),
"domain_configured": domain_configured,
"domain_name": domain_name_feat,
"needs_ddns": feat_meta.get("needs_ddns", False),
"extra_fields": extra_fields,
"conflicts_with": feat_meta.get("conflicts_with", []),
"port_requirements": feat_meta.get("port_requirements", []),
}
return {
"name": entry.get("name", ""),
"unit": unit,
@@ -1495,6 +1529,7 @@ async def api_service_detail(unit: str):
"port_statuses": port_statuses,
"external_ip": external_ip,
"internal_ip": internal_ip,
"feature": feature_entry,
}

View File

@@ -575,9 +575,34 @@ async function openServiceDetailModal(unit, name) {
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
'</div>' : "") +
'</div>';
} else if (!data.enabled) {
} else if (!data.enabled && !data.feature) {
html += '<div class="svc-detail-section">' +
'<p class="creds-empty">This service is not enabled in your configuration. You can enable it from the <strong>Feature Manager</strong> in the sidebar.</p>' +
'<p class="creds-empty">This service is not enabled in your configuration.</p>' +
'</div>';
}
// 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";
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">🔧 Addon Feature</div>' +
'<p class="svc-detail-desc">This is an optional addon feature. You can enable or disable it at any time.</p>' +
'<div class="svc-detail-addon-row">' +
'<span class="svc-detail-addon-status ' + addonStatusCls + '">' + addonStatusLabel + '</span>' +
'<button class="' + addonBtnCls + '" id="svc-detail-addon-btn">' + escHtml(addonBtnLabel) + '</button>' +
'</div>' +
'</div>';
}
@@ -590,6 +615,17 @@ async function openServiceDetailModal(unit, name) {
if (addBtn) addBtn.addEventListener("click", function() { openMatrixCreateUserModal(unit, name); });
if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(unit, name); });
}
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);
});
}
}
} catch (err) {
if ($credsBody) $credsBody.innerHTML = '<p class="creds-empty">Could not load service details.</p>';
}
@@ -1273,10 +1309,23 @@ function openDomainSetupModal(feat, onSaved) {
npubField = '<div class="domain-field-group"><label class="domain-field-label" for="domain-npub-input">Nostr Public Key (npub1...):</label><input class="domain-field-input" type="text" id="domain-npub-input" placeholder="npub1..." value="' + escHtml(currentNpub) + '" /></div>';
}
var externalIp = _cachedExternalIp || "your external IP";
$domainSetupBody.innerHTML =
'<div class="domain-setup-intro"><p>Before continuing, you need:</p><ol><li>A subdomain purchased on njal.la</li><li>A Dynamic DNS record for it</li></ol></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Subdomain:</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la DDNS URL:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="https://njal.la/update/?h=..." /><p class="domain-field-hint"> Paste the curl URL from your Njal.la dashboard\'s Dynamic record</p></div>' +
'<div class="domain-setup-intro">' +
'<p><strong>Before continuing:</strong></p>' +
'<ol>' +
'<li>Create an account at <a href="https://njal.la" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color);">https://njal.la</a></li>' +
'<li>Purchase your domain on Njal.la</li>' +
'<li>In the Njal.la web interface, create a <strong>Dynamic</strong> record pointing to this machine\'s external IP address:<br>' +
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
'<li>Njal.la will give you a curl command like:<br>' +
'<code style="font-size:0.8em;">curl &quot;https://njal.la/update/?h=sub.domain.com&amp;k=abc123&amp;auto&quot;</code></li>' +
'<li>Enter the subdomain and paste that curl command below</li>' +
'</ol>' +
'</div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Subdomain (e.g. myservice.example.com):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="myservice.example.com" /></div>' +
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl &quot;https://njal.la/update/?h=myservice.example.com&amp;k=abc123&amp;auto&quot;" /><p class="domain-field-hint"> Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
npubField +
'<div class="domain-field-actions"><button class="btn btn-close-modal" id="domain-setup-cancel-btn">Cancel</button><button class="btn btn-primary" id="domain-setup-save-btn">Save &amp; Enable</button></div>';
@@ -1569,9 +1618,7 @@ async function loadFeatureManager() {
try {
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);
// Feature Manager is now integrated into tile modals; sidebar rendering removed.
} catch (err) {
console.warn("Failed to load features:", err);
}

View File

@@ -54,13 +54,19 @@ body {
position: sticky;
top: 0;
z-index: 100;
justify-content: flex-end;
}
.header-bar .title {
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
flex: 1;
position: absolute;
left: 0;
right: 0;
text-align: center;
pointer-events: none;
white-space: nowrap;
}
.header-logo {
@@ -1530,6 +1536,19 @@ button.btn-reboot:hover:not(:disabled) {
margin-top: 6px;
}
.domain-setup-intro li {
margin-bottom: 6px;
}
.domain-setup-intro code {
background-color: #12121c;
padding: 2px 8px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.82em;
word-break: break-all;
}
.domain-field-group {
margin-bottom: 14px;
}
@@ -1810,6 +1829,28 @@ button.btn-reboot:hover:not(:disabled) {
gap: 8px;
}
/* Addon feature toggle row in service detail modal */
.svc-detail-addon-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 10px;
}
.svc-detail-addon-status {
font-size: 0.88rem;
font-weight: 600;
}
.addon-status--on {
color: var(--green);
}
.addon-status--off {
color: var(--text-dim);
}
/* ── Sidebar: compact feature card overrides ─────────────────────── */
.sidebar .feature-manager-section {

View File

@@ -51,7 +51,7 @@ let
]; }
{ name = "Zeus Connect"; unit = "zeus-connect-setup.service"; type = "system"; icon = "zeus"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
{ label = "Connection URL"; file = "/var/lib/secrets/zeus-connect-url"; qrcode = true; }
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus <EFBFBD><EFBFBD> Scan Node Config\n3. Scan the QR code above or paste the Connection URL"; }
{ label = "How to Connect"; value = "1. Download Zeus from App Store or Google Play\n2. Open Zeus Scan Node Config\n3. Scan the QR code above or paste the Connection URL"; }
]; }
{ name = "Mempool"; unit = "mempool.service"; type = "system"; icon = "mempool"; enabled = cfg.features.mempool; category = "bitcoin-apps"; credentials = [
{ label = "Tor Access"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }