Package portal and close UI API gaps
This commit is contained in:
1 parent
e71b069014
commit
9ce9efc82e
59 files changed
+29355
-2
No files matched your search
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 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',
|
||||
};
|
||||
|
||||
// ── 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 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 <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; }',
|
||||
|
||||
/* 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
|
||||
})();
|
||||
Reference in new issue
Block a user