// Clients Page Module class ClientsPage { constructor() { this.clients = []; this.init(); } async init() { // Load data await Promise.all([ this.loadClients(), this.loadClientUsageChart() ]); // Setup event listeners this.setupEventListeners(); } async loadClients() { try { const data = await window.api.get('/clients'); this.clients = data; this.renderClientsTable(); } catch (error) { console.error('Error loading clients:', error); window.authManager.showToast('Failed to load clients', 'error'); } } renderClientsTable() { const tableBody = document.querySelector('#clients-table tbody'); if (!tableBody) return; if (this.clients.length === 0) { tableBody.innerHTML = 'No clients configured'; return; } tableBody.innerHTML = this.clients.map(client => { const statusClass = client.status === 'active' ? 'success' : 'secondary'; const statusIcon = client.status === 'active' ? 'check-circle' : 'clock'; const created = luxon.DateTime.fromISO(client.created_at).toFormat('MMM dd, yyyy'); return ` ${client.id} ${client.name} sk-••••${client.id.substring(client.id.length - 4)} ${created} ${client.last_used ? window.api.formatTimeAgo(client.last_used) : 'Never'} ${client.requests_count.toLocaleString()} ${client.status} ${window._userRole === 'admin' ? `
` : ''} `; }).join(''); } async loadClientUsageChart() { try { const cm = window.chartManager || await window.waitForChartManager(); if (!cm) { this.showEmptyChart('client-usage-chart', 'Chart system unavailable'); return; } const data = await window.api.get('/usage/clients'); if (!data || data.length === 0) { this.showEmptyChart('client-usage-chart', 'No client usage data yet'); return; } const chartData = { labels: data.map(item => item.client_id), datasets: [{ label: 'Requests', data: data.map(item => item.requests), color: '#6366f1' }] }; cm.createHorizontalBarChart('client-usage-chart', chartData); } catch (error) { console.error('Error loading client usage chart:', error); this.showEmptyChart('client-usage-chart', 'Failed to load usage data'); } } showEmptyChart(canvasId, message) { const canvas = document.getElementById(canvasId); if (!canvas) return; const container = canvas.closest('.chart-container'); if (container) { canvas.style.display = 'none'; let msg = container.querySelector('.empty-chart-msg'); if (!msg) { msg = document.createElement('div'); msg.className = 'empty-chart-msg'; msg.style.cssText = 'display:flex;align-items:center;justify-content:center;height:200px;color:var(--fg4);font-size:0.9rem;'; container.appendChild(msg); } msg.textContent = message; } } setupEventListeners() { const addBtn = document.getElementById('add-client'); if (addBtn) { addBtn.onclick = () => this.showAddClientModal(); } } showAddClientModal() { const modal = document.createElement('div'); modal.className = 'modal active'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.querySelector('#confirm-create-client').onclick = async () => { const name = modal.querySelector('#new-client-name').value; const id = modal.querySelector('#new-client-id').value; if (!name) { window.authManager.showToast('Name is required', 'error'); return; } try { const result = await window.api.post('/clients', { name, client_id: id || null }); modal.remove(); this.loadClients(); // Show the generated token (copy-once dialog) if (result.token) { this.showTokenRevealModal(name, result.token); } else { window.authManager.showToast(`Client "${name}" created`, 'success'); } } catch (error) { window.authManager.showToast(error.message, 'error'); } }; } showTokenRevealModal(clientName, token) { const modal = document.createElement('div'); modal.className = 'modal active'; modal.innerHTML = ` `; document.body.appendChild(modal); modal.querySelector('#copy-token-btn').onclick = () => { navigator.clipboard.writeText(token).then(() => { window.authManager.showToast('Token copied to clipboard', 'success'); }); }; } async deleteClient(id) { if (!confirm(`Are you sure you want to delete client ${id}? This cannot be undone.`)) return; try { await window.api.delete(`/clients/${id}`); window.authManager.showToast('Client deleted', 'success'); this.loadClients(); } catch (error) { window.authManager.showToast(error.message, 'error'); } } async editClient(id) { try { const client = await window.api.get(`/clients/${id}`); this.showEditClientModal(client); } catch (error) { window.authManager.showToast(`Failed to load client: ${error.message}`, 'error'); } } showEditClientModal(client) { const modal = document.createElement('div'); modal.className = 'modal active'; modal.innerHTML = ` `; document.body.appendChild(modal); // Load tokens this.loadTokensList(client.id, modal); modal.querySelector('#generate-token-btn').onclick = async () => { try { const result = await window.api.post(`/clients/${client.id}/tokens`, { name: null }); if (result.token) { this.showTokenRevealModal(`${client.name} - New Token`, result.token); } this.loadTokensList(client.id, modal); } catch (error) { window.authManager.showToast(error.message, 'error'); } }; modal.querySelector('#confirm-edit-client').onclick = async () => { const name = modal.querySelector('#edit-client-name').value.trim(); const description = modal.querySelector('#edit-client-description').value.trim(); const rateLimitVal = modal.querySelector('#edit-client-rate-limit').value; const isActive = modal.querySelector('#edit-client-active').checked; if (!name) { window.authManager.showToast('Name is required', 'error'); return; } const payload = { name, description: description || null, is_active: isActive, rate_limit_per_minute: rateLimitVal ? parseInt(rateLimitVal, 10) : null, }; try { await window.api.put(`/clients/${client.id}`, payload); window.authManager.showToast(`Client "${name}" updated`, 'success'); modal.remove(); this.loadClients(); } catch (error) { window.authManager.showToast(error.message, 'error'); } }; } async loadTokensList(clientId, modal) { const container = modal.querySelector('#tokens-list'); if (!container) return; try { const tokens = await window.api.get(`/clients/${clientId}/tokens`); if (!tokens || tokens.length === 0) { container.innerHTML = '
No tokens. Generate one to enable API access.
'; return; } container.innerHTML = tokens.map(t => { const lastUsed = t.last_used_at ? luxon.DateTime.fromISO(t.last_used_at).toRelative() : 'Never'; return `
${t.token_masked} ${lastUsed}
`; }).join(''); } catch (error) { container.innerHTML = '
Failed to load tokens
'; } } async revokeToken(clientId, tokenId, btn) { if (!confirm('Revoke this token? Any services using it will lose access.')) return; try { await window.api.delete(`/clients/${clientId}/tokens/${tokenId}`); window.authManager.showToast('Token revoked', 'success'); // Reload tokens list in the open modal const modal = btn.closest('.modal'); if (modal) this.loadTokensList(clientId, modal); } catch (error) { window.authManager.showToast(error.message, 'error'); } } } window.initClients = async () => { window.clientsPage = new ClientsPage(); };