From 304df327e3c59c3359cbae340dc054aa988aad19 Mon Sep 17 00:00:00 2001 From: naturallaw77 Date: Fri, 3 Apr 2026 07:31:17 -0500 Subject: [PATCH] UX update for feature manager --- app/sovran_systemsos_web/static/app.js | 62 +++++++++++++++----------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/app/sovran_systemsos_web/static/app.js b/app/sovran_systemsos_web/static/app.js index d3d93ef..0f8274b 100644 --- a/app/sovran_systemsos_web/static/app.js +++ b/app/sovran_systemsos_web/static/app.js @@ -46,13 +46,15 @@ let _supportEnabledAt = null; let _cachedExternalIp = null; // Feature Manager state -let _featuresData = null; -let _rebuildLog = ""; -let _rebuildLogOffset = 0; -let _rebuildPollTimer = null; -let _rebuildFinished = false; -let _rebuildServerDown = false; -let _pendingToggle = null; // {feature, extra} waiting for domain/confirm +let _featuresData = null; +let _rebuildLog = ""; +let _rebuildLogOffset = 0; +let _rebuildPollTimer = null; +let _rebuildFinished = false; +let _rebuildServerDown = false; +let _pendingToggle = null; // {feature, extra} waiting for domain/confirm +let _rebuildFeatureName = ""; +let _rebuildIsEnabling = true; // ── DOM refs ────────────────────────────────────────────────────── @@ -205,14 +207,14 @@ function buildTile(svc) { if (dis) tile.title = svc.name + " is not enabled in custom.nix"; if (isSupport) { - tile.innerHTML = '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
Click to manage
'; + tile.innerHTML = '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
Click for help
'; tile.style.cursor = "pointer"; tile.addEventListener("click", function() { openSupportModal(); }); return tile; } var infoBtn = hasCreds ? '' : ""; - tile.innerHTML = infoBtn + '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
' + escHtml(st) + '
'; + tile.innerHTML = infoBtn + '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
' + st + '
'; var infoBtnEl = tile.querySelector(".tile-info-btn"); if (infoBtnEl) { @@ -342,13 +344,13 @@ async function openSupportModal() { function renderSupportInactive() { stopSupportTimer(); var ip = _cachedExternalIp || "loading…"; - $supportBody.innerHTML = '
🛟

Need help from Sovran Systems?

This will temporarily give Sovran Systems secure SSH access to your machine so we can diagnose and fix issues for you.

Your External IP' + escHtml(ip) + '

Give this IP to your Sovran Systems technician when asked.

What happens when you click Enable:

  1. A Sovran Systems SSH key is added to this machine
  2. You give us your External IP shown above
  3. We connect and help you remotely
  4. When done, you click End Support Session to remove the key

You can end the session at any time. The access key will be completely removed.

'; + $supportBody.innerHTML = '
🛟

Need help from Sovran Systems?

This will temporarily grant our support team SSH access to your machine so we can help diagnose and fix issues.

Your IP' + escHtml(ip) + '
This IP will be shared with Sovran Systems support
What happens:
  1. Our public SSH key is added to your machine
  2. We connect and help fix the issue
  3. You click "End Session" to remove our access

You can revoke access at any time

'; document.getElementById("btn-support-enable").addEventListener("click", enableSupport); } function renderSupportActive() { var ip = _cachedExternalIp || "loading…"; - $supportBody.innerHTML = '
🔓

Support Access is Active

Sovran Systems can currently connect to your machine via SSH.

Your External IP' + escHtml(ip) + '
Session Duration

When your support session is complete, click the button below to immediately remove the access key.

'; + $supportBody.innerHTML = '
🔓

Support Access is Active

Sovran Systems can currently connect to your machine via SSH.

Your IP' + escHtml(ip) + '
Duration

This will remove the SSH key immediately

'; document.getElementById("btn-support-disable").addEventListener("click", disableSupport); startSupportTimer(); } @@ -356,7 +358,7 @@ function renderSupportActive() { function renderSupportRemoved(verified) { stopSupportTimer(); var icon = verified ? "✅" : "⚠️"; - var msg = verified ? "The Sovran Systems SSH key has been completely removed from your machine. We no longer have any access." : "The key removal was requested but could not be fully verified. Please reboot your machine to be sure."; + var msg = verified ? "The Sovran Systems SSH key has been completely removed from your machine. We no longer have any access." : "The key removal was requested but could not be fully verified. Please reboot to ensure it is gone."; var vclass = verified ? "verified-gone" : "verify-warning"; var vlabel = verified ? "✓ Removed — No access" : "⚠ Verify by rebooting"; $supportBody.innerHTML = '
' + icon + '

