UNPKG

dragon-ui-claude

Version:

🐲 Ultra-fast, cross-platform Claude Code Max usage dashboard with dragon-inspired design, advanced background services, and multi-currency support

1,355 lines (1,144 loc) 61.8 kB
/** * Core Data Service * Central service that calculates all 75 unique values once * Provides consistent data to all tabs with individual refresh capability * Now uses Worker threads + SQLite database for blazing fast calculations */ const { Worker } = require('worker_threads'); const path = require('path'); const { modelPriceService } = require('./model-price-service.cjs'); class CoreDataService { constructor(dataLoader, pathManager) { this.dataLoader = dataLoader; this.pathManager = pathManager; // Currency support (data provided by currency-service.ts via store) this.currency = 'USD'; // Default currency this.exchangeRates = {}; // Provided by currency-service.ts this.billingCycleDay = 1; // Default billing cycle day // Auto-push callback for sending data to store.ts this.autoPushCallback = null; // Incremental loading state this.lastProcessedTimestamp = 0; this.lastRefreshTime = 0; this.processedEntryIds = new Set(); this.isInitialLoad = true; // Core calculated data - the 75 unique values this.coreData = { // Basic Financial totalCost: 0, cost: 0, currentCost: 0, costAmount: 0, // Sessions & Projects totalSessions: 0, sessions: 0, sessionsCount: 0, validSessions: 0, recentSessions: 0, totalProjects: 0, projectsCount: 0, // Averages & Ratios averageCostPerSession: 0, avgCostPerSession: 0, avgCostPerProject: 0, avgDuration: 0, avgSessionCost: 0, avgTokensPerSession: 0, // Tokens totalTokens: 0, tokens: 0, tokensUsed: 0, tokensCount: 0, // Costs per Unit costPer1MTokens: 0, costPerToken: 0, costPer1KTokens: 0, costPerConversation: 0, costPerEntry: 0, // Time & Duration started: null, duration: 0, timeLeft: 0, timeAgo: null, lastActivity: null, dateTime: null, sessionTimeLeft: 0, // Active/Live Data activeSession: null, sessionActive: false, activeDays: 0, entries: 0, // Status & State status: 'idle', sessionStatus: 'idle', // Models & Technical models: [], modelsCount: 0, modelsList: [], sessionId: null, block: null, blocks: [], blocksCount: 0, totalBlocks: 0, // Project Specific projectName: null, project: null, topRanking: 0, percentageOfTotal: 0, mostActiveProject: null, mostRecentActivity: null, // Session Analysis mostProductiveSession: null, longestSession: null, mostExpensiveSession: null, sessionNumber: 0, recentCount: 0, conversations: 0, efficiency: 0, // Time Periods daysTracked: 0, monthsTracked: 0, trackingPeriod: null, daysCount: 0, monthName: null, currentPeriod: 0, dailyAverage: 0, currentMonth: null, currentMonthCost: 0, // Projections & Trends projectedMonthly: 0, monthlyAverage: 0, quarterlyProjection: 0, yearlyProjection: 0, currentRunRate: 0, growthTrend: 0, // Performance Metrics costEfficiency: 0, tokensPerSession: 0, tokensPerMinute: 0, estimatedHourlyCost: 0, projectedSessionCost: 0, sessionTimeProgress: 0, progressPercentage: 0, tokensPerHour: 0, entriesPerHour: 0, projectedPerHour: 0, sessionRate: 0, // Additional Analysis highestSpendingMonth: null, mostActiveMonth: null, vsAvgPercentage: 0, timeRanges: [], dailyBreakdown: [], last7DaysTotal: 0, activityData: [], // Live Monitor Data for Active Tab liveMetrics: {}, activityWindows: [], peakActivity: 0, averageActivity: 0, timeSinceLastActivity: null, isSystemActive: false, // Gap Detection Data gaps: [], gapStatistics: {}, productivityPatterns: {}, workPattern: 'mixed-pattern', // Model Breakdown Data modelBreakdown: [], modelStats: {}, modelEfficiency: [], costDistribution: {}, // Export capabilities exportFormats: ['json', 'csv', 'markdown', 'html', 'txt'], // Currency data currentCurrency: 'USD', supportedCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF', 'CNY', 'INR', 'BRL'], exchangeRates: {}, lastRateUpdate: null }; // Individual tab data refresh timestamps this.lastRefresh = { overview: 0, projects: 0, sessions: 0, monthly: 0, daily: 0, active: 0 }; // Tab-specific refresh intervals (in ms) this.refreshIntervals = { overview: 30000, // 30s projects: 60000, // 1min sessions: 60000, // 1min monthly: 300000, // 5min daily: 120000, // 2min active: 5000 // 5s - most frequent for live data }; this.isLoading = false; } /** * Helper method to yield control back to the event loop * This prevents blocking the UI during heavy calculations */ async yieldToEventLoop() { return new Promise(resolve => setImmediate(resolve)); } /** * Load and calculate core data - SMART INCREMENTAL MODE * Only processes new/changed data after initial load */ async calculateCoreData() { if (this.isLoading) return this.coreData; this.isLoading = true; const currentTime = Date.now(); try { // Get fresh data from DataLoader const activePaths = this.pathManager.getAllPaths().active; await this.dataLoader.loadAllUsageEntries(activePaths); const allEntries = this.dataLoader.getAllUsageEntries(); if (this.isInitialLoad) { // First load - process everything console.log(`[LOAD] CoreDataService: INITIAL LOAD - Processing ${allEntries.length} entries with Worker thread...`); const result = await this.calculateInWorker(allEntries); Object.assign(this.coreData, result); // Track processed entries this.processedEntryIds = new Set(allEntries.map(e => e.id || `${e.timestamp}-${e.sessionId}`)); this.lastProcessedTimestamp = Math.max(...allEntries.map(e => new Date(e.timestamp).getTime())); this.isInitialLoad = false; console.log(`[OK] CoreDataService: Initial load completed - ${allEntries.length} entries processed`); } else { // Incremental refresh - only process new entries const newEntries = allEntries.filter(entry => { const entryId = entry.id || `${entry.timestamp}-${entry.sessionId}`; const entryTime = new Date(entry.timestamp).getTime(); return !this.processedEntryIds.has(entryId) || entryTime > this.lastProcessedTimestamp; }); if (newEntries.length === 0) { console.log('[OK] CoreDataService: No new data to process - using cached results'); this.lastRefreshTime = currentTime; return this.coreData; } console.log(`[LOAD] CoreDataService: INCREMENTAL UPDATE - Processing ${newEntries.length} new entries (${allEntries.length} total)`); // Process only new entries and merge with existing data const incrementalResult = await this.calculateIncrementalUpdate(newEntries, allEntries); Object.assign(this.coreData, incrementalResult); // Update tracking newEntries.forEach(entry => { const entryId = entry.id || `${entry.timestamp}-${entry.sessionId}`; this.processedEntryIds.add(entryId); }); this.lastProcessedTimestamp = Math.max(this.lastProcessedTimestamp, ...newEntries.map(e => new Date(e.timestamp).getTime())); console.log(`[OK] CoreDataService: Incremental update completed - ${newEntries.length} new entries processed`); } this.lastRefreshTime = currentTime; // Auto-push to store.ts if callback is set if (this.autoPushCallback) { this.autoPushCallback({ ...this.coreData }); } return this.coreData; } catch (error) { console.error('[ERR] CoreDataService: Error calculating core data:', error); throw error; } finally { this.isLoading = false; } } /** * Smart incremental update - only recalculates affected metrics */ async calculateIncrementalUpdate(newEntries, allEntries) { console.log(`[CALC] CoreDataService: Smart incremental calculation for ${newEntries.length} new entries`); // Always use Worker thread for proper monthly calculations // Even small updates need proper monthly/billing period recalculation console.log(`[CALC] CoreDataService: Using Worker thread for proper monthly calculations`); return this.calculateInWorker(allEntries, true); } /** * Direct incremental calculation for small updates (< 1000 entries) */ async calculateIncrementalDirect(newEntries) { const updates = { ...this.coreData }; // Process new entries directly let newCost = 0; let newTokens = 0; const newSessions = new Set(); const newProjects = new Set(); for (const entry of newEntries) { newCost += (entry.cost || 0); newTokens += (entry.total_tokens || entry.input_tokens + entry.output_tokens || 0); if (entry.sessionId) newSessions.add(entry.sessionId); if (entry.project || entry.cwd) newProjects.add(entry.project || entry.cwd); } // Update totals updates.totalCost += newCost; updates.cost = updates.totalCost; updates.currentCost = updates.totalCost; updates.costAmount = updates.totalCost; updates.totalTokens += newTokens; updates.tokens = updates.totalTokens; updates.tokensUsed = updates.totalTokens; updates.tokensCount = updates.totalTokens; // Update session counts (check for truly new sessions) const existingSessions = new Set(updates.sessionsData?.map(s => s.sessionId) || []); const actuallyNewSessions = Array.from(newSessions).filter(s => !existingSessions.has(s)); updates.totalSessions += actuallyNewSessions.length; updates.sessions = updates.totalSessions; updates.sessionsCount = updates.totalSessions; updates.validSessions = updates.totalSessions; // Update project counts const existingProjects = new Set(updates.projectsData?.map(p => p.project) || []); const actuallyNewProjects = Array.from(newProjects).filter(p => !existingProjects.has(p)); updates.totalProjects += actuallyNewProjects.length; updates.projectsCount = updates.totalProjects; // Recalculate averages if (updates.totalSessions > 0) { updates.averageCostPerSession = updates.totalCost / updates.totalSessions; updates.avgCostPerSession = updates.averageCostPerSession; updates.avgTokensPerSession = updates.totalTokens / updates.totalSessions; } // Update daily data for new entries (simple approximation for small updates) if (updates.dailyData && newEntries.length > 0) { const today = new Date().toISOString().split('T')[0]; let todayEntry = updates.dailyData.find(d => d.date === today); if (!todayEntry) { todayEntry = { date: today, totalCost: 0, totalTokens: 0, sessions: 0, models: [], isToday: true, isYesterday: false, isRecent: true }; updates.dailyData.unshift(todayEntry); } // Add new data to today's entry todayEntry.totalCost += newCost; todayEntry.totalTokens += newTokens; // Recalculate last 7 days activity chart data const last7Days = updates.dailyData.slice(0, 7).reverse(); updates.dailyBreakdown = last7Days; updates.last7DaysTotal = last7Days.reduce((sum, day) => sum + day.totalCost, 0); updates.activityData = last7Days.map(day => ({ date: day.date, cost: day.totalCost, tokens: day.totalTokens, sessions: day.sessions, label: new Date(day.date).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }) })); } // Update monthly data for current month (simple approximation) if (updates.monthlyData && newEntries.length > 0) { const currentMonth = new Date().toISOString().slice(0, 7); let currentMonthEntry = updates.monthlyData.find(m => m.month === currentMonth); if (!currentMonthEntry) { const now = new Date(); currentMonthEntry = { month: currentMonth, year: now.getFullYear(), monthNumber: now.getMonth() + 1, monthName: now.toLocaleDateString('en-US', { month: 'long' }), totalCost: 0, totalTokens: 0, sessions: 0, daysActive: 0, models: [], avgCostPerDay: 0, avgCostPerSession: 0, isCurrent: true }; updates.monthlyData.unshift(currentMonthEntry); } // Add new costs to current month currentMonthEntry.totalCost += newCost; currentMonthEntry.totalTokens += newTokens; // Update monthly summary fields updates.currentMonth = currentMonthEntry.monthName; updates.currentMonthCost = currentMonthEntry.totalCost; updates.currentPeriod = currentMonthEntry.totalCost; updates.monthsTracked = updates.monthlyData.length; } // Update overview fields with new data if (newEntries.length > 0) { // Update models list const existingModels = new Set(updates.models || []); for (const entry of newEntries) { if (entry.model) existingModels.add(entry.model); } updates.models = Array.from(existingModels); updates.modelsCount = updates.models.length; updates.modelsList = updates.models; // Update active days count if (updates.dailyData) { updates.activeDays = updates.dailyData.length; updates.daysTracked = updates.dailyData.length; } } // Update active session data from database this.updateActiveSessionData(updates); console.log(`[FAST] CoreDataService: Direct incremental update completed - +$${newCost.toFixed(4)}, +${newTokens} tokens, activity: ${updates.activityData?.length || 0} days`); return updates; } /** * Update active session data from database (for incremental updates) */ updateActiveSessionData(updates) { try { const now = Date.now(); const db = this.dataLoader.getDatabase(); // Get database from dataLoader const recentEntries = db.getRecentEntries(30); // Last 30 minutes console.log(`[ACTIVE UPDATE] Found ${recentEntries.length} entries in last 30 minutes`); let activeSessionId = null; if (recentEntries.length > 0) { // Find the most recent session const latestEntry = recentEntries.reduce((latest, entry) => { return new Date(entry.timestamp) > new Date(latest.timestamp) ? entry : latest; }); activeSessionId = latestEntry.session_id; const minutesAgo = Math.round((now - new Date(latestEntry.timestamp).getTime()) / (1000 * 60)); console.log(`[ACTIVE UPDATE] Latest entry: ${latestEntry.session_id}, ${minutesAgo}min ago`); } // Update active session data updates.activeSession = activeSessionId; updates.sessionActive = !!activeSessionId; updates.sessionId = activeSessionId; // Calculate current session data if active if (activeSessionId) { const sessionEntries = db.getSessionEntries(activeSessionId); if (sessionEntries.length > 0) { const sessionStart = new Date(Math.min(...sessionEntries.map(e => new Date(e.timestamp).getTime()))); const sessionEnd = new Date(Math.max(...sessionEntries.map(e => new Date(e.timestamp).getTime()))); const sessionCost = sessionEntries.reduce((sum, e) => sum + (e.cost || 0), 0); const sessionTokens = sessionEntries.reduce((sum, e) => sum + ((e.input_tokens || 0) + (e.output_tokens || 0) + (e.cache_creation_input_tokens || 0) + (e.cache_read_input_tokens || 0)), 0); const durationMinutes = Math.floor((sessionEnd.getTime() - sessionStart.getTime()) / (1000 * 60)); const timeLeftMinutes = Math.max(0, 300 - durationMinutes); // 5 hours = 300 minutes console.log(`[ACTIVE UPDATE] Current Session: ${sessionEntries.length} entries, cost=$${sessionCost.toFixed(4)}, tokens=${sessionTokens}, duration=${durationMinutes}min`); updates.started = sessionStart.toISOString(); updates.duration = durationMinutes; updates.currentCost = sessionCost; updates.currentTokens = sessionTokens; updates.timeLeft = timeLeftMinutes; updates.sessionTimeLeft = timeLeftMinutes; updates.lastActivity = sessionEnd.toISOString(); updates.status = timeLeftMinutes > 0 ? 'active' : 'expired'; updates.sessionStatus = updates.status; } } else { updates.status = 'idle'; updates.sessionStatus = 'idle'; updates.currentCost = 0; updates.currentTokens = 0; updates.duration = 0; updates.timeLeft = 0; updates.sessionTimeLeft = 0; console.log(`[ACTIVE UPDATE] No active session found`); } } catch (error) { console.error('[ERROR] Failed to update active session data:', error); } } /** * Run calculations in Worker thread */ async calculateInWorker(usageEntries, isIncremental = false) { return new Promise((resolve, reject) => { const workerPath = path.join(__dirname, 'core-data-worker.cjs'); const worker = new Worker(workerPath); // Send data to worker worker.postMessage({ type: 'calculateAllData', usageEntries, currency: this.currency, exchangeRates: this.exchangeRates, billingCycleDay: this.billingCycleDay || 1, isIncremental: isIncremental, existingData: isIncremental ? this.coreData : null }); // Handle worker messages worker.on('message', (message) => { if (message.type === 'progress') { console.log(`[LOAD] Worker Progress: ${message.message} (${message.progress.toFixed(1)}%)`); // Optionally send progress to UI if (this.autoPushCallback) { this.autoPushCallback({ ...this.coreData, _loadingProgress: { step: message.step, progress: message.progress, message: message.message } }); } } else if (message.type === 'result') { // Debug the currency problem BEFORE resolving console.log(`[DEBUG] MAIN: Currency: ${this.currency}`); console.log(`[DEBUG] MAIN: Total Cost: $${message.data.totalCost?.toFixed(2) || 0}`); console.log(`[DEBUG] MAIN: Current Month Cost: $${message.data.currentMonthCost?.toFixed(2) || 0}`); console.log(`[DEBUG] MAIN: Current Month: ${message.data.currentMonth}`); console.log(`[DEBUG] MAIN: Monthly Data Array Length: ${message.data.monthlyData?.length || 0}`); // Debug first monthly entry if (message.data.monthlyData && message.data.monthlyData.length > 0) { const firstMonth = message.data.monthlyData[0]; console.log(`[DEBUG] MAIN: First monthly entry:`, { month: firstMonth.monthName, cost: firstMonth.totalCost, isCurrent: firstMonth.isCurrent || firstMonth.isCurrentMonth }); } if (message.data.currentMonthCost > message.data.totalCost) { console.error('[CURRENCY BUG] MAIN: CURRENCY BUG DETECTED! Current Month ($${message.data.currentMonthCost?.toFixed(2)}) > Total Cost ($${message.data.totalCost?.toFixed(2)})'); // FORCE FIX the impossible values message.data.currentMonthCost = message.data.totalCost; message.data.currentPeriod = message.data.totalCost; // Also fix the monthly data entry if (message.data.monthlyData) { const currentMonthEntry = message.data.monthlyData.find(m => m.isCurrent || m.isCurrentMonth); if (currentMonthEntry) { currentMonthEntry.totalCost = message.data.totalCost; console.log(`[FIX] MAIN: Also fixed monthly data entry to $${message.data.totalCost?.toFixed(2)}`); } } console.log(`[FIX] MAIN: Fixed Current Month to match Total Cost: $${message.data.totalCost?.toFixed(2)}`); } worker.terminate(); resolve(message.data); } else if (message.type === 'error') { worker.terminate(); reject(new Error(message.error)); } }); worker.on('error', (error) => { worker.terminate(); reject(error); }); // Set timeout to prevent hanging setTimeout(() => { worker.terminate(); reject(new Error('Worker calculation timed out after 2 minutes')); }, 120000); // 2 minutes timeout }); } /** * Get data for specific tab with refresh check */ async getTabData(tabName) { const now = Date.now(); const lastRefresh = this.lastRefresh[tabName] || 0; const refreshInterval = this.refreshIntervals[tabName] || 60000; // Check if tab needs refresh if (now - lastRefresh > refreshInterval) { console.log(`[LOAD] CoreDataService: Refreshing ${tabName} tab data`); await this.calculateCoreData(); this.lastRefresh[tabName] = now; } // Return tab-specific data subset switch (tabName) { case 'overview': return this.getOverviewData(); case 'projects': return this.getProjectsData(); case 'sessions': return this.getSessionsData(); case 'monthly': return this.getMonthlyData(); case 'daily': return this.getDailyData(); case 'active': return this.getActiveData(); default: return this.coreData; } } /** * Force refresh specific tab */ async forceRefreshTab(tabName) { console.log(`[LOAD] CoreDataService: Force refreshing ${tabName} tab`); this.lastRefresh[tabName] = 0; // Reset timestamp to force refresh return await this.getTabData(tabName); } /** * Force refresh all data - bypasses incremental mode */ async forceRefreshAll() { console.log('[LOAD] CoreDataService: Force refreshing all data - FULL RECALCULATION'); // Reset incremental tracking to force full recalculation // DO NOT set isInitialLoad = true (causes January 2001 bugs!) this.lastProcessedTimestamp = 0; this.processedEntryIds.clear(); Object.keys(this.lastRefresh).forEach(tab => { this.lastRefresh[tab] = 0; }); return await this.calculateCoreData(); } /** * Update currency and exchange rates (called by store.ts) */ updateCurrency(newCurrency, exchangeRates) { console.log(`[CURRENCY] CoreDataService: Updating currency to ${newCurrency}`); this.currency = newCurrency; this.exchangeRates = exchangeRates; this.coreData.currentCurrency = newCurrency; this.coreData.exchangeRates = exchangeRates; console.log(`[OK] CoreDataService: Currency updated to ${newCurrency}`); } /** * Update billing cycle day (called by store.ts) */ updateBillingCycleDay(billingCycleDay) { console.log(`[BILLING] CoreDataService: Updating billing cycle day to ${billingCycleDay}`); this.billingCycleDay = billingCycleDay; console.log(`[OK] CoreDataService: Billing cycle day updated to ${billingCycleDay}`); } /** * Recalculate all data with new currency (called by store.ts after currency change) */ async recalculateWithNewCurrency() { console.log('[CURRENCY] CoreDataService: Recalculating all costs with new currency'); return await this.forceRefreshAll(); } // Tab-specific data getters getOverviewData() { return { totalCost: this.coreData.totalCost, totalSessions: this.coreData.totalSessions, averageCostPerSession: this.coreData.averageCostPerSession, avgTokensPerSession: this.coreData.avgTokensPerSession, totalTokens: this.coreData.totalTokens, activeSession: this.coreData.activeSession, currentCost: this.coreData.currentCost, costPer1MTokens: this.coreData.costPer1MTokens, dailyBreakdown: this.coreData.dailyBreakdown, last7DaysTotal: this.coreData.last7DaysTotal, activityData: this.coreData.activityData, activeDays: this.coreData.activeDays, status: this.coreData.status, lastActivity: this.coreData.lastActivity, started: this.coreData.started, duration: this.coreData.duration, tokens: this.coreData.tokens, models: this.coreData.models }; } getProjectsData() { return { totalProjects: this.coreData.totalProjects, totalCost: this.coreData.totalCost, avgCostPerProject: this.coreData.avgCostPerProject, projectsData: this.coreData.projectsData || [], mostActiveProject: this.coreData.mostActiveProject, mostRecentActivity: this.coreData.mostRecentActivity }; } getSessionsData() { return { totalSessions: this.coreData.totalSessions, validSessions: this.coreData.validSessions, avgCostPerSession: this.coreData.avgCostPerSession, avgDuration: this.coreData.avgDuration, recentSessions: this.coreData.recentSessions, mostProductiveSession: this.coreData.mostProductiveSession, longestSession: this.coreData.longestSession, mostExpensiveSession: this.coreData.mostExpensiveSession, totalCost: this.coreData.totalCost, totalTokens: this.coreData.totalTokens, avgTokensPerSession: this.coreData.avgTokensPerSession, costEfficiency: this.coreData.costEfficiency, sessionsData: this.coreData.sessionsData || [] }; } getMonthlyData() { return { monthsTracked: this.coreData.monthsTracked, currentPeriod: this.coreData.currentPeriod, dailyAverage: this.coreData.dailyAverage, totalCost: this.coreData.totalCost, projectedMonthly: this.coreData.projectedMonthly, monthlyData: this.coreData.monthlyData || [], highestSpendingMonth: this.coreData.highestSpendingMonth, mostActiveMonth: this.coreData.mostActiveMonth, growthTrend: this.coreData.growthTrend, monthlyAverage: this.coreData.monthlyAverage, quarterlyProjection: this.coreData.quarterlyProjection, yearlyProjection: this.coreData.yearlyProjection, currentRunRate: this.coreData.currentRunRate }; } getDailyData() { return { daysTracked: this.coreData.daysTracked, activeDays: this.coreData.activeDays, currentCost: this.coreData.currentCost, tokens: this.coreData.tokens, blocks: this.coreData.blocks, models: this.coreData.models, totalCost: this.coreData.totalCost, dailyAverage: this.coreData.dailyAverage, totalBlocks: this.coreData.totalBlocks, currentPeriod: this.coreData.currentPeriod, totalTokens: this.coreData.totalTokens, projectedMonthly: this.coreData.projectedMonthly, dailyData: this.coreData.dailyData || [] }; } getActiveData() { return { status: this.coreData.status, sessionActive: this.coreData.sessionActive, currentCost: this.coreData.currentCost, tokensUsed: this.coreData.tokensUsed, entries: this.coreData.entries, started: this.coreData.started, duration: this.coreData.duration, timeLeft: this.coreData.timeLeft, models: this.coreData.models, sessionId: this.coreData.sessionId, block: this.coreData.block, costPerEntry: this.coreData.costPerEntry, tokensPerMinute: this.coreData.tokensPerMinute, estimatedHourlyCost: this.coreData.estimatedHourlyCost, costPer1MTokens: this.coreData.costPer1MTokens, projectedSessionCost: this.coreData.projectedSessionCost, sessionTimeLeft: this.coreData.sessionTimeLeft, sessionTimeProgress: this.coreData.sessionTimeProgress, progressPercentage: this.coreData.progressPercentage, tokensPerHour: this.coreData.tokensPerHour, entriesPerHour: this.coreData.entriesPerHour, projectedPerHour: this.coreData.projectedPerHour, sessionStatus: this.coreData.sessionStatus, sessionRate: this.coreData.sessionRate, // Live Monitor Data liveMetrics: this.coreData.liveMetrics, activityWindows: this.coreData.activityWindows, peakActivity: this.coreData.peakActivity, averageActivity: this.coreData.averageActivity, timeSinceLastActivity: this.coreData.timeSinceLastActivity, isSystemActive: this.coreData.isSystemActive }; } // Calculation methods for each category async calculateBasicFinancial(usageEntries) { // Calculate costs in USD first, then convert const totalCostUSD = usageEntries.reduce((sum, entry) => sum + (entry.cost || 0), 0); this.coreData.totalCost = this.convertCurrency(totalCostUSD); this.coreData.cost = this.coreData.totalCost; this.coreData.currentCost = this.coreData.totalCost; // Will be updated with current session this.coreData.costAmount = this.coreData.totalCost; } async calculateSessionsAndProjects(usageEntries) { // Group by sessionId to get unique sessions const sessionMap = new Map(); const projectSet = new Set(); usageEntries.forEach(entry => { const sessionId = entry.sessionId || entry.fullSessionId || 'unknown'; if (!sessionMap.has(sessionId)) { sessionMap.set(sessionId, { sessionId, cost: 0, tokens: 0, conversations: 0, project: entry.project || entry.cwd }); } const session = sessionMap.get(sessionId); session.cost += this.convertCurrency(entry.cost || 0); session.tokens += entry.total_tokens || 0; session.conversations += 1; if (entry.project || entry.cwd) { projectSet.add(entry.project || entry.cwd); } }); const sessions = Array.from(sessionMap.values()); this.coreData.totalSessions = sessions.length; this.coreData.sessions = sessions.length; this.coreData.sessionsCount = sessions.length; this.coreData.validSessions = sessions.filter(s => s.cost > 0 || s.tokens > 0).length; this.coreData.recentSessions = Math.min(10, sessions.length); this.coreData.totalProjects = projectSet.size; this.coreData.projectsCount = projectSet.size; // Store sessions data for individual tabs this.coreData.sessionsData = sessions; } async calculateAveragesAndRatios(usageEntries) { if (this.coreData.totalSessions > 0) { this.coreData.averageCostPerSession = this.coreData.totalCost / this.coreData.totalSessions; this.coreData.avgCostPerSession = this.coreData.averageCostPerSession; this.coreData.avgSessionCost = this.coreData.averageCostPerSession; } if (this.coreData.totalProjects > 0) { this.coreData.avgCostPerProject = this.coreData.totalCost / this.coreData.totalProjects; } // Calculate average duration from sessions // TODO: Implement duration calculation from session timestamps this.coreData.avgDuration = 180; // Placeholder - 3 hours average if (this.coreData.totalSessions > 0) { this.coreData.avgTokensPerSession = this.coreData.totalTokens / this.coreData.totalSessions; } } async calculateTokens(usageEntries) { this.coreData.totalTokens = usageEntries.reduce((sum, entry) => sum + (entry.total_tokens || 0), 0); this.coreData.tokens = this.coreData.totalTokens; this.coreData.tokensUsed = this.coreData.totalTokens; this.coreData.tokensCount = this.coreData.totalTokens; } async calculateCostsPerUnit(usageEntries) { if (this.coreData.totalTokens > 0) { this.coreData.costPerToken = this.coreData.totalCost / this.coreData.totalTokens; this.coreData.costPer1MTokens = this.coreData.costPerToken * 1000000; this.coreData.costPer1KTokens = this.coreData.costPerToken * 1000; } const totalConversations = usageEntries.length; if (totalConversations > 0) { this.coreData.costPerConversation = this.coreData.totalCost / totalConversations; this.coreData.costPerEntry = this.coreData.costPerConversation; } } async calculateTimeAndDuration(usageEntries) { if (usageEntries.length > 0) { // Get most recent entry for current session const sortedEntries = usageEntries.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); const mostRecent = sortedEntries[0]; this.coreData.started = mostRecent.timestamp; this.coreData.lastActivity = mostRecent.timestamp; this.coreData.dateTime = mostRecent.timestamp; // Calculate time ago and duration const now = new Date(); const startTime = new Date(mostRecent.timestamp); const diffMs = now - startTime; const diffMinutes = Math.floor(diffMs / (1000 * 60)); this.coreData.duration = diffMinutes; this.coreData.timeAgo = `${diffMinutes}m ago`; // Calculate session time left (5 hour limit) const sessionLimitMinutes = 300; // 5 hours this.coreData.sessionTimeLeft = Math.max(0, sessionLimitMinutes - diffMinutes); this.coreData.timeLeft = this.coreData.sessionTimeLeft; } } async calculateActiveLiveData(usageEntries) { // Check if there's an active session (recent activity within 2 hours) if (usageEntries.length > 0) { const now = new Date(); const mostRecent = new Date(Math.max(...usageEntries.map(e => new Date(e.timestamp)))); const timeDiffMinutes = (now - mostRecent) / (1000 * 60); this.coreData.sessionActive = timeDiffMinutes < 120; // 2 hours this.coreData.activeSession = this.coreData.sessionActive ? 'In Progress' : null; this.coreData.status = this.coreData.sessionActive ? 'Live' : 'Idle'; } this.coreData.entries = usageEntries.length; // Calculate active days const uniqueDays = new Set(usageEntries.map(e => e.timestamp.split('T')[0])); this.coreData.activeDays = uniqueDays.size; } async calculateModelsAndTechnical(usageEntries) { const modelsSet = new Set(); usageEntries.forEach(entry => { if (entry.model) { modelsSet.add(entry.model); } }); this.coreData.models = Array.from(modelsSet); this.coreData.modelsCount = modelsSet.size; this.coreData.modelsList = this.coreData.models; // Session ID from most recent entry if (usageEntries.length > 0) { const sortedEntries = usageEntries.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); this.coreData.sessionId = sortedEntries[0].sessionId; this.coreData.block = sortedEntries[0].sessionId; // Same as session ID } // Calculate blocks (sessions grouped by 5-hour windows) // For now, use sessions as blocks this.coreData.blocks = this.coreData.sessionsData || []; this.coreData.blocksCount = this.coreData.totalSessions; this.coreData.totalBlocks = this.coreData.totalSessions; } async calculateProjectSpecific(usageEntries) { // Group by project const projectMap = new Map(); usageEntries.forEach(entry => { const project = entry.project || entry.cwd || 'unknown'; if (!projectMap.has(project)) { projectMap.set(project, { project, cost: 0, tokens: 0, sessions: 0, lastActivity: entry.timestamp }); } const proj = projectMap.get(project); proj.cost += entry.cost || 0; proj.tokens += entry.total_tokens || 0; proj.sessions += 1; if (entry.timestamp > proj.lastActivity) { proj.lastActivity = entry.timestamp; } }); const projects = Array.from(projectMap.values()).sort((a, b) => b.cost - a.cost); if (projects.length > 0) { this.coreData.mostActiveProject = projects[0].project; this.coreData.projectName = projects[0].project; this.coreData.project = projects[0].project; this.coreData.topRanking = 1; this.coreData.percentageOfTotal = (projects[0].cost / this.coreData.totalCost) * 100; // Most recent activity const mostRecentProject = projects.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity))[0]; this.coreData.mostRecentActivity = mostRecentProject.project; } // Store projects data this.coreData.projectsData = projects; } async calculateSessionAnalysis(usageEntries) { const sessions = this.coreData.sessionsData || []; if (sessions.length > 0) { // Most productive (highest tokens) const mostTokens = sessions.reduce((max, current) => current.tokens > max.tokens ? current : max, sessions[0]); this.coreData.mostProductiveSession = mostTokens.tokens; // Most expensive const mostExpensive = sessions.reduce((max, current) => current.cost > max.cost ? current : max, sessions[0]); this.coreData.mostExpensiveSession = mostExpensive.cost; // Longest session (placeholder - would need duration calculation) this.coreData.longestSession = 299; // minutes this.coreData.sessionNumber = sessions.length; this.coreData.recentCount = Math.min(10, sessions.length); const totalConversations = sessions.reduce((sum, s) => sum + s.conversations, 0); this.coreData.conversations = totalConversations; // Efficiency (tokens per minute) if (this.coreData.avgDuration > 0) { this.coreData.efficiency = this.coreData.avgTokensPerSession / this.coreData.avgDuration; } } } async calculateTimePeriods(usageEntries) { this.coreData.daysTracked = this.coreData.activeDays; // Calculate months - ensure at least 1 month if we have any data (with validation) const uniqueMonths = new Set(); usageEntries.forEach(e => { if (e.timestamp && typeof e.timestamp === 'string' && e.timestamp.length >= 7) { const monthStr = e.timestamp.substr(0, 7); // YYYY-MM const date = new Date(monthStr + '-01'); // Only add valid months from 2020 onwards if (!isNaN(date.getTime()) && date.getFullYear() >= 2020) { uniqueMonths.add(monthStr); } } }); // If we have any usage data but no valid months detected, add current month if (usageEntries.length > 0 && uniqueMonths.size === 0) { const currentMonth = new Date().toISOString().slice(0, 7); uniqueMonths.add(currentMonth); } this.coreData.monthsTracked = Math.max(uniqueMonths.size, usageEntries.length > 0 ? 1 : 0); this.coreData.trackingPeriod = `${this.coreData.daysTracked} days`; this.coreData.daysCount = this.coreData.daysTracked; if (usageEntries.length > 0) { const mostRecent = new Date(Math.max(...usageEntries.map(e => new Date(e.timestamp)))); this.coreData.monthName = mostRecent.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); } // Current period (last 7 days) const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const recentEntries = usageEntries.filter(e => new Date(e.timestamp) >= sevenDaysAgo); this.coreData.currentPeriod = recentEntries.reduce((sum, e) => sum + (e.cost || 0), 0); if (this.coreData.daysTracked > 0) { this.coreData.dailyAverage = this.coreData.totalCost / this.coreData.daysTracked; } } async calculateProjectionsAndTrends(usageEntries) { if (this.coreData.dailyAverage > 0) { this.coreData.projectedMonthly = this.coreData.dailyAverage * 30; this.coreData.monthlyAverage = this.coreData.projectedMonthly; this.coreData.quarterlyProjection = this.coreData.projectedMonthly * 3; this.coreData.yearlyProjection = this.coreData.projectedMonthly * 12; this.coreData.currentRunRate = this.coreData.yearlyProjection; } // Growth trend (placeholder - would need historical comparison) this.coreData.growthTrend = 0; // 0% change } async calculatePerformanceMetrics(usageEntries) { if (this.coreData.totalTokens > 0) { this.coreData.costEfficiency = this.coreData.totalCost / this.coreData.totalTokens; } if (this.coreData.totalSessions > 0) { this.coreData.tokensPerSession = this.coreData.totalTokens / this.coreData.totalSessions; } if (this.coreData.duration > 0) { this.coreData.tokensPerMinute = this.coreData.totalTokens / this.coreData.duration; this.coreData.tokensPerHour = this.coreData.tokensPerMinute * 60; } if (this.coreData.duration > 0) { this.coreData.estimatedHourlyCost = (this.coreData.currentCost / this.coreData.duration) * 60; this.coreData.entriesPerHour = (this.coreData.entries / this.coreData.duration) * 60; this.coreData.projectedPerHour = this.coreData.estimatedHourlyCost; } this.coreData.projectedSessionCost = this.coreData.currentCost; // For current session // Session progress (percentage of 5-hour limit) if (this.coreData.duration > 0) { this.coreData.sessionTimeProgress = (this.coreData.duration / 300) * 100; // 300 minutes = 5 hours this.coreData.progressPercentage = this.coreData.sessionTimeProgress; } this.coreData.sessionRate = `${this.coreData.estimatedHourlyCost}/hour`; // Live Monitor metrics for Active Tab this.coreData.liveMetrics = this.calculateLiveMetrics(usageEntries); this.coreData.activityWindows = this.calculateActivityWindows(usageEntries, 5); this.coreData.peakActivity = this.coreData.activityWindows.length > 0 ? Math.max(...this.coreData.activityWindows.map(w => w.entries), 0) : 0; this.coreData.averageActivity = this.coreData.activityWindows.length > 0 ? (this.coreData.activityWindows.reduce((sum, w) => sum + w.entries, 0) / this.coreData.activityWindows.length) : 0; this.coreData.isSystemActive = this.isSystemActive(usageEntries); this.coreData.timeSinceLastActivity = usageEntries.length > 0 ? this.getTimeSinceLastActivity(usageEntries.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))[0].timestamp) : null; } async calculateAdditionalAnalysis(usageEntries) { // Group by month for spending analysis (with validation) const monthlyMap = new Map(); usageEntries.forEach(entry => { if (entry.timestamp && typeof entry.timestamp === 'string' && entry.timestamp.length >= 7) { const month = entry.timestamp.substr(0, 7); // YYYY-MM const date = new Date(month + '-01'); // Only process valid months from 2020 onwards if (!isNaN(date.getTime()) && date.getFullYear() >= 2020) { if (!monthlyMap.has(month)) { monthlyMap.set(month, { cost: 0, sessions: 0 }); } monthlyMap.get(month).cost += entry.cost || 0; monthlyMap.get(month).sessions += 1; } } }); // Ensure current month exists if we have any data if (usageEntries.length > 0 && monthlyMap.size === 0) { const currentMonth = new Date().toISOString().slice(0, 7); monthlyMap.set(currentMonth, { cost: 0, sessions: 0 }); } const monthlyData = Array.from(monthlyMap.entries()).map(([month, data]) => ({ month, ...data })); if (monthlyData.length > 0) { const highestSpending = monthlyData.reduce((max, current) => current.cost > max.cost ? current : max, monthlyData[0]); this.coreData.highestSpendingMonth = highestSpending.month; const mostActive = monthlyData.reduce((max, current) => current.sessions > max.sessions ? current : max, monthlyData[0]); this.coreData.mostActiveMonth = mostActive.month; } this.coreData.vsAvgPercentage = 0; // Placeholder this.coreData.timeRanges = ['0h', '5h']; // Session time range // Calculate last 7 days breakdown and total const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); const last7Days = usageEntries.filter(e => new Date(e.timestamp) >= sevenDaysAgo); this.coreData.last7DaysTotal = last7Days.reduce((sum, e) => sum + (e.cost || 0), 0); // Daily breakdown for chart const dailyMap = new Map(); last7Days.forEach(entry => { const day = entry.timestamp.split('T')[0]; dailyMap.set(day, (dailyMap.get(day) || 0) + (entry.cost || 0)); }); this.coreData.dailyBreakdown = Array.from(dailyMap.entries()).map(([date, cost]) => ({ date, cost })); // Store additional data arrays this.coreData.monthlyData = monthlyData; this.coreData.dailyData = this.coreData.dailyBreakdown; // Update monthsTracked to match the actual monthly data we have this.coreData.monthsTracked = Math.max(this.coreData.monthsTracked, monthlyData.length); } /** * Get current session status for other services */ getSessionStatus() { return { sessionActive: this.coreData.sessionActive, status: this.coreData.status, duration: this.coreData.duration, currentCost: this.coreData.currentCost, entries: this.coreData.entries }; } /** * Calculate live metrics for Active Tab (from LiveMonitor) */ calculateLiveMetrics(entries) { if (entries.length === 0) return {}; const sortedEntries = entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); const firstEntry = sortedEntries[0]; const lastEntry = sortedEntries[sortedEntries.length - 1]; const sessionDuration = (new Date(lastEntry.timestamp) - new Date(firstEntry.timestamp)) / (1000 * 60); // Calculate activity windows (5-minute buckets) const activityWindows = this.calculateActivityWindows(entries, 5); return { sessionDuration: Math.round(sessionDuration), entriesPerMinute: sessionDuration > 0 ? (entries.length / sessionDuration).toFixed(2) : 0, activeWindows: activityWindows.length, peakActivity: Math.max(...activityWindows.map(w => w.entries), 0), averageActivity: activityWindows.length > 0 ? (activityWindows.reduce((sum, w) => sum + w.entries, 0) / activityWindows.length).toFixed(1) : 0, lastActivity: lastEntry.timestamp, timeSinceLastActivity: this.getTimeSinceLastActivity(lastEntry.timestamp) }; } /** * Calculate activity in time windows (5-minute buckets) */ calculateActivityWindows(entries, windowMinutes) { if (entries.length === 0) return []; const windows = []; const windowSize = windowMinutes * 60 * 1000; // milliseconds const sortedEntries = entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); const startTime = new Date(sortedEntries[0].timestamp).getTime(); const endTime = new Date(sortedEntries[sortedEntries.length - 1].timestamp).getTime(); for (let time = startTime; time <= endTime; time += windowSize) { const windowEnd = time + windowSize; const entriesInWindow = sortedEntries.filter(entry => { const entryTime = new Date(entry.timestamp).getTime(); return entryTime >= time && entryTime < windowEnd; }); if (entriesInWindow.length > 0) { // Calculate tokens for this window const windowTokens = entriesInWindow.reduce((sum, entry) => sum + (entry.total_tokens || 0), 0); windows.push({ startTime: new Date(time).toISOString(), endTime: new Date(windowEnd).toISOString(), entries: entriesInWindow.length, tokens: windowTokens }); } } return windows; } /** * Get time since last activity in human readable format */ getTimeSinceLastActivity(timestamp) { const now = new Date(); const lastActivity = new Date(timestamp); const diffMs = now.getTime() - lastActivity.getTime(); const diffMinutes = Math.floor(diffMs / (1000 * 60)); if (diffMinutes < 1) return 'Just now'; if (diffMinutes < 60) return `${diffMinutes} minutes ago`; const diffHours = Math.floor(diffMinutes / 60); if (diffHours < 24) return `${diffHours} hours ago`; const diffDays = Math.floor(diffHours / 24); return `${diffDays} days ago`; } /** * Get recent entries within time window (for live monitoring) */ getRecentEntries(entries, minutes) { const cutoffTime = Date.now() - (minutes * 60 * 1000); return entries.filter(entry => new Date(entry.timestamp).getTime() > cutoffTime ); } /** * Check if system is currently active (for real-time sta