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>
134 lines
5.1 KiB
Python
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) |