// Costs Page Module class CostsPage { constructor() { this.costData = null; this.period = '7d'; this.init(); } async init() { await Promise.all([ this.loadCostStats(), this.loadCostsChart(), this.loadBudgetTracking(), this.loadPricingTable() ]); this.setupEventListeners(); } /** Build query string from the current period. */ periodQuery() { if (this.period === 'custom') { const from = document.getElementById('costs-from')?.value; const to = document.getElementById('costs-to')?.value; let qs = '?period=custom'; if (from) qs += `&from=${from}T00:00:00Z`; if (to) qs += `&to=${to}T23:59:59Z`; return qs; } return `?period=${this.period}`; } async loadCostStats() { try { const qs = this.periodQuery(); const data = await window.api.get(`/usage/summary${qs}`); const totalCost = data.total_cost; const totalTokens = data.total_tokens || 0; const cacheReadTokens = data.total_cache_read_tokens || 0; const cacheWriteTokens = data.total_cache_write_tokens || 0; const todayCost = data.today_cost; const totalRequests = data.total_requests; // Compute days in the period for daily average let periodDays = 1; if (this.period === '7d') periodDays = 7; else if (this.period === '30d') periodDays = 30; else if (this.period === 'today') periodDays = 1; else if (this.period === 'all') periodDays = Math.max(1, 30); // rough fallback else if (this.period === 'custom') { const from = document.getElementById('costs-from')?.value; const to = document.getElementById('costs-to')?.value; if (from && to) { const diff = (new Date(to) - new Date(from)) / (1000 * 60 * 60 * 24); periodDays = Math.max(1, Math.ceil(diff)); } } this.costData = { totalCost, todayCost, totalRequests, avgDailyCost: totalCost / periodDays, cacheReadTokens, cacheWriteTokens, totalTokens, }; this.renderCostStats(); } catch (error) { console.error('Error loading cost stats:', error); } } renderCostStats() { const container = document.getElementById('cost-stats'); if (!container) return; const cacheHitRate = this.costData.totalTokens > 0 ? ((this.costData.cacheReadTokens / this.costData.totalTokens) * 100).toFixed(1) : '0.0'; const periodLabel = { 'today': 'Today', '7d': 'Past 7 Days', '30d': 'Past 30 Days', 'all': 'All Time', 'custom': 'Custom Range', }[this.period] || this.period; container.innerHTML = `
${window.api.formatCurrency(this.costData.totalCost)}
Total Cost
${window.api.formatCurrency(this.costData.todayCost)} today
${periodLabel}
Period
${this.costData.totalRequests.toLocaleString()} requests
${window.api.formatCurrency(this.costData.avgDailyCost)}
Daily Average
${cacheHitRate}% cache hit rate
${cacheHitRate}%
Cache Hit Rate
${window.api.formatNumber(this.costData.cacheReadTokens)} cached tokens
`; } async loadCostsChart() { try { const cm = window.chartManager || await window.waitForChartManager(); if (!cm) { this.showEmptyChart('costs-chart', 'Chart system unavailable'); return; } const qs = this.periodQuery(); const data = await window.api.get(`/usage/providers${qs}`); if (!data || data.length === 0) { this.showEmptyChart('costs-chart', 'No provider spending data for this period'); return; } const chartData = { labels: data.map(item => item.provider), datasets: [{ label: 'Cost ($)', data: data.map(item => item.cost), color: '#fe8019' }] }; cm.createBarChart('costs-chart', chartData); } catch (error) { console.error('Error loading costs chart:', error); this.showEmptyChart('costs-chart', 'Failed to load spending 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; } } async loadBudgetTracking() { const container = document.getElementById('budget-progress'); if (!container) return; try { const providers = await window.api.get('/providers'); container.innerHTML = providers.filter(p => p.id !== 'ollama').map(provider => { const used = provider.low_credit_threshold; const balance = provider.credit_balance; const percentage = balance > 0 ? Math.max(0, Math.min(100, (1 - (used / balance)) * 100)) : 0; return `
${provider.name} Balance ${window.api.formatCurrency(balance)}
`; }).join(''); } catch (error) { console.error('Error loading budgets:', error); } } async loadPricingTable() { try { // Only show models that have actually been used const data = await window.api.get('/models?used_only=true'); this.renderPricingTable(data); } catch (error) { console.error('Error loading pricing data:', error); } } renderPricingTable(data) { const tableBody = document.querySelector('#pricing-table tbody'); if (!tableBody) return; if (!data || data.length === 0) { tableBody.innerHTML = 'No models have been used yet'; return; } tableBody.innerHTML = data.map(row => { const cacheRead = row.cache_read_cost != null ? `${window.api.formatCurrency(row.cache_read_cost)} / 1M` : '--'; const cacheWrite = row.cache_write_cost != null ? `${window.api.formatCurrency(row.cache_write_cost)} / 1M` : '--'; return ` ${row.provider.toUpperCase()} ${row.id} ${window.api.formatCurrency(row.prompt_cost)} / 1M ${window.api.formatCurrency(row.completion_cost)} / 1M ${cacheRead} ${cacheWrite} `; }).join(''); } setupEventListeners() { // Period selector buttons const periodContainer = document.getElementById('costs-period'); if (periodContainer) { periodContainer.querySelectorAll('[data-period]').forEach(btn => { btn.onclick = () => { periodContainer.querySelectorAll('[data-period]').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const period = btn.dataset.period; this.period = period; // Show/hide custom range inputs const customRange = document.getElementById('costs-custom-range'); if (customRange) { customRange.style.display = period === 'custom' ? 'flex' : 'none'; } if (period !== 'custom') { this.refresh(); } }; }); } // Custom range apply button const applyCustom = document.getElementById('costs-apply-custom'); if (applyCustom) { applyCustom.onclick = () => this.refresh(); } } refresh() { this.loadCostStats(); this.loadCostsChart(); this.loadBudgetTracking(); // Pricing table doesn't change with period (it's model metadata, not usage) } } window.initCosts = async () => { window.costsPage = new CostsPage(); };