diff --git a/app/sovran_systemsos_web/static/app.js b/app/sovran_systemsos_web/static/app.js index b6bd0f2..3f39249 100644 --- a/app/sovran_systemsos_web/static/app.js +++ b/app/sovran_systemsos_web/static/app.js @@ -2,11 +2,11 @@ v7 — Status-only dashboard + Tech Support */ "use strict"; -const POLL_INTERVAL_SERVICES = 5000; // 5 s -const POLL_INTERVAL_UPDATES = 1800000; // 30 min -const UPDATE_POLL_INTERVAL = 2000; // 2 s while update is running -const REBOOT_CHECK_INTERVAL = 5000; // 5 s between reconnect attempts -const SUPPORT_TIMER_INTERVAL = 1000; // 1 s for session timer +const POLL_INTERVAL_SERVICES = 5000; +const POLL_INTERVAL_UPDATES = 1800000; +const UPDATE_POLL_INTERVAL = 2000; +const REBOOT_CHECK_INTERVAL = 5000; +const SUPPORT_TIMER_INTERVAL = 1000; const CATEGORY_ORDER = [ "infrastructure", @@ -65,9 +65,7 @@ const $supportCloseBtn = document.getElementById("support-close-btn"); // ── Helpers ─────────────────────────────────────────────────────── -function tileId(svc) { - return svc.unit + "::" + svc.name; -} +function tileId(svc) { return svc.unit + "::" + svc.name; } function statusClass(status) { if (!status) return "unknown"; @@ -86,36 +84,27 @@ function statusText(status, enabled) { } function escHtml(str) { - return String(str) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); + return String(str).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); } function linkify(str) { - const escaped = escHtml(str); - return escaped.replace( - /(https?:\/\/[^\s<]+)/g, - '$1' - ); + return escHtml(str).replace(/(https?:\/\/[^\s<]+)/g, '$1'); } function formatDuration(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); - if (h > 0) return `${h}h ${m}m ${s}s`; - if (m > 0) return `${m}m ${s}s`; - return `${s}s`; + if (h > 0) return h + "h " + m + "m " + s + "s"; + if (m > 0) return m + "m " + s + "s"; + return s + "s"; } // ── Fetch wrappers ──────────────────────────────────────────────── -async function apiFetch(path, options = {}) { - const res = await fetch(path, options); - if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); +async function apiFetch(path, options) { + const res = await fetch(path, options || {}); + if (!res.ok) throw new Error(res.status + " " + res.statusText); return res.json(); } @@ -123,157 +112,106 @@ async function apiFetch(path, options = {}) { function buildTiles(services, categoryLabels) { _servicesCache = services; - - const grouped = {}; - for (const svc of services) { - const cat = svc.category || "other"; + var grouped = {}; + for (var i = 0; i < services.length; i++) { + var cat = services[i].category || "other"; if (!grouped[cat]) grouped[cat] = []; - grouped[cat].push(svc); + grouped[cat].push(services[i]); } - $tilesArea.innerHTML = ""; - - const orderedKeys = [ - ...CATEGORY_ORDER.filter(k => grouped[k]), - ...Object.keys(grouped).filter(k => !CATEGORY_ORDER.includes(k)), - ]; - - for (const catKey of orderedKeys) { - const entries = grouped[catKey]; + var orderedKeys = CATEGORY_ORDER.filter(function(k) { return grouped[k]; }); + Object.keys(grouped).forEach(function(k) { + if (orderedKeys.indexOf(k) === -1) orderedKeys.push(k); + }); + for (var j = 0; j < orderedKeys.length; j++) { + var catKey = orderedKeys[j]; + var entries = grouped[catKey]; if (!entries || entries.length === 0) continue; - - const label = categoryLabels[catKey] || catKey; - - const section = document.createElement("div"); + var label = categoryLabels[catKey] || catKey; + var section = document.createElement("div"); section.className = "category-section"; section.dataset.category = catKey; - - section.innerHTML = ` -
${escHtml(label)}
-
-
- `; - - const grid = section.querySelector(".tiles-grid"); - for (const svc of entries) { - grid.appendChild(buildTile(svc)); + section.innerHTML = '
' + escHtml(label) + '

'; + var grid = section.querySelector(".tiles-grid"); + for (var k = 0; k < entries.length; k++) { + grid.appendChild(buildTile(entries[k])); } - $tilesArea.appendChild(section); } - if ($tilesArea.children.length === 0) { - $tilesArea.innerHTML = `

