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 `
`;
}).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 `
`;
}).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 `
Select category...
💻 Coding
🔍 Focused Browsing
🤝 Collaboration
📅 Meetings
📝 Miscellaneous
❓ Uncategorized
Classify
`;
}).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();
}
});