feat: Add CSV export buttons to all dashboard tabs

Each tab now has an Export CSV button that downloads the currently
displayed data as a CSV file, respecting the active time filter.
Exports: usage-over-time, top-users, top-models, top-agents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DJP 2026-03-12 08:42:16 -04:00
parent 203ba7197e
commit 80a7e0ecee
3 changed files with 113 additions and 3 deletions

View file

@ -329,6 +329,9 @@ body {
.table-header {
padding: 1.5rem 2rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.table-header h3 {
@ -376,6 +379,35 @@ tr:hover td {
font-weight: 500;
}
/* Export Button */
.btn-export {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 196, 7, 0.1);
color: var(--accent-primary);
border: 1px solid rgba(255, 196, 7, 0.2);
border-radius: 0.5rem;
font-family: "Montserrat", system-ui, sans-serif;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-export:hover {
background: rgba(255, 196, 7, 0.2);
border-color: rgba(255, 196, 7, 0.4);
}
.tab-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
/* Loading */
.loading {
display: flex;

View file

@ -69,6 +69,10 @@
<!-- Overview Tab -->
<div class="tab-content active" id="tab-overview">
<div class="tab-title-row">
<div></div>
<button class="btn-export" onclick="exportOverview()"><i data-lucide="download" style="width:16px;height:16px"></i> Export CSV</button>
</div>
<div class="stats-grid" id="summaryCards">
<div class="stat-card">
<div class="stat-icon cost"><i data-lucide="dollar-sign"></i></div>
@ -106,7 +110,7 @@
<!-- Users Tab -->
<div class="tab-content" id="tab-users">
<div class="table-container">
<div class="table-header"><h3>Top Users by Cost</h3></div>
<div class="table-header"><h3>Top Users by Cost</h3><button class="btn-export" onclick="exportUsers()"><i data-lucide="download" style="width:16px;height:16px"></i> Export CSV</button></div>
<table>
<thead>
<tr>
@ -126,7 +130,7 @@
<!-- Models Tab -->
<div class="tab-content" id="tab-models">
<div class="table-container">
<div class="table-header"><h3>Top Models by Cost</h3></div>
<div class="table-header"><h3>Top Models by Cost</h3><button class="btn-export" onclick="exportModels()"><i data-lucide="download" style="width:16px;height:16px"></i> Export CSV</button></div>
<table>
<thead>
<tr>
@ -153,7 +157,7 @@
<!-- Agents Tab -->
<div class="tab-content" id="tab-agents">
<div class="table-container">
<div class="table-header"><h3>Top Agents by Cost</h3></div>
<div class="table-header"><h3>Top Agents by Cost</h3><button class="btn-export" onclick="exportAgents()"><i data-lucide="download" style="width:16px;height:16px"></i> Export CSV</button></div>
<table>
<thead>
<tr>

View file

@ -5,6 +5,14 @@ const REFRESH_INTERVAL = 60000;
let currentPeriod = '24h';
let charts = {};
// --- Cached data for CSV export ---
let cachedData = {
usageOverTime: null,
topUsers: null,
topModels: null,
topAgents: null,
};
// --- API Helpers ---
function apiHeaders() {
const h = { 'Content-Type': 'application/json' };
@ -59,6 +67,68 @@ const CHART_COLORS = [
'#a78bfa', '#4ade80', '#fb923c', '#e879f9', '#22d3ee',
];
// --- CSV Export ---
function escCSV(val) {
const s = String(val == null ? '' : val);
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return '"' + s.replace(/"/g, '""') + '"';
}
return s;
}
function downloadCSV(headers, rows, filename) {
const lines = [headers.map(escCSV).join(',')];
for (const row of rows) {
lines.push(row.map(escCSV).join(','));
}
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
function exportOverview() {
const d = cachedData.usageOverTime;
if (!d || !d.data.length) return alert('No data to export');
downloadCSV(
['Time', 'Tokens', 'Cost (USD)'],
d.data.map(r => [r.time, r.tokens, r.cost.toFixed(6)]),
`usage-over-time-${currentPeriod}.csv`
);
}
function exportUsers() {
const d = cachedData.topUsers;
if (!d || !d.length) return alert('No data to export');
downloadCSV(
['Rank', 'Name', 'Email', 'Tokens', 'Cost (USD)', 'Conversations'],
d.map((u, i) => [i + 1, u.name, u.email, u.totalTokens, u.totalCost.toFixed(4), u.conversationCount]),
`top-users-${currentPeriod}.csv`
);
}
function exportModels() {
const d = cachedData.topModels;
if (!d || !d.length) return alert('No data to export');
downloadCSV(
['Rank', 'Model', 'Prompt Tokens', 'Completion Tokens', 'Prompt Cost (USD)', 'Completion Cost (USD)', 'Total Cost (USD)'],
d.map((m, i) => [i + 1, m.model, m.promptTokens, m.completionTokens, m.promptCost.toFixed(4), m.completionCost.toFixed(4), m.totalCost.toFixed(4)]),
`top-models-${currentPeriod}.csv`
);
}
function exportAgents() {
const d = cachedData.topAgents;
if (!d || !d.length) return alert('No data to export');
downloadCSV(
['Rank', 'Agent Name', 'Underlying Model', 'Provider', 'Tokens', 'Cost (USD)', 'Conversations'],
d.map((a, i) => [i + 1, a.agentName, a.underlyingModel, a.provider, a.totalTokens, a.totalCost.toFixed(4), a.conversationCount]),
`top-agents-${currentPeriod}.csv`
);
}
// --- Tab Navigation ---
document.querySelectorAll('.nav-links li').forEach(li => {
li.addEventListener('click', () => {
@ -99,6 +169,7 @@ async function loadSummary() {
async function loadUsageOverTime() {
try {
const d = await fetchAPI('usage-over-time');
cachedData.usageOverTime = d;
const labels = d.data.map(p => {
if (d.bucketType === 'hour') {
const dt = new Date(p.time);
@ -210,6 +281,7 @@ async function loadCostBreakdown() {
async function loadTopUsers() {
try {
const d = await fetchAPI('top-users?limit=20');
cachedData.topUsers = d;
const tbody = document.getElementById('usersTableBody');
tbody.innerHTML = d.map((u, i) => `
<tr>
@ -227,6 +299,7 @@ async function loadTopUsers() {
async function loadTopModels() {
try {
const d = await fetchAPI('top-models?limit=20');
cachedData.topModels = d;
const tbody = document.getElementById('modelsTableBody');
tbody.innerHTML = d.map((m, i) => `
<tr>
@ -270,6 +343,7 @@ async function loadTopModels() {
async function loadTopAgents() {
try {
const d = await fetchAPI('top-agents?limit=20');
cachedData.topAgents = d;
const tbody = document.getElementById('agentsTableBody');
if (d.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--text-muted)">No agent usage found in this period</td></tr>';