Support Session Ended

' + escHtml(msg) + '

SSH Key Status:' + vlabel + '
'; @@ -513,7 +515,9 @@ function saveErrorReport() { function doReboot() { if ($modal) $modal.classList.remove("open"); + if ($rebuildModal) $rebuildModal.classList.remove("open"); stopUpdatePoll(); + stopRebuildPoll(); if ($rebootOverlay) $rebootOverlay.classList.add("visible"); fetch("/api/reboot", { method: "POST" }).catch(function() {}); setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); @@ -536,8 +540,10 @@ function openRebuildModal() { _rebuildLogOffset = 0; _rebuildServerDown = false; _rebuildFinished = false; - if ($rebuildLog) $rebuildLog.textContent = ""; - if ($rebuildStatus) $rebuildStatus.textContent = "Rebuilding…"; + if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; } + var action = _rebuildIsEnabling ? "Enabling" : "Disabling"; + var label = _rebuildFeatureName || "feature"; + if ($rebuildStatus) $rebuildStatus.textContent = action + " " + label + "…"; if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning"); if ($rebuildReboot) $rebuildReboot.style.display = "none"; if ($rebuildSave) $rebuildSave.style.display = "none"; @@ -555,7 +561,7 @@ function closeRebuildModal() { function appendRebuildLog(text) { if (!text) return; _rebuildLog += text; - if ($rebuildLog) { $rebuildLog.textContent += text; $rebuildLog.scrollTop = $rebuildLog.scrollHeight; } + // Log is collected silently for error reports — not displayed to user } function startRebuildPoll() { @@ -571,7 +577,7 @@ async function pollRebuildStatus() { if (_rebuildFinished) return; try { var data = await apiFetch("/api/rebuild/status?offset=" + _rebuildLogOffset); - if (_rebuildServerDown) { _rebuildServerDown = false; appendRebuildLog("[Server reconnected]\n"); if ($rebuildStatus) $rebuildStatus.textContent = "Rebuilding…"; } + if (_rebuildServerDown) { _rebuildServerDown = false; } if (data.log) appendRebuildLog(data.log); _rebuildLogOffset = data.offset; if (data.running) return; @@ -579,7 +585,7 @@ async function pollRebuildStatus() { stopRebuildPoll(); onRebuildDone(data.result === "success"); } catch (err) { - if (!_rebuildServerDown) { _rebuildServerDown = true; appendRebuildLog("\n[Server restarting — waiting for it to come back…]\n"); if ($rebuildStatus) $rebuildStatus.textContent = "Server restarting…"; } + if (!_rebuildServerDown) { _rebuildServerDown = true; if ($rebuildStatus) $rebuildStatus.textContent = "Applying changes…"; } } } @@ -587,12 +593,11 @@ function onRebuildDone(success) { if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning"); if ($rebuildClose) $rebuildClose.disabled = false; if (success) { - if ($rebuildStatus) $rebuildStatus.textContent = "✓ Rebuild complete"; - if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex"; - // Refresh feature states - loadFeatureManager(); + if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done"; + // Auto-reload the page after a short delay so tiles and toggles reflect the new state + setTimeout(function() { window.location.reload(); }, 1200); } else { - if ($rebuildStatus) $rebuildStatus.textContent = "✗ Rebuild failed"; + if ($rebuildStatus) $rebuildStatus.textContent = "✗ Something went wrong"; if ($rebuildSave) $rebuildSave.style.display = "inline-flex"; if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex"; } @@ -680,12 +685,12 @@ function openDomainSetupModal(feat, onSaved) { } } } - npubField = '
'; + npubField = '
'; } $domainSetupBody.innerHTML = '

Before continuing, you need:

  1. A subdomain purchased on njal.la
  2. A Dynamic DNS record for it
' + - '
' + + '
' + '

ℹ Paste the curl URL from your Njal.la dashboard\'s Dynamic record

' + npubField + '
'; @@ -736,6 +741,13 @@ function closeDomainSetupModal() { // ── Feature toggle logic ────────────────────────────────────────── async function performFeatureToggle(featId, enabled, extra) { + // Look up feature name for the rebuild modal + _rebuildIsEnabling = enabled; + _rebuildFeatureName = featId; + if (_featuresData) { + var found = _featuresData.features.find(function(f) { return f.id === featId; }); + if (found) _rebuildFeatureName = found.name; + } try { var res = await fetch("/api/features/toggle", { method: "POST", @@ -1006,4 +1018,4 @@ async function init() { } } -document.addEventListener("DOMContentLoaded", init); +document.addEventListener("DOMContentLoaded", init); \ No newline at end of file