Files
turf_saas/consumption_history.html
CTO H3R7Tech 60e12cc4dd feat(HRT-226): Dashboard consommation IA + alertes visuelles
- 3 nouvelles pages HTML servies sous /dashboard/consumption*
- Proxy routes /api/v1/consumption/* -> port 8784 (consumption-tracker)
- Proxy routes /api/v1/ai/usage* -> port 8783 (AI Router)
- consumption_dashboard.html: KPI cards, Chart.js tokens/cost/provider/calls charts, period selector 24h/7j/30j, auto-refresh 30s, alertes visuelles temps reel
- consumption_history.html: Tableau pagine, filtres provider/status/date range, tri colonnes, export CSV
- consumption_alerts.html: CRUD alertes, toggle actif/inactif, seuils visuels badges, modal creation/edition

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-24 11:29:33 +02:00

328 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Consommation IA — Historique</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--green: #00c853; --green-d: #009624; --blue: #1e88e5;
--gold: #ffd600; --orange: #ff6d00;
--dark: #0d1117; --dark2: #161b22; --dark3: #21262d;
--text: #e6edf3; --muted: #8b949e; --border: #30363d;
--radius: 10px; --error: #f85149; --purple: #7c3aed;
}
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--dark); color: var(--text); min-height: 100vh; }
a { color: inherit; text-decoration: none; }
.topbar { display: flex; align-items: center; justify-content: space-between; padding: 16px 28px; border-bottom: 1px solid var(--border); background: var(--dark); position: sticky; top: 0; z-index: 10; flex-wrap: wrap; gap: 10px; }
.topbar-title { font-size: 1.1rem; font-weight: 700; display: flex; align-items: center; gap: 10px; }
.topbar-title a { color: var(--muted); font-weight: 400; font-size: .9rem; }
.topbar-title a:hover { color: var(--text); }
.topbar-right { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 16px; border-radius: 8px; font-size: .85rem; font-weight: 600; cursor: pointer; border: none; transition: all .2s; }
.btn-primary { background: var(--green); color: #000; }
.btn-primary:hover { background: var(--green-d); }
.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-ghost:hover { border-color: var(--muted); }
.btn-sm { padding: 5px 12px; font-size: .8rem; }
.content { padding: 28px; max-width: 1400px; margin: 0 auto; }
.filter-bar { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.filter-group { display: flex; flex-direction: column; gap: 4px; }
.filter-group label { font-size: .72rem; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; font-weight: 600; }
.filter-group select, .filter-group input {
background: var(--dark3); border: 1px solid var(--border); border-radius: 8px;
padding: 8px 12px; color: var(--text); font-size: .85rem; outline: none;
transition: border-color .2s; font-family: inherit;
}
.filter-group select:focus, .filter-group input:focus { border-color: var(--green); }
.table-card { background: var(--dark2); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; }
thead th { padding: 10px 14px; font-size: .75rem; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); text-align: left; border-bottom: 1px solid var(--border); background: var(--dark3); white-space: nowrap; cursor: pointer; user-select: none; }
thead th:hover { color: var(--text); }
tbody tr { border-bottom: 1px solid var(--border); transition: background .15s; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: var(--dark3); }
tbody td { padding: 10px 14px; font-size: .85rem; white-space: nowrap; }
.status-badge { padding: 2px 8px; border-radius: 10px; font-size: .72rem; font-weight: 700; }
.status-success { background: rgba(0,200,83,.15); color: var(--green); }
.status-error { background: rgba(248,81,73,.15); color: var(--error); }
.pagination { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 16px; flex-wrap: wrap; }
.page-btn { padding: 6px 14px; border-radius: 6px; font-size: .85rem; cursor: pointer; border: 1px solid var(--border); background: transparent; color: var(--muted); transition: all .15s; }
.page-btn:hover { border-color: var(--muted); color: var(--text); }
.page-btn.active { background: var(--green); color: #000; border-color: var(--green); }
.page-btn:disabled { opacity: .4; cursor: default; }
.page-info { font-size: .82rem; color: var(--muted); }
.loading { text-align: center; padding: 60px 20px; color: var(--muted); }
.loading .spinner { display: inline-block; width: 32px; height: 32px; border: 3px solid var(--border); border-top-color: var(--green); border-radius: 50%; animation: spin .8s linear infinite; margin-bottom: 12px; }
@keyframes spin { to { transform: rotate(360deg); } }
.home-btn{position:fixed;top:10px;left:10px;z-index:9999;background:linear-gradient(135deg,#00d9ff,#7b2cbf);color:#fff;border:none;border-radius:8px;padding:10px 15px;font-size:14px;cursor:pointer;text-decoration:none;display:flex;align-items:center;gap:8px;box-shadow:0 2px 10px rgba(0,217,255,0.3);transition:all 0.3s;font-family:-apple-system,BlinkMacSystemFont,sans-serif}.home-btn:hover{transform:translateY(-2px);box-shadow:0 4px 15px rgba(0,217,255,0.5)}
.empty-state { text-align: center; padding: 60px 20px; color: var(--muted); }
.empty-state .icon { font-size: 2.5rem; margin-bottom: 12px; }
.empty-state p { font-size: .9rem; }
.toast {
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
background: var(--dark2); border: 1px solid var(--border); border-radius: var(--radius);
padding: 14px 20px; font-size: .88rem; box-shadow: 0 8px 24px rgba(0,0,0,.4);
display: none; max-width: 400px;
}
.toast.show { display: block; animation: slideIn .3s ease; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
.toast-success { border-color: var(--green); }
.toast-error { border-color: var(--error); }
</style>
</head>
<body>
<a href="/" class="home-btn" title="Retour au portail"><span style="font-size:18px">🏠</span><span>Accueil</span></a>
<div class="topbar">
<div class="topbar-title">
<span>📋 Historique Consommation</span>
<a href="/dashboard/consumption">← Dashboard</a>
</div>
<div class="topbar-right">
<button class="btn btn-ghost btn-sm" onclick="exportCSV()">📥 Export CSV</button>
<button class="btn btn-ghost btn-sm" onclick="loadHistory()">🔄 Rafraîchir</button>
</div>
</div>
<div class="content">
<div class="filter-bar">
<div class="filter-group">
<label>Provider</label>
<select id="filter-provider" onchange="loadHistory()">
<option value="">Tous</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="google">Google</option>
<option value="mistral">Mistral</option>
<option value="deepseek">DeepSeek</option>
<option value="meta">Meta</option>
</select>
</div>
<div class="filter-group">
<label>Statut</label>
<select id="filter-status" onchange="loadHistory()">
<option value="">Tous</option>
<option value="success">Succès</option>
<option value="error">Erreur</option>
</select>
</div>
<div class="filter-group">
<label>Du</label>
<input type="date" id="filter-date-from" onchange="loadHistory()">
</div>
<div class="filter-group">
<label>Au</label>
<input type="date" id="filter-date-to" onchange="loadHistory()">
</div>
<div class="filter-group" style="align-self:flex-end">
<button class="btn btn-ghost btn-sm" onclick="document.getElementById('filter-provider').value='';document.getElementById('filter-status').value='';document.getElementById('filter-date-from').value='';document.getElementById('filter-date-to').value='';loadHistory()">✕ Réinitialiser</button>
</div>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<div>Chargement de l'historique...</div>
</div>
<div class="table-card" id="table-wrap" style="display:none">
<div style="overflow-x:auto">
<table>
<thead>
<tr>
<th onclick="sortBy('created_at')">Date ⬍</th>
<th onclick="sortBy('provider')">Provider ⬍</th>
<th onclick="sortBy('model')">Modèle ⬍</th>
<th onclick="sortBy('tokens_in')">Tokens In ⬍</th>
<th onclick="sortBy('tokens_out')">Tokens Out ⬍</th>
<th onclick="sortBy('tokens_total')">Total ⬍</th>
<th onclick="sortBy('cost_cents')">Coût ⬍</th>
<th onclick="sortBy('latency_ms')">Latence ⬍</th>
<th onclick="sortBy('status')">Statut ⬍</th>
</tr>
</thead>
<tbody id="history-body"></tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</div>
<div class="empty-state" id="empty-state" style="display:none">
<div class="icon">📭</div>
<p>Aucune donnée de consommation pour cette période.</p>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
var currentPage = 1;
var totalPages = 0;
var totalItems = 0;
var historyData = [];
var sortField = 'created_at';
var sortDir = 'desc';
function formatDate(isoStr) {
var d = new Date(isoStr);
return d.toLocaleDateString('fr-FR', {day:'2-digit', month:'short', year:'numeric'}) + ' ' +
d.toLocaleTimeString('fr-FR', {hour:'2-digit', minute:'2-digit'});
}
function formatNumber(n) {
if (n >= 1000000) return (n/1000000).toFixed(1) + 'M';
if (n >= 1000) return (n/1000).toFixed(1) + 'k';
return n.toLocaleString('fr-FR');
}
function showToast(msg, type) {
var t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast show toast-' + type;
setTimeout(function() { t.classList.remove('show'); }, 4000);
}
function sortBy(field) {
if (sortField === field) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortField = field;
sortDir = 'desc';
}
renderTable();
}
function renderTable() {
var tbody = document.getElementById('history-body');
var sorted = [...historyData].sort(function(a, b) {
var va = a[sortField], vb = b[sortField];
if (typeof va === 'string') va = va.toLowerCase();
if (typeof vb === 'string') vb = vb.toLowerCase();
if (va < vb) return sortDir === 'asc' ? -1 : 1;
if (va > vb) return sortDir === 'asc' ? 1 : -1;
return 0;
});
var html = '';
sorted.forEach(function(r) {
var cost = ((r.cost_cents || 0) / 100);
var statusClass = r.status === 'success' ? 'status-success' : 'status-error';
html += '<tr>' +
'<td>' + formatDate(r.created_at) + '</td>' +
'<td>' + (r.provider || '—') + '</td>' +
'<td>' + (r.model || '—') + '</td>' +
'<td>' + formatNumber(r.tokens_in || 0) + '</td>' +
'<td>' + formatNumber(r.tokens_out || 0) + '</td>' +
'<td><strong>' + formatNumber(r.tokens_total || 0) + '</strong></td>' +
'<td>' + cost.toFixed(4) + '€</td>' +
'<td>' + (r.latency_ms ? r.latency_ms + 'ms' : '—') + '</td>' +
'<td><span class="status-badge ' + statusClass + '">' + (r.status || '—') + '</span></td>' +
'</tr>';
});
if (!html) {
html = '<tr><td colspan="9" style="text-align:center;padding:40px;color:var(--muted)">Aucun résultat</td></tr>';
}
tbody.innerHTML = html;
}
function renderPagination() {
var el = document.getElementById('pagination');
if (totalPages <= 1) { el.innerHTML = ''; return; }
var html = '<span class="page-info">Page ' + currentPage + ' / ' + totalPages + ' (' + totalItems + ' entrées)</span>';
html += '<button class="page-btn" onclick="goPage(1)" ' + (currentPage <= 1 ? 'disabled' : '') + '>«</button>';
html += '<button class="page-btn" onclick="goPage(' + (currentPage - 1) + ')" ' + (currentPage <= 1 ? 'disabled' : '') + '></button>';
var start = Math.max(1, currentPage - 2);
var end = Math.min(totalPages, currentPage + 2);
for (var i = start; i <= end; i++) {
html += '<button class="page-btn' + (i === currentPage ? ' active' : '') + '" onclick="goPage(' + i + ')">' + i + '</button>';
}
html += '<button class="page-btn" onclick="goPage(' + (currentPage + 1) + ')" ' + (currentPage >= totalPages ? 'disabled' : '') + '></button>';
html += '<button class="page-btn" onclick="goPage(' + totalPages + ')" ' + (currentPage >= totalPages ? 'disabled' : '') + '>»</button>';
el.innerHTML = html;
}
function goPage(page) {
if (page < 1 || page > totalPages) return;
currentPage = page;
loadHistory();
}
async function loadHistory() {
document.getElementById('loading').style.display = '';
document.getElementById('table-wrap').style.display = 'none';
document.getElementById('empty-state').style.display = 'none';
var params = 'client_id=internal&page=' + currentPage + '&per_page=25';
var provider = document.getElementById('filter-provider').value;
var status = document.getElementById('filter-status').value;
var dateFrom = document.getElementById('filter-date-from').value;
var dateTo = document.getElementById('filter-date-to').value;
if (provider) params += '&provider=' + encodeURIComponent(provider);
if (dateFrom) params += '&start_date=' + dateFrom;
if (dateTo) params += '&end_date=' + dateTo;
try {
var r = await fetch('/api/v1/consumption/history?' + params);
var data = await r.json();
var items = data.history || [];
totalItems = data.total || 0;
totalPages = data.total_pages || 0;
document.getElementById('loading').style.display = 'none';
if (items.length === 0) {
document.getElementById('empty-state').style.display = '';
return;
}
document.getElementById('table-wrap').style.display = '';
historyData = items;
renderTable();
renderPagination();
} catch (e) {
document.getElementById('loading').innerHTML = '<div style="color:var(--error);padding:40px">❌ Erreur: ' + e.message + '</div>';
}
}
function exportCSV() {
if (!historyData || historyData.length === 0) {
showToast('Aucune donnée à exporter', 'error');
return;
}
var headers = ['Date','Provider','Modèle','Tokens In','Tokens Out','Tokens Total','Coût (€)','Latence (ms)','Statut'];
var rows = historyData.map(function(r) {
return [
r.created_at || '',
r.provider || '',
r.model || '',
r.tokens_in || 0,
r.tokens_out || 0,
r.tokens_total || 0,
((r.cost_cents || 0) / 100).toFixed(4),
r.latency_ms || '',
r.status || ''
].join(',');
});
var csv = '\uFEFF' + headers.join(',') + '\n' + rows.join('\n');
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'consommation_ia_' + new Date().toISOString().split('T')[0] + '.csv';
link.click();
showToast('CSV téléchargé', 'success');
}
loadHistory();
</script>
</body>
</html>