Ryan Malloy bec1606c86 Add comprehensive documentation system and tool call tracking
## 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 <noreply@anthropic.com>
2025-08-11 05:58:27 -06:00

142 lines
5.3 KiB
Python

"""
Session model for tracking individual development sessions.
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from sqlalchemy import String, Text, Integer, DateTime, JSON, ForeignKey, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, TimestampMixin
class Session(Base, TimestampMixin):
"""
Represents an individual development session within a project.
A session starts when Claude Code is launched or resumed and ends
when the user stops or the session is interrupted.
"""
__tablename__ = "sessions"
# Primary key
id: Mapped[int] = mapped_column(primary_key=True)
# Foreign key to project
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True)
# Session timing
start_time: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=func.now(),
index=True
)
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# Session metadata
session_type: Mapped[str] = mapped_column(String(50), nullable=False) # startup, resume, clear
working_directory: Mapped[str] = mapped_column(Text, nullable=False)
git_branch: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
environment: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True)
# Session statistics (updated as session progresses)
duration_minutes: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
activity_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
conversation_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
files_touched: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True)
# Relationships
project: Mapped["Project"] = relationship("Project", back_populates="sessions")
conversations: Mapped[List["Conversation"]] = relationship(
"Conversation",
back_populates="session",
cascade="all, delete-orphan",
order_by="Conversation.timestamp"
)
activities: Mapped[List["Activity"]] = relationship(
"Activity",
back_populates="session",
cascade="all, delete-orphan",
order_by="Activity.timestamp"
)
waiting_periods: Mapped[List["WaitingPeriod"]] = relationship(
"WaitingPeriod",
back_populates="session",
cascade="all, delete-orphan",
order_by="WaitingPeriod.start_time"
)
git_operations: Mapped[List["GitOperation"]] = relationship(
"GitOperation",
back_populates="session",
cascade="all, delete-orphan",
order_by="GitOperation.timestamp"
)
tool_calls: Mapped[List["ToolCall"]] = relationship(
"ToolCall",
back_populates="session",
cascade="all, delete-orphan",
order_by="ToolCall.timestamp"
)
def __repr__(self) -> str:
return f"<Session(id={self.id}, project_id={self.project_id}, type='{self.session_type}')>"
@property
def is_active(self) -> bool:
"""Check if this session is still active (not ended)."""
return self.end_time is None
@property
def calculated_duration_minutes(self) -> Optional[int]:
"""Calculate session duration in minutes."""
if self.end_time is None:
# Session is still active, calculate current duration
current_duration = datetime.utcnow() - self.start_time
return int(current_duration.total_seconds() / 60)
else:
# Session is finished
if self.duration_minutes is not None:
return self.duration_minutes
else:
duration = self.end_time - self.start_time
return int(duration.total_seconds() / 60)
def end_session(self, end_reason: str = "normal") -> None:
"""End the session and calculate final statistics."""
if self.end_time is None:
self.end_time = func.now()
self.duration_minutes = self.calculated_duration_minutes
# Update project statistics
if self.project:
unique_files = len(set(self.files_touched or []))
total_lines = sum(
(activity.lines_added or 0) + (activity.lines_removed or 0)
for activity in self.activities
)
total_tool_calls = len(self.tool_calls)
self.project.update_stats(
session_duration_minutes=self.duration_minutes or 0,
files_count=unique_files,
lines_count=total_lines,
tool_calls_count=total_tool_calls
)
def add_activity(self) -> None:
"""Increment activity counter."""
self.activity_count += 1
def add_conversation(self) -> None:
"""Increment conversation counter."""
self.conversation_count += 1
def add_file_touched(self, file_path: str) -> None:
"""Add a file to the list of files touched in this session."""
if self.files_touched is None:
self.files_touched = []
if file_path not in self.files_touched:
self.files_touched.append(file_path)