Initial commit: Claude Code Project Tracker
Add comprehensive development intelligence system that tracks: - Development sessions with automatic start/stop - Full conversation history with semantic search - Tool usage and file operation analytics - Think time and engagement analysis - Git activity correlation - Learning pattern recognition - Productivity insights and metrics Features: - FastAPI backend with SQLite database - Modern web dashboard with interactive charts - Claude Code hook integration for automatic tracking - Comprehensive test suite with 100+ tests - Complete API documentation (OpenAPI/Swagger) - Privacy-first design with local data storage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
44ed9936b7
48 changed files with 9707 additions and 0 deletions
390
app/dashboard/templates/analytics.html
Normal file
390
app/dashboard/templates/analytics.html
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Analytics - Claude Code Project Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>
|
||||
<i class="fas fa-chart-line me-2"></i>
|
||||
Development Analytics
|
||||
</h1>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary active" onclick="setTimePeriod(30)">
|
||||
30 Days
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="setTimePeriod(7)">
|
||||
7 Days
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="setTimePeriod(90)">
|
||||
90 Days
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Productivity Metrics -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-brain me-2"></i>
|
||||
Engagement Analysis
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<div class="display-6 text-primary mb-2" id="engagement-metric">-</div>
|
||||
<p class="text-muted">Engagement Score</p>
|
||||
<canvas id="engagementChart" height="150"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-clock me-2"></i>
|
||||
Time Analysis
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Average Session</span>
|
||||
<strong id="avg-session">-</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Think Time</span>
|
||||
<strong id="think-time-metric">-</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Files per Session</span>
|
||||
<strong id="files-per-session">-</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-graduation-cap me-2"></i>
|
||||
Learning Insights
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body" id="learning-insights">
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Development Patterns -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-chart-area me-2"></i>
|
||||
Development Patterns
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="patternsChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-user-clock me-2"></i>
|
||||
Working Hours
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="hoursChart" height="150"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tool Usage & Git Activity -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-tools me-2"></i>
|
||||
Tool Usage Statistics
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="tool-stats">
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fab fa-git-alt me-2"></i>
|
||||
Git Activity
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="git-stats">
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentTimePeriod = 30;
|
||||
let engagementChart = null;
|
||||
let patternsChart = null;
|
||||
let hoursChart = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadAnalyticsData();
|
||||
});
|
||||
|
||||
function setTimePeriod(days) {
|
||||
currentTimePeriod = days;
|
||||
|
||||
// Update active button
|
||||
document.querySelectorAll('.btn-group .btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
loadAnalyticsData();
|
||||
}
|
||||
|
||||
async function loadAnalyticsData() {
|
||||
try {
|
||||
const [
|
||||
productivityMetrics,
|
||||
developmentPatterns,
|
||||
learningInsights,
|
||||
toolUsageStats,
|
||||
gitStats
|
||||
] = await Promise.all([
|
||||
apiClient.getProductivityMetrics(null, currentTimePeriod),
|
||||
apiClient.getDevelopmentPatterns(null, currentTimePeriod),
|
||||
apiClient.getLearningInsights(null, currentTimePeriod),
|
||||
apiClient.getToolUsageStats(null, null, currentTimePeriod),
|
||||
apiClient.getGitActivityStats(null, currentTimePeriod)
|
||||
]);
|
||||
|
||||
updateProductivityMetrics(productivityMetrics);
|
||||
updateDevelopmentPatterns(developmentPatterns);
|
||||
updateLearningInsights(learningInsights);
|
||||
updateToolStats(toolUsageStats);
|
||||
updateGitStats(gitStats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load analytics data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateProductivityMetrics(metrics) {
|
||||
document.getElementById('engagement-metric').textContent =
|
||||
metrics.engagement_score?.toFixed(0) || '-';
|
||||
|
||||
document.getElementById('avg-session').textContent =
|
||||
ApiUtils.formatDuration(metrics.average_session_length || 0);
|
||||
|
||||
document.getElementById('think-time-metric').textContent =
|
||||
`${(metrics.think_time_average || 0).toFixed(1)}s`;
|
||||
|
||||
document.getElementById('files-per-session').textContent =
|
||||
(metrics.files_per_session || 0).toFixed(1);
|
||||
}
|
||||
|
||||
function updateDevelopmentPatterns(patterns) {
|
||||
if (patterns.working_hours) {
|
||||
updateWorkingHoursChart(patterns.working_hours);
|
||||
}
|
||||
|
||||
if (patterns.productivity_trends) {
|
||||
updatePatternsChart(patterns);
|
||||
}
|
||||
}
|
||||
|
||||
function updateWorkingHoursChart(workingHours) {
|
||||
const ctx = document.getElementById('hoursChart').getContext('2d');
|
||||
|
||||
if (hoursChart) {
|
||||
hoursChart.destroy();
|
||||
}
|
||||
|
||||
const hours = Array.from({length: 24}, (_, i) => i);
|
||||
const data = hours.map(hour => workingHours.distribution[hour] || 0);
|
||||
|
||||
hoursChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: hours.map(h => `${h}:00`),
|
||||
datasets: [{
|
||||
data: data,
|
||||
backgroundColor: '#007bff',
|
||||
borderRadius: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { maxTicksLimit: 8 }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateLearningInsights(insights) {
|
||||
const container = document.getElementById('learning-insights');
|
||||
|
||||
if (insights.message) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center text-muted">
|
||||
<i class="fas fa-info-circle mb-2"></i>
|
||||
<p class="small">${insights.message}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const topTopics = Object.entries(insights.learning_topics?.frequency || {})
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.slice(0, 3);
|
||||
|
||||
const insightsHtml = `
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">Most Discussed Topics</small>
|
||||
${topTopics.map(([topic, count]) => `
|
||||
<div class="d-flex justify-content-between align-items-center mt-1">
|
||||
<span class="small">${topic}</span>
|
||||
<span class="badge bg-primary">${count}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="fw-bold text-success">${insights.insights?.diverse_learning || 0}</div>
|
||||
<small class="text-muted">Topics Explored</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = insightsHtml;
|
||||
}
|
||||
|
||||
function updateToolStats(toolStats) {
|
||||
const container = document.getElementById('tool-stats');
|
||||
|
||||
if (!toolStats.length) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-tools mb-2"></i>
|
||||
<p class="small">No tool usage data</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const topTools = toolStats.slice(0, 5);
|
||||
const maxUsage = Math.max(...topTools.map(t => t.usage_count));
|
||||
|
||||
const toolsHtml = topTools.map(tool => {
|
||||
const percentage = (tool.usage_count / maxUsage) * 100;
|
||||
return `
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="small">
|
||||
<i class="${ApiUtils.getToolIcon(tool.tool_name)} me-1"></i>
|
||||
${tool.tool_name}
|
||||
</span>
|
||||
<span class="badge bg-${ApiUtils.getToolColor(tool.tool_name)}">${tool.usage_count}</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar bg-${ApiUtils.getToolColor(tool.tool_name)}"
|
||||
style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = toolsHtml;
|
||||
}
|
||||
|
||||
function updateGitStats(gitStats) {
|
||||
const container = document.getElementById('git-stats');
|
||||
|
||||
if (!gitStats.total_operations) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fab fa-git-alt mb-2"></i>
|
||||
<p class="small">No git activity data</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const statsHtml = `
|
||||
<div class="row g-3 text-center">
|
||||
<div class="col-6">
|
||||
<div class="fw-bold text-primary">${gitStats.total_operations}</div>
|
||||
<small class="text-muted">Operations</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="fw-bold text-success">${gitStats.success_rate || 0}%</div>
|
||||
<small class="text-muted">Success Rate</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Operation Types</small>
|
||||
</div>
|
||||
${Object.entries(gitStats.operations_by_type || {}).map(([type, count]) => `
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="small">${type}</span>
|
||||
<span class="badge bg-secondary">${count}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
|
||||
container.innerHTML = statsHtml;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue