/** * 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 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', }; // ── 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 = [ '
', '
', '
🔒
', '

Feature Locked

', '

', '
', ' Contact Sales to Upgrade', ' ', '
', '
', ].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 closeModal() { var m = document.getElementById('cezen-upgrade-modal'); if (m) m.classList.remove('cezen-modal-open'); } // ── 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(); // ── Accent color CSS variable ─────────────────────────────────────────── document.documentElement.style.setProperty('--accent', accent); document.documentElement.style.setProperty('--accent-dark', shadeColor(accent, -20)); // ── Page ──────────────────────────────────────────────────────── 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; }', /* 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 })();