Backend: - Add web server module with Axum (localhost:2759 by default) - Create REST API endpoints (/api/stats, /api/dashboard, /api/health) - Add AI module with NPU support via ONNX Runtime + DirectML - Support Intel AI Boost NPU on Intel Core Ultra processors - Add 'serve' command to CLI for dashboard server Frontend: - Modern dashboard with Tailwind CSS and Chart.js - Real-time activity statistics and visualizations - Category distribution pie chart - Daily activity trend line chart - Recent activities table with filtering AI/ML: - NPU device detection and DirectML configuration - ONNX Runtime integration for model inference - Fallback to rule-based classification when no model loaded - Support for future AI model integration Dependencies: - axum 0.7 (web framework) - tower + tower-http (middleware and static files) - ort 2.0.0-rc.10 (ONNX Runtime with DirectML) - ndarray 0.16 + tokenizers 0.20 (ML utilities) All tests passing (27/27) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
268 lines
8.2 KiB
JavaScript
268 lines
8.2 KiB
JavaScript
// Activity Tracker Dashboard - Main JavaScript
|
|
|
|
let categoryChart = null;
|
|
let timeChart = null;
|
|
|
|
// Category colors
|
|
const CATEGORY_COLORS = {
|
|
'Development': '#3b82f6',
|
|
'Meeting': '#8b5cf6',
|
|
'Research': '#10b981',
|
|
'Design': '#f59e0b',
|
|
'Other': '#6b7280'
|
|
};
|
|
|
|
// Format time in minutes to human readable
|
|
function formatTime(minutes) {
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
if (hours > 0) {
|
|
return `${hours}h ${mins}m`;
|
|
}
|
|
return `${mins}m`;
|
|
}
|
|
|
|
// Format date
|
|
function formatDate(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('fr-FR', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
// Format datetime
|
|
function formatDateTime(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('fr-FR', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// Update stats cards
|
|
function updateStatsCards(stats) {
|
|
document.getElementById('total-captures').textContent = stats.total_captures.toLocaleString();
|
|
document.getElementById('storage-size').textContent = stats.total_size_mb.toFixed(2) + ' MB';
|
|
document.getElementById('oldest-capture').textContent = formatDateTime(stats.oldest_capture);
|
|
document.getElementById('newest-capture').textContent = formatDateTime(stats.newest_capture);
|
|
}
|
|
|
|
// Create category pie chart
|
|
function createCategoryChart(categories) {
|
|
const ctx = document.getElementById('category-chart').getContext('2d');
|
|
|
|
if (categoryChart) {
|
|
categoryChart.destroy();
|
|
}
|
|
|
|
const labels = Object.keys(categories);
|
|
const data = Object.values(categories);
|
|
const colors = labels.map(label => CATEGORY_COLORS[label] || '#6b7280');
|
|
|
|
categoryChart = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
data: data,
|
|
backgroundColor: colors,
|
|
borderColor: '#1f2937',
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
color: '#9ca3af',
|
|
padding: 15,
|
|
font: {
|
|
size: 12
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const label = context.label || '';
|
|
const value = context.parsed || 0;
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = ((value / total) * 100).toFixed(1);
|
|
return `${label}: ${value} captures (${percentage}%)`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Create time trend chart
|
|
function createTimeChart(activities) {
|
|
const ctx = document.getElementById('time-chart').getContext('2d');
|
|
|
|
if (timeChart) {
|
|
timeChart.destroy();
|
|
}
|
|
|
|
// Sort activities by date
|
|
activities.sort((a, b) => a.date.localeCompare(b.date));
|
|
|
|
const labels = activities.map(a => formatDate(a.date));
|
|
const datasets = [];
|
|
|
|
// Get all unique categories
|
|
const allCategories = new Set();
|
|
activities.forEach(activity => {
|
|
Object.keys(activity.categories).forEach(cat => allCategories.add(cat));
|
|
});
|
|
|
|
// Create dataset for each category
|
|
allCategories.forEach(category => {
|
|
const data = activities.map(activity => activity.categories[category] || 0);
|
|
datasets.push({
|
|
label: category,
|
|
data: data,
|
|
backgroundColor: CATEGORY_COLORS[category] || '#6b7280',
|
|
borderColor: CATEGORY_COLORS[category] || '#6b7280',
|
|
borderWidth: 2,
|
|
tension: 0.4
|
|
});
|
|
});
|
|
|
|
timeChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: datasets
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
color: '#9ca3af',
|
|
callback: function(value) {
|
|
return formatTime(value);
|
|
}
|
|
},
|
|
grid: {
|
|
color: '#374151'
|
|
}
|
|
},
|
|
x: {
|
|
ticks: {
|
|
color: '#9ca3af'
|
|
},
|
|
grid: {
|
|
color: '#374151'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
color: '#9ca3af',
|
|
padding: 15,
|
|
font: {
|
|
size: 12
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const label = context.dataset.label || '';
|
|
const value = context.parsed.y || 0;
|
|
return `${label}: ${formatTime(value)}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update recent activities table
|
|
function updateActivitiesTable(activities) {
|
|
const tbody = document.getElementById('activities-tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
if (activities.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="px-6 py-4 text-center text-gray-400">No activities found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
// Sort by date descending
|
|
activities.sort((a, b) => b.date.localeCompare(a.date));
|
|
|
|
activities.forEach(activity => {
|
|
const row = document.createElement('tr');
|
|
row.className = 'hover:bg-gray-700';
|
|
|
|
const categories = activity.categories;
|
|
row.innerHTML = `
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">${formatDate(activity.date)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold">${formatTime(activity.total_time_minutes)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">${formatTime(categories.Development || 0)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">${formatTime(categories.Meeting || 0)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">${formatTime(categories.Research || 0)}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">${formatTime(categories.Other || 0)}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Load dashboard data
|
|
async function loadDashboard() {
|
|
try {
|
|
const response = await fetch('/api/dashboard?days=7');
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Update stats
|
|
updateStatsCards(data.stats);
|
|
|
|
// Update charts
|
|
createCategoryChart(data.stats.captures_by_category);
|
|
createTimeChart(data.recent_activities);
|
|
|
|
// Update table
|
|
updateActivitiesTable(data.recent_activities);
|
|
|
|
// Update last update time
|
|
const now = new Date();
|
|
document.getElementById('last-update').textContent =
|
|
`Last update: ${now.toLocaleTimeString('fr-FR')}`;
|
|
|
|
} catch (error) {
|
|
console.error('Error loading dashboard:', error);
|
|
document.getElementById('activities-tbody').innerHTML =
|
|
`<tr><td colspan="6" class="px-6 py-4 text-center text-red-400">Error loading data: ${error.message}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
// Initialize dashboard
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadDashboard();
|
|
|
|
// Auto-refresh every 5 minutes
|
|
setInterval(loadDashboard, 5 * 60 * 1000);
|
|
});
|