From bec1606c8622f3e9a3c4ec578e75688d181f7b13 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 11 Aug 2025 05:58:27 -0600 Subject: [PATCH] Add comprehensive documentation system and tool call tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Documentation System - Create complete documentation hub at /dashboard/docs with: - Getting Started guide with quick setup and troubleshooting - Hook Setup Guide with platform-specific configurations - API Reference with all endpoints and examples - FAQ with searchable questions and categories - Add responsive design with interactive features - Update navigation in base template ## Tool Call Tracking - Add ToolCall model for tracking Claude Code tool usage - Create /api/tool-calls endpoints for recording and analytics - Add tool_call hook type with auto-session detection - Include tool calls in project statistics and recalculation - Track tool names, parameters, execution time, and success rates ## Project Enhancements - Add project timeline and statistics pages (fix 404 errors) - Create recalculation script for fixing zero statistics - Update project stats to include tool call counts - Enhance session model with tool call relationships ## Infrastructure - Switch from requirements.txt to pyproject.toml/uv.lock - Add data import functionality for claude.json files - Update database connection to include all new models - Add comprehensive API documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/conversations.py | 48 + app/api/importer.py | 390 +++++ app/api/tool_calls.py | 254 +++ app/dashboard/routes.py | 73 + app/dashboard/static/js/api-client.js | 8 + app/dashboard/templates/base.html | 14 +- app/dashboard/templates/conversations.html | 100 +- .../templates/docs/api-reference.html | 540 ++++++ app/dashboard/templates/docs/faq.html | 784 +++++++++ .../templates/docs/getting-started.html | 349 ++++ app/dashboard/templates/docs/hook-setup.html | 483 ++++++ app/dashboard/templates/docs/index.html | 293 ++++ app/dashboard/templates/import.html | 313 ++++ app/dashboard/templates/project_stats.html | 353 ++++ app/dashboard/templates/project_timeline.html | 214 +++ app/database/connection.py | 2 +- app/models/__init__.py | 2 + app/models/activity.py | 14 +- app/models/project.py | 4 +- app/models/session.py | 10 +- app/models/tool_call.py | 33 + import_claude_data.py | 154 ++ main.py | 4 +- pyproject.toml | 124 ++ recalculate_project_stats.py | 109 ++ requirements.txt | 11 - uv.lock | 1499 +++++++++++++++++ 27 files changed, 6150 insertions(+), 32 deletions(-) create mode 100644 app/api/importer.py create mode 100644 app/api/tool_calls.py create mode 100644 app/dashboard/templates/docs/api-reference.html create mode 100644 app/dashboard/templates/docs/faq.html create mode 100644 app/dashboard/templates/docs/getting-started.html create mode 100644 app/dashboard/templates/docs/hook-setup.html create mode 100644 app/dashboard/templates/docs/index.html create mode 100644 app/dashboard/templates/import.html create mode 100644 app/dashboard/templates/project_stats.html create mode 100644 app/dashboard/templates/project_timeline.html create mode 100644 app/models/tool_call.py create mode 100644 import_claude_data.py create mode 100644 pyproject.toml create mode 100644 recalculate_project_stats.py delete mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/app/api/conversations.py b/app/api/conversations.py index 0df3168..a11c496 100644 --- a/app/api/conversations.py +++ b/app/api/conversations.py @@ -83,6 +83,54 @@ async def log_conversation( ) +@router.get("/conversations", response_model=List[ConversationSearchResult]) +async def get_conversations( + project_id: Optional[int] = Query(None, description="Filter by project ID"), + limit: int = Query(50, description="Maximum number of results"), + offset: int = Query(0, description="Number of results to skip"), + db: AsyncSession = Depends(get_db) +): + """ + Get recent conversations with optional project filtering. + """ + try: + # Build query + query = select(Conversation).options( + selectinload(Conversation.session).selectinload(Session.project) + ) + + # Add project filter if specified + if project_id: + query = query.join(Session).where(Session.project_id == project_id) + + # Order by timestamp descending, add pagination + query = query.order_by(Conversation.timestamp.desc()).offset(offset).limit(limit) + + result = await db.execute(query) + conversations = result.scalars().all() + + # Convert to response format + results = [] + for conversation in conversations: + results.append(ConversationSearchResult( + id=conversation.id, + project_name=conversation.session.project.name, + timestamp=conversation.timestamp, + user_prompt=conversation.user_prompt, + claude_response=conversation.claude_response, + relevance_score=1.0, # All results are equally relevant when just listing + context=[] # No context snippets needed for listing + )) + + return results + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get conversations: {str(e)}" + ) + + @router.get("/conversations/search", response_model=List[ConversationSearchResult]) async def search_conversations( query: str = Query(..., description="Search query"), diff --git a/app/api/importer.py b/app/api/importer.py new file mode 100644 index 0000000..4899e07 --- /dev/null +++ b/app/api/importer.py @@ -0,0 +1,390 @@ +""" +Data importer for Claude Code .claude.json file. + +This module provides functionality to import historical data from the +.claude.json configuration file into the project tracker. +""" + +import json +import os +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Optional, Any + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select + +from app.database.connection import get_db +from app.models.project import Project +from app.models.session import Session +from app.models.conversation import Conversation + +router = APIRouter() + + +class ClaudeJsonImporter: + """Importer for .claude.json data.""" + + def __init__(self, db: AsyncSession): + self.db = db + + async def import_from_file(self, file_path: str) -> Dict[str, Any]: + """Import data from .claude.json file.""" + if not os.path.exists(file_path): + raise FileNotFoundError(f"Claude configuration file not found: {file_path}") + + try: + with open(file_path, 'r', encoding='utf-8') as f: + claude_data = json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in Claude configuration file: {e}") + + results = { + "projects_imported": 0, + "sessions_estimated": 0, + "conversations_imported": 0, + "errors": [] + } + + # Import basic usage statistics + await self._import_usage_stats(claude_data, results) + + # Import projects and their history + if "projects" in claude_data: + await self._import_projects(claude_data["projects"], results) + + return results + + async def _import_usage_stats(self, claude_data: Dict[str, Any], results: Dict[str, Any]): + """Import basic usage statistics.""" + # We could create a synthetic "Claude Code Usage" project to track overall stats + if claude_data.get("numStartups") and claude_data.get("firstStartTime"): + try: + first_start = datetime.fromisoformat( + claude_data["firstStartTime"].replace('Z', '+00:00') + ) + + # Create a synthetic project for overall Claude Code usage + usage_project = await self._get_or_create_project( + name="Claude Code Usage Statistics", + path="", + description="Imported usage statistics from .claude.json" + ) + + # Estimate session distribution over time + num_startups = claude_data["numStartups"] + days_since_first = (datetime.now() - first_start.replace(tzinfo=None)).days + + if days_since_first > 0: + # Create estimated sessions spread over the usage period + await self._create_estimated_sessions( + usage_project, + first_start.replace(tzinfo=None), + num_startups, + days_since_first + ) + results["sessions_estimated"] = num_startups + + except Exception as e: + results["errors"].append(f"Failed to import usage stats: {e}") + + async def _import_projects(self, projects_data: Dict[str, Any], results: Dict[str, Any]): + """Import project data from .claude.json.""" + for project_path, project_info in projects_data.items(): + try: + # Skip system paths or non-meaningful paths + if project_path in ["", "/", "/tmp"]: + continue + + # Extract project name from path + project_name = Path(project_path).name or "Unknown Project" + + # Create or get existing project + project = await self._get_or_create_project( + name=project_name, + path=project_path + ) + + results["projects_imported"] += 1 + + # Import conversation history if available + if "history" in project_info and isinstance(project_info["history"], list): + conversation_count = await self._import_project_history( + project, + project_info["history"] + ) + results["conversations_imported"] += conversation_count + + except Exception as e: + results["errors"].append(f"Failed to import project {project_path}: {e}") + + async def _get_or_create_project( + self, + name: str, + path: str, + description: Optional[str] = None + ) -> Project: + """Get existing project or create new one.""" + # Check if project already exists + result = await self.db.execute( + select(Project).where(Project.path == path) + ) + existing_project = result.scalars().first() + + if existing_project: + return existing_project + + # Try to detect languages from path + languages = self._detect_languages(path) + + # Create new project + project = Project( + name=name, + path=path, + languages=languages + ) + + self.db.add(project) + await self.db.commit() + await self.db.refresh(project) + + return project + + def _detect_languages(self, project_path: str) -> Optional[List[str]]: + """Attempt to detect programming languages from project directory.""" + languages = [] + + try: + if os.path.exists(project_path) and os.path.isdir(project_path): + # Look for common files to infer languages + files = os.listdir(project_path) + + # Python + if any(f.endswith(('.py', '.pyx', '.pyi')) for f in files) or 'requirements.txt' in files: + languages.append('python') + + # JavaScript/TypeScript + if any(f.endswith(('.js', '.jsx', '.ts', '.tsx')) for f in files) or 'package.json' in files: + if any(f.endswith(('.ts', '.tsx')) for f in files): + languages.append('typescript') + else: + languages.append('javascript') + + # Go + if any(f.endswith('.go') for f in files) or 'go.mod' in files: + languages.append('go') + + # Rust + if any(f.endswith('.rs') for f in files) or 'Cargo.toml' in files: + languages.append('rust') + + # Java + if any(f.endswith('.java') for f in files) or 'pom.xml' in files: + languages.append('java') + + except (OSError, PermissionError): + # If we can't read the directory, that's okay + pass + + return languages if languages else None + + async def _create_estimated_sessions( + self, + project: Project, + first_start: datetime, + num_startups: int, + days_since_first: int + ): + """Create estimated sessions based on startup count.""" + # Check if we already have sessions for this project + existing_sessions = await self.db.execute( + select(Session).where( + Session.project_id == project.id, + Session.session_type == "startup" + ) + ) + if existing_sessions.scalars().first(): + return # Sessions already exist, skip creation + + # Don't create too many sessions - limit to reasonable estimates + max_sessions = min(num_startups, 50) # Cap at 50 sessions + + # Distribute sessions over the time period + if days_since_first > 0: + sessions_per_day = max_sessions / days_since_first + + for i in range(max_sessions): + # Spread sessions over the time period + days_offset = int(i / sessions_per_day) if sessions_per_day > 0 else i + session_time = first_start + timedelta(days=days_offset) + + # Estimate session duration (30-180 minutes) + import random + duration = random.randint(30, 180) + + session = Session( + project_id=project.id, + start_time=session_time, + end_time=session_time + timedelta(minutes=duration), + session_type="startup", + working_directory=project.path, + duration_minutes=duration, + activity_count=random.randint(5, 25), # Estimated activity + conversation_count=random.randint(2, 8) # Estimated conversations + ) + + self.db.add(session) + + await self.db.commit() + + async def _import_project_history( + self, + project: Project, + history: List[Dict[str, Any]] + ) -> int: + """Import conversation history for a project.""" + # Check if we already have history conversations for this project + existing_conversations = await self.db.execute( + select(Conversation).where( + Conversation.context.like('%"imported_from": ".claude.json"%'), + Conversation.session.has(Session.project_id == project.id) + ) + ) + if existing_conversations.scalars().first(): + return 0 # History already imported, skip + + conversation_count = 0 + + # Create a synthetic session for imported history + history_session = Session( + project_id=project.id, + start_time=datetime.now() - timedelta(days=30), # Assume recent + session_type="history_import", # Different type to avoid conflicts + working_directory=project.path, + activity_count=len(history), + conversation_count=len(history) + ) + + self.db.add(history_session) + await self.db.commit() + await self.db.refresh(history_session) + + # Import each history entry as a conversation + for i, entry in enumerate(history[:20]): # Limit to 20 entries + try: + display_text = entry.get("display", "") + if display_text: + conversation = Conversation( + session_id=history_session.id, + timestamp=history_session.start_time + timedelta(minutes=i * 5), + user_prompt=display_text, + exchange_type="user_prompt", + context={"imported_from": ".claude.json"} + ) + + self.db.add(conversation) + conversation_count += 1 + except Exception as e: + # Skip problematic entries + continue + + if conversation_count > 0: + await self.db.commit() + + return conversation_count + + +@router.post("/import/claude-json") +async def import_claude_json( + file_path: Optional[str] = None, + db: AsyncSession = Depends(get_db) +): + """ + Import data from .claude.json file. + + If no file_path is provided, tries to find .claude.json in the user's home directory. + """ + if not file_path: + # Try default location + home_path = Path.home() / ".claude.json" + file_path = str(home_path) + + try: + importer = ClaudeJsonImporter(db) + results = await importer.import_from_file(file_path) + + return { + "success": True, + "message": "Import completed successfully", + "results": results + } + + except FileNotFoundError as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Claude configuration file not found: {e}" + ) + 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 +): + """ + Preview what would be imported from .claude.json file without actually importing. + """ + if not file_path: + home_path = Path.home() / ".claude.json" + file_path = str(home_path) + + if not os.path.exists(file_path): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Claude configuration file not found" + ) + + try: + with open(file_path, 'r', encoding='utf-8') as f: + claude_data = json.load(f) + 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_path": file_path, + "file_size_mb": round(os.path.getsize(file_path) / (1024 * 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( + len(proj.get("history", [])) + for proj in claude_data["projects"].values() + ) + preview["history_entries"] = total_history + + return preview \ No newline at end of file diff --git a/app/api/tool_calls.py b/app/api/tool_calls.py new file mode 100644 index 0000000..5b8aa02 --- /dev/null +++ b/app/api/tool_calls.py @@ -0,0 +1,254 @@ +""" +Tool call tracking API endpoints. +""" + +import json +from datetime import datetime +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, desc +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.tool_call import ToolCall + +router = APIRouter() + + +class ToolCallRequest(BaseModel): + """Request schema for recording a tool call.""" + session_id: Optional[str] = Field(None, description="Session ID (auto-detected if not provided)") + tool_name: str = Field(..., description="Name of the tool being called") + parameters: Optional[dict] = Field(None, description="Tool parameters as JSON object") + result_status: Optional[str] = Field("success", description="Result status: success, error, timeout") + error_message: Optional[str] = Field(None, description="Error message if failed") + execution_time_ms: Optional[int] = Field(None, description="Execution time in milliseconds") + timestamp: Optional[datetime] = Field(None, description="Timestamp of the tool call") + + +class ToolCallResponse(BaseModel): + """Response schema for tool call operations.""" + id: int + session_id: str + tool_name: str + result_status: str + timestamp: datetime + message: str + + +class ToolUsageStats(BaseModel): + """Statistics about tool usage.""" + tool_name: str + total_calls: int + success_calls: int + error_calls: int + avg_execution_time_ms: Optional[float] + success_rate: float + + +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 + + +@router.post("/tool-calls", response_model=ToolCallResponse, status_code=status.HTTP_201_CREATED) +async def record_tool_call( + request: ToolCallRequest, + db: AsyncSession = Depends(get_db) +): + """ + Record a tool call made during a Claude Code session. + + This endpoint is called by Claude Code hooks when tools are used. + It automatically detects the current session if not provided. + """ + try: + # Get session ID + session_id = request.session_id + if not session_id: + session_id = await get_current_session_id() + + if not session_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No active session found. Please provide session_id or ensure a session is running." + ) + + # Verify 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" + ) + + # Create tool call record + tool_call = ToolCall( + session_id=str(session_id), + tool_name=request.tool_name, + parameters=json.dumps(request.parameters) if request.parameters else None, + result_status=request.result_status or "success", + error_message=request.error_message, + execution_time_ms=request.execution_time_ms, + timestamp=request.timestamp or datetime.utcnow() + ) + + db.add(tool_call) + await db.commit() + await db.refresh(tool_call) + + return ToolCallResponse( + id=tool_call.id, + session_id=tool_call.session_id, + tool_name=tool_call.tool_name, + result_status=tool_call.result_status, + timestamp=tool_call.timestamp, + message=f"Tool call '{request.tool_name}' recorded successfully" + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to record tool call: {str(e)}" + ) + + +@router.get("/tool-calls/session/{session_id}", response_model=List[dict]) +async def get_session_tool_calls( + session_id: str, + db: AsyncSession = Depends(get_db) +): + """Get all tool calls for a specific session.""" + try: + result = await db.execute( + select(ToolCall) + .where(ToolCall.session_id == session_id) + .order_by(desc(ToolCall.timestamp)) + ) + tool_calls = result.scalars().all() + + return [ + { + "id": tc.id, + "tool_name": tc.tool_name, + "parameters": json.loads(tc.parameters) if tc.parameters else None, + "result_status": tc.result_status, + "error_message": tc.error_message, + "execution_time_ms": tc.execution_time_ms, + "timestamp": tc.timestamp + } + for tc in tool_calls + ] + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get tool calls: {str(e)}" + ) + + +@router.get("/tool-calls/stats", response_model=List[ToolUsageStats]) +async def get_tool_usage_stats( + project_id: Optional[int] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + db: AsyncSession = Depends(get_db) +): + """Get tool usage statistics with optional filtering.""" + try: + # Base query + query = select( + ToolCall.tool_name, + func.count(ToolCall.id).label("total_calls"), + func.sum(func.case([(ToolCall.result_status == "success", 1)], else_=0)).label("success_calls"), + func.sum(func.case([(ToolCall.result_status == "error", 1)], else_=0)).label("error_calls"), + func.avg(ToolCall.execution_time_ms).label("avg_execution_time_ms") + ) + + # Apply filters + if project_id: + query = query.join(Session).where(Session.project_id == project_id) + + if start_date: + query = query.where(ToolCall.timestamp >= start_date) + + if end_date: + query = query.where(ToolCall.timestamp <= end_date) + + # Group by tool name + query = query.group_by(ToolCall.tool_name).order_by(desc("total_calls")) + + result = await db.execute(query) + rows = result.all() + + stats = [] + for row in rows: + success_rate = (row.success_calls / row.total_calls * 100) if row.total_calls > 0 else 0 + + stats.append(ToolUsageStats( + tool_name=row.tool_name, + total_calls=row.total_calls, + success_calls=row.success_calls, + error_calls=row.error_calls, + avg_execution_time_ms=float(row.avg_execution_time_ms) if row.avg_execution_time_ms else None, + success_rate=round(success_rate, 2) + )) + + return stats + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get tool usage stats: {str(e)}" + ) + + +@router.get("/tool-calls/popular", response_model=List[dict]) +async def get_popular_tools( + limit: int = 10, + project_id: Optional[int] = None, + db: AsyncSession = Depends(get_db) +): + """Get the most frequently used tools.""" + try: + query = select( + ToolCall.tool_name, + func.count(ToolCall.id).label("usage_count"), + func.max(ToolCall.timestamp).label("last_used") + ) + + if project_id: + query = query.join(Session).where(Session.project_id == project_id) + + query = query.group_by(ToolCall.tool_name).order_by(desc("usage_count")).limit(limit) + + result = await db.execute(query) + rows = result.all() + + return [ + { + "tool_name": row.tool_name, + "usage_count": row.usage_count, + "last_used": row.last_used + } + for row in rows + ] + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get popular tools: {str(e)}" + ) \ No newline at end of file diff --git a/app/dashboard/routes.py b/app/dashboard/routes.py index 1364495..83ea799 100644 --- a/app/dashboard/routes.py +++ b/app/dashboard/routes.py @@ -46,4 +46,77 @@ async def dashboard_conversations(request: Request, db: AsyncSession = Depends(g return templates.TemplateResponse("conversations.html", { "request": request, "title": "Conversations - Claude Code Tracker" + }) + + +@dashboard_router.get("/dashboard/import", response_class=HTMLResponse) +async def dashboard_import(request: Request, db: AsyncSession = Depends(get_db)): + """Data import page.""" + return templates.TemplateResponse("import.html", { + "request": request, + "title": "Import Data - Claude Code Tracker" + }) + + +@dashboard_router.get("/dashboard/projects/{project_id}/timeline", response_class=HTMLResponse) +async def dashboard_project_timeline(request: Request, project_id: int, db: AsyncSession = Depends(get_db)): + """Project timeline page.""" + return templates.TemplateResponse("project_timeline.html", { + "request": request, + "title": f"Timeline - Project {project_id}", + "project_id": project_id + }) + + +@dashboard_router.get("/dashboard/projects/{project_id}/stats", response_class=HTMLResponse) +async def dashboard_project_stats(request: Request, project_id: int, db: AsyncSession = Depends(get_db)): + """Project statistics page.""" + return templates.TemplateResponse("project_stats.html", { + "request": request, + "title": f"Statistics - Project {project_id}", + "project_id": project_id + }) + + +@dashboard_router.get("/dashboard/docs", response_class=HTMLResponse) +async def dashboard_docs(request: Request, db: AsyncSession = Depends(get_db)): + """Documentation overview page.""" + return templates.TemplateResponse("docs/index.html", { + "request": request, + "title": "Documentation - Claude Code Tracker" + }) + + +@dashboard_router.get("/dashboard/docs/{section}", response_class=HTMLResponse) +async def dashboard_docs_section(request: Request, section: str, db: AsyncSession = Depends(get_db)): + """Documentation section pages.""" + # Map section names to templates and titles + sections = { + "getting-started": { + "template": "docs/getting-started.html", + "title": "Getting Started - Claude Code Tracker" + }, + "hook-setup": { + "template": "docs/hook-setup.html", + "title": "Hook Setup Guide - Claude Code Tracker" + }, + "api-reference": { + "template": "docs/api-reference.html", + "title": "API Reference - Claude Code Tracker" + }, + "faq": { + "template": "docs/faq.html", + "title": "FAQ - Claude Code Tracker" + } + } + + if section not in sections: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail=f"Documentation section '{section}' not found") + + section_info = sections[section] + return templates.TemplateResponse(section_info["template"], { + "request": request, + "title": section_info["title"], + "section": section }) \ No newline at end of file diff --git a/app/dashboard/static/js/api-client.js b/app/dashboard/static/js/api-client.js index 24ec8d2..24fef0e 100644 --- a/app/dashboard/static/js/api-client.js +++ b/app/dashboard/static/js/api-client.js @@ -92,6 +92,14 @@ class ApiClient { } // Conversations API + async getConversations(projectId = null, limit = 50, offset = 0) { + let url = `/conversations?limit=${limit}&offset=${offset}`; + if (projectId) { + url += `&project_id=${projectId}`; + } + return this.request(url); + } + async searchConversations(query, projectId = null, limit = 20) { let url = `/conversations/search?query=${encodeURIComponent(query)}&limit=${limit}`; if (projectId) { diff --git a/app/dashboard/templates/base.html b/app/dashboard/templates/base.html index 54c9471..91846fa 100644 --- a/app/dashboard/templates/base.html +++ b/app/dashboard/templates/base.html @@ -47,12 +47,24 @@ Conversations + +