Ryan Malloy 44ed9936b7 Initial commit: Claude Code Project Tracker
Add comprehensive development intelligence system that tracks:
- Development sessions with automatic start/stop
- Full conversation history with semantic search
- Tool usage and file operation analytics
- Think time and engagement analysis
- Git activity correlation
- Learning pattern recognition
- Productivity insights and metrics

Features:
- FastAPI backend with SQLite database
- Modern web dashboard with interactive charts
- Claude Code hook integration for automatic tracking
- Comprehensive test suite with 100+ tests
- Complete API documentation (OpenAPI/Swagger)
- Privacy-first design with local data storage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 02:59:21 -06:00

134 lines
5.1 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"
)
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
)
self.project.update_stats(
session_duration_minutes=self.duration_minutes or 0,
files_count=unique_files,
lines_count=total_lines
)
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)