From 0a2dc493145504c3b4217e7f39b11285472b5251 Mon Sep 17 00:00:00 2001 From: Jino Jose Date: Tue, 30 Jun 2026 16:39:02 +0530 Subject: [PATCH] Guard unavailable appliance service links --- cezen-portal/branding.js | 98 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/cezen-portal/branding.js b/cezen-portal/branding.js index 717f0b3..6ec890e 100644 --- a/cezen-portal/branding.js +++ b/cezen-portal/branding.js @@ -48,6 +48,14 @@ max: 'Max', }; + var SERVICE_PORTS = { + '3001': { name: 'open-webui', label: 'Open WebUI' }, + '11434': { name: 'ollama', label: 'Ollama' }, + '8000': { name: 'chromadb', label: 'ChromaDB' }, + '8888': { name: 'jupyter', label: 'Jupyter' }, + '8080': { name: 'cezen-api', label: 'Nexus API' }, + }; + // ── Upgrade modal (injected once into the DOM) ────────────────────────────── function ensureModal() { if (document.getElementById('cezen-upgrade-modal')) return; @@ -80,9 +88,84 @@ document.getElementById('cezen-upgrade-modal').classList.add('cezen-modal-open'); } + function openServiceModal(label) { + ensureModal(); + document.getElementById('cezen-upgrade-title').textContent = label + ' is not running'; + document.getElementById('cezen-upgrade-body').textContent = + 'This service is not currently available on this appliance. Check the home page service status, or ask the administrator to enable and start the service for this tier.'; + document.getElementById('cezen-upgrade-cta').style.display = 'none'; + document.getElementById('cezen-upgrade-modal').classList.add('cezen-modal-open'); + } + function closeModal() { var m = document.getElementById('cezen-upgrade-modal'); if (m) m.classList.remove('cezen-modal-open'); + var cta = document.getElementById('cezen-upgrade-cta'); + if (cta) cta.style.display = ''; + } + + function currentBaseForPort(port) { + var protocol = location.protocol === 'https:' ? 'https:' : 'http:'; + var host = location.hostname || 'ai.local'; + return protocol + '//' + host + ':' + port; + } + + function rewriteAiLocalText(value) { + if (!value || value.indexOf('ai.local') === -1) return value; + return value + .replace(/https?:\/\/ai\.local:3001/g, currentBaseForPort('3001')) + .replace(/https?:\/\/ai\.local:11434/g, currentBaseForPort('11434')) + .replace(/https?:\/\/ai\.local:8000/g, currentBaseForPort('8000')) + .replace(/https?:\/\/ai\.local:8888/g, currentBaseForPort('8888')) + .replace(/https?:\/\/ai\.local:8080/g, currentBaseForPort('8080')); + } + + function normalizeApplianceLinks() { + document.querySelectorAll('a[href]').forEach(function (link) { + var href = link.getAttribute('href') || ''; + if (href.indexOf('ai.local') !== -1) link.setAttribute('href', rewriteAiLocalText(href)); + }); + + var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { + acceptNode: function (node) { + var tag = node.parentNode && node.parentNode.tagName; + if (tag === 'SCRIPT' || tag === 'STYLE') return NodeFilter.FILTER_REJECT; + return node.nodeValue.indexOf('ai.local') !== -1 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; + } + }); + var textNodes = []; + while (walker.nextNode()) textNodes.push(walker.currentNode); + textNodes.forEach(function (node) { node.nodeValue = rewriteAiLocalText(node.nodeValue); }); + } + + function serviceForHref(href) { + try { + var u = new URL(href, location.href); + return SERVICE_PORTS[u.port || (u.protocol === 'https:' ? '443' : '80')]; + } catch (e) { + return null; + } + } + + function applyServiceAvailability() { + fetch('/api/services', { credentials: 'include' }) + .then(function (r) { return r.ok ? r.json() : []; }) + .then(function (services) { + var online = {}; + services.forEach(function (svc) { online[svc.name] = !!svc.ok; }); + document.querySelectorAll('a[href]').forEach(function (link) { + var svc = serviceForHref(link.getAttribute('href') || ''); + if (!svc || online[svc.name]) return; + if (link.classList.contains('cezen-service-unavailable')) return; + link.classList.add('cezen-service-unavailable'); + link.setAttribute('title', svc.label + ' is not running on this appliance'); + link.addEventListener('click', function (e) { + e.preventDefault(); + openServiceModal(svc.label); + }); + }); + }) + .catch(function () { /* Public pages or offline backend: leave links unchanged. */ }); } // ── Apply tier locks to all nav links ────────────────────────────────────── @@ -129,6 +212,8 @@ var tierSlug = b.tier_slug || 'basic'; ensureApplianceNavLink(); + normalizeApplianceLinks(); + applyServiceAvailability(); // ── Accent color CSS variable ─────────────────────────────────────────── document.documentElement.style.setProperty('--accent', accent); @@ -229,6 +314,19 @@ '}', 'a.cezen-locked:hover { opacity: 0.7; }', + 'a.cezen-service-unavailable {', + ' opacity: 0.58;', + ' cursor: not-allowed !important;', + '}', + 'a.cezen-service-unavailable::after {', + ' content: " offline";', + ' display: inline-block;', + ' margin-left: 6px;', + ' color: #dc2626;', + ' font-size: 0.75em;', + ' font-weight: 700;', + '}', + /* Lock badge */ '.cezen-lock-badge {', ' font-size: 0.65em;',