No services configured.

`; + $tilesArea.innerHTML = '

No services configured.

'; } } function buildTile(svc) { - const isSupport = svc.type === "support"; - const sc = statusClass(svc.status); - const st = statusText(svc.status, svc.enabled); - const dis = !svc.enabled; - const hasCreds = svc.has_credentials && svc.enabled; + var isSupport = svc.type === "support"; + var sc = statusClass(svc.status); + var st = statusText(svc.status, svc.enabled); + var dis = !svc.enabled; + var hasCreds = svc.has_credentials && svc.enabled; - const tile = document.createElement("div"); + var tile = document.createElement("div"); tile.className = "service-tile" + (dis ? " disabled" : "") + (isSupport ? " support-tile" : ""); tile.dataset.unit = svc.unit; tile.dataset.tileId = tileId(svc); - if (dis) tile.title = `${svc.name} is not enabled in custom.nix`; + if (dis) tile.title = svc.name + " is not enabled in custom.nix"; if (isSupport) { - // Support tile — clickable, no info button, no status dot - tile.innerHTML = ` - ${escHtml(svc.name)} - -
${escHtml(svc.name)}
-
- Click to manage -
- `; + tile.innerHTML = '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
Click to manage
'; tile.style.cursor = "pointer"; - tile.addEventListener("click", () => openSupportModal()); + tile.addEventListener("click", function() { openSupportModal(); }); return tile; } - // Normal tile - const infoBtn = hasCreds - ? `` - : ""; + var infoBtn = hasCreds ? '' : ""; + tile.innerHTML = infoBtn + '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
' + escHtml(st) + '
'; - tile.innerHTML = ` - ${infoBtn} - ${escHtml(svc.name)} - -
${escHtml(svc.name)}
-
- - ${escHtml(st)} -
- `; - - const infoBtnEl = tile.querySelector(".tile-info-btn"); + var infoBtnEl = tile.querySelector(".tile-info-btn"); if (infoBtnEl) { - infoBtnEl.addEventListener("click", (e) => { + infoBtnEl.addEventListener("click", function(e) { e.stopPropagation(); openCredsModal(svc.unit, svc.name); }); } - return tile; } -// ── Render: live update (no DOM rebuild) ────────────────────────── +// ── Render: live update ─────────────────────────────────────────── function updateTiles(services) { _servicesCache = services; - - for (const svc of services) { - const id = CSS.escape(tileId(svc)); - const tile = $tilesArea.querySelector(`.service-tile[data-tile-id="${id}"]`); + for (var i = 0; i < services.length; i++) { + var svc = services[i]; + if (svc.type === "support") continue; + var id = CSS.escape(tileId(svc)); + var tile = $tilesArea.querySelector('.service-tile[data-tile-id="' + id + '"]'); if (!tile) continue; - - if (svc.type === "support") continue; // Support tile doesn't have a systemd status - - const sc = statusClass(svc.status); - const st = statusText(svc.status, svc.enabled); - - const dot = tile.querySelector(".status-dot"); - const text = tile.querySelector(".status-text"); - - if (dot) { dot.className = `status-dot ${sc}`; } - if (text) { text.textContent = st; } + var sc = statusClass(svc.status); + var st = statusText(svc.status, svc.enabled); + var dot = tile.querySelector(".status-dot"); + var text = tile.querySelector(".status-text"); + if (dot) dot.className = "status-dot " + sc; + if (text) text.textContent = st; } } // ── Service polling ─────────────────────────────────────────────── -let _firstLoad = true; +var _firstLoad = true; async function refreshServices() { try { - const services = await apiFetch("/api/services"); - if (_firstLoad) { - buildTiles(services, _categoryLabels); - _firstLoad = false; - } else { - updateTiles(services); - } - } catch (err) { - console.warn("Failed to fetch services:", err); - } + var services = await apiFetch("/api/services"); + if (_firstLoad) { buildTiles(services, _categoryLabels); _firstLoad = false; } + else { updateTiles(services); } + } catch (err) { console.warn("Failed to fetch services:", err); } } // ── Network IPs ─────────────────────────────────────────────────── async function loadNetwork() { try { - const data = await apiFetch("/api/network"); + var data = await apiFetch("/api/network"); if ($internalIp) $internalIp.textContent = data.internal_ip || "—"; if ($externalIp) $externalIp.textContent = data.external_ip || "—"; _cachedExternalIp = data.external_ip || "unavailable"; @@ -287,14 +225,10 @@ async function loadNetwork() { async function checkUpdates() { try { - const data = await apiFetch("/api/updates/check"); - const hasUpdates = !!data.available; - if ($updateBadge) { - $updateBadge.classList.toggle("visible", hasUpdates); - } - if ($updateBtn) { - $updateBtn.classList.toggle("has-updates", hasUpdates); - } + var data = await apiFetch("/api/updates/check"); + var hasUpdates = !!data.available; + if ($updateBadge) $updateBadge.classList.toggle("visible", hasUpdates); + if ($updateBtn) $updateBtn.classList.toggle("has-updates", hasUpdates); } catch (_) {} } @@ -302,72 +236,45 @@ async function checkUpdates() { async function openCredsModal(unit, name) { if (!$credsModal) return; - if ($credsTitle) $credsTitle.textContent = name + " — Connection Info"; - if ($credsBody) $credsBody.innerHTML = '

Loading…

'; - + if ($credsBody) $credsBody.innerHTML = '

Loading…

'; $credsModal.classList.add("open"); - try { - const data = await apiFetch(`/api/credentials/${encodeURIComponent(unit)}`); - + var data = await apiFetch("/api/credentials/" + encodeURIComponent(unit)); if (!data.credentials || data.credentials.length === 0) { $credsBody.innerHTML = '

No connection info available yet.

'; return; } - - let html = ""; - for (const cred of data.credentials) { - const id = "cred-" + Math.random().toString(36).substring(2, 8); - const displayValue = linkify(cred.value); - - let qrBlock = ""; + var html = ""; + for (var i = 0; i < data.credentials.length; i++) { + var cred = data.credentials[i]; + var id = "cred-" + Math.random().toString(36).substring(2, 8); + var displayValue = linkify(cred.value); + var qrBlock = ""; if (cred.qrcode) { - qrBlock = ` -
- QR Code for ${escHtml(cred.label)} -
Scan with Zeus app on your phone
-
- `; + qrBlock = '
QR Code for ' + escHtml(cred.label) + '
Scan with Zeus app on your phone
'; } - - html += ` -
-
${escHtml(cred.label)}
- ${qrBlock} -
-
${displayValue}
- -
-
- `; + html += '
' + escHtml(cred.label) + '
' + qrBlock + '
' + displayValue + '
'; } $credsBody.innerHTML = html; - - $credsBody.querySelectorAll(".creds-copy-btn").forEach(btn => { - btn.addEventListener("click", () => { - const target = document.getElementById(btn.dataset.target); + $credsBody.querySelectorAll(".creds-copy-btn").forEach(function(btn) { + btn.addEventListener("click", function() { + var target = document.getElementById(btn.dataset.target); if (target) { - navigator.clipboard.writeText(target.textContent).then(() => { + navigator.clipboard.writeText(target.textContent).then(function() { btn.textContent = "Copied!"; btn.classList.add("copied"); - setTimeout(() => { - btn.textContent = "Copy"; - btn.classList.remove("copied"); - }, 1500); - }).catch(() => {}); + setTimeout(function() { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 1500); + }).catch(function() {}); } }); }); - } catch (err) { $credsBody.innerHTML = '

Could not load credentials.

'; } } -function closeCredsModal() { - if ($credsModal) $credsModal.classList.remove("open"); -} +function closeCredsModal() { if ($credsModal) $credsModal.classList.remove("open"); } // ── Tech Support modal ──────────────────────────────────────────── @@ -375,15 +282,10 @@ async function openSupportModal() { if (!$supportModal) return; $supportModal.classList.add("open"); $supportBody.innerHTML = '

Checking support status…

'; - try { - const status = await apiFetch("/api/support/status"); - if (status.active) { - _supportEnabledAt = status.enabled_at; - renderSupportActive(); - } else { - renderSupportInactive(); - } + var status = await apiFetch("/api/support/status"); + if (status.active) { _supportEnabledAt = status.enabled_at; renderSupportActive(); } + else { renderSupportInactive(); } } catch (err) { $supportBody.innerHTML = '

Could not check support status.

'; } @@ -391,117 +293,34 @@ async function openSupportModal() { function renderSupportInactive() { stopSupportTimer(); - const 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. -
  3. You give us your External IP shown above
  4. -
  5. We connect and help you remotely
  6. -
  7. When done, you click End Support Session to remove the key
  8. -
-
- - -

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

-
- `; - + 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.

