Backend: wrap SUM() queries with COALESCE in handle_time_series, handle_clients_usage, and handle_detailed_usage to prevent NULL-induced panics when no data exists for a time window. Frontend: add showEmptyChart() empty-state messages and error feedback across overview, analytics, costs, and clients pages. Rewrite analytics loadCharts() to use Promise.allSettled() so each chart renders independently on partial API failures.
197 lines
7.4 KiB
JavaScript
197 lines
7.4 KiB
JavaScript
// 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 = '<tr><td colspan="8" class="text-center">No clients configured</td></tr>';
|
|
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 `
|
|
<tr>
|
|
<td><span class="badge-client">${client.id}</span></td>
|
|
<td><strong>${client.name}</strong></td>
|
|
<td>
|
|
<code class="token-display">sk-••••${client.id.substring(client.id.length - 4)}</code>
|
|
</td>
|
|
<td>${created}</td>
|
|
<td>${client.last_used ? window.api.formatTimeAgo(client.last_used) : 'Never'}</td>
|
|
<td>${client.requests_count.toLocaleString()}</td>
|
|
<td>
|
|
<span class="status-badge ${statusClass}">
|
|
<i class="fas fa-${statusIcon}"></i>
|
|
${client.status}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn-action" title="Edit" onclick="window.clientsPage.editClient('${client.id}')">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn-action danger" title="Delete" onclick="window.clientsPage.deleteClient('${client.id}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
async loadClientUsageChart() {
|
|
try {
|
|
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'
|
|
}]
|
|
};
|
|
|
|
window.chartManager.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 = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Create New API Client</h3>
|
|
<button class="modal-close" onclick="this.closest('.modal').remove()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-control">
|
|
<label for="new-client-name">Display Name</label>
|
|
<input type="text" id="new-client-name" placeholder="e.g. My Coding Assistant" required>
|
|
</div>
|
|
<div class="form-control">
|
|
<label for="new-client-id">Custom ID (Optional)</label>
|
|
<input type="text" id="new-client-id" placeholder="e.g. personal-app">
|
|
<small>Leave empty to generate automatically</small>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="this.closest('.modal').remove()">Cancel</button>
|
|
<button class="btn btn-primary" id="confirm-create-client">Create Client</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 {
|
|
await window.api.post('/clients', { name, client_id: id || null });
|
|
window.authManager.showToast(`Client "${name}" created`, 'success');
|
|
modal.remove();
|
|
this.loadClients();
|
|
} catch (error) {
|
|
window.authManager.showToast(error.message, 'error');
|
|
}
|
|
};
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
editClient(id) {
|
|
window.authManager.showToast('Edit client not implemented yet', 'info');
|
|
}
|
|
}
|
|
|
|
window.initClients = async () => {
|
|
window.clientsPage = new ClientsPage();
|
|
};
|