aipackage/cezen-portal/branding.js
2026-06-30 16:39:02 +05:30

458 lines
19 KiB
JavaScript

/**
* branding.js — Nexus One AI White-Label Branding + Tier Feature Gating
* Fetches /api/settings/branding (public, no auth) and:
* 1. Applies org name, logo, accent color, and page title across all pages.
* 2. Locks nav items that require a higher tier, showing an upgrade prompt.
* Inject BEFORE </body> on every portal page.
*/
(function () {
'use strict';
// ── Tier hierarchy ──────────────────────────────────────────────────────────
var TIER_RANK = { starter: 0, basic: 1, pro: 2, max: 3 };
// ── Feature → minimum tier required ────────────────────────────────────────
// Keyed by partial href match (filename). Value = minimum slug to access.
var FEATURE_TIERS = {
// Basic-only features (not available on Starter)
'analytics.html': 'basic',
'api-keys.html': 'basic',
'benchmark.html': 'basic',
'chat-multi.html': 'basic',
'feedback.html': 'basic',
'notifications.html': 'basic',
'prompt-studio.html': 'basic',
'model-compare.html': 'basic',
// Pro-only features
'agents.html': 'pro',
'api-playground.html':'pro',
'chatrooms.html': 'pro',
'connectors.html': 'pro',
'evals.html': 'pro',
'guardrails.html': 'pro',
'meeting.html': 'pro',
'rag-quality.html': 'pro',
'router.html': 'pro',
'schedules.html': 'pro',
'teams.html': 'pro',
'training.html': 'pro',
'workflow.html': 'pro',
};
// Human-readable tier names for the upgrade prompt
var TIER_NAMES = {
starter: 'Starter',
basic: 'Basic',
pro: 'Pro',
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;
var modal = document.createElement('div');
modal.id = 'cezen-upgrade-modal';
modal.innerHTML = [
'<div id="cezen-upgrade-backdrop"></div>',
'<div id="cezen-upgrade-box">',
' <div id="cezen-upgrade-icon">🔒</div>',
' <h3 id="cezen-upgrade-title">Feature Locked</h3>',
' <p id="cezen-upgrade-body"></p>',
' <div id="cezen-upgrade-actions">',
' <a href="mailto:sales@cezentech.com" id="cezen-upgrade-cta">Contact Sales to Upgrade</a>',
' <button id="cezen-upgrade-close">Close</button>',
' </div>',
'</div>',
].join('');
document.body.appendChild(modal);
document.getElementById('cezen-upgrade-close').onclick = closeModal;
document.getElementById('cezen-upgrade-backdrop').onclick = closeModal;
}
function openModal(featureName, requiredTier) {
ensureModal();
var name = TIER_NAMES[requiredTier] || requiredTier;
document.getElementById('cezen-upgrade-title').textContent = featureName + ' — Upgrade Required';
document.getElementById('cezen-upgrade-body').textContent =
'This feature is available on the ' + name + ' tier and above. ' +
'Contact Cezentech to upgrade your Nexus One AI package.';
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 ──────────────────────────────────────
function applyTierGating(tierSlug) {
var rank = TIER_RANK[tierSlug] !== undefined ? TIER_RANK[tierSlug] : 1;
document.querySelectorAll('a[href]').forEach(function (link) {
var href = link.getAttribute('href') || '';
var file = href.split('/').pop().split('?')[0];
var minTier = FEATURE_TIERS[file];
if (!minTier) return;
var minRank = TIER_RANK[minTier] !== undefined ? TIER_RANK[minTier] : 99;
if (rank >= minRank) return; // user has access — do nothing
// Mark the link as locked
if (link.classList.contains('cezen-locked')) return; // already processed
link.classList.add('cezen-locked');
// Add lock badge after the link text (if not already there)
var badge = document.createElement('span');
badge.className = 'cezen-lock-badge';
badge.setAttribute('title', 'Requires ' + TIER_NAMES[minTier] + ' tier');
badge.textContent = '🔒';
link.appendChild(badge);
// Intercept click — show upgrade modal instead of navigating
var featureName = (link.textContent || file).replace('🔒', '').trim();
link.addEventListener('click', function (e) {
e.preventDefault();
openModal(featureName, minTier);
});
});
}
// ── Main branding apply ─────────────────────────────────────────────────────
function applyBranding(b) {
var orgName = b.org_name || 'Nexus One AI';
var stackName = b.stack_name || 'Nexus One AI';
var logoUrl = b.logo_url || '';
var accent = b.accent_color || '#0D9488';
var footer = b.footer_text || 'Powered by Cezen';
var tier = b.tier_label || 'Basic Tier';
var tierSlug = b.tier_slug || 'basic';
ensureApplianceNavLink();
normalizeApplianceLinks();
applyServiceAvailability();
// ── Accent color CSS variable ───────────────────────────────────────────
document.documentElement.style.setProperty('--accent', accent);
document.documentElement.style.setProperty('--accent-dark', shadeColor(accent, -20));
// ── Page <title> ────────────────────────────────────────────────────────
if (document.title) {
document.title = document.title
.replace(/Cezen AI Suite/gi, stackName)
.replace(/Cezen AI/gi, orgName)
.replace(/Nexus One AI/gi, stackName);
}
// ── data-brand text nodes ───────────────────────────────────────────────
document.querySelectorAll('[data-brand="org"]').forEach(function (el) {
el.textContent = orgName;
});
document.querySelectorAll('[data-brand="stack"]').forEach(function (el) {
el.textContent = stackName;
});
document.querySelectorAll('[data-brand="tier"]').forEach(function (el) {
el.textContent = tier;
// Set slug for CSS tier-colour targeting
if (el.classList.contains('nav-tier-badge')) {
el.setAttribute('data-tier-slug', tierSlug);
}
});
document.querySelectorAll('[data-brand="footer"]').forEach(function (el) {
el.textContent = footer;
});
// ── Replace hardcoded text strings ─────────────────────────────────────
replaceText('.sidebar-brand-text, .brand-name, .navbar-brand', 'Cezen AI Suite', stackName);
replaceText('.sidebar-brand-text, .brand-name, .navbar-brand', 'Cezen AI', orgName);
replaceText('.sidebar-brand-text, .brand-name, .navbar-brand', 'Nexus One AI', orgName);
replaceText('.tier-badge, .tier-label', 'Basic Tier', tier);
replaceText('.tier-badge, .tier-label', 'Starter Tier', tier);
replaceText('footer, .footer-text', 'Powered by Cezen', footer);
// ── Logo ────────────────────────────────────────────────────────────────
if (logoUrl) {
var navLogoSlot = document.getElementById('nav-org-logo');
if (navLogoSlot) {
var navImg = document.createElement('img');
navImg.src = logoUrl;
navImg.alt = orgName + ' Logo';
navLogoSlot.innerHTML = '';
navLogoSlot.appendChild(navImg);
}
document.querySelectorAll('.brand-logo, [data-brand="logo"]').forEach(function (el) {
if (el.tagName === 'IMG') {
el.src = logoUrl;
el.alt = orgName + ' Logo';
} else {
var img = document.createElement('img');
img.src = logoUrl;
img.alt = orgName + ' Logo';
img.style.cssText = 'max-height:36px;width:auto;object-fit:contain;';
el.innerHTML = '';
el.appendChild(img);
}
});
}
// ── Accent <style> block ────────────────────────────────────────────────
var style = document.getElementById('cezen-brand-style');
if (!style) {
style = document.createElement('style');
style.id = 'cezen-brand-style';
document.head.appendChild(style);
}
style.textContent = [
':root { --accent: ' + accent + '; --accent-dark: ' + shadeColor(accent, -20) + '; }',
'a.brand-accent { color: ' + accent + ' !important; }',
/* btn-primary intentionally excluded — brand colour is fixed in style.css */
'.btn-accent { background: ' + accent + ' !important; border-color: ' + accent + ' !important; }',
'.btn-accent:hover { background: ' + shadeColor(accent, -20) + ' !important; }',
'.sidebar-nav .nav-link.active { border-left-color: ' + accent + ' !important; color: ' + accent + ' !important; }',
'.progress-bar { background-color: ' + accent + ' !important; }',
].join('\n');
// ── Tier feature gating ─────────────────────────────────────────────────
applyTierGating(tierSlug);
injectTierGatingStyles();
}
// ── Tier gating CSS (injected once) ────────────────────────────────────────
function injectTierGatingStyles() {
if (document.getElementById('cezen-tier-style')) return;
var s = document.createElement('style');
s.id = 'cezen-tier-style';
s.textContent = [
/* Locked nav links */
'a.cezen-locked {',
' opacity: 0.55;',
' cursor: not-allowed !important;',
' position: relative;',
'}',
'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;',
' margin-left: 5px;',
' vertical-align: middle;',
' pointer-events: none;',
'}',
/* Upgrade modal backdrop */
'#cezen-upgrade-modal {',
' display: none;',
' position: fixed;',
' inset: 0;',
' z-index: 99999;',
' align-items: center;',
' justify-content: center;',
'}',
'#cezen-upgrade-modal.cezen-modal-open { display: flex; }',
'#cezen-upgrade-backdrop {',
' position: absolute;',
' inset: 0;',
' background: rgba(15,23,42,0.65);',
' backdrop-filter: blur(3px);',
'}',
/* Modal box */
'#cezen-upgrade-box {',
' position: relative;',
' background: #fff;',
' border-radius: 14px;',
' padding: 40px 36px 32px;',
' max-width: 420px;',
' width: 90%;',
' text-align: center;',
' box-shadow: 0 24px 60px rgba(0,0,0,0.22);',
' animation: cezen-modal-in 0.2s ease;',
'}',
'@keyframes cezen-modal-in {',
' from { opacity:0; transform: scale(0.92) translateY(12px); }',
' to { opacity:1; transform: scale(1) translateY(0); }',
'}',
'#cezen-upgrade-icon { font-size: 2.4rem; margin-bottom: 10px; }',
'#cezen-upgrade-title {',
' font-size: 1.15rem;',
' font-weight: 700;',
' color: #0f172a;',
' margin: 0 0 10px;',
'}',
'#cezen-upgrade-body {',
' font-size: 0.9rem;',
' color: #475569;',
' line-height: 1.55;',
' margin: 0 0 24px;',
'}',
'#cezen-upgrade-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }',
'#cezen-upgrade-cta {',
' display: inline-block;',
' background: #0D9488;',
' color: #fff !important;',
' padding: 9px 20px;',
' border-radius: 7px;',
' font-size: 0.875rem;',
' font-weight: 600;',
' text-decoration: none;',
' transition: background 0.18s;',
'}',
'#cezen-upgrade-cta:hover { background: #0b7a6f; }',
'#cezen-upgrade-close {',
' background: #f1f5f9;',
' border: none;',
' border-radius: 7px;',
' padding: 9px 20px;',
' font-size: 0.875rem;',
' color: #334155;',
' cursor: pointer;',
' transition: background 0.18s;',
'}',
'#cezen-upgrade-close:hover { background: #e2e8f0; }',
].join('\n');
document.head.appendChild(s);
}
function replaceText(selector, from, to) {
try {
document.querySelectorAll(selector).forEach(function (el) {
if (el.childNodes.length === 1 && el.childNodes[0].nodeType === Node.TEXT_NODE) {
el.textContent = el.textContent.replace(
new RegExp(from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), to
);
}
});
} catch (e) { /* selector may not exist */ }
}
function ensureApplianceNavLink() {
if (document.querySelector('a[href="appliance.html"]')) return;
document.querySelectorAll('.nav-drop-cat').forEach(function (cat) {
if ((cat.textContent || '').trim().toUpperCase() !== 'SYSTEM /') return;
var next = cat.nextElementSibling;
var link = document.createElement('a');
link.href = 'appliance.html';
link.textContent = 'Appliance Ops';
if (location.pathname.endsWith('/appliance.html')) link.className = 'active';
cat.parentNode.insertBefore(link, next || null);
});
}
function shadeColor(hex, pct) {
var num = parseInt(hex.replace('#', ''), 16);
var r = Math.min(255, Math.max(0, (num >> 16) + Math.round(2.55 * pct)));
var g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) + Math.round(2.55 * pct)));
var b = Math.min(255, Math.max(0, (num & 0xff) + Math.round(2.55 * pct)));
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
// ── Fetch branding and apply ────────────────────────────────────────────────
fetch('/api/settings/branding', { credentials: 'include' })
.then(function (r) { return r.ok ? r.json() : {}; })
.then(function (data) { applyBranding(data); })
.catch(function () { applyBranding({}); }); // fallback: apply defaults silently
})();