Features Added: • Docker containerization with multi-stage Python 3.12 build • Caddy reverse proxy integration with automatic SSL • File upload interface for .claude.json imports with preview • Comprehensive hook system with 39+ hook types across 9 categories • Complete documentation system with Docker and import guides Technical Improvements: • Enhanced database models with hook tracking capabilities • Robust file validation and error handling for uploads • Production-ready Docker compose configuration • Health checks and resource limits for containers • Database initialization scripts for containerized deployments Documentation: • Docker Deployment Guide with troubleshooting • Data Import Guide with step-by-step instructions • Updated Getting Started guide with new features • Enhanced documentation index with responsive grid layout 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
505 lines
19 KiB
HTML
505 lines
19 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Import Data - 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-download me-2"></i>
|
|
Import Claude Code Data
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-file-import me-2"></i>
|
|
Import from .claude.json
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="text-muted">
|
|
Import your historical Claude Code usage data from the <code>~/.claude.json</code> file.
|
|
This will create projects and estimate sessions based on your past usage.
|
|
</p>
|
|
|
|
<!-- Import Method Tabs -->
|
|
<ul class="nav nav-pills mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="upload-tab" data-bs-toggle="pill"
|
|
data-bs-target="#upload-panel" type="button" role="tab">
|
|
<i class="fas fa-cloud-upload-alt me-1"></i>
|
|
Upload File
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="path-tab" data-bs-toggle="pill"
|
|
data-bs-target="#path-panel" type="button" role="tab">
|
|
<i class="fas fa-folder-open me-1"></i>
|
|
File Path
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content">
|
|
<!-- Upload File Panel -->
|
|
<div class="tab-pane fade show active" id="upload-panel" role="tabpanel">
|
|
<div class="mb-3">
|
|
<label for="file-upload" class="form-label">Select .claude.json file</label>
|
|
<input type="file" class="form-control" id="file-upload"
|
|
accept=".json" onchange="handleFileSelect(event)">
|
|
<div class="form-text">
|
|
Upload your <code>.claude.json</code> file directly from your computer
|
|
</div>
|
|
</div>
|
|
|
|
<div id="file-info" class="alert alert-info" style="display: none;">
|
|
<div id="file-details"></div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-outline-primary" onclick="previewUpload()"
|
|
id="preview-upload-btn" disabled>
|
|
<i class="fas fa-eye me-1"></i>
|
|
Preview Upload
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="runUpload()"
|
|
id="import-upload-btn" disabled>
|
|
<i class="fas fa-cloud-upload-alt me-1"></i>
|
|
Import Upload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Path Panel -->
|
|
<div class="tab-pane fade" id="path-panel" role="tabpanel">
|
|
<form id="import-form">
|
|
<div class="mb-3">
|
|
<label for="file-path" class="form-label">File Path (optional)</label>
|
|
<input type="text" class="form-control" id="file-path"
|
|
placeholder="Leave empty to use ~/.claude.json">
|
|
<div class="form-text">
|
|
If left empty, will try to import from the default location: <code>~/.claude.json</code>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-outline-primary" onclick="previewImport()">
|
|
<i class="fas fa-eye me-1"></i>
|
|
Preview Import
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="runImport()">
|
|
<i class="fas fa-download me-1"></i>
|
|
Import Data
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results -->
|
|
<div id="import-results" class="mt-4" style="display: none;">
|
|
<hr>
|
|
<div id="results-content"></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-info-circle me-2"></i>
|
|
What gets imported?
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul class="list-unstyled">
|
|
<li class="mb-2">
|
|
<i class="fas fa-folder text-primary me-2"></i>
|
|
<strong>Projects</strong><br>
|
|
<small class="text-muted">All project directories you've used with Claude Code</small>
|
|
</li>
|
|
<li class="mb-2">
|
|
<i class="fas fa-chart-line text-success me-2"></i>
|
|
<strong>Usage Statistics</strong><br>
|
|
<small class="text-muted">Total startups and estimated session distribution</small>
|
|
</li>
|
|
<li class="mb-2">
|
|
<i class="fas fa-comments text-info me-2"></i>
|
|
<strong>History Entries</strong><br>
|
|
<small class="text-muted">Brief conversation topics from your history</small>
|
|
</li>
|
|
<li class="mb-2">
|
|
<i class="fas fa-code text-warning me-2"></i>
|
|
<strong>Language Detection</strong><br>
|
|
<small class="text-muted">Automatically detect programming languages used</small>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-shield-alt me-1"></i>
|
|
<strong>Privacy:</strong> All data stays local. No external services are used.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
Important Notes
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul class="small text-muted">
|
|
<li>Import creates estimated data based on available information</li>
|
|
<li>Timestamps are estimated based on usage patterns</li>
|
|
<li>This is safe to run multiple times (won't create duplicates)</li>
|
|
<li>Large files may take a few moments to process</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let selectedFile = null;
|
|
|
|
function handleFileSelect(event) {
|
|
const file = event.target.files[0];
|
|
selectedFile = file;
|
|
|
|
if (file) {
|
|
// Validate file type
|
|
if (!file.name.endsWith('.json')) {
|
|
showError('Please select a JSON file (.json)');
|
|
resetFileUpload();
|
|
return;
|
|
}
|
|
|
|
// Check file size (10MB limit)
|
|
const maxSize = 10 * 1024 * 1024; // 10MB
|
|
if (file.size > maxSize) {
|
|
showError('File too large. Maximum size is 10MB.');
|
|
resetFileUpload();
|
|
return;
|
|
}
|
|
|
|
// Show file info
|
|
const fileInfo = document.getElementById('file-info');
|
|
const fileDetails = document.getElementById('file-details');
|
|
|
|
fileDetails.innerHTML = `
|
|
<strong>Selected file:</strong> ${file.name}<br>
|
|
<strong>Size:</strong> ${(file.size / 1024).toFixed(1)} KB<br>
|
|
<strong>Modified:</strong> ${new Date(file.lastModified).toLocaleString()}
|
|
`;
|
|
|
|
fileInfo.style.display = 'block';
|
|
|
|
// Enable buttons
|
|
document.getElementById('preview-upload-btn').disabled = false;
|
|
document.getElementById('import-upload-btn').disabled = false;
|
|
} else {
|
|
resetFileUpload();
|
|
}
|
|
}
|
|
|
|
function resetFileUpload() {
|
|
selectedFile = null;
|
|
document.getElementById('file-info').style.display = 'none';
|
|
document.getElementById('preview-upload-btn').disabled = true;
|
|
document.getElementById('import-upload-btn').disabled = true;
|
|
}
|
|
|
|
async function previewUpload() {
|
|
if (!selectedFile) {
|
|
showError('Please select a file first');
|
|
return;
|
|
}
|
|
|
|
const resultsDiv = document.getElementById('import-results');
|
|
const resultsContent = document.getElementById('results-content');
|
|
|
|
// Show loading
|
|
resultsContent.innerHTML = `
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Previewing...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Analyzing uploaded file...</p>
|
|
</div>
|
|
`;
|
|
resultsDiv.style.display = 'block';
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', selectedFile);
|
|
|
|
const response = await fetch('/api/import/claude-json/preview-upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
showPreviewResults(data, true); // Pass true to indicate this is an upload
|
|
} else {
|
|
showError(data.detail || 'Preview failed');
|
|
}
|
|
} catch (error) {
|
|
showError('Network error: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function runUpload() {
|
|
if (!selectedFile) {
|
|
showError('Please select a file first');
|
|
return;
|
|
}
|
|
|
|
// Confirm import
|
|
if (!confirm('This will import data into your tracker database. Continue?')) {
|
|
return;
|
|
}
|
|
|
|
const resultsDiv = document.getElementById('import-results');
|
|
const resultsContent = document.getElementById('results-content');
|
|
|
|
// Show loading
|
|
resultsContent.innerHTML = `
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border text-success" role="status">
|
|
<span class="visually-hidden">Importing...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Importing uploaded file...</p>
|
|
</div>
|
|
`;
|
|
resultsDiv.style.display = 'block';
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', selectedFile);
|
|
|
|
const response = await fetch('/api/import/claude-json/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
showImportResults(data, true); // Pass true to indicate this is an upload
|
|
} else {
|
|
showError(data.detail || 'Import failed');
|
|
}
|
|
} catch (error) {
|
|
showError('Network error: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function previewImport() {
|
|
const filePath = document.getElementById('file-path').value.trim();
|
|
const resultsDiv = document.getElementById('import-results');
|
|
const resultsContent = document.getElementById('results-content');
|
|
|
|
// Show loading
|
|
resultsContent.innerHTML = `
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Previewing...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Analyzing .claude.json file...</p>
|
|
</div>
|
|
`;
|
|
resultsDiv.style.display = 'block';
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (filePath) {
|
|
params.append('file_path', filePath);
|
|
}
|
|
|
|
const response = await fetch(`/api/import/claude-json/preview?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
showPreviewResults(data);
|
|
} else {
|
|
showError(data.detail || 'Preview failed');
|
|
}
|
|
} catch (error) {
|
|
showError('Network error: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function runImport() {
|
|
const filePath = document.getElementById('file-path').value.trim();
|
|
const resultsDiv = document.getElementById('import-results');
|
|
const resultsContent = document.getElementById('results-content');
|
|
|
|
// Confirm import
|
|
if (!confirm('This will import data into your tracker database. Continue?')) {
|
|
return;
|
|
}
|
|
|
|
// Show loading
|
|
resultsContent.innerHTML = `
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border text-success" role="status">
|
|
<span class="visually-hidden">Importing...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Importing Claude Code data...</p>
|
|
</div>
|
|
`;
|
|
resultsDiv.style.display = 'block';
|
|
|
|
try {
|
|
const requestBody = {};
|
|
if (filePath) {
|
|
requestBody.file_path = filePath;
|
|
}
|
|
|
|
const response = await fetch('/api/import/claude-json', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
showImportResults(data);
|
|
} else {
|
|
showError(data.detail || 'Import failed');
|
|
}
|
|
} catch (error) {
|
|
showError('Network error: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function showPreviewResults(data, isUpload = false) {
|
|
const html = `
|
|
<div class="alert alert-info">
|
|
<h6><i class="fas fa-eye me-1"></i> Import Preview</h6>
|
|
<hr>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<p><strong>File:</strong> <code>${isUpload ? data.file_name : data.file_path}</code></p>
|
|
<p><strong>Size:</strong> ${data.file_size_mb} MB</p>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<p><strong>Projects:</strong> ${data.projects.total_count}</p>
|
|
<p><strong>History entries:</strong> ${data.history_entries}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<strong>Claude Usage Statistics:</strong>
|
|
<ul class="mt-2">
|
|
<li>Total startups: ${data.claude_usage.num_startups}</li>
|
|
<li>First start: ${data.claude_usage.first_start_time || 'N/A'}</li>
|
|
<li>Prompt queue uses: ${data.claude_usage.prompt_queue_use_count}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
${data.projects.paths.length > 0 ? `
|
|
<div class="mt-3">
|
|
<strong>Sample projects:</strong>
|
|
<ul class="mt-2">
|
|
${data.projects.paths.map(path => `<li><code>${path}</code></li>`).join('')}
|
|
${data.projects.has_more ? '<li><em>... and more</em></li>' : ''}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div class="text-center">
|
|
<button class="btn btn-success" onclick="${isUpload ? 'runUpload()' : 'runImport()'}">
|
|
<i class="fas fa-check me-1"></i>
|
|
Looks good - Import this data
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('results-content').innerHTML = html;
|
|
}
|
|
|
|
function showImportResults(data, isUpload = false) {
|
|
const results = data.results;
|
|
const hasErrors = results.errors && results.errors.length > 0;
|
|
|
|
let successMessage = '<h6><i class="fas fa-check me-1"></i> Import Completed Successfully!</h6>';
|
|
if (isUpload) {
|
|
successMessage += `<p class="mb-0"><strong>File:</strong> ${data.file_name} (${data.file_size_kb} KB)</p>`;
|
|
}
|
|
|
|
const html = `
|
|
<div class="alert alert-success">
|
|
${successMessage}
|
|
<hr>
|
|
<div class="row">
|
|
<div class="col-md-4 text-center">
|
|
<div class="display-6 text-primary">${results.projects_imported}</div>
|
|
<small>Projects</small>
|
|
</div>
|
|
<div class="col-md-4 text-center">
|
|
<div class="display-6 text-success">${results.sessions_estimated}</div>
|
|
<small>Sessions</small>
|
|
</div>
|
|
<div class="col-md-4 text-center">
|
|
<div class="display-6 text-info">${results.conversations_imported}</div>
|
|
<small>Conversations</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${hasErrors ? `
|
|
<div class="alert alert-warning">
|
|
<h6><i class="fas fa-exclamation-triangle me-1"></i> Warnings</h6>
|
|
<ul class="mb-0">
|
|
${results.errors.map(error => `<li>${error}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="text-center">
|
|
<a href="/dashboard" class="btn btn-primary">
|
|
<i class="fas fa-tachometer-alt me-1"></i>
|
|
View Dashboard
|
|
</a>
|
|
<a href="/dashboard/projects" class="btn btn-outline-primary ms-2">
|
|
<i class="fas fa-folder me-1"></i>
|
|
View Projects
|
|
</a>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('results-content').innerHTML = html;
|
|
}
|
|
|
|
function showError(message) {
|
|
const html = `
|
|
<div class="alert alert-danger">
|
|
<h6><i class="fas fa-exclamation-circle me-1"></i> Error</h6>
|
|
<p class="mb-0">${message}</p>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('results-content').innerHTML = html;
|
|
}
|
|
</script>
|
|
{% endblock %} |