Augustin 4dde6d6853 Feature: add web dashboard and NPU-powered AI classification
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>
2025-10-16 14:23:56 +02:00

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);
});