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>
This commit is contained in:
Ryan Malloy 2025-08-11 08:02:09 -06:00
parent bec1606c86
commit 50c80596d0
36 changed files with 4334 additions and 172 deletions

View file

@ -29,28 +29,79 @@
This will create projects and estimate sessions based on your past usage.
</p>
<!-- Import Form -->
<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>
<!-- 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>
<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>
<!-- 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>
</form>
</div>
<!-- Results -->
<div id="import-results" class="mt-4" style="display: none;">
@ -122,6 +173,142 @@
{% 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');
@ -204,14 +391,14 @@ async function runImport() {
}
}
function showPreviewResults(data) {
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>${data.file_path}</code></p>
<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">
@ -241,7 +428,7 @@ function showPreviewResults(data) {
</div>
<div class="text-center">
<button class="btn btn-success" onclick="runImport()">
<button class="btn btn-success" onclick="${isUpload ? 'runUpload()' : 'runImport()'}">
<i class="fas fa-check me-1"></i>
Looks good - Import this data
</button>
@ -251,13 +438,18 @@ function showPreviewResults(data) {
document.getElementById('results-content').innerHTML = html;
}
function showImportResults(data) {
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">
<h6><i class="fas fa-check me-1"></i> Import Completed Successfully!</h6>
${successMessage}
<hr>
<div class="row">
<div class="col-md-4 text-center">