'; document.getElementById("btn-support-enable").addEventListener("click", enableSupport); } function renderSupportActive() { - const 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. -

- - -
- `; - + 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.

'; document.getElementById("btn-support-disable").addEventListener("click", disableSupport); startSupportTimer(); } function renderSupportRemoved(verified) { stopSupportTimer(); - const icon = verified ? "✅" : "⚠️"; - const 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."; - - $supportBody.innerHTML = ` -
-
${icon}
-

Support Session Ended

-

${escHtml(msg)}

- -
- SSH Key Status: - - ${verified ? "✓ Removed — No access" : "⚠ Verify by rebooting"} - -
- - -
- `; - + 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 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 + '
'; document.getElementById("btn-support-done").addEventListener("click", closeSupportModal); } async function enableSupport() { - const btn = document.getElementById("btn-support-enable"); + var btn = document.getElementById("btn-support-enable"); if (btn) { btn.disabled = true; btn.textContent = "Enabling…"; } try { await apiFetch("/api/support/enable", { method: "POST" }); - const status = await apiFetch("/api/support/status"); + var status = await apiFetch("/api/support/status"); _supportEnabledAt = status.enabled_at; renderSupportActive(); } catch (err) { @@ -511,10 +330,10 @@ async function enableSupport() { } async function disableSupport() { - const btn = document.getElementById("btn-support-disable"); + var btn = document.getElementById("btn-support-disable"); if (btn) { btn.disabled = true; btn.textContent = "Removing key…"; } try { - const result = await apiFetch("/api/support/disable", { method: "POST" }); + var result = await apiFetch("/api/support/disable", { method: "POST" }); renderSupportRemoved(result.verified); } catch (err) { if (btn) { btn.disabled = false; btn.textContent = "End Support Session"; } @@ -529,16 +348,13 @@ function startSupportTimer() { } function stopSupportTimer() { - if (_supportTimerInt) { - clearInterval(_supportTimerInt); - _supportTimerInt = null; - } + if (_supportTimerInt) { clearInterval(_supportTimerInt); _supportTimerInt = null; } } function updateSupportTimer() { - const el = document.getElementById("support-timer"); + var el = document.getElementById("support-timer"); if (!el || !_supportEnabledAt) return; - const elapsed = (Date.now() / 1000) - _supportEnabledAt; + var elapsed = (Date.now() / 1000) - _supportEnabledAt; el.textContent = formatDuration(Math.max(0, elapsed)); } @@ -555,13 +371,12 @@ function openUpdateModal() { _updateLogOffset = 0; _serverWasDown = false; _updateFinished = false; - if ($modalLog) $modalLog.textContent = ""; - if ($modalStatus) $modalStatus.textContent = "Starting update…"; - if ($modalSpinner) $modalSpinner.classList.add("spinning"); - if ($btnReboot) { $btnReboot.style.display = "none"; } - if ($btnSave) { $btnSave.style.display = "none"; } - if ($btnCloseModal) { $btnCloseModal.disabled = true; } - + if ($modalLog) $modalLog.textContent = ""; + if ($modalStatus) $modalStatus.textContent = "Starting update…"; + if ($modalSpinner) $modalSpinner.classList.add("spinning"); + if ($btnReboot) $btnReboot.style.display = "none"; + if ($btnSave) $btnSave.style.display = "none"; + if ($btnCloseModal) $btnCloseModal.disabled = true; $modal.classList.add("open"); startUpdate(); } @@ -575,29 +390,22 @@ function closeUpdateModal() { function appendLog(text) { if (!text) return; _updateLog += text; - if ($modalLog) { - $modalLog.textContent += text; - $modalLog.scrollTop = $modalLog.scrollHeight; - } + if ($modalLog) { $modalLog.textContent += text; $modalLog.scrollTop = $modalLog.scrollHeight; } } function startUpdate() { fetch("/api/updates/run", { method: "POST" }) - .then(response => { - if (!response.ok) { - return response.text().then(t => { throw new Error(t); }); - } + .then(function(response) { + if (!response.ok) return response.text().then(function(t) { throw new Error(t); }); return response.json(); }) - .then(data => { - if (data.status === "already_running") { - appendLog("[Update already in progress, attaching…]\n\n"); - } + .then(function(data) { + if (data.status === "already_running") appendLog("[Update already in progress, attaching…]\n\n"); if ($modalStatus) $modalStatus.textContent = "Updating…"; startUpdatePoll(); }) - .catch(err => { - appendLog(`[Error: failed to start update — ${err}]\n`); + .catch(function(err) { + appendLog("[Error: failed to start update — " + err + "]\n"); onUpdateDone(false); }); } @@ -608,5 +416,104 @@ function startUpdatePoll() { } function stopUpdatePoll() { - if (_updatePollTimer) { - clearInterval(_ \ No newline at end of file + if (_updatePollTimer) { clearInterval(_updatePollTimer); _updatePollTimer = null; } +} + +async function pollUpdateStatus() { + if (_updateFinished) return; + try { + var data = await apiFetch("/api/updates/status?offset=" + _updateLogOffset); + if (_serverWasDown) { _serverWasDown = false; appendLog("[Server reconnected]\n"); if ($modalStatus) $modalStatus.textContent = "Updating…"; } + if (data.log) appendLog(data.log); + _updateLogOffset = data.offset; + if (data.running) return; + _updateFinished = true; + stopUpdatePoll(); + if (data.result === "success") onUpdateDone(true); + else onUpdateDone(false); + } catch (err) { + if (!_serverWasDown) { _serverWasDown = true; appendLog("\n[Server restarting — waiting for it to come back…]\n"); if ($modalStatus) $modalStatus.textContent = "Server restarting…"; } + } +} + +function onUpdateDone(success) { + if ($modalSpinner) $modalSpinner.classList.remove("spinning"); + if ($btnCloseModal) $btnCloseModal.disabled = false; + if (success) { + if ($modalStatus) $modalStatus.textContent = "✓ Update complete"; + if ($btnReboot) $btnReboot.style.display = "inline-flex"; + } else { + if ($modalStatus) $modalStatus.textContent = "✗ Update failed"; + if ($btnSave) $btnSave.style.display = "inline-flex"; + if ($btnReboot) $btnReboot.style.display = "inline-flex"; + } +} + +function saveErrorReport() { + var blob = new Blob([_updateLog], { type: "text/plain" }); + var url = URL.createObjectURL(blob); + var a = document.createElement("a"); + a.href = url; + a.download = "sovran-update-error-" + new Date().toISOString().split(".")[0].replace(/:/g, "-") + ".txt"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// ── Reboot ──────────────────────────────────────────────────────── + +function doReboot() { + if ($modal) $modal.classList.remove("open"); + stopUpdatePoll(); + if ($rebootOverlay) $rebootOverlay.classList.add("visible"); + fetch("/api/reboot", { method: "POST" }).catch(function() {}); + setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); +} + +function waitForServerReboot() { + fetch("/api/config", { cache: "no-store" }) + .then(function(res) { + if (res.ok) window.location.reload(); + else setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); + }) + .catch(function() { setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); }); +} + +// ── Event listeners ─────────────────────────────────────────────── + +if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); +if ($refreshBtn) $refreshBtn.addEventListener("click", function() { refreshServices(); }); +if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal); +if ($btnReboot) $btnReboot.addEventListener("click", doReboot); +if ($btnSave) $btnSave.addEventListener("click", saveErrorReport); +if ($credsCloseBtn) $credsCloseBtn.addEventListener("click", closeCredsModal); +if ($supportCloseBtn) $supportCloseBtn.addEventListener("click", closeSupportModal); + +if ($modal) $modal.addEventListener("click", function(e) { if (e.target === $modal) closeUpdateModal(); }); +if ($credsModal) $credsModal.addEventListener("click", function(e) { if (e.target === $credsModal) closeCredsModal(); }); +if ($supportModal) $supportModal.addEventListener("click", function(e) { if (e.target === $supportModal) closeSupportModal(); }); + +// ── Init ────────────────────────────────────────────────────────── + +async function init() { + try { + var cfg = await apiFetch("/api/config"); + if (cfg.category_order) { + for (var i = 0; i < cfg.category_order.length; i++) { + _categoryLabels[cfg.category_order[i][0]] = cfg.category_order[i][1]; + } + } + var badge = document.getElementById("role-badge"); + if (badge && cfg.role_label) badge.textContent = cfg.role_label; + } catch (_) {} + + await refreshServices(); + loadNetwork(); + checkUpdates(); + + setInterval(refreshServices, POLL_INTERVAL_SERVICES); + setInterval(checkUpdates, POLL_INTERVAL_UPDATES); +} + +document.addEventListener("DOMContentLoaded", init); \ No newline at end of file diff --git a/app/sovran_systemsos_web/static/style.css b/app/sovran_systemsos_web/static/style.css index a2dbec6..cbfa0e1 100644 --- a/app/sovran_systemsos_web/static/style.css +++ b/app/sovran_systemsos_web/static/style.css @@ -756,4 +756,207 @@ button.btn-reboot:hover:not(:disabled) { width: 200px; height: 200px; } -} \ No newline at end of file + +/* ── Tech Support tile ───────────────────────────────────────────── */ + +.support-tile { + border-color: var(--accent-color); + border-width: 2px; + border-style: dashed; +} + +.support-tile:hover { + border-color: #a8c8ff; + border-style: solid; +} + +.support-status-label { + font-size: 0.75rem; + color: var(--accent-color); + font-weight: 600; +} + +/* ── Tech Support modal content ──────────────────────────────────── */ + +.support-section { + text-align: center; + padding: 8px 0; +} + +.support-icon-big { + font-size: 3rem; + margin-bottom: 12px; +} + +.support-heading { + font-size: 1.15rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; +} + +.support-active-heading { + color: var(--yellow); +} + +.support-desc { + font-size: 0.88rem; + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 20px; + max-width: 480px; + margin-left: auto; + margin-right: auto; +} + +.support-info-box { + background-color: #12121c; + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 16px 20px; + margin: 0 auto 20px; + max-width: 400px; +} + +.support-active-box { + border-color: var(--yellow); +} + +.support-info-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; +} + +.support-info-label { + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-dim); +} + +.support-info-value { + font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace; + font-size: 0.92rem; + color: var(--accent-color); + font-weight: 600; +} + +.support-info-hint { + font-size: 0.78rem; + color: var(--text-dim); + margin-top: 8px; + font-style: italic; +} + +.support-steps { + text-align: left; + max-width: 420px; + margin: 0 auto 24px; +} + +.support-steps-title { + font-size: 0.82rem; + font-weight: 700; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.support-steps ol { + padding-left: 20px; + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.8; +} + +.support-btn-enable { + background-color: var(--green); + color: #fff; + padding: 12px 32px; + font-size: 1rem; + font-weight: 700; + border-radius: 10px; +} + +.support-btn-enable:hover:not(:disabled) { + background-color: #27ae6e; +} + +.support-btn-disable { + background-color: var(--red); + color: #fff; + padding: 12px 32px; + font-size: 1rem; + font-weight: 700; + border-radius: 10px; +} + +.support-btn-disable:hover:not(:disabled) { + background-color: #c41520; +} + +.support-active-note { + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: 20px; + max-width: 420px; + margin-left: auto; + margin-right: auto; +} + +.support-fine-print { + font-size: 0.75rem; + color: var(--text-dim); + margin-top: 12px; + font-style: italic; +} + +.support-verify-box { + background-color: #12121c; + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 16px 20px; + margin: 20px auto; + max-width: 400px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.support-verify-label { + font-size: 0.82rem; + font-weight: 700; + color: var(--text-dim); +} + +.support-verify-value { + font-size: 0.88rem; + font-weight: 700; +} + +.support-verify-value.verified-gone { + color: var(--green); +} + +.support-verify-value.verify-warning { + color: var(--yellow); +} + +.support-btn-done { + background-color: var(--border-color); + color: var(--text-primary); + padding: 10px 28px; + font-size: 0.92rem; + font-weight: 600; + border-radius: 10px; +} + +.support-btn-done:hover:not(:disabled) { + background-color: #5a5c72; + + +} + +} diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index 94970ed..1b7455b 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -67,6 +67,19 @@ + + +