// ============================================================ // Admin Panel — V10 // ============================================================ let authToken = localStorage.getItem('chatia_token'); let charts = {}; function toast(msg, type = 'default') { const container = document.getElementById('toastContainer'); const el = document.createElement('div'); el.className = `toast ${type}`; el.textContent = msg; container.appendChild(el); setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 200); }, 3000); } async function api(path) { const resp = await fetch(path, { headers: { 'Authorization': `Bearer ${authToken}` } }); if (resp.status === 401) { document.getElementById('unauthorized').style.display = 'flex'; document.getElementById('adminContent').style.display = 'none'; throw new Error('Unauthorized'); } if (resp.status === 403) { document.getElementById('unauthorized').style.display = 'flex'; document.getElementById('unauthorized').querySelector('h1').textContent = 'Accès admin requis'; document.getElementById('unauthorized').querySelector('p').textContent = 'Votre compte n\'est pas administrateur.'; document.getElementById('adminContent').style.display = 'none'; throw new Error('Forbidden'); } return resp; } function fmtDate(ts) { if (!ts) return '—'; return new Date(ts * 1000).toLocaleString(); } function fmtNum(n) { if (!n && n !== 0) return '—'; return n.toLocaleString(); } const Admin = { async init() { if (!authToken) { document.getElementById('unauthorized').style.display = 'flex'; document.getElementById('adminContent').style.display = 'none'; return; } // Verify admin try { const meResp = await api('/me'); if (!meResp.ok) throw new Error('Not logged in'); const me = await meResp.json(); if (!me.is_admin) { document.getElementById('unauthorized').style.display = 'flex'; document.getElementById('unauthorized').querySelector('h1').textContent = 'Accès admin requis'; document.getElementById('unauthorized').querySelector('p').textContent = 'Votre compte n\'est pas administrateur.'; document.getElementById('adminContent').style.display = 'none'; return; } } catch (e) { document.getElementById('unauthorized').style.display = 'flex'; document.getElementById('adminContent').style.display = 'none'; return; } document.getElementById('adminContent').style.display = 'block'; await this.loadStats(); await this.loadUsers(); await this.loadUsage(); await this.loadAudit(); }, switchTab(ev, tabId) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); ev.currentTarget.classList.add('active'); document.getElementById(tabId).classList.add('active'); }, async loadStats() { try { const resp = await api('/admin/stats'); if (!resp.ok) throw new Error('Failed'); const s = await resp.json(); document.getElementById('statTotalUsers').textContent = fmtNum(s.total_users); document.getElementById('statNew7').textContent = fmtNum(s.new_7d); document.getElementById('statNew20').textContent = fmtNum(s.new_20d); document.getElementById('statNew30').textContent = fmtNum(s.new_30d); document.getElementById('statActive24').textContent = fmtNum(s.active_24h); document.getElementById('statActive7').textContent = fmtNum(s.active_7d); document.getElementById('statBanned').textContent = fmtNum(s.banned); document.getElementById('statJobs').textContent = fmtNum(s.jobs_running); document.getElementById('statTokens30').textContent = fmtNum(s.tokens_30d); document.getElementById('statTokensTotal').textContent = fmtNum(s.tokens_total); const growthEl = document.getElementById('statGrowth'); if (s.growth_pct !== null) { const sign = s.growth_pct >= 0 ? '+' : ''; const cls = s.growth_pct > 0 ? 'up' : (s.growth_pct < 0 ? 'down' : 'neutral'); growthEl.className = `stat-trend ${cls}`; growthEl.innerHTML = ` ${sign}${s.growth_pct}% vs 30j précédents`; } // Charts this.renderDailyChart(s.daily_new); this.renderHourlyChart(s.hourly_activity); this.renderTopModelsChart(s.top_models); } catch (e) { toast('Erreur chargement stats: ' + e.message, 'error'); } }, renderDailyChart(data) { const ctx = document.getElementById('chartDaily').getContext('2d'); if (charts.daily) charts.daily.destroy(); charts.daily = new Chart(ctx, { type: 'line', data: { labels: data.map(d => d.date), datasets: [{ label: 'Inscriptions', data: data.map(d => d.count), borderColor: '#7c5cff', backgroundColor: 'rgba(124, 92, 255, 0.1)', tension: 0.4, fill: true, pointRadius: 4, pointBackgroundColor: '#7c5cff', }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { color: '#a0a0b8' }, grid: { color: 'rgba(255,255,255,0.05)' } }, x: { ticks: { color: '#a0a0b8', maxRotation: 45 }, grid: { color: 'rgba(255,255,255,0.05)' } } } } }); }, renderHourlyChart(data) { const ctx = document.getElementById('chartHourly').getContext('2d'); if (charts.hourly) charts.hourly.destroy(); const hours = Array.from({ length: 24 }, (_, i) => i); const counts = hours.map(h => { const found = data.find(d => d.hour === h); return found ? found.count : 0; }); charts.hourly = new Chart(ctx, { type: 'bar', data: { labels: hours.map(h => h + 'h'), datasets: [{ label: 'Connexions', data: counts, backgroundColor: '#7c5cff', borderRadius: 4, }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { color: '#a0a0b8' }, grid: { color: 'rgba(255,255,255,0.05)' } }, x: { ticks: { color: '#a0a0b8' }, grid: { display: false } } } } }); }, renderTopModelsChart(data) { const ctx = document.getElementById('chartTopModels').getContext('2d'); if (charts.topModels) charts.topModels.destroy(); charts.topModels = new Chart(ctx, { type: 'bar', data: { labels: data.map(d => d.model), datasets: [{ label: 'Appels', data: data.map(d => d.calls), backgroundColor: ['#7c5cff', '#00d4ff', '#4ade80', '#fbbf24', '#ff4d6d', '#ff8a3d', '#a78bfa', '#22d3ee'], borderRadius: 4, }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true, ticks: { color: '#a0a0b8' }, grid: { color: 'rgba(255,255,255,0.05)' } }, y: { ticks: { color: '#a0a0b8' }, grid: { display: false } } } } }); }, async loadUsers() { try { const resp = await api('/admin/users'); if (!resp.ok) throw new Error('Failed'); const data = await resp.json(); const tbody = document.getElementById('usersTbody'); tbody.innerHTML = ''; data.users.forEach(u => { const tr = document.createElement('tr'); const status = u.is_banned ? 'Banni' : 'Actif'; const role = u.is_admin ? 'Admin' : 'User'; const banBtn = u.is_banned ? `` : ``; const deleteBtn = ``; tr.innerHTML = `