398 lines
18 KiB
JavaScript
398 lines
18 KiB
JavaScript
/**
|
|
* Main App Logic for Intaleq Dashboard (Vanilla Version)
|
|
*/
|
|
|
|
const app = {
|
|
state: {
|
|
tenant: null,
|
|
keys: [],
|
|
showKeys: {}, // { id: boolean }
|
|
activePage: 'home'
|
|
},
|
|
|
|
init: async () => {
|
|
try {
|
|
console.log('🚀 Intaleq Dashboard Initializing Core UI...');
|
|
app.updateHeader();
|
|
app.bindEvents();
|
|
|
|
// Check for payment success/fail status earlier
|
|
app.checkPaymentStatus();
|
|
|
|
// Trigger initial routing based on URL hash
|
|
app.handleRouting();
|
|
|
|
if (window.lucide) lucide.createIcons();
|
|
} catch (e) {
|
|
console.error('Core UI Init failed', e);
|
|
}
|
|
},
|
|
|
|
checkPaymentStatus: () => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const status = params.get('payment_status');
|
|
const txId = params.get('id');
|
|
|
|
if (status === 'success') {
|
|
console.log('🏁 Payment Success Detected! Updating UI...');
|
|
app.showSuccessModal(txId);
|
|
|
|
// Force refresh stats to show PRO plan
|
|
app.fetchStats();
|
|
|
|
// Clean URL without refresh
|
|
const newUrl = window.location.pathname + window.location.hash;
|
|
window.history.replaceState({}, document.title, newUrl);
|
|
}
|
|
},
|
|
|
|
showSuccessModal: (id) => {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 z-[100] flex items-center justify-center p-6 bg-slate-950/90 backdrop-blur-xl animate-in fade-in duration-500';
|
|
modal.innerHTML = `
|
|
<div class="bg-white rounded-[3rem] p-12 max-w-lg w-full text-center shadow-[0_0_100px_rgba(37,99,235,0.3)] border border-white/20 relative overflow-hidden animate-in zoom-in-95 duration-500">
|
|
<div class="absolute -top-24 -left-24 w-48 h-48 bg-blue-600/20 blur-[80px] rounded-full"></div>
|
|
|
|
<div class="relative z-10">
|
|
<div class="w-24 h-24 bg-emerald-500 rounded-full mx-auto flex items-center justify-center mb-8 shadow-2xl">
|
|
<i data-lucide="shield-check" class="w-12 h-12 text-white"></i>
|
|
</div>
|
|
|
|
<h2 class="text-4xl font-black text-slate-900 mb-4" data-i18n="welcome">Upgrade Successful!</h2>
|
|
<p class="text-slate-500 font-bold mb-8 italic">Your account has been upgraded to <span class="text-blue-600">PRO Plan</span>. Enjoy 50,000 monthly requests!</p>
|
|
|
|
<div class="p-6 bg-slate-50 rounded-3xl border border-slate-100 flex items-center justify-between mb-10">
|
|
<div class="text-left">
|
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Transaction ID</p>
|
|
<p class="font-mono text-sm font-bold text-slate-600">${id || 'N/A'}</p>
|
|
</div>
|
|
<div class="bg-blue-600 text-white px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest shadow-lg">PRO ACTIVE</div>
|
|
</div>
|
|
|
|
<button id="close-success-modal" class="w-full bg-slate-900 text-white py-5 rounded-2xl font-black text-lg hover:scale-[1.02] active:scale-95 transition-all shadow-2xl">
|
|
LET'S BUILD
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
if (window.lucide) lucide.createIcons();
|
|
|
|
document.getElementById('close-success-modal').onclick = () => {
|
|
modal.classList.add('animate-out', 'fade-out', 'zoom-out-95');
|
|
setTimeout(() => modal.remove(), 400);
|
|
};
|
|
},
|
|
|
|
onAuthenticated: async () => {
|
|
console.log('🔑 User Authenticated. Initializing Data Flow...');
|
|
await app.init(); // Setup UI first
|
|
await app.fetchData();
|
|
app.updateStats();
|
|
},
|
|
|
|
bindEvents: () => {
|
|
window.addEventListener('hashchange', app.handleRouting);
|
|
|
|
// Form submission for new key
|
|
const createKeyForm = document.getElementById('create-key-form');
|
|
if (createKeyForm) {
|
|
createKeyForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
await app.createKey();
|
|
});
|
|
}
|
|
},
|
|
|
|
handleRouting: () => {
|
|
const hash = window.location.hash.replace('#', '') || 'home';
|
|
app.state.activePage = hash;
|
|
|
|
// Update UI
|
|
document.querySelectorAll('.page-section').forEach(s => s.classList.remove('active'));
|
|
const activeSection = document.getElementById(hash);
|
|
if (activeSection) activeSection.classList.add('active');
|
|
|
|
// Update Nav
|
|
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
|
|
const activeLink = document.getElementById(`nav-${hash}`);
|
|
if (activeLink) activeLink.classList.add('active');
|
|
|
|
// Specialized page logic
|
|
if (hash === 'playground') {
|
|
playground.init(app.state.keys);
|
|
} else if (hash === 'analytics') {
|
|
analytics.init();
|
|
} else if (hash === 'billing') {
|
|
billing.init();
|
|
} else if (hash === 'refinement') {
|
|
refinement.init();
|
|
} else if (hash === 'docs') {
|
|
docs.init();
|
|
}
|
|
},
|
|
|
|
fetchData: async () => {
|
|
const headers = auth.getAuthHeader();
|
|
try {
|
|
// Fetch Tenant
|
|
const tenantRes = await fetch('/api/auth/management/me', { headers });
|
|
if (tenantRes.ok) {
|
|
app.state.tenant = await tenantRes.json();
|
|
app.updateHeader();
|
|
}
|
|
|
|
// Fetch Keys
|
|
if (app.state.tenant) {
|
|
const keysRes = await fetch('/api/auth/management/keys', { headers });
|
|
if (keysRes.ok) {
|
|
app.state.keys = await keysRes.json();
|
|
app.renderKeysTable();
|
|
}
|
|
}
|
|
// Update Quota Widget
|
|
const summaryRes = await fetch('/api/usage/summary', { headers });
|
|
if (summaryRes.ok) {
|
|
const summary = await summaryRes.json();
|
|
app.updateQuotaWidget(summary);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Dashboard data fetch partially failed', error);
|
|
}
|
|
},
|
|
|
|
updateQuotaWidget: (summary) => {
|
|
const percent = parseFloat(summary.percentage) || 0;
|
|
const used = summary.monthlyUsage || 0;
|
|
const total = summary.limit || 8000;
|
|
const left = Math.max(total - used, 0);
|
|
|
|
// Sidebar Widget
|
|
const bar = document.getElementById('quota-bar');
|
|
const pctText = document.getElementById('quota-percent');
|
|
const leftText = document.getElementById('quota-text');
|
|
|
|
if (bar) {
|
|
bar.style.width = `${percent}%`;
|
|
// Color shift based on usage
|
|
bar.classList.remove('from-blue-600', 'to-blue-400', 'from-orange-500', 'to-orange-400', 'from-red-600', 'to-red-400');
|
|
if (percent > 90) {
|
|
bar.classList.add('from-red-600', 'to-red-400');
|
|
} else if (percent > 70) {
|
|
bar.classList.add('from-orange-500', 'to-orange-400');
|
|
} else {
|
|
bar.classList.add('from-blue-600', 'to-blue-400');
|
|
}
|
|
}
|
|
if (pctText) pctText.textContent = `${percent}%`;
|
|
if (leftText) leftText.textContent = `${left.toLocaleString()} requests left`;
|
|
|
|
// Billing Section Widget (if present)
|
|
const bBar = document.getElementById('billing-quota-bar');
|
|
const bPct = document.getElementById('billing-quota-percent');
|
|
const bUsed = document.getElementById('billing-quota-used');
|
|
const bTotal = document.getElementById('billing-quota-total');
|
|
|
|
if (bBar) bBar.style.width = `${percent}%`;
|
|
if (bPct) bPct.textContent = `${percent}%`;
|
|
if (bUsed) bUsed.textContent = `${used.toLocaleString()} used`;
|
|
if (bTotal) bTotal.textContent = `${total.toLocaleString()} limit`;
|
|
},
|
|
|
|
updateHeader: () => {
|
|
if (!app.state.tenant) return;
|
|
document.getElementById('tenant-name').textContent = app.state.tenant.name;
|
|
document.getElementById('tenant-email').textContent = app.state.tenant.email;
|
|
document.getElementById('welcome-msg').textContent = `Welcome back, ${app.state.tenant.name.split(' ')[0]}`;
|
|
|
|
// Role-based UI visibility
|
|
if (app.state.tenant.role === 'ADMIN') {
|
|
const auditLink = document.getElementById('nav-refinement');
|
|
if (auditLink) auditLink.classList.remove('hidden');
|
|
}
|
|
|
|
// Update user avatar if photoUrl exists
|
|
if (app.state.tenant.photoUrl) {
|
|
const avatarContainer = document.querySelector('.w-10.h-10.rounded-full.bg-slate-900');
|
|
if (avatarContainer) {
|
|
avatarContainer.innerHTML = `<img src="${app.state.tenant.photoUrl}" class="w-full h-full rounded-full object-cover border border-blue-500/20" alt="Profile">`;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateStats: async () => {
|
|
try {
|
|
const headers = auth.getAuthHeader();
|
|
const res = await fetch('/api/usage/summary', { headers });
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
|
|
// Update KPI Cards
|
|
document.getElementById('active-keys-count').textContent = app.state.keys.length;
|
|
|
|
// Detailed Stats from getUsageSummary
|
|
if (document.getElementById('stat-total-req')) {
|
|
document.getElementById('stat-total-req').textContent = data.monthlyUsage.toLocaleString();
|
|
}
|
|
if (document.getElementById('stat-success-percent')) {
|
|
document.getElementById('stat-success-percent').textContent = `${data.successRate}%`;
|
|
document.getElementById('stat-success-rate').textContent = data.successRate >= 95 ? 'Excellent' : 'Stable';
|
|
}
|
|
if (document.getElementById('stat-avg-latency')) {
|
|
document.getElementById('stat-avg-latency').textContent = `${data.avgLatency}ms`;
|
|
document.getElementById('stat-latency-val').textContent = `-${Math.round(data.avgLatency * 0.1)}ms`;
|
|
}
|
|
|
|
// Update Progress bar & Quota card
|
|
const bar = document.getElementById('usage-progress-bar');
|
|
const label = document.getElementById('usage-percentage-label');
|
|
const limitText = document.getElementById('usage-limit-text');
|
|
|
|
if (bar) bar.style.width = `${data.percentage}%`;
|
|
if (label) label.textContent = `${data.percentage}% USED`;
|
|
if (limitText) {
|
|
const remaining = Math.max(0, data.limit - data.monthlyUsage);
|
|
limitText.textContent = `${remaining.toLocaleString()} requests left`;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update stats', error);
|
|
}
|
|
|
|
// Inject sample bars for visual flair if chart not ready
|
|
const container = document.getElementById('traffic-bars');
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
// High-density bars for a tech/premium feel
|
|
const values = [40, 60, 55, 80, 70, 45, 90, 85, 60, 40, 30, 55, 75, 40, 60, 55, 80, 70, 45, 90, 85, 60, 40, 30, 55, 75, 50, 65, 80, 70, 30];
|
|
values.forEach((h, index) => {
|
|
const bar = document.createElement('div');
|
|
bar.className = 'flex-1 bg-gradient-to-t from-blue-600/10 to-blue-400/60 rounded-t-[2px] relative group hover:to-blue-300 transition-all duration-500';
|
|
bar.style.height = `${h}%`;
|
|
bar.style.transitionDelay = `${index * 20}ms`;
|
|
bar.innerHTML = `<div class="absolute -top-10 left-1/2 -translate-x-1/2 glass px-2 py-1 rounded text-[10px] font-bold opacity-0 group-hover:opacity-100 transition-opacity z-10">${h}k</div>`;
|
|
container.appendChild(bar);
|
|
});
|
|
}
|
|
},
|
|
|
|
renderKeysTable: () => {
|
|
const tbody = document.getElementById('keys-table-body');
|
|
const createBtn = document.getElementById('create-key-btn');
|
|
|
|
if (!tbody) return;
|
|
|
|
if (app.state.keys.length === 0) {
|
|
tbody.innerHTML = `<tr><td colspan="5" class="py-20 text-center text-slate-500 font-medium">No API keys found. Create one to get started.</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
// Enable create button (removed previous limit restrictiveness)
|
|
createBtn.disabled = false;
|
|
createBtn.title = "Create a new API key";
|
|
createBtn.innerHTML = '<i data-lucide="plus" class="w-4 h-4"></i> Create Key';
|
|
|
|
tbody.innerHTML = app.state.keys.map(key => {
|
|
const isVisible = app.state.showKeys[key.id];
|
|
const maskedKey = isVisible ? key.key : "in_••••••••••••••••••••••••";
|
|
const eyeIcon = isVisible ? 'eye-off' : 'eye';
|
|
|
|
return `
|
|
<tr class="hover:bg-white/[0.02] transition-colors">
|
|
<td class="px-6 py-6 font-medium">${key.name}</td>
|
|
<td class="px-6 py-6">
|
|
<div class="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1.5 w-fit border border-slate-800">
|
|
<code class="text-xs text-blue-400 font-mono">${maskedKey}</code>
|
|
<div class="flex items-center gap-1 ml-2 border-l border-slate-800 pl-2">
|
|
<button onclick="app.toggleKeyVisibility('${key.id}')" class="text-slate-500 hover:text-white">
|
|
<i data-lucide="${eyeIcon}" class="w-3.5 h-3.5"></i>
|
|
</button>
|
|
<button class="text-slate-500 hover:text-white" onclick="app.copyToClipboard('${key.key}')">
|
|
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6">
|
|
<span class="flex items-center gap-1.5 text-xs font-bold ${key.isActive ? 'text-emerald-400' : 'text-slate-500'}">
|
|
<div class="w-1.5 h-1.5 rounded-full ${key.isActive ? 'bg-emerald-400 animate-pulse' : 'bg-slate-500'}"></div>
|
|
${key.isActive ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-6">
|
|
<div class="flex gap-2">
|
|
${(key.allowedOrigins && key.allowedOrigins.length > 0) ?
|
|
key.allowedOrigins.map(org => `<span class="px-2 py-0.5 rounded-md bg-blue-500/10 text-[10px] uppercase font-black text-blue-400 flex items-center gap-1"><i data-lucide="globe" class="w-2.5 h-2.5"></i> ${org}</span>`).join('') :
|
|
`<span class="px-2 py-0.5 rounded-md bg-slate-800 text-[10px] uppercase font-black text-slate-400 flex items-center gap-1"><i data-lucide="globe" class="w-2.5 h-2.5"></i> Universal</span>`
|
|
}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6 text-right">
|
|
<button class="p-2 text-slate-500 hover:text-white" onclick="app.fetchData()">
|
|
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
lucide.createIcons();
|
|
},
|
|
|
|
toggleKeyVisibility: (id) => {
|
|
app.state.showKeys[id] = !app.state.showKeys[id];
|
|
app.renderKeysTable();
|
|
},
|
|
|
|
copyToClipboard: (text) => {
|
|
navigator.clipboard.writeText(text);
|
|
// Simple toast or just feedback
|
|
console.log('Copied to clipboard');
|
|
},
|
|
|
|
toggleModal: (id, show) => {
|
|
const modal = document.getElementById(id);
|
|
if (modal) {
|
|
if (show) modal.classList.remove('hidden');
|
|
else modal.classList.add('hidden');
|
|
}
|
|
},
|
|
|
|
createKey: async () => {
|
|
const name = document.getElementById('new-key-name').value;
|
|
const btn = document.getElementById('btn-submit-key');
|
|
|
|
if (!app.state.tenant) return;
|
|
|
|
try {
|
|
btn.disabled = true;
|
|
btn.textContent = 'Creating...';
|
|
|
|
const res = await fetch('/api/auth/management/keys', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...auth.getAuthHeader()
|
|
},
|
|
body: JSON.stringify({ name, rateLimit: 100 })
|
|
});
|
|
|
|
if (res.ok) {
|
|
app.toggleModal('create-key-modal', false);
|
|
await app.fetchData();
|
|
document.getElementById('new-key-name').value = '';
|
|
} else {
|
|
alert('Failed to create key');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Create Key';
|
|
}
|
|
}
|
|
};
|
|
|
|
// Script sequence is now controlled by auth.js onAuthenticated lifecycle
|
|
// document.addEventListener('DOMContentLoaded', app.init);
|