Fix 5 issues: Caddy domain, Zeus emoji, Feature Manager in tiles, header centering, domain dialog content
Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/faca798f-6820-4db6-adc9-d5a5c9ac1ba1 Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
d7cb97aa73
commit
dd9ff2f4b2
@@ -241,7 +241,6 @@ SERVICE_DOMAIN_MAP: dict[str, str] = {
|
|||||||
"phpfpm-wordpress.service": "wordpress",
|
"phpfpm-wordpress.service": "wordpress",
|
||||||
"haven-relay.service": "haven",
|
"haven-relay.service": "haven",
|
||||||
"livekit.service": "element-calling",
|
"livekit.service": "element-calling",
|
||||||
"caddy.service": "matrix", # Caddy serves the main domain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# For features that share a unit, disambiguate by icon field
|
# 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()
|
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
|
# Find the service config entry
|
||||||
entry = next((s for s in services if s.get("unit") == unit), None)
|
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:
|
else:
|
||||||
health = status # loading states, etc.
|
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 {
|
return {
|
||||||
"name": entry.get("name", ""),
|
"name": entry.get("name", ""),
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
@@ -1495,6 +1529,7 @@ async def api_service_detail(unit: str):
|
|||||||
"port_statuses": port_statuses,
|
"port_statuses": port_statuses,
|
||||||
"external_ip": external_ip,
|
"external_ip": external_ip,
|
||||||
"internal_ip": internal_ip,
|
"internal_ip": internal_ip,
|
||||||
|
"feature": feature_entry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -575,9 +575,34 @@ async function openServiceDetailModal(unit, name) {
|
|||||||
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
|
'<button class="matrix-action-btn" id="matrix-change-pw-btn">🔑 Change Password</button>' +
|
||||||
'</div>' : "") +
|
'</div>' : "") +
|
||||||
'</div>';
|
'</div>';
|
||||||
} else if (!data.enabled) {
|
} else if (!data.enabled && !data.feature) {
|
||||||
html += '<div class="svc-detail-section">' +
|
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">\uD83D\uDD27 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>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +615,17 @@ async function openServiceDetailModal(unit, name) {
|
|||||||
if (addBtn) addBtn.addEventListener("click", function() { openMatrixCreateUserModal(unit, name); });
|
if (addBtn) addBtn.addEventListener("click", function() { openMatrixCreateUserModal(unit, name); });
|
||||||
if (changePwBtn) changePwBtn.addEventListener("click", function() { openMatrixChangePasswordModal(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) {
|
} catch (err) {
|
||||||
if ($credsBody) $credsBody.innerHTML = '<p class="creds-empty">Could not load service details.</p>';
|
if ($credsBody) $credsBody.innerHTML = '<p class="creds-empty">Could not load service details.</p>';
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1293,7 @@ function closeSslEmailModal() {
|
|||||||
|
|
||||||
function openDomainSetupModal(feat, onSaved) {
|
function openDomainSetupModal(feat, onSaved) {
|
||||||
if (!$domainSetupModal) return;
|
if (!$domainSetupModal) return;
|
||||||
if ($domainSetupTitle) $domainSetupTitle.textContent = "🌐 Domain Setup — " + feat.name;
|
if ($domainSetupTitle) $domainSetupTitle.textContent = "\uD83C\uDF10 Domain Setup \u2014 " + feat.name;
|
||||||
|
|
||||||
var npubField = "";
|
var npubField = "";
|
||||||
if (feat.id === "haven") {
|
if (feat.id === "haven") {
|
||||||
@@ -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>';
|
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 =
|
$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-setup-intro">' +
|
||||||
'<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>' +
|
'<p><strong>Before continuing:</strong></p>' +
|
||||||
'<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>' +
|
'<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 "https://njal.la/update/?h=sub.domain.com&k=abc123&auto"</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 DDNS Curl Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=myservice.example.com&k=abc123&auto"" /><p class="domain-field-hint">\u2139 Paste the curl URL from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
||||||
npubField +
|
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 & Enable</button></div>';
|
'<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 & Enable</button></div>';
|
||||||
|
|
||||||
@@ -1569,9 +1618,7 @@ async function loadFeatureManager() {
|
|||||||
try {
|
try {
|
||||||
var data = await apiFetch("/api/features");
|
var data = await apiFetch("/api/features");
|
||||||
_featuresData = data;
|
_featuresData = data;
|
||||||
renderFeatureManager(data);
|
// Feature Manager is now integrated into tile modals; sidebar rendering removed.
|
||||||
// After rendering, do a batch domain check for all features that have a configured domain
|
|
||||||
_checkFeatureManagerDomains(data);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Failed to load features:", err);
|
console.warn("Failed to load features:", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,13 +54,19 @@ body {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-bar .title {
|
.header-bar .title {
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
flex: 1;
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
@@ -1530,6 +1536,19 @@ button.btn-reboot:hover:not(:disabled) {
|
|||||||
margin-top: 6px;
|
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 {
|
.domain-field-group {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
@@ -1810,6 +1829,28 @@ button.btn-reboot:hover:not(:disabled) {
|
|||||||
gap: 8px;
|
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: compact feature card overrides ─────────────────────── */
|
||||||
|
|
||||||
.sidebar .feature-manager-section {
|
.sidebar .feature-manager-section {
|
||||||
|
|||||||
@@ -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 = [
|
{ 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 = "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 = [
|
{ 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://"; }
|
{ label = "Tor Access"; file = "/var/lib/tor/onion/mempool-frontend/hostname"; prefix = "http://"; }
|
||||||
|
|||||||
Reference in New Issue
Block a user