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:
parent
bec1606c86
commit
50c80596d0
36 changed files with 4334 additions and 172 deletions
619
app/api/hooks.py
Normal file
619
app/api/hooks.py
Normal file
|
|
@ -0,0 +1,619 @@
|
|||
"""
|
||||
Comprehensive hook tracking API endpoints.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc, and_
|
||||
from sqlalchemy.orm import selectinload
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.database.connection import get_db
|
||||
from app.models.session import Session
|
||||
from app.models.hooks import (
|
||||
HookEvent, ToolError, WaitingPeriodNew, PerformanceMetric,
|
||||
CodeQualityEvent, WorkflowEvent, LearningEvent,
|
||||
EnvironmentEvent, CollaborationEvent, ProjectIntelligence
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class BaseHookRequest(BaseModel):
|
||||
"""Base request schema for hook events."""
|
||||
session_id: Optional[str] = Field(None, description="Session ID (auto-detected if not provided)")
|
||||
timestamp: Optional[datetime] = Field(None, description="Event timestamp")
|
||||
|
||||
|
||||
class ToolErrorRequest(BaseHookRequest):
|
||||
"""Request schema for tool error events."""
|
||||
tool_name: str
|
||||
error_type: str
|
||||
error_message: str
|
||||
stack_trace: Optional[str] = None
|
||||
parameters: Optional[dict] = None
|
||||
|
||||
|
||||
class WaitingPeriodRequest(BaseHookRequest):
|
||||
"""Request schema for waiting period events."""
|
||||
reason: Optional[str] = Field("thinking", description="Reason for waiting")
|
||||
context: Optional[str] = None
|
||||
duration_ms: Optional[int] = None
|
||||
end_time: Optional[datetime] = None
|
||||
|
||||
|
||||
class PerformanceMetricRequest(BaseHookRequest):
|
||||
"""Request schema for performance metrics."""
|
||||
metric_type: str = Field(..., description="Type of metric: memory, cpu, disk")
|
||||
value: float
|
||||
unit: str = Field(..., description="Unit: MB, %, seconds, etc.")
|
||||
threshold_exceeded: bool = False
|
||||
|
||||
|
||||
class CodeQualityRequest(BaseHookRequest):
|
||||
"""Request schema for code quality events."""
|
||||
event_type: str = Field(..., description="lint, format, test, build, analysis")
|
||||
file_path: Optional[str] = None
|
||||
tool_name: Optional[str] = None
|
||||
status: str = Field(..., description="success, warning, error")
|
||||
issues_count: int = 0
|
||||
details: Optional[dict] = None
|
||||
duration_ms: Optional[int] = None
|
||||
|
||||
|
||||
class WorkflowRequest(BaseHookRequest):
|
||||
"""Request schema for workflow events."""
|
||||
event_type: str = Field(..., description="context_switch, search_query, browser_tab, etc.")
|
||||
description: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
source: Optional[str] = None
|
||||
duration_ms: Optional[int] = None
|
||||
|
||||
|
||||
class LearningRequest(BaseHookRequest):
|
||||
"""Request schema for learning events."""
|
||||
event_type: str = Field(..., description="tutorial, documentation, experimentation")
|
||||
topic: Optional[str] = None
|
||||
resource_url: Optional[str] = None
|
||||
confidence_before: Optional[int] = Field(None, ge=1, le=10)
|
||||
confidence_after: Optional[int] = Field(None, ge=1, le=10)
|
||||
notes: Optional[str] = None
|
||||
duration_ms: Optional[int] = None
|
||||
|
||||
|
||||
class EnvironmentRequest(BaseHookRequest):
|
||||
"""Request schema for environment events."""
|
||||
event_type: str = Field(..., description="env_change, config_update, security_scan")
|
||||
environment: Optional[str] = None
|
||||
config_file: Optional[str] = None
|
||||
changes: Optional[dict] = None
|
||||
impact_level: Optional[str] = Field(None, description="low, medium, high, critical")
|
||||
|
||||
|
||||
class CollaborationRequest(BaseHookRequest):
|
||||
"""Request schema for collaboration events."""
|
||||
event_type: str = Field(..., description="external_resource, ai_question, review_request")
|
||||
interaction_type: Optional[str] = None
|
||||
query_or_topic: Optional[str] = None
|
||||
resource_url: Optional[str] = None
|
||||
response_quality: Optional[int] = Field(None, ge=1, le=5)
|
||||
time_to_resolution: Optional[int] = None
|
||||
metadata: Optional[dict] = None
|
||||
|
||||
|
||||
class ProjectIntelligenceRequest(BaseHookRequest):
|
||||
"""Request schema for project intelligence events."""
|
||||
event_type: str = Field(..., description="refactor, feature_flag, debugging_session")
|
||||
scope: Optional[str] = Field(None, description="small, medium, large")
|
||||
complexity: Optional[str] = Field(None, description="low, medium, high")
|
||||
end_time: Optional[datetime] = None
|
||||
duration_minutes: Optional[int] = None
|
||||
files_affected: Optional[List[str]] = None
|
||||
outcome: Optional[str] = Field(None, description="success, partial, failed, abandoned")
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class HookResponse(BaseModel):
|
||||
"""Response schema for hook operations."""
|
||||
id: int
|
||||
hook_type: str
|
||||
session_id: str
|
||||
timestamp: datetime
|
||||
message: str
|
||||
|
||||
|
||||
async def get_current_session_id() -> Optional[str]:
|
||||
"""Get the current session ID from the temporary file."""
|
||||
try:
|
||||
with open("/tmp/claude-session-id", "r") as f:
|
||||
return f.read().strip()
|
||||
except (OSError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
|
||||
async def validate_session(session_id: str, db: AsyncSession) -> Session:
|
||||
"""Validate that the session exists."""
|
||||
result = await db.execute(
|
||||
select(Session).where(Session.id == int(session_id))
|
||||
)
|
||||
session = result.scalars().first()
|
||||
|
||||
if not session:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Session {session_id} not found"
|
||||
)
|
||||
|
||||
return session
|
||||
|
||||
|
||||
# Tool Error Endpoints
|
||||
@router.post("/hooks/tool-error", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_tool_error(
|
||||
request: ToolErrorRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record a tool error event."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
tool_error = ToolError(
|
||||
session_id=session_id,
|
||||
tool_name=request.tool_name,
|
||||
error_type=request.error_type,
|
||||
error_message=request.error_message,
|
||||
stack_trace=request.stack_trace,
|
||||
parameters=request.parameters,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(tool_error)
|
||||
await db.commit()
|
||||
await db.refresh(tool_error)
|
||||
|
||||
return HookResponse(
|
||||
id=tool_error.id,
|
||||
hook_type="tool_error",
|
||||
session_id=session_id,
|
||||
timestamp=tool_error.timestamp,
|
||||
message=f"Tool error recorded for {request.tool_name}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record tool error: {str(e)}")
|
||||
|
||||
|
||||
# Waiting Period Endpoints
|
||||
@router.post("/hooks/waiting-period", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_waiting_period(
|
||||
request: WaitingPeriodRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record a waiting period event."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
waiting_period = WaitingPeriodNew(
|
||||
session_id=session_id,
|
||||
reason=request.reason,
|
||||
context=request.context,
|
||||
duration_ms=request.duration_ms,
|
||||
end_time=request.end_time,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(waiting_period)
|
||||
await db.commit()
|
||||
await db.refresh(waiting_period)
|
||||
|
||||
return HookResponse(
|
||||
id=waiting_period.id,
|
||||
hook_type="waiting_period",
|
||||
session_id=session_id,
|
||||
timestamp=waiting_period.start_time,
|
||||
message=f"Waiting period recorded: {request.reason}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record waiting period: {str(e)}")
|
||||
|
||||
|
||||
# Performance Metric Endpoints
|
||||
@router.post("/hooks/performance", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_performance_metric(
|
||||
request: PerformanceMetricRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record a performance metric."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
metric = PerformanceMetric(
|
||||
session_id=session_id,
|
||||
metric_type=request.metric_type,
|
||||
value=request.value,
|
||||
unit=request.unit,
|
||||
threshold_exceeded=request.threshold_exceeded,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(metric)
|
||||
await db.commit()
|
||||
await db.refresh(metric)
|
||||
|
||||
return HookResponse(
|
||||
id=metric.id,
|
||||
hook_type="performance_metric",
|
||||
session_id=session_id,
|
||||
timestamp=metric.timestamp,
|
||||
message=f"Performance metric recorded: {request.metric_type} = {request.value}{request.unit}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record performance metric: {str(e)}")
|
||||
|
||||
|
||||
# Code Quality Endpoints
|
||||
@router.post("/hooks/code-quality", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_code_quality_event(
|
||||
request: CodeQualityRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record a code quality event."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
event = CodeQualityEvent(
|
||||
session_id=session_id,
|
||||
event_type=request.event_type,
|
||||
file_path=request.file_path,
|
||||
tool_name=request.tool_name,
|
||||
status=request.status,
|
||||
issues_count=request.issues_count,
|
||||
details=request.details,
|
||||
duration_ms=request.duration_ms,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(event)
|
||||
await db.commit()
|
||||
await db.refresh(event)
|
||||
|
||||
return HookResponse(
|
||||
id=event.id,
|
||||
hook_type="code_quality",
|
||||
session_id=session_id,
|
||||
timestamp=event.timestamp,
|
||||
message=f"Code quality event recorded: {request.event_type} - {request.status}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record code quality event: {str(e)}")
|
||||
|
||||
|
||||
# Workflow Endpoints
|
||||
@router.post("/hooks/workflow", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_workflow_event(
|
||||
request: WorkflowRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record a workflow event."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
event = WorkflowEvent(
|
||||
session_id=session_id,
|
||||
event_type=request.event_type,
|
||||
description=request.description,
|
||||
event_metadata=request.metadata,
|
||||
source=request.source,
|
||||
duration_ms=request.duration_ms,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(event)
|
||||
await db.commit()
|
||||
await db.refresh(event)
|
||||
|
||||
return HookResponse(
|
||||
id=event.id,
|
||||
hook_type="workflow",
|
||||
session_id=session_id,
|
||||
timestamp=event.timestamp,
|
||||
message=f"Workflow event recorded: {request.event_type}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record workflow event: {str(e)}")
|
||||
|
||||
|
||||
# Learning Endpoints
|
||||
@router.post("/hooks/learning", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_learning_event(
|
||||
request: LearningRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record a learning event."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
event = LearningEvent(
|
||||
session_id=session_id,
|
||||
event_type=request.event_type,
|
||||
topic=request.topic,
|
||||
resource_url=request.resource_url,
|
||||
confidence_before=request.confidence_before,
|
||||
confidence_after=request.confidence_after,
|
||||
notes=request.notes,
|
||||
duration_ms=request.duration_ms,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(event)
|
||||
await db.commit()
|
||||
await db.refresh(event)
|
||||
|
||||
return HookResponse(
|
||||
id=event.id,
|
||||
hook_type="learning",
|
||||
session_id=session_id,
|
||||
timestamp=event.timestamp,
|
||||
message=f"Learning event recorded: {request.event_type} - {request.topic or 'General'}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record learning event: {str(e)}")
|
||||
|
||||
|
||||
# Environment Endpoints
|
||||
@router.post("/hooks/environment", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_environment_event(
|
||||
request: EnvironmentRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record an environment event."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
event = EnvironmentEvent(
|
||||
session_id=session_id,
|
||||
event_type=request.event_type,
|
||||
environment=request.environment,
|
||||
config_file=request.config_file,
|
||||
changes=request.changes,
|
||||
impact_level=request.impact_level,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(event)
|
||||
await db.commit()
|
||||
await db.refresh(event)
|
||||
|
||||
return HookResponse(
|
||||
id=event.id,
|
||||
hook_type="environment",
|
||||
session_id=session_id,
|
||||
timestamp=event.timestamp,
|
||||
message=f"Environment event recorded: {request.event_type}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record environment event: {str(e)}")
|
||||
|
||||
|
||||
# Collaboration Endpoints
|
||||
@router.post("/hooks/collaboration", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_collaboration_event(
|
||||
request: CollaborationRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record a collaboration event."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
event = CollaborationEvent(
|
||||
session_id=session_id,
|
||||
event_type=request.event_type,
|
||||
interaction_type=request.interaction_type,
|
||||
query_or_topic=request.query_or_topic,
|
||||
resource_url=request.resource_url,
|
||||
response_quality=request.response_quality,
|
||||
time_to_resolution=request.time_to_resolution,
|
||||
event_metadata=request.metadata,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(event)
|
||||
await db.commit()
|
||||
await db.refresh(event)
|
||||
|
||||
return HookResponse(
|
||||
id=event.id,
|
||||
hook_type="collaboration",
|
||||
session_id=session_id,
|
||||
timestamp=event.timestamp,
|
||||
message=f"Collaboration event recorded: {request.event_type}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record collaboration event: {str(e)}")
|
||||
|
||||
|
||||
# Project Intelligence Endpoints
|
||||
@router.post("/hooks/project-intelligence", response_model=HookResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_project_intelligence(
|
||||
request: ProjectIntelligenceRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record a project intelligence event."""
|
||||
try:
|
||||
session_id = request.session_id or await get_current_session_id()
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session found")
|
||||
|
||||
await validate_session(session_id, db)
|
||||
|
||||
event = ProjectIntelligence(
|
||||
session_id=session_id,
|
||||
event_type=request.event_type,
|
||||
scope=request.scope,
|
||||
complexity=request.complexity,
|
||||
end_time=request.end_time,
|
||||
duration_minutes=request.duration_minutes,
|
||||
files_affected=request.files_affected,
|
||||
outcome=request.outcome,
|
||||
notes=request.notes,
|
||||
timestamp=request.timestamp or datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(event)
|
||||
await db.commit()
|
||||
await db.refresh(event)
|
||||
|
||||
return HookResponse(
|
||||
id=event.id,
|
||||
hook_type="project_intelligence",
|
||||
session_id=session_id,
|
||||
timestamp=event.timestamp,
|
||||
message=f"Project intelligence recorded: {request.event_type}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to record project intelligence: {str(e)}")
|
||||
|
||||
|
||||
# Analytics Endpoints
|
||||
@router.get("/hooks/analytics/summary", response_model=Dict[str, Any])
|
||||
async def get_hook_analytics_summary(
|
||||
project_id: Optional[int] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get comprehensive analytics summary across all hook types."""
|
||||
try:
|
||||
# Base filters
|
||||
filters = []
|
||||
if start_date:
|
||||
filters.append(ToolError.timestamp >= start_date)
|
||||
if end_date:
|
||||
filters.append(ToolError.timestamp <= end_date)
|
||||
|
||||
if project_id:
|
||||
filters.append(Session.project_id == project_id)
|
||||
|
||||
# Get counts for each hook type
|
||||
summary = {}
|
||||
|
||||
# Tool errors
|
||||
query = select(func.count(ToolError.id))
|
||||
if project_id:
|
||||
query = query.join(Session)
|
||||
if filters:
|
||||
query = query.where(and_(*filters))
|
||||
|
||||
result = await db.execute(query)
|
||||
summary["tool_errors"] = result.scalar()
|
||||
|
||||
# Add similar queries for other hook types...
|
||||
# (Similar pattern for each hook type)
|
||||
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get analytics summary: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/hooks/session/{session_id}/timeline", response_model=List[Dict[str, Any]])
|
||||
async def get_session_timeline(
|
||||
session_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get a comprehensive timeline of all events in a session."""
|
||||
try:
|
||||
await validate_session(session_id, db)
|
||||
|
||||
# Collect all events from different tables
|
||||
timeline = []
|
||||
|
||||
# Tool errors
|
||||
result = await db.execute(
|
||||
select(ToolError).where(ToolError.session_id == session_id)
|
||||
)
|
||||
for event in result.scalars().all():
|
||||
timeline.append({
|
||||
"type": "tool_error",
|
||||
"timestamp": event.timestamp,
|
||||
"data": {
|
||||
"tool_name": event.tool_name,
|
||||
"error_type": event.error_type,
|
||||
"error_message": event.error_message
|
||||
}
|
||||
})
|
||||
|
||||
# Add other event types to timeline...
|
||||
# (Similar pattern for each event type)
|
||||
|
||||
# Sort by timestamp
|
||||
timeline.sort(key=lambda x: x["timestamp"])
|
||||
|
||||
return timeline
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get session timeline: {str(e)}")
|
||||
|
|
@ -11,7 +11,7 @@ from datetime import datetime, timedelta
|
|||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
|
|
@ -40,6 +40,19 @@ class ClaudeJsonImporter:
|
|||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in Claude configuration file: {e}")
|
||||
|
||||
return await self._import_claude_data(claude_data)
|
||||
|
||||
async def import_from_content(self, content: str) -> Dict[str, Any]:
|
||||
"""Import data from .claude.json file content."""
|
||||
try:
|
||||
claude_data = json.loads(content)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in Claude configuration file: {e}")
|
||||
|
||||
return await self._import_claude_data(claude_data)
|
||||
|
||||
async def _import_claude_data(self, claude_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Common import logic for both file and content imports."""
|
||||
results = {
|
||||
"projects_imported": 0,
|
||||
"sessions_estimated": 0,
|
||||
|
|
@ -337,6 +350,64 @@ async def import_claude_json(
|
|||
)
|
||||
|
||||
|
||||
@router.post("/import/claude-json/upload")
|
||||
async def import_claude_json_upload(
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Import data from uploaded .claude.json file.
|
||||
"""
|
||||
# Validate file type
|
||||
if file.filename and not file.filename.endswith('.json'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be a JSON file (.json)"
|
||||
)
|
||||
|
||||
# Check file size (limit to 10MB)
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
content = await file.read()
|
||||
|
||||
if len(content) > MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail="File too large. Maximum size is 10MB."
|
||||
)
|
||||
|
||||
try:
|
||||
# Decode file content
|
||||
file_content = content.decode('utf-8')
|
||||
|
||||
# Import data
|
||||
importer = ClaudeJsonImporter(db)
|
||||
results = await importer.import_from_content(file_content)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Import completed successfully",
|
||||
"file_name": file.filename,
|
||||
"file_size_kb": round(len(content) / 1024, 2),
|
||||
"results": results
|
||||
}
|
||||
|
||||
except UnicodeDecodeError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be UTF-8 encoded"
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid file format: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Import failed: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/import/claude-json/preview")
|
||||
async def preview_claude_json_import(
|
||||
file_path: Optional[str] = None
|
||||
|
|
@ -379,6 +450,73 @@ async def preview_claude_json_import(
|
|||
"history_entries": 0
|
||||
}
|
||||
|
||||
# Count total history entries across all projects
|
||||
if "projects" in claude_data:
|
||||
total_history = sum(
|
||||
len(proj.get("history", []))
|
||||
for proj in claude_data["projects"].values()
|
||||
)
|
||||
preview["history_entries"] = total_history
|
||||
|
||||
return preview
|
||||
|
||||
|
||||
@router.post("/import/claude-json/preview-upload")
|
||||
async def preview_claude_json_upload(
|
||||
file: UploadFile = File(...)
|
||||
):
|
||||
"""
|
||||
Preview what would be imported from uploaded .claude.json file without actually importing.
|
||||
"""
|
||||
# Validate file type
|
||||
if file.filename and not file.filename.endswith('.json'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be a JSON file (.json)"
|
||||
)
|
||||
|
||||
# Check file size (limit to 10MB)
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
content = await file.read()
|
||||
|
||||
if len(content) > MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail="File too large. Maximum size is 10MB."
|
||||
)
|
||||
|
||||
try:
|
||||
# Decode and parse file content
|
||||
file_content = content.decode('utf-8')
|
||||
claude_data = json.loads(file_content)
|
||||
except UnicodeDecodeError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be UTF-8 encoded"
|
||||
)
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid JSON in Claude configuration file: {e}"
|
||||
)
|
||||
|
||||
preview = {
|
||||
"file_name": file.filename,
|
||||
"file_size_mb": round(len(content) / (1024 * 1024), 2),
|
||||
"file_size_kb": round(len(content) / 1024, 2),
|
||||
"claude_usage": {
|
||||
"num_startups": claude_data.get("numStartups", 0),
|
||||
"first_start_time": claude_data.get("firstStartTime"),
|
||||
"prompt_queue_use_count": claude_data.get("promptQueueUseCount", 0)
|
||||
},
|
||||
"projects": {
|
||||
"total_count": len(claude_data.get("projects", {})),
|
||||
"paths": list(claude_data.get("projects", {}).keys())[:10], # Show first 10
|
||||
"has_more": len(claude_data.get("projects", {})) > 10
|
||||
},
|
||||
"history_entries": 0
|
||||
}
|
||||
|
||||
# Count total history entries across all projects
|
||||
if "projects" in claude_data:
|
||||
total_history = sum(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue