UNPKG

claude-code-templates

Version:

CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects

1,541 lines (1,382 loc) โ€ข 69.7 kB
/** * DashboardPage - Analytics overview page without conversations * Focuses on metrics, charts, and system performance data */ class DashboardPage { constructor(container, services) { this.container = container; this.dataService = services.data; this.stateService = services.state; this.chartService = services.chart; this.components = {}; this.refreshInterval = null; this.isInitialized = false; // Initialize header component this.headerComponent = null; // Subscribe to state changes this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this)); } /** * Initialize the dashboard page */ async initialize() { if (this.isInitialized) return; console.log('๐Ÿ“Š Initializing DashboardPage...'); try { console.log('๐Ÿ“Š Step 1: Rendering dashboard...'); await this.render(); console.log('โœ… Dashboard rendered'); // Now that DOM is ready, we can show loading this.stateService.setLoading(true); console.log('๐Ÿ“Š Step 2: Loading initial data...'); await this.loadInitialData(); console.log('โœ… Initial data loaded'); console.log('๐Ÿ“Š Step 3: Initializing components with data...'); await this.initializeComponents(); console.log('โœ… Components initialized'); console.log('๐Ÿ“Š Step 4: Starting periodic refresh...'); this.startPeriodicRefresh(); console.log('โœ… Periodic refresh started'); this.isInitialized = true; console.log('๐ŸŽ‰ DashboardPage fully initialized!'); } catch (error) { console.error('โŒ Error during dashboard initialization:', error); // Even if there's an error, show the dashboard with fallback data this.showFallbackDashboard(); } finally { console.log('๐Ÿ“Š Clearing loading state...'); this.stateService.setLoading(false); } } /** * Show fallback dashboard when initialization fails */ showFallbackDashboard() { console.log('๐Ÿ†˜ Showing fallback dashboard...'); try { const demoData = { summary: { totalConversations: 0, claudeSessions: 0, claudeSessionsDetail: 'no sessions', totalTokens: 0, activeProjects: 0, dataSize: '0 MB' }, detailedTokenUsage: { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 }, conversations: [] }; this.updateSummaryDisplay(demoData.summary, demoData.detailedTokenUsage, demoData); this.updateLastUpdateTime(); this.stateService.setError('Dashboard loaded in offline mode'); this.isInitialized = true; } catch (fallbackError) { console.error('โŒ Fallback dashboard also failed:', fallbackError); } } /** * Handle state changes from StateService * @param {Object} state - New state * @param {string} action - Action that caused the change */ handleStateChange(state, action) { switch (action) { case 'update_conversations': this.updateSummaryDisplay(state.summary); break; case 'update_conversation_states': this.updateSystemStatus(state.conversationStates); break; case 'set_loading': this.updateLoadingState(state.isLoading); break; case 'set_error': this.updateErrorState(state.error); break; } } /** * Render the dashboard page structure */ async render() { this.container.innerHTML = ` <div class="dashboard-page"> <!-- Page Header (will be replaced by HeaderComponent) --> <div id="dashboard-header-container"></div> <!-- Action Buttons --> <div class="action-buttons-container"> <button class="action-btn-small" id="refresh-dashboard" title="Refresh data"> <span class="btn-icon-small">๐Ÿ”„</span> Refresh </button> <button class="action-btn-small" id="export-data" title="Export analytics data"> <span class="btn-icon-small">๐Ÿ“ค</span> Export </button> </div> <!-- Loading State --> <div class="loading-state" id="dashboard-loading" style="display: none;"> <div class="loading-spinner"></div> <span class="loading-text">Loading dashboard...</span> </div> <!-- Error State --> <div class="error-state" id="dashboard-error" style="display: none;"> <div class="error-content"> <span class="error-icon">โš ๏ธ</span> <span class="error-message"></span> <button class="error-retry" id="retry-load">Retry</button> </div> </div> <!-- Main Dashboard Content --> <div class="dashboard-content"> <!-- Key Metrics Cards --> <div class="metrics-cards-container"> <!-- Conversations Card --> <div class="metric-card"> <div class="metric-primary"> <span class="metric-primary-value" id="totalConversations">0</span> <span class="metric-primary-label">Total Conversations</span> </div> <div class="metric-secondary"> <div class="metric-secondary-item"> <span class="metric-secondary-label">This Month:</span> <span class="metric-secondary-value" id="conversationsMonth">0</span> </div> <div class="metric-secondary-item"> <span class="metric-secondary-label">This Week:</span> <span class="metric-secondary-value" id="conversationsWeek">0</span> </div> <div class="metric-secondary-item"> <span class="metric-secondary-label">Active:</span> <span class="metric-secondary-value" id="activeConversations">0</span> </div> </div> </div> <!-- Sessions Card --> <div class="metric-card"> <div class="metric-primary"> <span class="metric-primary-value" id="claudeSessions">0</span> <span class="metric-primary-label">Total Sessions</span> </div> <div class="metric-secondary"> <div class="metric-secondary-item"> <span class="metric-secondary-label">This Month:</span> <span class="metric-secondary-value" id="sessionsMonth">0</span> </div> <div class="metric-secondary-item"> <span class="metric-secondary-label">This Week:</span> <span class="metric-secondary-value" id="sessionsWeek">0</span> </div> <div class="metric-secondary-item"> <span class="metric-secondary-label">Projects:</span> <span class="metric-secondary-value" id="activeProjects">0</span> </div> </div> </div> <!-- Tokens Card --> <div class="metric-card"> <div class="metric-primary"> <span class="metric-primary-value" id="totalTokens">0</span> <span class="metric-primary-label">Total Tokens</span> </div> <div class="metric-secondary"> <div class="metric-secondary-item"> <span class="metric-secondary-label">Input:</span> <span class="metric-secondary-value" id="inputTokens">0</span> </div> <div class="metric-secondary-item"> <span class="metric-secondary-label">Output:</span> <span class="metric-secondary-value" id="outputTokens">0</span> </div> <div class="metric-secondary-item"> <span class="metric-secondary-label">Cache:</span> <span class="metric-secondary-value" id="cacheTokens">0</span> </div> </div> </div> <!-- Agents Card --> <div class="metric-card"> <div class="metric-primary"> <span class="metric-primary-value" id="totalAgentInvocations">0</span> <span class="metric-primary-label">Total Agent Uses</span> </div> <div class="metric-secondary"> <div class="metric-secondary-item"> <span class="metric-secondary-label">Types:</span> <span class="metric-secondary-value" id="totalAgentTypes">0</span> </div> <div class="metric-secondary-item"> <span class="metric-secondary-label">Top Agent:</span> <span class="metric-secondary-value" id="topAgentName">None</span> </div> <div class="metric-secondary-item"> <span class="metric-secondary-label">Adoption:</span> <span class="metric-secondary-value" id="agentAdoption">0%</span> </div> </div> </div> </div> <!-- Session Timer Section --> <div class="session-timer-section"> <div class="section-title"> <h2>Current Session</h2> </div> <div id="session-timer-container"> <!-- SessionTimer component will be mounted here --> </div> </div> <!-- Date Range Controls --> <div class="chart-controls"> <div class="chart-controls-left"> <label class="filter-label">date range:</label> <input type="date" id="dateFrom" class="date-input"> <span class="date-separator">to</span> <input type="date" id="dateTo" class="date-input"> <button class="filter-btn" id="applyDateFilter">apply</button> </div> </div> <!-- Charts Container - Organized by Sections --> <div class="charts-container"> <!-- SECTION 1: Token Analytics --> <div class="chart-section"> <div class="section-header"> <h3 class="section-title">๐Ÿ”ข Token Analytics</h3> <p class="section-description">Monitor token consumption patterns and efficiency</p> </div> <div class="section-charts"> <div class="chart-card"> <div class="chart-title"> Token Usage Over Time </div> <canvas id="tokenChart" class="chart-canvas"></canvas> </div> <div class="chart-card"> <div class="chart-title"> Token Distribution by Type </div> <canvas id="tokenTypeChart" class="chart-canvas"></canvas> </div> <div class="chart-card"> <div class="chart-title"> Token Usage Over Time </div> <canvas id="tokenTimelineChart" class="chart-canvas"></canvas> </div> </div> </div> <!-- SECTION 2: Workflow Intelligence --> <div class="chart-section"> <div class="section-header"> <h3 class="section-title">๐Ÿค– Workflow Intelligence</h3> <p class="section-description">Analyze agent usage and automation patterns</p> </div> <div class="section-charts"> <div class="chart-card"> <div class="chart-title"> Agent Usage Distribution </div> <canvas id="agentUsageChart" class="chart-canvas"></canvas> </div> <div class="chart-card"> <div class="chart-title"> Agent Activity Timeline </div> <canvas id="agentTimelineChart" class="chart-canvas"></canvas> </div> <div class="chart-card"> <div class="chart-title"> Workflow Efficiency Score </div> <canvas id="workflowEfficiencyChart" class="chart-canvas"></canvas> </div> </div> </div> <!-- SECTION 3: Productivity Analytics --> <div class="chart-section"> <div class="section-header"> <h3 class="section-title">๐Ÿ“ˆ Productivity Analytics</h3> <p class="section-description">Track project activity and tool utilization</p> </div> <div class="section-charts"> <div class="chart-card"> <div class="chart-title"> Project Activity Distribution </div> <canvas id="projectChart" class="chart-canvas"></canvas> </div> <div class="chart-card"> <div class="chart-title"> Tool Usage Patterns </div> <canvas id="toolChart" class="chart-canvas"></canvas> </div> <div class="chart-card"> <div class="chart-title"> Daily Productivity Trends </div> <canvas id="productivityChart" class="chart-canvas"></canvas> </div> </div> </div> </div> </div> </div> `; this.bindEvents(); this.initializeHeaderComponent(); } /** * Initialize the header component */ initializeHeaderComponent() { const headerContainer = this.container.querySelector('#dashboard-header-container'); if (headerContainer && typeof HeaderComponent !== 'undefined') { this.headerComponent = new HeaderComponent(headerContainer, { title: 'Claude Code Analytics Dashboard', subtitle: 'Real-time monitoring and analytics for Claude Code sessions', version: 'v1.13.2', // Fallback version showVersionBadge: true, showLastUpdate: true, showThemeSwitch: true, showGitHubLink: true, dataService: this.dataService // Pass DataService for dynamic version loading }); this.headerComponent.render(); } } /** * Initialize child components */ async initializeComponents() { // Initialize SessionTimer if available const sessionTimerContainer = this.container.querySelector('#session-timer-container'); if (sessionTimerContainer && typeof SessionTimer !== 'undefined') { try { this.components.sessionTimer = new SessionTimer( sessionTimerContainer, this.dataService, this.stateService ); await this.components.sessionTimer.initialize(); } catch (error) { console.warn('SessionTimer initialization failed:', error); // Show fallback content sessionTimerContainer.innerHTML = ` <div class="session-timer-placeholder"> <p>Session timer not available</p> </div> `; } } // Initialize Charts with data if available await this.initializeChartsAsync(); // Initialize Activity Feed this.initializeActivityFeed(); } /** * Initialize charts asynchronously to prevent blocking main dashboard */ async initializeChartsAsync() { try { console.log('๐Ÿ“Š Starting asynchronous chart initialization...'); await this.initializeCharts(); // Update charts with data if available if (this.allData) { console.log('๐Ÿ“Š Updating charts with loaded data...'); this.updateChartData(this.allData); console.log('โœ… Charts updated with data'); } } catch (error) { console.error('โŒ Chart initialization failed, dashboard will work without charts:', error); // Dashboard continues to work without charts } } /** * Initialize charts (Token Usage, Project Distribution, Tool Usage) */ async initializeCharts() { // Destroy existing charts if they exist if (this.components.tokenChart) { this.components.tokenChart.destroy(); this.components.tokenChart = null; } if (this.components.projectChart) { this.components.projectChart.destroy(); this.components.projectChart = null; } if (this.components.toolChart) { this.components.toolChart.destroy(); this.components.toolChart = null; } // Longer delay to ensure DOM is fully ready and previous charts are destroyed await new Promise(resolve => setTimeout(resolve, 250)); // Get canvas elements with strict validation const tokenCanvas = this.container.querySelector('#tokenChart'); const projectCanvas = this.container.querySelector('#projectChart'); const toolCanvas = this.container.querySelector('#toolChart'); // Validate all canvas elements exist and are properly attached to DOM if (!tokenCanvas || !projectCanvas || !toolCanvas) { console.error('โŒ Chart canvas elements not found in DOM'); console.log('Available elements:', { tokenCanvas: !!tokenCanvas, projectCanvas: !!projectCanvas, toolCanvas: !!toolCanvas }); return; // Don't initialize charts if canvas elements are missing } // Verify canvas elements are properly connected to the DOM if (!document.body.contains(tokenCanvas) || !document.body.contains(projectCanvas) || !document.body.contains(toolCanvas)) { console.error('โŒ Chart canvas elements not properly attached to DOM'); return; } // Force destroy any existing Chart instances try { if (Chart.getChart(tokenCanvas)) { console.log('๐Ÿงน Destroying existing tokenChart instance'); Chart.getChart(tokenCanvas).destroy(); } if (Chart.getChart(projectCanvas)) { console.log('๐Ÿงน Destroying existing projectChart instance'); Chart.getChart(projectCanvas).destroy(); } if (Chart.getChart(toolCanvas)) { console.log('๐Ÿงน Destroying existing toolChart instance'); Chart.getChart(toolCanvas).destroy(); } } catch (error) { console.warn('Warning during chart cleanup:', error); } // Validate canvas dimensions and ensure they're properly sized const canvases = [tokenCanvas, projectCanvas, toolCanvas]; for (const canvas of canvases) { if (canvas.offsetWidth === 0 || canvas.offsetHeight === 0) { console.error('โŒ Canvas has zero dimensions, waiting for layout...'); await new Promise(resolve => setTimeout(resolve, 100)); if (canvas.offsetWidth === 0 || canvas.offsetHeight === 0) { console.error('โŒ Canvas still has zero dimensions after wait'); return; } } } // Token Usage Chart (Linear) if (tokenCanvas) { try { console.log('๐Ÿ“Š Creating token chart...'); this.components.tokenChart = new Chart(tokenCanvas, { type: 'line', data: { labels: [], datasets: [{ label: 'Tokens', data: [], borderColor: '#d57455', backgroundColor: 'rgba(213, 116, 85, 0.1)', tension: 0.4, fill: true }] }, options: this.getTokenChartOptions() }); console.log('โœ… Token chart created successfully'); } catch (error) { console.error('โŒ Error creating token chart:', error); } } // Project Activity Distribution Chart (Pie) if (projectCanvas) { try { console.log('๐Ÿ“Š Creating project chart...'); this.components.projectChart = new Chart(projectCanvas, { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: [ '#d57455', '#3fb950', '#f97316', '#a5d6ff', '#f85149', '#7d8590', '#ffd33d', '#bf91f3' ], borderWidth: 0 }] }, options: this.getProjectChartOptions() }); console.log('โœ… Project chart created successfully'); } catch (error) { console.error('โŒ Error creating project chart:', error); } } // Tool Usage Trends Chart (Bar) if (toolCanvas) { try { console.log('๐Ÿ“Š Creating tool chart...'); this.components.toolChart = new Chart(toolCanvas, { type: 'bar', data: { labels: [], datasets: [{ label: 'Usage Count', data: [], backgroundColor: [ 'rgba(75, 192, 192, 0.6)', 'rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(255, 206, 86, 0.6)', 'rgba(153, 102, 255, 0.6)', 'rgba(255, 159, 64, 0.6)' ], borderColor: [ 'rgba(75, 192, 192, 1)', 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)' ], borderWidth: 1 }] }, options: this.getToolChartOptions() }); console.log('โœ… Tool chart created successfully'); } catch (error) { console.error('โŒ Error creating tool chart:', error); } } console.log('๐ŸŽ‰ All charts initialized successfully'); // Initialize date inputs this.initializeDateInputs(); } /** * Get token chart options */ getTokenChartOptions() { return { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', axis: 'x', intersect: false }, plugins: { legend: { display: false }, tooltip: { enabled: true, mode: 'nearest', backgroundColor: '#161b22', titleColor: '#d57455', bodyColor: '#c9d1d9', borderColor: '#30363d', borderWidth: 1, cornerRadius: 4, displayColors: false, animation: { duration: 200 }, callbacks: { title: function(context) { return `Date: ${context[0].label}`; }, label: function(context) { return `Tokens: ${context.parsed.y.toLocaleString()}`; } } } }, scales: { x: { grid: { color: '#30363d' }, ticks: { color: '#7d8590' } }, y: { beginAtZero: true, grid: { color: '#30363d' }, ticks: { color: '#7d8590', callback: function(value) { return value.toLocaleString(); } } } } }; } /** * Get project chart options */ getProjectChartOptions() { return { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#c9d1d9', padding: 15, usePointStyle: true } }, tooltip: { enabled: true, backgroundColor: '#161b22', titleColor: '#d57455', bodyColor: '#c9d1d9', borderColor: '#30363d', borderWidth: 1, cornerRadius: 4, displayColors: false, animation: { duration: 200 }, callbacks: { title: function(context) { return `Project: ${context[0].label}`; }, label: function(context) { const total = context.dataset.data.reduce((sum, value) => sum + value, 0); const percentage = ((context.parsed / total) * 100).toFixed(1); return `${context.parsed.toLocaleString()} conversations (${percentage}%)`; } } } }, cutout: '60%' }; } /** * Get tool chart options */ getToolChartOptions() { return { responsive: true, maintainAspectRatio: false, interaction: { mode: 'nearest', axis: 'x', intersect: false }, plugins: { legend: { display: false }, tooltip: { enabled: true, mode: 'nearest', backgroundColor: '#161b22', titleColor: '#d57455', bodyColor: '#c9d1d9', borderColor: '#30363d', borderWidth: 1, cornerRadius: 4, displayColors: false, animation: { duration: 200 }, callbacks: { title: function(context) { return `Tool: ${context[0].label}`; }, label: function(context) { return `Usage: ${context.parsed.y.toLocaleString()} times`; } } } }, scales: { x: { grid: { color: '#30363d' }, ticks: { color: '#7d8590', maxRotation: 45 } }, y: { beginAtZero: true, grid: { color: '#30363d' }, ticks: { color: '#7d8590', stepSize: 1 } } } }; } /** * Initialize activity feed */ initializeActivityFeed() { const activityFeed = this.container.querySelector('#activity-feed'); // Check if activity feed element exists if (!activityFeed) { console.log('โ„น๏ธ Activity feed element not found, skipping initialization'); return; } // Sample activity data (would be replaced with real data) const activities = [ { type: 'session_start', message: 'New Claude Code session started', timestamp: new Date(), icon: '๐Ÿš€' }, { type: 'conversation_update', message: 'Conversation state updated', timestamp: new Date(Date.now() - 5 * 60 * 1000), icon: '๐Ÿ’ฌ' }, { type: 'system_event', message: 'Analytics server started', timestamp: new Date(Date.now() - 10 * 60 * 1000), icon: 'โšก' } ]; activityFeed.innerHTML = activities.map(activity => ` <div class="activity-item"> <div class="activity-icon">${activity.icon}</div> <div class="activity-content"> <div class="activity-message">${activity.message}</div> <div class="activity-time">${this.formatTimestamp(activity.timestamp)}</div> </div> </div> `).join(''); } /** * Format timestamp for display * @param {Date} timestamp - Timestamp to format * @returns {string} Formatted timestamp */ formatTimestamp(timestamp) { const now = new Date(); const diff = now - timestamp; const minutes = Math.floor(diff / (1000 * 60)); if (minutes < 1) return 'Just now'; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; return timestamp.toLocaleDateString(); } /** * Bind event listeners */ bindEvents() { // Refresh button const refreshBtn = this.container.querySelector('#refresh-dashboard'); if (refreshBtn) { refreshBtn.addEventListener('click', () => this.refreshData()); } // Export button const exportBtn = this.container.querySelector('#export-data'); if (exportBtn) { exportBtn.addEventListener('click', () => this.exportData()); } // Date filter controls const applyDateFilter = this.container.querySelector('#applyDateFilter'); if (applyDateFilter) { applyDateFilter.addEventListener('click', () => this.applyDateFilter()); } // Token popover events const totalTokens = this.container.querySelector('#totalTokens'); if (totalTokens) { totalTokens.addEventListener('mouseenter', () => this.showTokenPopover()); totalTokens.addEventListener('mouseleave', () => this.hideTokenPopover()); totalTokens.addEventListener('click', () => this.showTokenPopover()); } // Error retry const retryBtn = this.container.querySelector('#retry-load'); if (retryBtn) { retryBtn.addEventListener('click', () => this.loadInitialData()); } } /** * Load initial data */ async loadInitialData() { try { const [conversationsData, statesData, agentData] = await Promise.all([ this.dataService.getConversations(), this.dataService.getConversationStates(), this.dataService.cachedFetch('/api/agents') ]); this.stateService.updateConversations(conversationsData.conversations); this.stateService.updateSummary(conversationsData.summary); this.stateService.updateConversationStates(statesData); // Store agent data for charts this.agentData = agentData; // Update dashboard with original format this.updateSummaryDisplay( conversationsData.summary, conversationsData.detailedTokenUsage, conversationsData ); this.updateLastUpdateTime(); this.updateChartData(conversationsData); this.updateAgentCharts(agentData); } catch (error) { console.error('Error loading initial data:', error); // Try to provide fallback demo data const demoData = { summary: { totalConversations: 0, claudeSessions: 0, claudeSessionsDetail: 'no sessions', totalTokens: 0, activeProjects: 0, dataSize: '0 MB' }, detailedTokenUsage: { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 }, conversations: [] }; this.updateSummaryDisplay(demoData.summary, demoData.detailedTokenUsage, demoData); this.updateLastUpdateTime(); this.stateService.setError('Using offline mode - server connection failed'); } } /** * Refresh all data */ async refreshData() { const refreshBtn = this.container.querySelector('#refresh-dashboard'); if (!refreshBtn) return; refreshBtn.disabled = true; refreshBtn.classList.add('loading'); const btnIcon = refreshBtn.querySelector('.btn-icon-small'); if (btnIcon) { btnIcon.classList.add('spin'); } try { this.dataService.clearCache(); await this.loadInitialData(); } catch (error) { console.error('Error refreshing data:', error); this.stateService.setError('Failed to refresh data'); } finally { refreshBtn.disabled = false; refreshBtn.classList.remove('loading'); if (btnIcon) { btnIcon.classList.remove('spin'); } } } /** * Update summary display (New Cards format) * @param {Object} summary - Summary data * @param {Object} detailedTokenUsage - Detailed token breakdown * @param {Object} allData - Complete dataset */ updateSummaryDisplay(summary, detailedTokenUsage, allData) { if (!summary) return; // Calculate additional metrics const now = new Date(); const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1); const thisWeek = new Date(now.setDate(now.getDate() - now.getDay())); // Update primary metrics const totalConversations = this.container.querySelector('#totalConversations'); const claudeSessions = this.container.querySelector('#claudeSessions'); const totalTokens = this.container.querySelector('#totalTokens'); if (totalConversations) totalConversations.textContent = summary.totalConversations?.toLocaleString() || '0'; if (claudeSessions) claudeSessions.textContent = summary.claudeSessions?.toLocaleString() || '0'; if (totalTokens) totalTokens.textContent = summary.totalTokens?.toLocaleString() || '0'; // Update conversation secondary metrics const conversationsMonth = this.container.querySelector('#conversationsMonth'); const conversationsWeek = this.container.querySelector('#conversationsWeek'); const activeConversations = this.container.querySelector('#activeConversations'); if (conversationsMonth) conversationsMonth.textContent = this.calculateTimeRangeCount(allData?.conversations, thisMonth).toLocaleString(); if (conversationsWeek) conversationsWeek.textContent = this.calculateTimeRangeCount(allData?.conversations, thisWeek).toLocaleString(); if (activeConversations) activeConversations.textContent = summary.activeConversations?.toLocaleString() || '0'; // Update session secondary metrics const sessionsMonth = this.container.querySelector('#sessionsMonth'); const sessionsWeek = this.container.querySelector('#sessionsWeek'); const activeProjects = this.container.querySelector('#activeProjects'); if (sessionsMonth) sessionsMonth.textContent = Math.max(1, Math.floor((summary.claudeSessions || 0) * 0.3)).toLocaleString(); if (sessionsWeek) sessionsWeek.textContent = Math.max(1, Math.floor((summary.claudeSessions || 0) * 0.1)).toLocaleString(); if (activeProjects) activeProjects.textContent = summary.activeProjects?.toLocaleString() || '0'; // Update token secondary metrics if (detailedTokenUsage) { this.updateTokenBreakdown(detailedTokenUsage); } // Update agent metrics if available if (this.agentData) { this.updateAgentMetrics(this.agentData); } // Store data for chart updates this.allData = allData; } /** * Calculate count of items within a time range * @param {Array} items - Items with lastModified property * @param {Date} fromDate - Start date * @returns {number} Count of items */ calculateTimeRangeCount(items, fromDate) { if (!items || !Array.isArray(items)) return 0; return items.filter(item => { if (!item.lastModified) return false; const itemDate = new Date(item.lastModified); return itemDate >= fromDate; }).length; } /** * Update token breakdown in cards * @param {Object} tokenUsage - Detailed token usage */ updateTokenBreakdown(tokenUsage) { const inputTokens = this.container.querySelector('#inputTokens'); const outputTokens = this.container.querySelector('#outputTokens'); const cacheTokens = this.container.querySelector('#cacheTokens'); if (inputTokens) inputTokens.textContent = tokenUsage.inputTokens?.toLocaleString() || '0'; if (outputTokens) outputTokens.textContent = tokenUsage.outputTokens?.toLocaleString() || '0'; // Combine cache creation and read tokens const totalCache = (tokenUsage.cacheCreationTokens || 0) + (tokenUsage.cacheReadTokens || 0); if (cacheTokens) cacheTokens.textContent = totalCache.toLocaleString(); } /** * Update agent metrics in the agents card * @param {Object} agentData - Agent analytics data */ updateAgentMetrics(agentData) { if (!agentData) return; const totalAgentInvocations = this.container.querySelector('#totalAgentInvocations'); const totalAgentTypes = this.container.querySelector('#totalAgentTypes'); const topAgentName = this.container.querySelector('#topAgentName'); const agentAdoption = this.container.querySelector('#agentAdoption'); // Update primary metric - total invocations if (totalAgentInvocations) { totalAgentInvocations.textContent = agentData.totalAgentInvocations?.toLocaleString() || '0'; } // Update secondary metrics if (totalAgentTypes) { totalAgentTypes.textContent = agentData.totalAgentTypes?.toLocaleString() || '0'; } if (topAgentName) { const topAgent = agentData.agentStats?.[0]; if (topAgent) { topAgentName.textContent = topAgent.name; topAgentName.title = `${topAgent.totalInvocations} uses`; } else { topAgentName.textContent = 'None'; } } if (agentAdoption) { const adoptionRate = agentData.efficiency?.adoptionRate || '0'; agentAdoption.textContent = adoptionRate + '%'; } } /** * Show token popover */ showTokenPopover() { const popover = this.container.querySelector('#tokenPopover'); if (popover) { popover.style.display = 'block'; } } /** * Hide token popover */ hideTokenPopover() { const popover = this.container.querySelector('#tokenPopover'); if (popover) { popover.style.display = 'none'; } } /** * Initialize date inputs */ initializeDateInputs() { const dateFrom = this.container.querySelector('#dateFrom'); const dateTo = this.container.querySelector('#dateTo'); if (!dateFrom || !dateTo) return; const today = new Date(); const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); dateFrom.value = sevenDaysAgo.toISOString().split('T')[0]; dateTo.value = today.toISOString().split('T')[0]; } /** * Get date range from inputs */ getDateRange() { const dateFrom = this.container.querySelector('#dateFrom'); const dateTo = this.container.querySelector('#dateTo'); let fromDate = new Date(); fromDate.setDate(fromDate.getDate() - 7); // Default to 7 days ago let toDate = new Date(); if (dateFrom && dateFrom.value) { fromDate = new Date(dateFrom.value); } if (dateTo && dateTo.value) { toDate = new Date(dateTo.value); toDate.setHours(23, 59, 59, 999); // Include full day } return { fromDate, toDate }; } /** * Apply date filter */ applyDateFilter() { if (this.allData) { this.updateChartData(this.allData); } if (this.agentData) { this.updateAgentCharts(this.agentData); } } /** * Refresh charts */ async refreshCharts() { const refreshBtn = this.container.querySelector('#refreshCharts'); if (refreshBtn) { refreshBtn.disabled = true; refreshBtn.textContent = 'refreshing...'; } try { await this.loadInitialData(); } finally { if (refreshBtn) { refreshBtn.disabled = false; refreshBtn.textContent = 'refresh charts'; } } } /** * Update system status * @param {Object} states - Conversation states */ updateSystemStatus(states) { const activeCount = Object.values(states).filter(state => state === 'active').length; // Update WebSocket status const wsStatus = this.container.querySelector('#websocket-status'); if (wsStatus) { const indicator = wsStatus.querySelector('.status-indicator'); indicator.className = `status-indicator ${activeCount > 0 ? 'connected' : 'disconnected'}`; wsStatus.lastChild.textContent = activeCount > 0 ? 'Connected' : 'Disconnected'; } } /** * Update chart data with real analytics * @param {Object} data - Analytics data */ updateChartData(data) { if (!data || !data.conversations) return; // Token Analytics Section this.updateTokenChart(data.conversations); this.updateTokenTypeChart(data); this.updateTokenTimelineChart(data); // Productivity Analytics Section this.updateProjectChart(data.conversations); this.updateToolChart(data.conversations); this.updateProductivityChart(data); // Legacy tool summary (keeping for now) this.updateToolSummary(data.conversations); } /** * Update token usage chart */ updateTokenChart(conversations) { if (!this.components.tokenChart) { console.warn('Token chart not initialized'); return; } const { fromDate, toDate } = this.getDateRange(); const filteredConversations = conversations.filter(conv => { const convDate = new Date(conv.lastModified); return convDate >= fromDate && convDate <= toDate; }); // Group by date and sum tokens const tokensByDate = {}; filteredConversations.forEach(conv => { const date = new Date(conv.lastModified).toDateString(); tokensByDate[date] = (tokensByDate[date] || 0) + (conv.tokens || 0); }); const sortedDates = Object.keys(tokensByDate).sort((a, b) => new Date(a) - new Date(b)); const labels = sortedDates.map(date => new Date(date).toLocaleDateString()); const data = sortedDates.map(date => tokensByDate[date]); console.log('๐Ÿ“Š Token chart - tokensByDate:', tokensByDate); console.log('๐Ÿ“Š Token chart - Labels:', labels); console.log('๐Ÿ“Š Token chart - Data:', data); this.components.tokenChart.data.labels = labels; this.components.tokenChart.data.datasets[0].data = data; this.components.tokenChart.update(); } /** * Update project distribution chart */ updateProjectChart(conversations) { if (!this.components.projectChart) { console.warn('Project chart not initialized'); return; } const { fromDate, toDate } = this.getDateRange(); const filteredConversations = conversations.filter(conv => { const convDate = new Date(conv.lastModified); return convDate >= fromDate && convDate <= toDate; }); // Group by project and sum tokens const projectTokens = {}; filteredConversations.forEach(conv => { const project = conv.project || 'Unknown'; projectTokens[project] = (projectTokens[project] || 0) + (conv.tokens || 0); }); const labels = Object.keys(projectTokens); const data = Object.values(projectTokens); this.components.projectChart.data.labels = labels; this.components.projectChart.data.datasets[0].data = data; this.components.projectChart.update(); } /** * Update tool usage chart */ updateToolChart(conversations) { if (!this.components.toolChart) { console.warn('Tool chart not initialized'); return; } const { fromDate, toDate } = this.getDateRange(); const toolStats = {}; conversations.forEach(conv => { if (conv.toolUsage && conv.toolUsage.toolTimeline) { conv.toolUsage.toolTimeline.forEach(entry => { const entryDate = new Date(entry.timestamp); if (entryDate >= fromDate && entryDate <= toDate) { toolStats[entry.tool] = (toolStats[entry.tool] || 0) + 1; } }); } }); const sortedTools = Object.entries(toolStats) .sort((a, b) => b[1] - a[1]) .slice(0, 10); const labels = sortedTools.map(([tool]) => tool.length > 15 ? tool.substring(0, 15) + '...' : tool); const data = sortedTools.map(([, count]) => count); this.components.toolChart.data.labels = labels; this.components.toolChart.data.datasets[0].data = data; this.components.toolChart.update(); } /** * Update tool summary panel */ updateToolSummary(conversations) { const toolSummary = this.container.querySelector('#toolSummary'); if (!toolSummary) return; const { fromDate, toDate } = this.getDateRange(); const toolStats = {}; let totalToolCalls = 0; let conversationsWithTools = 0; conversations.forEach(conv => { if (conv.toolUsage && conv.toolUsage.toolTimeline) { let convHasTools = false; conv.toolUsage.toolTimeline.forEach(entry => { const entryDate = new Date(entry.timestamp); if (entryDate >= fromDate && entryDate <= toDate) { toolStats[entry.tool] = (toolStats[entry.tool] || 0) + 1; totalToolCalls++; convHasTools = true; } }); if (convHasTools) conversationsWithTools++; } }); const uniqueTools = Object.keys(toolStats).length; const topTool = Object.entries(toolStats).sort((a, b) => b[1] - a[1])[0]; toolSummary.innerHTML = ` <div class="tool-stat"> <span class="tool-stat-label">Total Tool Calls</span> <span class="tool-stat-value">${totalToolCalls.toLocaleString()}</span> </div> <div class="tool-stat"> <span class="tool-stat-label">Unique Tools Used</span> <span class="tool-stat-value">${uniqueTools}</span> </div> <div class="tool-stat"> <span class="tool-stat-label">Conversation Coverage</span> <span class="tool-stat-value">${Math.round((conversationsWithTools / conversations.length) * 100)}%</span> </div> ${topTool ? ` <div class="tool-top-tool"> <div class="tool-icon">๐Ÿ› ๏ธ</div> <div class="tool-info"> <div class="tool-name">${topTool[0]}</div> <div class="tool-usage">${topTool[1]} calls</div> </div> </div> ` : ''} `; } /** * Update agent usage charts * @param {Object} agentData - Agent analytics data */ updateAgentCharts(agentData) { if (!agentData || !agentData.agentStats) { console.warn('No agent data available for charts'); return; } this.updateAgentUsageChart(agentData); this.updateAgentTimelineChart(agentData); this.updateWorkflowEfficiencyChart(agentData); } /** * Update agent usage distribution chart * @param {Object} agentData - Agent analytics data */ updateAgentUsageChart(agentData) { const canvas = this.container.querySelector('#agentUsageChart'); if (!canvas) { console.warn('Agent usage chart canvas not found'); return; } // Destroy existing chart if it exists const existingChart = Chart.getChart(canvas); if (existingChart) { existingChart.destroy(); } const ctx = canvas.getContext('2d'); const agentStats = agentData.agentStats || []; if (agentStats.length === 0) { // Show "no data" message ctx.fillStyle = '#7d8590'; ctx.textAlign = 'center'; ctx.font = '14px Monaco, monospace'; ctx.fillText('No agent usage data', canvas.width / 2, canvas.height / 2); return; } new Chart(ctx, { type: 'doughnut', data: { labels: agentStats.map(agent => agent.name), datasets: [{ data: agentStats.map(agent => agent.totalInvocations), backgroundColor: agentStats.map(agent => agent.color), borderColor: '#0d1117', borderWidth: 2, hoverBorderWidth: 3 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#c9d1d9', padding: 10, usePointStyle: true, font: { family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace", size: 11 } } }, tooltip: { titleFont: { family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace" }, bodyFont: { family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace" }, callbacks: { label: function(context) { const agent = agentStats[context.dataIndex]; return `${agent.name}: ${context.parsed} uses (${agent.uniqueConversations} conversations)`; } } } }, cutout: '60%' } }); } /** * Update agent usage timeline chart * @param {Object} agentData - Agent analytics data */ updateAgentTimelineChart(agentData) { const canvas = this.container.querySelector('#agentTimelineChart'); if (!canvas) { console.warn('Agent timeline chart canvas not found'); return; } // Destroy existing chart if it exists const existingChart = Chart.getChart(canvas); if (existingChart) { existingChart.destroy(); } const ctx = canvas.getContext('2d'); const usageByDay = agentData.usageByDay || []; if (usageByDay.length === 0) { // Show "no data" message ctx.fillStyle = '#7d8590'; ctx.textAlign = 'center'; ctx.font = '14px Monaco, monospace'; ctx.fillText('No timeline data', canvas.width / 2, canvas.height / 2); return; } new Chart(ctx, { type: 'line', data: { labels: usageByDay.map(d => new Date(d.date).toLocaleDateString()), datasets: [{ label: 'Agent Usage', data: usageByDay.map(d => d.count), borderColor: '#3fb950', backgroundColor: 'rgba(63, 185, 80, 0.1)', borderWidth: 2, fill: true, tension: 0.3, pointBackgroundColor: '#3fb950', pointBorderColor: '#ffffff', pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#c9d1d9', font: { family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace", size: 11 } } }, tooltip: { titleFont: { family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace" }, bodyFont: { family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace" }, callbacks: { label: function(context) { return `Agent invocations: ${context.parsed.y}`; } } } },