class ProductivityDashboard { constructor() { this.chart = null; this.refreshInterval = null; this.init(); } async init() { await this.loadInitialData(); this.setupEventListeners(); this.startAutoRefresh(); this.setupChart(); } async loadInitialData() { try { await Promise.all([ this.updateStats(), this.updateActivity(), this.updateAchievements(), this.updateXPBreakdown(), this.updateLogs(), this.loadConfig(), this.updateClassifications(), this.updateUnclassified() ]); } catch (error) { console.error('Failed to load initial data:', error); this.showMessage('Failed to load dashboard data', 'error'); } } async updateStatsAndActivity() { try { const response = await fetch('/api/stats'); const data = await response.json(); // Update header stats document.getElementById('current-level').textContent = data.today.level; document.getElementById('current-xp').textContent = data.today.xp; document.getElementById('current-streak').textContent = data.streaks.current_streak; // Update progress bars const focusHours = Math.floor(data.today.focus_time / 3600); const focusMinutes = Math.floor((data.today.focus_time % 3600) / 60); const meetingHours = Math.floor(data.today.meeting_time / 3600); const meetingMinutes = Math.floor((data.today.meeting_time % 3600) / 60); document.getElementById('focus-time').textContent = `${focusHours}h ${focusMinutes}m`; document.getElementById('meeting-time').textContent = `${meetingHours}h ${meetingMinutes}m`; document.getElementById('focus-sessions').textContent = data.today.focus_sessions; // Update progress bars (assuming 8 hours = 100%) const focusPercent = Math.min((data.today.focus_time / (8 * 3600)) * 100, 100); const meetingPercent = Math.min((data.today.meeting_time / (4 * 3600)) * 100, 100); document.getElementById('focus-progress').style.width = `${focusPercent}%`; document.getElementById('meeting-progress').style.width = `${meetingPercent}%`; // Update recent activity const activityContainer = document.getElementById('recent-activity'); if (data.recent_activity && data.recent_activity.length > 0) { activityContainer.innerHTML = data.recent_activity.map(activity => { const date = new Date(activity.timestamp); const timeStr = date.toLocaleTimeString(); const durationMin = Math.floor(activity.duration_seconds / 60); return `
${this.capitalizeFirst(activity.type)}
${activity.application} • ${durationMin}m • ${timeStr}
`; }).join(''); } else { activityContainer.innerHTML = '
No recent activity
'; } } catch (error) { console.error('Failed to update stats and activity:', error); } } // Backward compatibility methods async updateStats() { return this.updateStatsAndActivity(); } async updateActivity() { return this.updateStatsAndActivity(); } async updateAchievements() { try { const response = await fetch('/api/achievements?limit=5'); const achievements = await response.json(); const achievementsContainer = document.getElementById('achievements-list'); if (achievements && achievements.length > 0) { achievementsContainer.innerHTML = achievements.map(achievement => { const date = new Date(achievement.achieved_at); const dateStr = date.toLocaleDateString(); return `
${achievement.name}
${achievement.description} • +${achievement.xp_reward} XP • ${dateStr}
`; }).join(''); } else { achievementsContainer.innerHTML = '
No achievements yet
'; } } catch (error) { console.error('Failed to update achievements:', error); } } async updateXPBreakdown() { try { const response = await fetch('/api/xp-breakdown'); const breakdown = await response.json(); const breakdownContainer = document.getElementById('xp-breakdown'); if (breakdown && Object.keys(breakdown).length > 0) { const totalXP = Object.values(breakdown).reduce((sum, xp) => sum + xp, 0); breakdownContainer.innerHTML = Object.entries(breakdown) .sort(([,a], [,b]) => b - a) // Sort by XP amount descending .map(([source, xp]) => { const percentage = totalXP > 0 ? ((xp / totalXP) * 100).toFixed(1) : 0; const icon = this.getXPSourceIcon(source); return `
${icon} ${this.formatXPSourceName(source)} +${xp} XP
${percentage}%
`; }).join(''); } else { breakdownContainer.innerHTML = '
No XP earned today
'; } } catch (error) { console.error('Failed to update XP breakdown:', error); // If the endpoint doesn't exist yet, show a placeholder const breakdownContainer = document.getElementById('xp-breakdown'); if (breakdownContainer) { breakdownContainer.innerHTML = '
XP breakdown coming soon...
'; } } } getXPSourceIcon(source) { const icons = { 'coding': '💻', 'focused_browsing': '🔍', 'collaboration': '🤝', 'meetings': '📅', 'misc': '📝', 'uncategorized': '❓', 'focus_session': '🎯', 'achievement': '🏆', 'manual_boost': '🚀', // Legacy category support 'browsing': '🌐', 'communication': '💬', 'meeting': '🤝', 'terminal': '⌨️', 'security': '🔐', 'other': '📝' }; return icons[source] || '📊'; } formatXPSourceName(source) { const names = { 'coding': 'Coding', 'focused_browsing': 'Focused Browsing', 'collaboration': 'Collaboration', 'meetings': 'Meetings', 'misc': 'Miscellaneous', 'uncategorized': 'Uncategorized', 'focus_session': 'Focus Sessions', 'achievement': 'Achievements', 'manual_boost': 'Manual Boosts', // Legacy category support 'browsing': 'Web Browsing', 'communication': 'Communication', 'meeting': 'Meetings', 'terminal': 'Terminal/CLI', 'security': 'Security Tools', 'other': 'Other Activities' }; return names[source] || source.charAt(0).toUpperCase() + source.slice(1); } async updateLogs() { try { const level = document.getElementById('log-level').value; const url = level ? `/api/logs?level=${level}&count=50` : '/api/logs?count=50'; const response = await fetch(url); const data = await response.json(); const logsContainer = document.getElementById('logs-container'); if (data.logs && data.logs.length > 0) { logsContainer.innerHTML = data.logs.map(log => { const logClass = this.getLogClass(log); return `
${this.escapeHtml(log)}
`; }).join(''); } else { logsContainer.innerHTML = '
No logs available
'; } // Auto-scroll to bottom logsContainer.scrollTop = logsContainer.scrollHeight; } catch (error) { console.error('Failed to update logs:', error); } } async loadConfig() { try { const response = await fetch('/api/config'); const config = await response.json(); // Update config inputs document.getElementById('coding-xp').value = config.xp_rewards?.base_multipliers?.coding || 10; document.getElementById('research-xp').value = config.xp_rewards?.base_multipliers?.research || 8; document.getElementById('meeting-xp').value = config.xp_rewards?.base_multipliers?.meeting || 3; document.getElementById('focus-bonus').value = config.xp_rewards?.focus_session_bonuses?.base_xp_per_minute || 5; } catch (error) { console.error('Failed to load config:', error); } } async saveConfig() { try { const updates = { 'xp_rewards.base_multipliers.coding': parseInt(document.getElementById('coding-xp').value), 'xp_rewards.base_multipliers.research': parseInt(document.getElementById('research-xp').value), 'xp_rewards.base_multipliers.meeting': parseInt(document.getElementById('meeting-xp').value), 'xp_rewards.focus_session_bonuses.base_xp_per_minute': parseInt(document.getElementById('focus-bonus').value) }; const response = await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(updates) }); if (response.ok) { this.showMessage('Configuration saved successfully!', 'success'); } else { throw new Error('Failed to save configuration'); } } catch (error) { console.error('Failed to save config:', error); this.showMessage('Failed to save configuration', 'error'); } } async setupChart() { try { const response = await fetch('/api/stats/history?days=7'); const history = await response.json(); const ctx = document.getElementById('xp-chart').getContext('2d'); const labels = history.map(day => { const date = new Date(day.date); return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); }); const xpData = history.map(day => day.xp); const levelData = history.map(day => day.level); this.chart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: 'XP', data: xpData, borderColor: '#667eea', backgroundColor: 'rgba(102, 126, 234, 0.1)', tension: 0.4, fill: true, yAxisID: 'y' }, { label: 'Level', data: levelData, borderColor: '#764ba2', backgroundColor: 'rgba(118, 75, 162, 0.1)', tension: 0.4, fill: false, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, scales: { x: { display: true, title: { display: true, text: 'Date' } }, y: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'XP' }, }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'Level' }, grid: { drawOnChartArea: false, }, } }, plugins: { legend: { display: true, position: 'top' }, title: { display: false } } } }); } catch (error) { console.error('Failed to setup chart:', error); } } async updateChart() { try { const response = await fetch('/api/stats/history?days=7'); const history = await response.json(); if (this.chart) { const labels = history.map(day => { const date = new Date(day.date); return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); }); const xpData = history.map(day => day.xp); const levelData = history.map(day => day.level); // Update chart data this.chart.data.labels = labels; this.chart.data.datasets[0].data = xpData; this.chart.data.datasets[1].data = levelData; // Refresh the chart this.chart.update('none'); // 'none' for no animation during updates } } catch (error) { console.error('Failed to update chart:', error); } } setupEventListeners() { // Save config button document.getElementById('save-config').addEventListener('click', () => { this.saveConfig(); }); // Refresh logs button document.getElementById('refresh-logs').addEventListener('click', () => { this.updateLogs(); }); // Log level filter document.getElementById('log-level').addEventListener('change', () => { this.updateLogs(); }); } startAutoRefresh() { // Refresh data every 30 seconds this.refreshInterval = setInterval(() => { this.updateStatsAndActivity(); this.updateChart(); this.updateAchievements(); this.updateXPBreakdown(); }, 30000); } stopAutoRefresh() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } } getLogClass(logEntry) { if (logEntry.includes('[ERROR]')) return 'error'; if (logEntry.includes('[WARN]')) return 'warn'; if (logEntry.includes('[INFO]')) return 'info'; if (logEntry.includes('[DEBUG]')) return 'debug'; return ''; } capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } showMessage(message, type = 'info') { // Create message element const messageEl = document.createElement('div'); messageEl.className = `message ${type}`; messageEl.textContent = message; // Insert at top of container const container = document.querySelector('.container'); container.insertBefore(messageEl, container.firstChild); // Remove after 5 seconds setTimeout(() => { if (messageEl.parentNode) { messageEl.parentNode.removeChild(messageEl); } }, 5000); } async updateClassifications() { try { const response = await fetch('/api/classifications'); const classifications = await response.json(); const classificationsContainer = document.getElementById('classifications-list'); if (classifications && classifications.length > 0) { classificationsContainer.innerHTML = classifications.map(classification => { const categoryIcon = this.getCategoryIcon(classification.category_id); const categoryName = this.formatCategoryName(classification.category_id); return `
${categoryIcon} ${classification.application_name} ${categoryName}
`; }).join(''); } else { classificationsContainer.innerHTML = '
No classifications yet
'; } } catch (error) { console.error('Failed to update classifications:', error); } } async updateUnclassified() { try { const response = await fetch('/api/unclassified'); const unclassified = await response.json(); const unclassifiedContainer = document.getElementById('unclassified-list'); if (unclassified && unclassified.length > 0) { unclassifiedContainer.innerHTML = unclassified.map(app => { const lastSeen = new Date(app.last_seen); const timeStr = lastSeen.toLocaleDateString(); return `
${app.application_name} ${app.occurrence_count} times Last: ${timeStr}
`; }).join(''); } else { unclassifiedContainer.innerHTML = '
No unclassified applications
'; } } catch (error) { console.error('Failed to update unclassified applications:', error); } } async classifyApplication(applicationName, selectId) { try { const selectElement = document.getElementById(selectId); const categoryId = selectElement.value; if (!categoryId) { this.showMessage('Please select a category', 'error'); return; } const response = await fetch('/api/classifications', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ application_name: applicationName, category_id: categoryId }) }); if (response.ok) { this.showMessage(`${applicationName} classified as ${this.formatCategoryName(categoryId)}`, 'success'); await this.updateClassifications(); await this.updateUnclassified(); } else { throw new Error('Failed to classify application'); } } catch (error) { console.error('Failed to classify application:', error); this.showMessage('Failed to classify application', 'error'); } } async deleteClassification(applicationName) { try { const encodedName = encodeURIComponent(applicationName); const response = await fetch(`/api/classifications/${encodedName}`, { method: 'DELETE' }); if (response.ok) { this.showMessage(`Classification for ${applicationName} removed`, 'success'); await this.updateClassifications(); await this.updateUnclassified(); } else { throw new Error('Failed to delete classification'); } } catch (error) { console.error('Failed to delete classification:', error); this.showMessage('Failed to delete classification', 'error'); } } getCategoryIcon(categoryId) { const icons = { 'coding': '💻', 'focused_browsing': '🔍', 'collaboration': '🤝', 'meetings': '📅', 'misc': '📝', 'uncategorized': '❓', // Legacy category support 'browsing': '🌐', 'communication': '💬', 'meeting': '🤝', 'terminal': '⌨️', 'security': '🔐', 'other': '📝' }; return icons[categoryId] || '📊'; } formatCategoryName(categoryId) { const names = { 'coding': 'Coding', 'focused_browsing': 'Focused Browsing', 'collaboration': 'Collaboration', 'meetings': 'Meetings', 'misc': 'Miscellaneous', 'uncategorized': 'Uncategorized', // Legacy category support 'browsing': 'Web Browsing', 'communication': 'Communication', 'meeting': 'Meetings', 'terminal': 'Terminal/CLI', 'security': 'Security Tools', 'other': 'Other' }; return names[categoryId] || categoryId.charAt(0).toUpperCase() + categoryId.slice(1); } destroy() { this.stopAutoRefresh(); if (this.chart) { this.chart.destroy(); } } } // Initialize dashboard when page loads document.addEventListener('DOMContentLoaded', () => { window.dashboard = new ProductivityDashboard(); }); // Cleanup on page unload window.addEventListener('beforeunload', () => { if (window.dashboard) { window.dashboard.destroy(); } });