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>
This commit is contained in:
commit
44ed9936b7
48 changed files with 9707 additions and 0 deletions
134
app/models/session.py
Normal file
134
app/models/session.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
"""
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue