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)}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue