Ryan Malloy 50c80596d0 Add comprehensive Docker deployment and file upload functionality
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>
2025-08-11 08:02:09 -06:00

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 %}