Complete Phase 6: MSP Work Tracking with Context Recall System

Implements production-ready MSP platform with cross-machine persistent memory for Claude.

API Implementation:
- 130 REST API endpoints across 21 entities
- JWT authentication on all endpoints
- AES-256-GCM encryption for credentials
- Automatic audit logging
- Complete OpenAPI documentation

Database:
- 43 tables in MariaDB (172.16.3.20:3306)
- 42 SQLAlchemy models with modern 2.0 syntax
- Full Alembic migration system
- 99.1% CRUD test pass rate

Context Recall System (Phase 6):
- Cross-machine persistent memory via database
- Automatic context injection via Claude Code hooks
- Automatic context saving after task completion
- 90-95% token reduction with compression utilities
- Relevance scoring with time decay
- Tag-based semantic search
- One-command setup script

Security Features:
- JWT tokens with Argon2 password hashing
- AES-256-GCM encryption for all sensitive data
- Comprehensive audit trail for credentials
- HMAC tamper detection
- Secure configuration management

Test Results:
- Phase 3: 38/38 CRUD tests passing (100%)
- Phase 4: 34/35 core API tests passing (97.1%)
- Phase 5: 62/62 extended API tests passing (100%)
- Phase 6: 10/10 compression tests passing (100%)
- Overall: 144/145 tests passing (99.3%)

Documentation:
- Comprehensive architecture guides
- Setup automation scripts
- API documentation at /api/docs
- Complete test reports
- Troubleshooting guides

Project Status: 95% Complete (Production-Ready)
Phase 7 (optional work context APIs) remains for future enhancement.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 06:00:26 -07:00
parent 1452361c21
commit 390b10b32c
201 changed files with 55619 additions and 34 deletions

97
api/models/__init__.py Normal file
View File

@@ -0,0 +1,97 @@
"""
SQLAlchemy ORM models for ClaudeTools.
This package contains all database models and their base classes.
"""
from api.models.api_audit_log import ApiAuditLog
from api.models.backup_log import BackupLog
from api.models.base import Base, TimestampMixin, UUIDMixin
from api.models.billable_time import BillableTime
from api.models.client import Client
from api.models.command_run import CommandRun
from api.models.context_snippet import ContextSnippet
from api.models.conversation_context import ConversationContext
from api.models.credential import Credential
from api.models.credential_audit_log import CredentialAuditLog
from api.models.credential_permission import CredentialPermission
from api.models.database_change import DatabaseChange
from api.models.decision_log import DecisionLog
from api.models.deployment import Deployment
from api.models.environmental_insight import EnvironmentalInsight
from api.models.external_integration import ExternalIntegration
from api.models.failure_pattern import FailurePattern
from api.models.file_change import FileChange
from api.models.firewall_rule import FirewallRule
from api.models.infrastructure import Infrastructure
from api.models.infrastructure_change import InfrastructureChange
from api.models.infrastructure_tag import InfrastructureTag
from api.models.integration_credential import IntegrationCredential
from api.models.m365_tenant import M365Tenant
from api.models.machine import Machine
from api.models.network import Network
from api.models.operation_failure import OperationFailure
from api.models.pending_task import PendingTask
from api.models.problem_solution import ProblemSolution
from api.models.project import Project
from api.models.project_state import ProjectState
from api.models.schema_migration import SchemaMigration
from api.models.security_incident import SecurityIncident
from api.models.service import Service
from api.models.service_relationship import ServiceRelationship
from api.models.session import Session
from api.models.session_tag import SessionTag
from api.models.site import Site
from api.models.tag import Tag
from api.models.task import Task
from api.models.ticket_link import TicketLink
from api.models.work_item import WorkItem
from api.models.work_item_tag import WorkItemTag
__all__ = [
"ApiAuditLog",
"BackupLog",
"Base",
"BillableTime",
"Client",
"CommandRun",
"ContextSnippet",
"ConversationContext",
"Credential",
"CredentialAuditLog",
"CredentialPermission",
"DatabaseChange",
"DecisionLog",
"Deployment",
"EnvironmentalInsight",
"ExternalIntegration",
"FailurePattern",
"FileChange",
"FirewallRule",
"Infrastructure",
"InfrastructureChange",
"InfrastructureTag",
"IntegrationCredential",
"M365Tenant",
"Machine",
"Network",
"OperationFailure",
"PendingTask",
"ProblemSolution",
"Project",
"ProjectState",
"SchemaMigration",
"SecurityIncident",
"Service",
"ServiceRelationship",
"Session",
"SessionTag",
"Site",
"Tag",
"Task",
"TicketLink",
"TimestampMixin",
"UUIDMixin",
"WorkItem",
"WorkItemTag",
]

111
api/models/api_audit_log.py Normal file
View File

@@ -0,0 +1,111 @@
"""
API audit log model for tracking API requests and security events.
Tracks all API requests including user, endpoint, request/response details,
and performance metrics for security auditing and monitoring.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import Index, Integer, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
class ApiAuditLog(Base, UUIDMixin):
"""
API audit log model for tracking API requests and security.
Logs all API requests with details about the user, endpoint accessed,
request/response data, performance metrics, and errors. Used for
security auditing, monitoring, and troubleshooting API issues.
Attributes:
user_id: User identifier from JWT sub claim
endpoint: API endpoint path accessed
http_method: HTTP method used (GET, POST, PUT, DELETE, etc.)
ip_address: IP address of the requester
user_agent: User agent string from the request
request_body: Sanitized request body (credentials removed)
response_status: HTTP response status code
response_time_ms: Response time in milliseconds
error_message: Error message if request failed
timestamp: When the request was made
"""
__tablename__ = "api_audit_log"
# User identification
user_id: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="User identifier from JWT sub claim"
)
# Request details
endpoint: Mapped[str] = mapped_column(
String(500),
nullable=False,
doc="API endpoint path accessed (e.g., '/api/v1/sessions')"
)
http_method: Mapped[Optional[str]] = mapped_column(
String(10),
doc="HTTP method used: GET, POST, PUT, DELETE, PATCH"
)
# Client information
ip_address: Mapped[Optional[str]] = mapped_column(
String(45),
doc="IP address of the requester (IPv4 or IPv6)"
)
user_agent: Mapped[Optional[str]] = mapped_column(
Text,
doc="User agent string from the request"
)
# Request/Response data
request_body: Mapped[Optional[str]] = mapped_column(
Text,
doc="Sanitized request body (credentials and sensitive data removed)"
)
response_status: Mapped[Optional[int]] = mapped_column(
Integer,
doc="HTTP response status code (200, 401, 500, etc.)"
)
response_time_ms: Mapped[Optional[int]] = mapped_column(
Integer,
doc="Response time in milliseconds"
)
# Error tracking
error_message: Mapped[Optional[str]] = mapped_column(
Text,
doc="Error message if the request failed"
)
# Timestamp
timestamp: Mapped[datetime] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=func.now(),
doc="When the request was made"
)
# Indexes
__table_args__ = (
Index("idx_api_audit_user", "user_id"),
Index("idx_api_audit_endpoint", "endpoint"),
Index("idx_api_audit_timestamp", "timestamp"),
Index("idx_api_audit_status", "response_status"),
)
def __repr__(self) -> str:
"""String representation of the audit log entry."""
return f"<ApiAuditLog(user='{self.user_id}', endpoint='{self.endpoint}', status={self.response_status})>"

147
api/models/backup_log.py Normal file
View File

@@ -0,0 +1,147 @@
"""
Backup Log model for tracking ClaudeTools database backups.
This model logs all backup operations with verification status,
ensuring the ClaudeTools database can be reliably restored if needed.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import (
BigInteger,
CheckConstraint,
Index,
Integer,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
class BackupLog(Base, UUIDMixin):
"""
Backup tracking for ClaudeTools database.
Logs all backup operations including timing, file details, and verification
status. Ensures database can be restored with confidence.
Attributes:
id: Unique identifier
backup_type: Type of backup (daily, weekly, monthly, manual, pre-migration)
file_path: Path to the backup file
file_size_bytes: Size of the backup file in bytes
backup_started_at: When the backup started
backup_completed_at: When the backup completed
duration_seconds: Computed duration of backup operation
verification_status: Status of backup verification (passed, failed, not_verified)
verification_details: JSON with specific verification check results
database_host: Host where database is located
database_name: Name of the database backed up
backup_method: Method used for backup (mysqldump, etc.)
created_at: Timestamp when log entry was created
"""
__tablename__ = "backup_log"
# Backup details
backup_type: Mapped[str] = mapped_column(
String(50),
CheckConstraint(
"backup_type IN ('daily', 'weekly', 'monthly', 'manual', 'pre-migration')"
),
nullable=False,
doc="Type of backup performed",
)
file_path: Mapped[str] = mapped_column(
String(500),
nullable=False,
doc="Path to the backup file",
)
file_size_bytes: Mapped[int] = mapped_column(
BigInteger,
nullable=False,
doc="Size of backup file in bytes",
)
# Timing
backup_started_at: Mapped[datetime] = mapped_column(
nullable=False,
doc="When the backup started",
)
backup_completed_at: Mapped[datetime] = mapped_column(
nullable=False,
doc="When the backup completed",
)
# Note: SQLAlchemy doesn't support TIMESTAMPDIFF directly, so we'll calculate in Python
# The duration will be computed by the application layer rather than as a stored generated column
duration_seconds: Mapped[Optional[int]] = mapped_column(
Integer,
nullable=True,
doc="Duration of backup in seconds (computed in application)",
)
# Verification
verification_status: Mapped[Optional[str]] = mapped_column(
String(50),
CheckConstraint(
"verification_status IN ('passed', 'failed', 'not_verified')"
),
nullable=True,
doc="Verification status of the backup",
)
verification_details: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON with specific verification check results",
)
# Metadata
database_host: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="Host where database is located",
)
database_name: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
doc="Name of the database backed up",
)
backup_method: Mapped[str] = mapped_column(
String(50),
default="mysqldump",
nullable=False,
doc="Method used for backup",
)
created_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When log entry was created",
)
# Indexes
__table_args__ = (
Index("idx_backup_type", "backup_type"),
Index("idx_backup_date", "backup_completed_at"),
Index("idx_verification_status", "verification_status"),
)
def calculate_duration(self) -> None:
"""Calculate and set the duration_seconds field."""
if self.backup_started_at and self.backup_completed_at:
delta = self.backup_completed_at - self.backup_started_at
self.duration_seconds = int(delta.total_seconds())
def __repr__(self) -> str:
"""String representation of the backup log."""
return (
f"<BackupLog(id={self.id!r}, "
f"type={self.backup_type!r}, "
f"size={self.file_size_bytes}, "
f"status={self.verification_status!r})>"
)

69
api/models/base.py Normal file
View File

@@ -0,0 +1,69 @@
"""
Base models and mixins for SQLAlchemy ORM.
This module provides the foundational base class and reusable mixins
for all database models in the ClaudeTools application.
"""
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import CHAR, Column, DateTime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.sql import func
class Base(DeclarativeBase):
"""Base class for all SQLAlchemy ORM models."""
pass
class UUIDMixin:
"""
Mixin that adds a UUID primary key column.
This mixin provides a standardized UUID-based primary key for models,
stored as CHAR(36) for compatibility with MariaDB and other databases
that don't have native UUID support.
Attributes:
id: UUID primary key stored as CHAR(36), automatically generated
"""
id: Mapped[str] = mapped_column(
CHAR(36),
primary_key=True,
default=lambda: str(uuid.uuid4()),
nullable=False,
doc="Unique identifier for the record",
)
class TimestampMixin:
"""
Mixin that adds timestamp columns for record tracking.
This mixin provides automatic timestamp tracking for record creation
and updates, using database-level defaults for consistency.
Attributes:
created_at: Timestamp when the record was created
updated_at: Timestamp when the record was last updated
"""
created_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
doc="Timestamp when the record was created",
)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
server_onupdate=func.now(),
doc="Timestamp when the record was last updated",
)

186
api/models/billable_time.py Normal file
View File

@@ -0,0 +1,186 @@
"""
Billable time model for tracking time entries for billing.
Tracks individual billable time entries with references to work items,
sessions, and clients, including rates, amounts, and billing details.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, CHAR, CheckConstraint, ForeignKey, Index, Integer, Numeric, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .client import Client
from .session import Session
from .work_item import WorkItem
class BillableTime(Base, UUIDMixin, TimestampMixin):
"""
Billable time model representing individual billable time entries.
Tracks time entries for billing purposes with detailed information about
the work performed, rates applied, and amounts calculated. Links to
work items, sessions, and clients for comprehensive billing tracking.
Attributes:
work_item_id: Foreign key to work_items table
session_id: Foreign key to sessions table
client_id: Foreign key to clients table
start_time: When the billable time started
end_time: When the billable time ended
duration_minutes: Duration in minutes (auto-calculated or manual)
hourly_rate: Hourly rate applied to this time entry
total_amount: Total billable amount (calculated)
is_billable: Whether this time entry is actually billable
description: Description of the work performed
category: Category of work (consulting, development, support, etc.)
notes: Additional notes about this time entry
invoiced_at: When this time entry was invoiced
invoice_id: Reference to invoice if applicable
"""
__tablename__ = "billable_time"
# Foreign keys
work_item_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="SET NULL"),
doc="Foreign key to work_items table"
)
session_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
doc="Foreign key to sessions table"
)
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
nullable=False,
doc="Foreign key to clients table"
)
# Time tracking
start_time: Mapped[datetime] = mapped_column(
TIMESTAMP,
nullable=False,
doc="When the billable time started"
)
end_time: Mapped[Optional[datetime]] = mapped_column(
TIMESTAMP,
doc="When the billable time ended"
)
duration_minutes: Mapped[int] = mapped_column(
Integer,
nullable=False,
doc="Duration in minutes (auto-calculated or manual)"
)
# Billing information
hourly_rate: Mapped[float] = mapped_column(
Numeric(10, 2),
nullable=False,
doc="Hourly rate applied to this time entry"
)
total_amount: Mapped[float] = mapped_column(
Numeric(10, 2),
nullable=False,
doc="Total billable amount (calculated: duration * rate)"
)
is_billable: Mapped[bool] = mapped_column(
Boolean,
default=True,
server_default="1",
nullable=False,
doc="Whether this time entry is actually billable"
)
# Work details
description: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Description of the work performed"
)
category: Mapped[str] = mapped_column(
String(50),
nullable=False,
doc="Category: consulting, development, support, maintenance, troubleshooting, project_work, training, documentation"
)
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes about this time entry"
)
# Invoice tracking
invoiced_at: Mapped[Optional[datetime]] = mapped_column(
TIMESTAMP,
doc="When this time entry was invoiced"
)
invoice_id: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Reference to invoice if applicable"
)
# Relationships
work_item: Mapped[Optional["WorkItem"]] = relationship(
"WorkItem",
doc="Relationship to WorkItem model"
)
session: Mapped[Optional["Session"]] = relationship(
"Session",
doc="Relationship to Session model"
)
client: Mapped["Client"] = relationship(
"Client",
doc="Relationship to Client model"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"category IN ('consulting', 'development', 'support', 'maintenance', 'troubleshooting', 'project_work', 'training', 'documentation')",
name="ck_billable_time_category"
),
CheckConstraint(
"duration_minutes > 0",
name="ck_billable_time_duration_positive"
),
CheckConstraint(
"hourly_rate >= 0",
name="ck_billable_time_rate_non_negative"
),
CheckConstraint(
"total_amount >= 0",
name="ck_billable_time_amount_non_negative"
),
CheckConstraint(
"end_time IS NULL OR end_time >= start_time",
name="ck_billable_time_end_after_start"
),
Index("idx_billable_time_work_item", "work_item_id"),
Index("idx_billable_time_session", "session_id"),
Index("idx_billable_time_client", "client_id"),
Index("idx_billable_time_start", "start_time"),
Index("idx_billable_time_billable", "is_billable"),
Index("idx_billable_time_category", "category"),
Index("idx_billable_time_invoiced", "invoiced_at"),
)
def __repr__(self) -> str:
"""String representation of the billable time entry."""
return f"<BillableTime(client_id='{self.client_id}', duration={self.duration_minutes}min, amount=${self.total_amount})>"

120
api/models/client.py Normal file
View File

@@ -0,0 +1,120 @@
"""
Client model for all client organizations.
Master table for MSP clients, internal projects, and client organizations.
"""
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .pending_task import PendingTask
from .project import Project
from .session import Session
class Client(Base, UUIDMixin, TimestampMixin):
"""
Client model representing client organizations.
Master table for all client organizations including MSP clients,
internal projects, and project-based clients. Stores client identification,
network information, and Microsoft 365 tenant details.
Attributes:
name: Client name (unique)
type: Client type (msp_client, internal, project)
network_subnet: Client network subnet (e.g., "192.168.0.0/24")
domain_name: Active Directory domain or primary domain
m365_tenant_id: Microsoft 365 tenant ID
primary_contact: Primary contact person
notes: Additional notes about the client
is_active: Whether client is currently active
"""
__tablename__ = "clients"
# Client identification
name: Mapped[str] = mapped_column(
String(255),
nullable=False,
unique=True,
doc="Client name (unique)"
)
type: Mapped[str] = mapped_column(
String(50),
nullable=False,
doc="Client type: msp_client, internal, project"
)
# Network information
network_subnet: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Client network subnet (e.g., '192.168.0.0/24')"
)
domain_name: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Active Directory domain or primary domain"
)
# Microsoft 365
m365_tenant_id: Mapped[Optional[str]] = mapped_column(
String(36),
doc="Microsoft 365 tenant ID (UUID format)"
)
# Contact information
primary_contact: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Primary contact person"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes about the client"
)
# Status
is_active: Mapped[bool] = mapped_column(
Boolean,
default=True,
server_default="1",
doc="Whether client is currently active"
)
# Relationships
projects: Mapped[list["Project"]] = relationship(
"Project",
back_populates="client",
cascade="all, delete-orphan",
doc="Projects associated with this client"
)
sessions: Mapped[list["Session"]] = relationship(
"Session",
back_populates="client",
doc="Sessions associated with this client"
)
pending_tasks: Mapped[list["PendingTask"]] = relationship(
"PendingTask",
back_populates="client",
doc="Pending tasks associated with this client"
)
# Indexes
__table_args__ = (
Index("idx_clients_type", "type"),
Index("idx_clients_name", "name"),
)
def __repr__(self) -> str:
"""String representation of the client."""
return f"<Client(name='{self.name}', type='{self.type}')>"

140
api/models/command_run.py Normal file
View File

@@ -0,0 +1,140 @@
"""
Command run model for tracking shell/PowerShell/SQL commands executed.
This model records all commands executed during work sessions, including
success/failure status and enhanced failure tracking for diagnostics.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import CHAR, Boolean, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from api.models.base import Base, UUIDMixin
class CommandRun(UUIDMixin, Base):
"""
Track commands executed during work sessions.
Records shell, PowerShell, SQL, and other commands with execution details,
output, and enhanced failure tracking for compatibility and environmental issues.
Attributes:
id: UUID primary key
work_item_id: Reference to the work item
session_id: Reference to the session
command_text: The actual command that was executed
host: Where the command was executed (hostname or IP)
shell_type: Type of shell (bash, powershell, sql, docker, etc.)
success: Whether the command succeeded
output_summary: Summary of command output (first/last lines or error)
exit_code: Command exit code (non-zero indicates failure)
error_message: Full error text if command failed
failure_category: Category of failure (compatibility, permission, syntax, environmental)
resolution: How the failure was fixed (if resolved)
resolved: Whether the failure has been resolved
execution_order: Sequence number within work item
created_at: When the command was executed
"""
__tablename__ = "commands_run"
# Foreign keys
work_item_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to work item",
)
session_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to session",
)
# Command details
command_text: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="The actual command that was executed",
)
host: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="Where the command was executed (hostname or IP)",
)
shell_type: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
doc="Type of shell (bash, powershell, sql, docker, etc.)",
)
success: Mapped[Optional[bool]] = mapped_column(
Boolean,
nullable=True,
doc="Whether the command succeeded",
)
output_summary: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Summary of command output (first/last lines or error)",
)
# Failure tracking
exit_code: Mapped[Optional[int]] = mapped_column(
Integer,
nullable=True,
doc="Command exit code (non-zero indicates failure)",
)
error_message: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Full error text if command failed",
)
failure_category: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
doc="Category of failure (compatibility, permission, syntax, environmental)",
)
resolution: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="How the failure was fixed (if resolved)",
)
resolved: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
server_default="0",
doc="Whether the failure has been resolved",
)
# Execution metadata
execution_order: Mapped[Optional[int]] = mapped_column(
Integer,
nullable=True,
doc="Sequence number within work item",
)
# Timestamp
created_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the command was executed",
)
# Table constraints
__table_args__ = (
Index("idx_commands_work_item", "work_item_id"),
Index("idx_commands_session", "session_id"),
Index("idx_commands_host", "host"),
Index("idx_commands_success", "success"),
Index("idx_commands_failure_category", "failure_category"),
)
def __repr__(self) -> str:
"""String representation of the command run."""
cmd_preview = self.command_text[:50] + "..." if len(self.command_text) > 50 else self.command_text
return f"<CommandRun(id={self.id}, command={cmd_preview}, success={self.success})>"

View File

@@ -0,0 +1,124 @@
"""
ContextSnippet model for storing reusable context snippets.
Stores small, highly compressed pieces of information like technical decisions,
configurations, patterns, and lessons learned for quick retrieval.
"""
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Float, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .client import Client
from .project import Project
class ContextSnippet(Base, UUIDMixin, TimestampMixin):
"""
ContextSnippet model for storing reusable context snippets.
Stores small, highly compressed pieces of information like technical
decisions, configurations, patterns, and lessons learned. These snippets
are designed for quick retrieval and reuse across conversations.
Attributes:
category: Category of snippet (tech_decision, configuration, pattern, lesson_learned)
title: Brief title describing the snippet
dense_content: Highly compressed information content
structured_data: JSON object for optional structured representation
tags: JSON array of tags for retrieval and categorization
project_id: Foreign key to projects (optional)
client_id: Foreign key to clients (optional)
relevance_score: Float score for ranking relevance (default 1.0)
usage_count: Integer count of how many times this snippet was retrieved (default 0)
project: Relationship to Project model
client: Relationship to Client model
"""
__tablename__ = "context_snippets"
# Foreign keys
project_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("projects.id", ondelete="SET NULL"),
doc="Foreign key to projects (optional)"
)
client_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("clients.id", ondelete="SET NULL"),
doc="Foreign key to clients (optional)"
)
# Snippet metadata
category: Mapped[str] = mapped_column(
String(100),
nullable=False,
doc="Category: tech_decision, configuration, pattern, lesson_learned"
)
title: Mapped[str] = mapped_column(
String(200),
nullable=False,
doc="Brief title describing the snippet"
)
# Content
dense_content: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Highly compressed information content"
)
structured_data: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON object for optional structured representation"
)
# Retrieval metadata
tags: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of tags for retrieval and categorization"
)
relevance_score: Mapped[float] = mapped_column(
Float,
default=1.0,
server_default="1.0",
doc="Float score for ranking relevance (default 1.0)"
)
usage_count: Mapped[int] = mapped_column(
Integer,
default=0,
server_default="0",
doc="Integer count of how many times this snippet was retrieved"
)
# Relationships
project: Mapped[Optional["Project"]] = relationship(
"Project",
doc="Relationship to Project model"
)
client: Mapped[Optional["Client"]] = relationship(
"Client",
doc="Relationship to Client model"
)
# Indexes
__table_args__ = (
Index("idx_context_snippets_project", "project_id"),
Index("idx_context_snippets_client", "client_id"),
Index("idx_context_snippets_category", "category"),
Index("idx_context_snippets_relevance", "relevance_score"),
Index("idx_context_snippets_usage", "usage_count"),
)
def __repr__(self) -> str:
"""String representation of the context snippet."""
return f"<ContextSnippet(title='{self.title}', category='{self.category}', usage={self.usage_count})>"

View File

@@ -0,0 +1,135 @@
"""
ConversationContext model for storing Claude's conversation context.
Stores compressed summaries of conversations, sessions, and project states
for cross-machine recall and context continuity.
"""
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Float, ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .machine import Machine
from .project import Project
from .session import Session
class ConversationContext(Base, UUIDMixin, TimestampMixin):
"""
ConversationContext model for storing Claude's conversation context.
Stores compressed, structured summaries of conversations, work sessions,
and project states to enable Claude to recall important context across
different machines and conversation sessions.
Attributes:
session_id: Foreign key to sessions (optional - not all contexts are work sessions)
project_id: Foreign key to projects (optional)
context_type: Type of context (session_summary, project_state, general_context)
title: Brief title describing the context
dense_summary: Compressed, structured summary (JSON or dense text)
key_decisions: JSON array of important decisions made
current_state: JSON object describing what's currently in progress
tags: JSON array of tags for retrieval and categorization
relevance_score: Float score for ranking relevance (default 1.0)
machine_id: Foreign key to machines (which machine created this context)
session: Relationship to Session model
project: Relationship to Project model
machine: Relationship to Machine model
"""
__tablename__ = "conversation_contexts"
# Foreign keys
session_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("sessions.id", ondelete="SET NULL"),
doc="Foreign key to sessions (optional - not all contexts are work sessions)"
)
project_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("projects.id", ondelete="SET NULL"),
doc="Foreign key to projects (optional)"
)
machine_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("machines.id", ondelete="SET NULL"),
doc="Foreign key to machines (which machine created this context)"
)
# Context metadata
context_type: Mapped[str] = mapped_column(
String(50),
nullable=False,
doc="Type of context: session_summary, project_state, general_context"
)
title: Mapped[str] = mapped_column(
String(200),
nullable=False,
doc="Brief title describing the context"
)
# Context content
dense_summary: Mapped[Optional[str]] = mapped_column(
Text,
doc="Compressed, structured summary (JSON or dense text)"
)
key_decisions: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of important decisions made"
)
current_state: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON object describing what's currently in progress"
)
# Retrieval metadata
tags: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of tags for retrieval and categorization"
)
relevance_score: Mapped[float] = mapped_column(
Float,
default=1.0,
server_default="1.0",
doc="Float score for ranking relevance (default 1.0)"
)
# Relationships
session: Mapped[Optional["Session"]] = relationship(
"Session",
doc="Relationship to Session model"
)
project: Mapped[Optional["Project"]] = relationship(
"Project",
doc="Relationship to Project model"
)
machine: Mapped[Optional["Machine"]] = relationship(
"Machine",
doc="Relationship to Machine model"
)
# Indexes
__table_args__ = (
Index("idx_conversation_contexts_session", "session_id"),
Index("idx_conversation_contexts_project", "project_id"),
Index("idx_conversation_contexts_machine", "machine_id"),
Index("idx_conversation_contexts_type", "context_type"),
Index("idx_conversation_contexts_relevance", "relevance_score"),
)
def __repr__(self) -> str:
"""String representation of the conversation context."""
return f"<ConversationContext(title='{self.title}', type='{self.context_type}', relevance={self.relevance_score})>"

231
api/models/credential.py Normal file
View File

@@ -0,0 +1,231 @@
"""
Credential model for secure storage of authentication credentials.
This model stores various types of credentials (passwords, API keys, OAuth tokens, etc.)
with encryption for sensitive fields.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import (
Boolean,
CHAR,
CheckConstraint,
ForeignKey,
Index,
Integer,
LargeBinary,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.models.base import Base, TimestampMixin, UUIDMixin
class Credential(UUIDMixin, TimestampMixin, Base):
"""
Stores authentication credentials for various services.
Supports multiple credential types including passwords, API keys, OAuth,
SSH keys, and more. Sensitive data is stored encrypted using AES-256-GCM.
Attributes:
id: UUID primary key
client_id: Reference to client this credential belongs to
service_id: Reference to service this credential is for
infrastructure_id: Reference to infrastructure component
credential_type: Type of credential (password, api_key, oauth, etc.)
service_name: Display name for the service (e.g., "Gitea Admin")
username: Username for authentication
password_encrypted: AES-256-GCM encrypted password
api_key_encrypted: Encrypted API key
client_id_oauth: OAuth client ID
client_secret_encrypted: Encrypted OAuth client secret
tenant_id_oauth: OAuth tenant ID
public_key: SSH public key (not encrypted)
token_encrypted: Encrypted bearer/access token
connection_string_encrypted: Encrypted connection string
integration_code: Integration code for services like Autotask
external_url: External URL for the service
internal_url: Internal URL for the service
custom_port: Custom port number if applicable
role_description: Description of access level/role
requires_vpn: Whether VPN is required for access
requires_2fa: Whether 2FA is required
ssh_key_auth_enabled: Whether SSH key authentication is enabled
access_level: Description of access level
expires_at: When the credential expires
last_rotated_at: When the credential was last rotated
is_active: Whether the credential is currently active
created_at: Creation timestamp
updated_at: Last update timestamp
"""
__tablename__ = "credentials"
# Foreign keys
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
nullable=True,
doc="Reference to client",
)
service_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("services.id", ondelete="CASCADE"),
nullable=True,
doc="Reference to service",
)
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="CASCADE"),
nullable=True,
doc="Reference to infrastructure component",
)
# Credential type and service info
credential_type: Mapped[str] = mapped_column(
String(50),
nullable=False,
doc="Type of credential",
)
service_name: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="Display name for the service",
)
# Authentication fields
username: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="Username for authentication",
)
password_encrypted: Mapped[Optional[bytes]] = mapped_column(
LargeBinary,
nullable=True,
doc="AES-256-GCM encrypted password",
)
api_key_encrypted: Mapped[Optional[bytes]] = mapped_column(
LargeBinary,
nullable=True,
doc="Encrypted API key",
)
# OAuth fields
client_id_oauth: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="OAuth client ID",
)
client_secret_encrypted: Mapped[Optional[bytes]] = mapped_column(
LargeBinary,
nullable=True,
doc="Encrypted OAuth client secret",
)
tenant_id_oauth: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="OAuth tenant ID",
)
# SSH and token fields
public_key: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="SSH public key",
)
token_encrypted: Mapped[Optional[bytes]] = mapped_column(
LargeBinary,
nullable=True,
doc="Encrypted bearer/access token",
)
connection_string_encrypted: Mapped[Optional[bytes]] = mapped_column(
LargeBinary,
nullable=True,
doc="Encrypted connection string",
)
integration_code: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="Integration code for services like Autotask",
)
# Metadata
external_url: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
doc="External URL for the service",
)
internal_url: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
doc="Internal URL for the service",
)
custom_port: Mapped[Optional[int]] = mapped_column(
Integer,
nullable=True,
doc="Custom port number",
)
role_description: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
doc="Description of access level/role",
)
requires_vpn: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
server_default="0",
doc="Whether VPN is required",
)
requires_2fa: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
server_default="0",
doc="Whether 2FA is required",
)
ssh_key_auth_enabled: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
server_default="0",
doc="Whether SSH key authentication is enabled",
)
access_level: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
doc="Description of access level",
)
# Lifecycle
expires_at: Mapped[Optional[datetime]] = mapped_column(
nullable=True,
doc="Expiration timestamp",
)
last_rotated_at: Mapped[Optional[datetime]] = mapped_column(
nullable=True,
doc="Last rotation timestamp",
)
is_active: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
server_default="1",
doc="Whether the credential is active",
)
# Table constraints
__table_args__ = (
CheckConstraint(
"credential_type IN ('password', 'api_key', 'oauth', 'ssh_key', 'shared_secret', 'jwt', 'connection_string', 'certificate')",
name="ck_credentials_type",
),
Index("idx_credentials_client", "client_id"),
Index("idx_credentials_service", "service_id"),
Index("idx_credentials_type", "credential_type"),
Index("idx_credentials_active", "is_active"),
)
def __repr__(self) -> str:
"""String representation of the credential."""
return f"<Credential(id={self.id}, service_name={self.service_name}, type={self.credential_type})>"

View File

@@ -0,0 +1,95 @@
"""
Credential audit log model for tracking credential access and modifications.
This model provides a comprehensive audit trail for all credential-related
operations including views, updates, rotations, and decryptions.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import CHAR, CheckConstraint, ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from api.models.base import Base, UUIDMixin
class CredentialAuditLog(UUIDMixin, Base):
"""
Audit trail for credential access and modifications.
Records all operations performed on credentials including who accessed them,
when, from where, and what action was performed.
Attributes:
id: UUID primary key
credential_id: Reference to the credential
action: Type of action performed (view, create, update, delete, rotate, decrypt)
user_id: User who performed the action (JWT sub claim)
ip_address: IP address of the user
user_agent: Browser/client user agent
details: JSON string with additional context about the action
timestamp: When the action was performed
"""
__tablename__ = "credential_audit_log"
# Foreign keys
credential_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("credentials.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to credential",
)
# Action details
action: Mapped[str] = mapped_column(
String(50),
nullable=False,
doc="Type of action performed",
)
user_id: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="User who performed the action (JWT sub claim)",
)
# Context information
ip_address: Mapped[Optional[str]] = mapped_column(
String(45),
nullable=True,
doc="IP address (IPv4 or IPv6)",
)
user_agent: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Browser/client user agent string",
)
details: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON string with additional context (what changed, why, etc.)",
)
# Timestamp
timestamp: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the action was performed",
)
# Table constraints
__table_args__ = (
CheckConstraint(
"action IN ('view', 'create', 'update', 'delete', 'rotate', 'decrypt')",
name="ck_credential_audit_action",
),
Index("idx_cred_audit_credential", "credential_id"),
Index("idx_cred_audit_user", "user_id"),
Index("idx_cred_audit_timestamp", "timestamp"),
)
def __repr__(self) -> str:
"""String representation of the audit log entry."""
return f"<CredentialAuditLog(id={self.id}, action={self.action}, user={self.user_id}, timestamp={self.timestamp})>"

View File

@@ -0,0 +1,88 @@
"""
Credential permission model for access control.
This model manages fine-grained access control for credentials,
supporting future team expansion with role-based permissions.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import (
CHAR,
CheckConstraint,
ForeignKey,
Index,
String,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from api.models.base import Base, UUIDMixin
class CredentialPermission(UUIDMixin, Base):
"""
Access control for credentials.
Manages who can access specific credentials and what level of access they have.
Supports read, write, and admin permission levels.
Attributes:
id: UUID primary key
credential_id: Reference to the credential
user_id: User or role ID who has access
permission_level: Level of access (read, write, admin)
granted_at: When the permission was granted
granted_by: Who granted the permission
"""
__tablename__ = "credential_permissions"
# Foreign keys
credential_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("credentials.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to credential",
)
# Permission details
user_id: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="User or role ID who has access",
)
permission_level: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
doc="Level of access",
)
# Metadata
granted_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the permission was granted",
)
granted_by: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="Who granted the permission",
)
# Table constraints
__table_args__ = (
CheckConstraint(
"permission_level IN ('read', 'write', 'admin')",
name="ck_credential_permissions_level",
),
UniqueConstraint("credential_id", "user_id", name="uq_credential_user"),
Index("idx_cred_perm_credential", "credential_id"),
Index("idx_cred_perm_user", "user_id"),
)
def __repr__(self) -> str:
"""String representation of the credential permission."""
return f"<CredentialPermission(id={self.id}, user={self.user_id}, level={self.permission_level})>"

View File

@@ -0,0 +1,152 @@
"""
Database change model for tracking database schema and data modifications.
Tracks database changes including schema modifications, data updates, index
creation, optimizations, and cleanup operations with backup tracking.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import BigInteger, Boolean, CHAR, CheckConstraint, ForeignKey, Index, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
if TYPE_CHECKING:
from .infrastructure import Infrastructure
from .session import Session
from .work_item import WorkItem
class DatabaseChange(Base, UUIDMixin):
"""
Database change model for tracking database modifications.
Records all database schema and data changes including DDL operations,
data modifications, index management, optimizations, and cleanup tasks.
Tracks affected rows, backup status, and freed space for audit and
rollback purposes.
Attributes:
work_item_id: Foreign key to work_items table (required)
session_id: Foreign key to sessions table (required)
database_name: Name of the database that was modified
infrastructure_id: Foreign key to infrastructure table
change_type: Type of database change
sql_executed: SQL statements that were executed
rows_affected: Number of rows affected by the change
size_freed_bytes: Bytes freed by cleanup operations
backup_taken: Whether a backup was taken before the change
backup_location: Path or identifier of the backup
created_at: When the change was made
work_item: Relationship to WorkItem model
session: Relationship to Session model
infrastructure: Relationship to Infrastructure model
"""
__tablename__ = "database_changes"
# Foreign keys
work_item_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
nullable=False,
doc="Foreign key to work_items table (required)"
)
session_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
doc="Foreign key to sessions table (required)"
)
database_name: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="Name of the database that was modified"
)
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="SET NULL"),
doc="Foreign key to infrastructure table"
)
# Change details
change_type: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Type of change: schema, data, index, optimization, cleanup, migration"
)
sql_executed: Mapped[Optional[str]] = mapped_column(
Text,
doc="SQL statements that were executed"
)
rows_affected: Mapped[Optional[int]] = mapped_column(
BigInteger,
doc="Number of rows affected by the change"
)
size_freed_bytes: Mapped[Optional[int]] = mapped_column(
BigInteger,
doc="Bytes freed by cleanup operations"
)
# Backup tracking
backup_taken: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
nullable=False,
doc="Whether a backup was taken before the change"
)
backup_location: Mapped[Optional[str]] = mapped_column(
String(500),
doc="Path or identifier of the backup"
)
# Timestamp
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=func.now(),
doc="When the change was made"
)
# Relationships
work_item: Mapped["WorkItem"] = relationship(
"WorkItem",
back_populates="database_changes",
doc="Relationship to WorkItem model"
)
session: Mapped["Session"] = relationship(
"Session",
back_populates="database_changes",
doc="Relationship to Session model"
)
infrastructure: Mapped[Optional["Infrastructure"]] = relationship(
"Infrastructure",
back_populates="database_changes",
doc="Relationship to Infrastructure model"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"change_type IN ('schema', 'data', 'index', 'optimization', 'cleanup', 'migration')",
name="ck_database_changes_type"
),
Index("idx_db_changes_work_item", "work_item_id"),
Index("idx_db_changes_database", "database_name"),
)
def __repr__(self) -> str:
"""String representation of the database change."""
return f"<DatabaseChange(database='{self.database_name}', type='{self.change_type}', rows={self.rows_affected})>"

115
api/models/decision_log.py Normal file
View File

@@ -0,0 +1,115 @@
"""
DecisionLog model for tracking important decisions made during work.
Stores decisions with their rationale, alternatives considered, and impact
to provide decision history and context for future work.
"""
from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .project import Project
from .session import Session
class DecisionLog(Base, UUIDMixin, TimestampMixin):
"""
DecisionLog model for tracking important decisions made during work.
Stores decisions with their type, rationale, alternatives considered,
and impact assessment. This provides a decision history that can be
referenced in future conversations and work sessions.
Attributes:
decision_type: Type of decision (technical, architectural, process, security)
decision_text: What was decided (the actual decision)
rationale: Why this decision was made
alternatives_considered: JSON array of other options that were considered
impact: Impact level (low, medium, high, critical)
project_id: Foreign key to projects (optional)
session_id: Foreign key to sessions (optional)
tags: JSON array of tags for retrieval and categorization
project: Relationship to Project model
session: Relationship to Session model
"""
__tablename__ = "decision_logs"
# Foreign keys
project_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("projects.id", ondelete="SET NULL"),
doc="Foreign key to projects (optional)"
)
session_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("sessions.id", ondelete="SET NULL"),
doc="Foreign key to sessions (optional)"
)
# Decision metadata
decision_type: Mapped[str] = mapped_column(
String(100),
nullable=False,
doc="Type of decision: technical, architectural, process, security"
)
impact: Mapped[str] = mapped_column(
String(50),
default="medium",
server_default="medium",
doc="Impact level: low, medium, high, critical"
)
# Decision content
decision_text: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="What was decided (the actual decision)"
)
rationale: Mapped[Optional[str]] = mapped_column(
Text,
doc="Why this decision was made"
)
alternatives_considered: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of other options that were considered"
)
# Retrieval metadata
tags: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of tags for retrieval and categorization"
)
# Relationships
project: Mapped[Optional["Project"]] = relationship(
"Project",
doc="Relationship to Project model"
)
session: Mapped[Optional["Session"]] = relationship(
"Session",
doc="Relationship to Session model"
)
# Indexes
__table_args__ = (
Index("idx_decision_logs_project", "project_id"),
Index("idx_decision_logs_session", "session_id"),
Index("idx_decision_logs_type", "decision_type"),
Index("idx_decision_logs_impact", "impact"),
)
def __repr__(self) -> str:
"""String representation of the decision log."""
decision_preview = self.decision_text[:50] + "..." if len(self.decision_text) > 50 else self.decision_text
return f"<DecisionLog(type='{self.decision_type}', impact='{self.impact}', decision='{decision_preview}')>"

167
api/models/deployment.py Normal file
View File

@@ -0,0 +1,167 @@
"""
Deployment model for tracking software and configuration deployments.
Tracks deployments of code, configuration, database changes, containers,
and service restarts with version control and rollback capabilities.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, CHAR, CheckConstraint, ForeignKey, Index, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
if TYPE_CHECKING:
from .infrastructure import Infrastructure
from .service import Service
from .session import Session
from .work_item import WorkItem
class Deployment(Base, UUIDMixin):
"""
Deployment model for tracking software and configuration deployments.
Records deployments of code, configuration files, database changes,
containers, and service restarts. Includes version tracking, source/
destination paths, and rollback procedures for operational safety.
Attributes:
work_item_id: Foreign key to work_items table (required)
session_id: Foreign key to sessions table (required)
infrastructure_id: Foreign key to infrastructure table
service_id: Foreign key to services table
deployment_type: Type of deployment (code, config, database, etc.)
version: Version identifier for this deployment
description: Detailed description of what was deployed
deployed_from: Source path or repository
deployed_to: Destination path or target system
rollback_available: Whether rollback is possible
rollback_procedure: Instructions for rolling back this deployment
created_at: When the deployment occurred
work_item: Relationship to WorkItem model
session: Relationship to Session model
infrastructure: Relationship to Infrastructure model
service: Relationship to Service model
"""
__tablename__ = "deployments"
# Foreign keys
work_item_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
nullable=False,
doc="Foreign key to work_items table (required)"
)
session_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
doc="Foreign key to sessions table (required)"
)
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="SET NULL"),
doc="Foreign key to infrastructure table"
)
service_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("services.id", ondelete="SET NULL"),
doc="Foreign key to services table"
)
# Deployment details
deployment_type: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Type of deployment: code, config, database, container, service_restart"
)
version: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Version identifier for this deployment"
)
description: Mapped[Optional[str]] = mapped_column(
Text,
doc="Detailed description of what was deployed"
)
# Source and destination
deployed_from: Mapped[Optional[str]] = mapped_column(
String(500),
doc="Source path or repository (e.g., /home/user/app, git@github.com:user/repo)"
)
deployed_to: Mapped[Optional[str]] = mapped_column(
String(500),
doc="Destination path or target system (e.g., /var/www/app, container-name)"
)
# Rollback capability
rollback_available: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
nullable=False,
doc="Whether rollback is possible for this deployment"
)
rollback_procedure: Mapped[Optional[str]] = mapped_column(
Text,
doc="Instructions for rolling back this deployment"
)
# Timestamp
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=func.now(),
doc="When the deployment occurred"
)
# Relationships
work_item: Mapped["WorkItem"] = relationship(
"WorkItem",
back_populates="deployments",
doc="Relationship to WorkItem model"
)
session: Mapped["Session"] = relationship(
"Session",
back_populates="deployments",
doc="Relationship to Session model"
)
infrastructure: Mapped[Optional["Infrastructure"]] = relationship(
"Infrastructure",
back_populates="deployments",
doc="Relationship to Infrastructure model"
)
service: Mapped[Optional["Service"]] = relationship(
"Service",
back_populates="deployments",
doc="Relationship to Service model"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"deployment_type IN ('code', 'config', 'database', 'container', 'service_restart')",
name="ck_deployments_type"
),
Index("idx_deployments_work_item", "work_item_id"),
Index("idx_deployments_infrastructure", "infrastructure_id"),
Index("idx_deployments_service", "service_id"),
)
def __repr__(self) -> str:
"""String representation of the deployment."""
return f"<Deployment(type='{self.deployment_type}', version='{self.version}', to='{self.deployed_to}')>"

View File

@@ -0,0 +1,145 @@
"""
Environmental Insight model for Context Learning system.
This model stores generated insights about client/infrastructure environments,
helping Claude learn from failures and provide better suggestions over time.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import (
CHAR,
CheckConstraint,
ForeignKey,
Index,
Integer,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
class EnvironmentalInsight(Base, UUIDMixin, TimestampMixin):
"""
Environmental insights for client/infrastructure environments.
Stores learned insights about environmental constraints, configurations,
and best practices discovered through failure analysis and verification.
Used to generate insights.md files and provide context-aware suggestions.
Attributes:
id: Unique identifier
client_id: Reference to the client this insight applies to
infrastructure_id: Reference to specific infrastructure if applicable
insight_category: Category of insight (command_constraints, service_configuration, etc.)
insight_title: Brief title describing the insight
insight_description: Detailed markdown-formatted description
examples: JSON array of command/configuration examples
source_pattern_id: Reference to failure pattern that generated this insight
confidence_level: How confident we are (confirmed, likely, suspected)
verification_count: Number of times this insight has been verified
priority: Priority level (1-10, higher = more important)
last_verified: When this insight was last verified
created_at: When the insight was created
updated_at: When the insight was last updated
"""
__tablename__ = "environmental_insights"
# Foreign keys
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
nullable=True,
doc="Client this insight applies to",
)
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="CASCADE"),
nullable=True,
doc="Specific infrastructure if applicable",
)
# Insight content
insight_category: Mapped[str] = mapped_column(
String(100),
nullable=False,
doc="Category of insight",
)
insight_title: Mapped[str] = mapped_column(
String(500),
nullable=False,
doc="Brief title describing the insight",
)
insight_description: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Detailed markdown-formatted description",
)
examples: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON array of command/configuration examples",
)
# Metadata
source_pattern_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("failure_patterns.id", ondelete="SET NULL"),
nullable=True,
doc="Failure pattern that generated this insight",
)
confidence_level: Mapped[Optional[str]] = mapped_column(
String(20),
nullable=True,
doc="Confidence level in this insight",
)
verification_count: Mapped[int] = mapped_column(
Integer,
default=1,
server_default="1",
nullable=False,
doc="Number of times verified",
)
priority: Mapped[int] = mapped_column(
Integer,
default=5,
server_default="5",
nullable=False,
doc="Priority level (1-10, higher = more important)",
)
last_verified: Mapped[Optional[datetime]] = mapped_column(
nullable=True,
doc="When this insight was last verified",
)
# Indexes and constraints
__table_args__ = (
CheckConstraint(
"insight_category IN ('command_constraints', 'service_configuration', 'version_limitations', 'custom_installations', 'network_constraints', 'permissions')",
name="ck_insights_category",
),
CheckConstraint(
"confidence_level IN ('confirmed', 'likely', 'suspected')",
name="ck_insights_confidence",
),
Index("idx_insights_client", "client_id"),
Index("idx_insights_infrastructure", "infrastructure_id"),
Index("idx_insights_category", "insight_category"),
)
# Relationships
# client = relationship("Client", back_populates="environmental_insights")
# infrastructure = relationship("Infrastructure", back_populates="environmental_insights")
# source_pattern = relationship("FailurePattern", back_populates="generated_insights")
def __repr__(self) -> str:
"""String representation of the environmental insight."""
return (
f"<EnvironmentalInsight(id={self.id!r}, "
f"category={self.insight_category!r}, "
f"title={self.insight_title!r})>"
)

View File

@@ -0,0 +1,127 @@
"""
External Integration model for tracking external system interactions.
This model logs all interactions with external systems like SyncroMSP,
MSP Backups, Zapier webhooks, and other third-party integrations.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import CHAR, ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
class ExternalIntegration(Base, UUIDMixin):
"""
External integration tracking for third-party system interactions.
Logs all API calls, webhook triggers, and data exchanges with external
systems. Useful for debugging, auditing, and understanding integration patterns.
Attributes:
id: Unique identifier
session_id: Reference to the session during which integration occurred
work_item_id: Reference to the work item this integration relates to
integration_type: Type of integration (syncro_ticket, msp_backups, zapier_webhook)
external_id: External system's identifier (ticket ID, asset ID, etc.)
external_url: Direct link to the external resource
action: What action was performed (created, updated, linked, attached)
direction: Direction of data flow (outbound, inbound)
request_data: JSON data that was sent to external system
response_data: JSON data received from external system
created_at: When the integration occurred
created_by: User who authorized the integration
"""
__tablename__ = "external_integrations"
# Foreign keys
session_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=True,
doc="Session during which integration occurred",
)
work_item_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
nullable=True,
doc="Work item this integration relates to",
)
# Integration details
integration_type: Mapped[str] = mapped_column(
String(100),
nullable=False,
doc="Type of integration (syncro_ticket, msp_backups, zapier_webhook, etc.)",
)
external_id: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="External system's identifier (ticket ID, asset ID, etc.)",
)
external_url: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
doc="Direct link to the external resource",
)
# Action tracking
action: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
doc="Action performed (created, updated, linked, attached)",
)
direction: Mapped[Optional[str]] = mapped_column(
String(20),
nullable=True,
doc="Direction of data flow (outbound, inbound)",
)
# Data
request_data: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON data sent to external system",
)
response_data: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON data received from external system",
)
# Metadata
created_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the integration occurred",
)
created_by: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
doc="User who authorized the integration",
)
# Indexes
__table_args__ = (
Index("idx_ext_int_session", "session_id"),
Index("idx_ext_int_type", "integration_type"),
Index("idx_ext_int_external", "external_id"),
)
# Relationships
# session = relationship("Session", back_populates="external_integrations")
# work_item = relationship("WorkItem", back_populates="external_integrations")
def __repr__(self) -> str:
"""String representation of the external integration."""
return (
f"<ExternalIntegration(id={self.id!r}, "
f"type={self.integration_type!r}, "
f"action={self.action!r}, "
f"external_id={self.external_id!r})>"
)

View File

@@ -0,0 +1,184 @@
"""
Failure pattern model for tracking recurring environmental and compatibility issues.
This model identifies and documents patterns of failures across systems and clients,
enabling proactive problem resolution and system insights.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import (
Boolean,
CHAR,
CheckConstraint,
ForeignKey,
Index,
Integer,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from api.models.base import Base, TimestampMixin, UUIDMixin
class FailurePattern(UUIDMixin, TimestampMixin, Base):
"""
Track recurring failure patterns and environmental limitations.
Documents patterns of failures that occur due to compatibility issues,
environmental limitations, or system-specific constraints. Used to build
institutional knowledge and prevent repeated mistakes.
Attributes:
id: UUID primary key
infrastructure_id: Reference to affected infrastructure
client_id: Reference to affected client
pattern_type: Type of failure pattern
pattern_signature: Brief identifier for the pattern
error_pattern: Regex or keywords to match this failure
affected_systems: JSON array of affected systems
triggering_commands: JSON array of command patterns that trigger this
triggering_operations: JSON array of operation types that trigger this
failure_description: Detailed description of the failure
root_cause: Why this failure occurs
recommended_solution: The recommended approach to avoid/fix this
alternative_approaches: JSON array of alternative solutions
occurrence_count: How many times this pattern has been observed
first_seen: When this pattern was first observed
last_seen: When this pattern was last observed
severity: Impact level (blocking, major, minor, info)
is_active: Whether this pattern is still relevant
added_to_insights: Whether this has been added to insights.md
created_at: Creation timestamp
updated_at: Last update timestamp
"""
__tablename__ = "failure_patterns"
# Foreign keys
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="CASCADE"),
nullable=True,
doc="Reference to affected infrastructure",
)
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
nullable=True,
doc="Reference to affected client",
)
# Pattern identification
pattern_type: Mapped[str] = mapped_column(
String(100),
nullable=False,
doc="Type of failure pattern",
)
pattern_signature: Mapped[str] = mapped_column(
String(500),
nullable=False,
doc="Brief identifier for the pattern (e.g., 'PowerShell 7 cmdlets on Server 2008')",
)
error_pattern: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Regex or keywords to match this failure (e.g., 'Get-LocalUser.*not recognized')",
)
# Context
affected_systems: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON array of affected systems (e.g., ['all_server_2008', 'D2TESTNAS'])",
)
triggering_commands: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON array of command patterns that trigger this failure",
)
triggering_operations: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON array of operation types that trigger this failure",
)
# Resolution
failure_description: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Detailed description of the failure",
)
root_cause: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Why this failure occurs (e.g., 'Server 2008 only has PowerShell 2.0')",
)
recommended_solution: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="The recommended approach to avoid/fix this (e.g., 'Use Get-WmiObject instead')",
)
alternative_approaches: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON array of alternative solutions",
)
# Metadata
occurrence_count: Mapped[int] = mapped_column(
Integer,
nullable=False,
server_default="1",
doc="How many times this pattern has been observed",
)
first_seen: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When this pattern was first observed",
)
last_seen: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When this pattern was last observed",
)
severity: Mapped[Optional[str]] = mapped_column(
String(20),
nullable=True,
doc="Impact level",
)
is_active: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
server_default="1",
doc="Whether this pattern is still relevant",
)
added_to_insights: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
server_default="0",
doc="Whether this has been added to insights.md",
)
# Table constraints
__table_args__ = (
CheckConstraint(
"pattern_type IN ('command_compatibility', 'version_mismatch', 'permission_denied', 'service_unavailable', 'configuration_error', 'environmental_limitation')",
name="ck_failure_patterns_type",
),
CheckConstraint(
"severity IN ('blocking', 'major', 'minor', 'info')",
name="ck_failure_patterns_severity",
),
Index("idx_failure_infrastructure", "infrastructure_id"),
Index("idx_failure_client", "client_id"),
Index("idx_failure_pattern_type", "pattern_type"),
Index("idx_failure_signature", "pattern_signature"),
)
def __repr__(self) -> str:
"""String representation of the failure pattern."""
return f"<FailurePattern(id={self.id}, signature={self.pattern_signature}, severity={self.severity}, count={self.occurrence_count})>"

99
api/models/file_change.py Normal file
View File

@@ -0,0 +1,99 @@
"""
File change model for tracking file operations during work sessions.
This model records all file modifications, creations, deletions, and renames
performed during work sessions.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import CHAR, CheckConstraint, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from api.models.base import Base, UUIDMixin
class FileChange(UUIDMixin, Base):
"""
Track file changes during work sessions.
Records all file operations including creations, modifications, deletions,
renames, and backups performed during work items.
Attributes:
id: UUID primary key
work_item_id: Reference to the work item
session_id: Reference to the session
file_path: Path to the file that was changed
change_type: Type of change (created, modified, deleted, renamed, backed_up)
backup_path: Path to backup if one was created
size_bytes: File size in bytes
description: Description of the change
created_at: When the change was recorded
"""
__tablename__ = "file_changes"
# Foreign keys
work_item_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to work item",
)
session_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to session",
)
# File details
file_path: Mapped[str] = mapped_column(
String(1000),
nullable=False,
doc="Path to the file that was changed",
)
change_type: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
doc="Type of change",
)
backup_path: Mapped[Optional[str]] = mapped_column(
String(1000),
nullable=True,
doc="Path to backup file if created",
)
size_bytes: Mapped[Optional[int]] = mapped_column(
Integer,
nullable=True,
doc="File size in bytes",
)
description: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Description of the change",
)
# Timestamp
created_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the change was recorded",
)
# Table constraints
__table_args__ = (
CheckConstraint(
"change_type IN ('created', 'modified', 'deleted', 'renamed', 'backed_up')",
name="ck_file_changes_type",
),
Index("idx_file_changes_work_item", "work_item_id"),
Index("idx_file_changes_session", "session_id"),
)
def __repr__(self) -> str:
"""String representation of the file change."""
return f"<FileChange(id={self.id}, file={self.file_path}, type={self.change_type})>"

108
api/models/firewall_rule.py Normal file
View File

@@ -0,0 +1,108 @@
"""
Firewall rule model for network security rules.
Firewall rules track network security rules for documentation and audit trail
purposes, including source/destination CIDRs, ports, protocols, and actions.
"""
from typing import Optional
from sqlalchemy import CHAR, CheckConstraint, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base, TimestampMixin, UUIDMixin
class FirewallRule(Base, UUIDMixin, TimestampMixin):
"""
Firewall rule model for network security rules.
Tracks firewall rules for documentation and audit trail purposes,
including source and destination CIDRs, ports, protocols, and
allow/deny/drop actions.
Attributes:
infrastructure_id: Reference to the infrastructure this rule applies to
rule_name: Name of the firewall rule
source_cidr: Source CIDR notation
destination_cidr: Destination CIDR notation
port: Port number
protocol: Protocol (tcp, udp, icmp)
action: Action to take (allow, deny, drop)
rule_order: Order of the rule in the firewall
notes: Additional notes
created_at: When the rule was created
created_by: Who created the rule
"""
__tablename__ = "firewall_rules"
# Foreign keys
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="CASCADE"),
doc="Reference to the infrastructure this rule applies to"
)
# Rule identification
rule_name: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Name of the firewall rule"
)
# Rule configuration
source_cidr: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Source CIDR notation"
)
destination_cidr: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Destination CIDR notation"
)
port: Mapped[Optional[int]] = mapped_column(
Integer,
doc="Port number"
)
protocol: Mapped[Optional[str]] = mapped_column(
String(20),
doc="Protocol: tcp, udp, icmp"
)
action: Mapped[Optional[str]] = mapped_column(
String(20),
doc="Action: allow, deny, drop"
)
# Rule ordering
rule_order: Mapped[Optional[int]] = mapped_column(
Integer,
doc="Order of the rule in the firewall"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes"
)
# Audit information
created_by: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Who created the rule"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"action IN ('allow', 'deny', 'drop')",
name="ck_firewall_rules_action"
),
Index("idx_firewall_infra", "infrastructure_id"),
)
def __repr__(self) -> str:
"""String representation of the firewall rule."""
return f"<FirewallRule(rule_name='{self.rule_name}', action='{self.action}')>"

View File

@@ -0,0 +1,198 @@
"""
Infrastructure model for hardware and virtual assets.
Infrastructure represents servers, network devices, workstations, and other
IT assets with detailed configuration and environmental constraints.
"""
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, CHAR, CheckConstraint, ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .database_change import DatabaseChange
from .deployment import Deployment
from .infrastructure_change import InfrastructureChange
class Infrastructure(Base, UUIDMixin, TimestampMixin):
"""
Infrastructure model representing IT assets.
Tracks physical servers, virtual machines, containers, network devices,
NAS storage, workstations, and other infrastructure components with
detailed configuration and environmental constraints.
Attributes:
client_id: Reference to the client
site_id: Reference to the site this infrastructure is located at
asset_type: Type of asset (physical_server, virtual_machine, etc.)
hostname: Hostname of the infrastructure
ip_address: IP address (IPv4 or IPv6)
mac_address: MAC address
os: Operating system name
os_version: Operating system version
role_description: Description of the infrastructure's role
parent_host_id: Reference to parent host for VMs/containers
status: Current status (active, migration_source, etc.)
environmental_notes: Special environmental constraints or notes
powershell_version: PowerShell version if applicable
shell_type: Shell type (bash, cmd, powershell, sh)
package_manager: Package manager (apt, yum, chocolatey, none)
has_gui: Whether the system has a GUI
limitations: JSON array of limitations
notes: Additional notes
"""
__tablename__ = "infrastructure"
# Foreign keys
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
doc="Reference to the client"
)
site_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("sites.id", ondelete="SET NULL"),
doc="Reference to the site this infrastructure is located at"
)
# Asset identification
asset_type: Mapped[str] = mapped_column(
String(50),
nullable=False,
doc="Type: physical_server, virtual_machine, container, network_device, nas_storage, workstation, firewall, domain_controller"
)
hostname: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="Hostname of the infrastructure"
)
ip_address: Mapped[Optional[str]] = mapped_column(
String(45),
doc="IP address (IPv4 or IPv6)"
)
mac_address: Mapped[Optional[str]] = mapped_column(
String(17),
doc="MAC address"
)
# Operating system
os: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Operating system name (e.g., 'Ubuntu 22.04', 'Windows Server 2022')"
)
os_version: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Operating system version (e.g., '6.22', '2008 R2', '22.04')"
)
# Role and hierarchy
role_description: Mapped[Optional[str]] = mapped_column(
Text,
doc="Description of the infrastructure's role"
)
parent_host_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="SET NULL"),
doc="Reference to parent host for VMs/containers"
)
# Status
status: Mapped[str] = mapped_column(
String(50),
default="active",
server_default="active",
nullable=False,
doc="Status: active, migration_source, migration_destination, decommissioned"
)
# Environmental constraints
environmental_notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Special environmental constraints or notes (e.g., 'Manual WINS install', 'ReadyNAS OS, SMB1 only')"
)
powershell_version: Mapped[Optional[str]] = mapped_column(
String(20),
doc="PowerShell version (e.g., '2.0', '5.1', '7.4')"
)
shell_type: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Shell type: bash, cmd, powershell, sh"
)
package_manager: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Package manager: apt, yum, chocolatey, none"
)
has_gui: Mapped[bool] = mapped_column(
Boolean,
default=True,
server_default="1",
nullable=False,
doc="Whether the system has a GUI"
)
limitations: Mapped[Optional[str]] = mapped_column(
Text,
doc='JSON array of limitations (e.g., ["no_ps7", "smb1_only", "dos_6.22_commands"])'
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes"
)
# Relationships
deployments: Mapped[list["Deployment"]] = relationship(
"Deployment",
back_populates="infrastructure",
doc="Relationship to Deployment model"
)
database_changes: Mapped[list["DatabaseChange"]] = relationship(
"DatabaseChange",
back_populates="infrastructure",
doc="Relationship to DatabaseChange model"
)
infrastructure_changes: Mapped[list["InfrastructureChange"]] = relationship(
"InfrastructureChange",
back_populates="infrastructure",
doc="Relationship to InfrastructureChange model"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"asset_type IN ('physical_server', 'virtual_machine', 'container', 'network_device', 'nas_storage', 'workstation', 'firewall', 'domain_controller')",
name="ck_infrastructure_asset_type"
),
CheckConstraint(
"status IN ('active', 'migration_source', 'migration_destination', 'decommissioned')",
name="ck_infrastructure_status"
),
Index("idx_infrastructure_client", "client_id"),
Index("idx_infrastructure_type", "asset_type"),
Index("idx_infrastructure_hostname", "hostname"),
Index("idx_infrastructure_parent", "parent_host_id"),
Index("idx_infrastructure_os", "os"),
)
def __repr__(self) -> str:
"""String representation of the infrastructure."""
return f"<Infrastructure(hostname='{self.hostname}', asset_type='{self.asset_type}')>"

View File

@@ -0,0 +1,165 @@
"""
Infrastructure change model for tracking infrastructure modifications.
Tracks changes to infrastructure including DNS, firewall, routing, SSL,
containers, and other infrastructure components with audit trail and
rollback procedures.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, CHAR, CheckConstraint, ForeignKey, Index, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
if TYPE_CHECKING:
from .infrastructure import Infrastructure
from .session import Session
from .work_item import WorkItem
class InfrastructureChange(Base, UUIDMixin):
"""
Infrastructure change model for audit trail of infrastructure modifications.
Records changes to infrastructure components including DNS configuration,
firewall rules, routing tables, SSL certificates, containers, service
configurations, hardware, network, and storage. Tracks before/after state,
rollback procedures, and verification status for operational safety.
Attributes:
work_item_id: Foreign key to work_items table (required)
session_id: Foreign key to sessions table (required)
infrastructure_id: Foreign key to infrastructure table
change_type: Type of infrastructure change
target_system: System or component that was modified
before_state: State before the change (configuration snapshot)
after_state: State after the change (configuration snapshot)
is_permanent: Whether this is a permanent change or temporary
rollback_procedure: Instructions for rolling back this change
verification_performed: Whether verification was performed after change
verification_notes: Notes about verification testing
created_at: When the change was made
work_item: Relationship to WorkItem model
session: Relationship to Session model
infrastructure: Relationship to Infrastructure model
"""
__tablename__ = "infrastructure_changes"
# Foreign keys
work_item_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
nullable=False,
doc="Foreign key to work_items table (required)"
)
session_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
doc="Foreign key to sessions table (required)"
)
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="SET NULL"),
doc="Foreign key to infrastructure table"
)
# Change details
change_type: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Type of change: dns, firewall, routing, ssl, container, service_config, hardware, network, storage"
)
target_system: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="System or component that was modified (e.g., 'jupiter', 'UDM-Pro', 'web-container')"
)
# State tracking
before_state: Mapped[Optional[str]] = mapped_column(
Text,
doc="Configuration or state before the change (snapshot, config dump, etc.)"
)
after_state: Mapped[Optional[str]] = mapped_column(
Text,
doc="Configuration or state after the change (snapshot, config dump, etc.)"
)
# Change characteristics
is_permanent: Mapped[bool] = mapped_column(
Boolean,
default=True,
server_default="1",
nullable=False,
doc="Whether this is a permanent change or temporary (e.g., for testing)"
)
rollback_procedure: Mapped[Optional[str]] = mapped_column(
Text,
doc="Instructions for rolling back this change if needed"
)
# Verification
verification_performed: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
nullable=False,
doc="Whether verification testing was performed after the change"
)
verification_notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Notes about verification testing (what was tested, results, etc.)"
)
# Timestamp
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=func.now(),
doc="When the change was made"
)
# Relationships
work_item: Mapped["WorkItem"] = relationship(
"WorkItem",
back_populates="infrastructure_changes",
doc="Relationship to WorkItem model"
)
session: Mapped["Session"] = relationship(
"Session",
back_populates="infrastructure_changes",
doc="Relationship to Session model"
)
infrastructure: Mapped[Optional["Infrastructure"]] = relationship(
"Infrastructure",
back_populates="infrastructure_changes",
doc="Relationship to Infrastructure model"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"change_type IN ('dns', 'firewall', 'routing', 'ssl', 'container', 'service_config', 'hardware', 'network', 'storage')",
name="ck_infrastructure_changes_type"
),
Index("idx_infra_changes_work_item", "work_item_id"),
Index("idx_infra_changes_session", "session_id"),
Index("idx_infra_changes_infrastructure", "infrastructure_id"),
)
def __repr__(self) -> str:
"""String representation of the infrastructure change."""
return f"<InfrastructureChange(type='{self.change_type}', target='{self.target_system}', permanent={self.is_permanent})>"

View File

@@ -0,0 +1,56 @@
"""
Infrastructure Tag junction table for many-to-many relationship.
This model creates the many-to-many relationship between infrastructure and tags,
allowing flexible categorization and filtering of infrastructure items.
"""
from sqlalchemy import CHAR, ForeignKey, Index, PrimaryKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base
class InfrastructureTag(Base):
"""
Junction table linking infrastructure to tags.
Implements many-to-many relationship between infrastructure and tags tables.
Allows infrastructure items to be tagged with multiple categories for filtering
and organization (e.g., docker, postgresql, backup-server, production).
Attributes:
infrastructure_id: Foreign key to infrastructure table
tag_id: Foreign key to tags table
"""
__tablename__ = "infrastructure_tags"
# Composite primary key
infrastructure_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="CASCADE"),
nullable=False,
doc="Infrastructure item being tagged",
)
tag_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("tags.id", ondelete="CASCADE"),
nullable=False,
doc="Tag applied to the infrastructure",
)
# Table constraints and indexes
__table_args__ = (
PrimaryKeyConstraint("infrastructure_id", "tag_id"),
Index("idx_it_infrastructure", "infrastructure_id"),
Index("idx_it_tag", "tag_id"),
)
# Relationships
# infrastructure = relationship("Infrastructure", back_populates="tags")
# tag = relationship("Tag", back_populates="infrastructure_items")
def __repr__(self) -> str:
"""String representation of the infrastructure tag relationship."""
return f"<InfrastructureTag(infrastructure_id={self.infrastructure_id!r}, tag_id={self.tag_id!r})>"

View File

@@ -0,0 +1,130 @@
"""
Integration Credential model for storing external system authentication.
This model securely stores OAuth tokens, API keys, and other credentials
needed to authenticate with external integrations like SyncroMSP, MSP Backups, etc.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import (
Boolean,
CheckConstraint,
Index,
LargeBinary,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base, TimestampMixin, UUIDMixin
class IntegrationCredential(Base, UUIDMixin, TimestampMixin):
"""
Integration credentials for external system authentication.
Stores encrypted credentials (API keys, OAuth tokens) for integrations.
Each integration type has one record with its authentication credentials.
All sensitive data is encrypted using AES-256-GCM.
Attributes:
id: Unique identifier
integration_name: Unique name of the integration (syncro, msp_backups, zapier)
credential_type: Type of credential (oauth, api_key, basic_auth)
api_key_encrypted: Encrypted API key (if credential_type is api_key)
oauth_token_encrypted: Encrypted OAuth access token
oauth_refresh_token_encrypted: Encrypted OAuth refresh token
oauth_expires_at: When the OAuth token expires
api_base_url: Base URL for API calls
webhook_url: Webhook URL for receiving callbacks
is_active: Whether this integration is currently active
last_tested_at: When the connection was last tested
last_test_status: Result of last connection test
created_at: When the credential was created
updated_at: When the credential was last updated
"""
__tablename__ = "integration_credentials"
# Integration identification
integration_name: Mapped[str] = mapped_column(
String(100),
unique=True,
nullable=False,
doc="Unique name of integration (syncro, msp_backups, zapier)",
)
# Credential type and encrypted values
credential_type: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
doc="Type of credential",
)
api_key_encrypted: Mapped[Optional[bytes]] = mapped_column(
LargeBinary,
nullable=True,
doc="Encrypted API key (AES-256-GCM)",
)
oauth_token_encrypted: Mapped[Optional[bytes]] = mapped_column(
LargeBinary,
nullable=True,
doc="Encrypted OAuth access token",
)
oauth_refresh_token_encrypted: Mapped[Optional[bytes]] = mapped_column(
LargeBinary,
nullable=True,
doc="Encrypted OAuth refresh token",
)
oauth_expires_at: Mapped[Optional[datetime]] = mapped_column(
nullable=True,
doc="When the OAuth token expires",
)
# Endpoints
api_base_url: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
doc="Base URL for API calls",
)
webhook_url: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
doc="Webhook URL for receiving callbacks",
)
# Status
is_active: Mapped[bool] = mapped_column(
Boolean,
default=True,
nullable=False,
doc="Whether this integration is active",
)
last_tested_at: Mapped[Optional[datetime]] = mapped_column(
nullable=True,
doc="When the connection was last tested",
)
last_test_status: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
doc="Result of last connection test",
)
# Indexes and constraints
__table_args__ = (
CheckConstraint(
"credential_type IN ('oauth', 'api_key', 'basic_auth')",
name="ck_integration_credential_type",
),
Index("idx_int_cred_name", "integration_name"),
)
def __repr__(self) -> str:
"""String representation of the integration credential."""
return (
f"<IntegrationCredential(id={self.id!r}, "
f"name={self.integration_name!r}, "
f"type={self.credential_type!r}, "
f"active={self.is_active})>"
)

86
api/models/m365_tenant.py Normal file
View File

@@ -0,0 +1,86 @@
"""
Microsoft 365 tenant model for tracking M365 tenants.
M365 tenants represent Microsoft 365 tenant configurations for clients
including tenant IDs, domains, and CIPP integration.
"""
from typing import Optional
from sqlalchemy import CHAR, ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base, TimestampMixin, UUIDMixin
class M365Tenant(Base, UUIDMixin, TimestampMixin):
"""
Microsoft 365 tenant model for tracking M365 configurations.
Tracks Microsoft 365 tenant information including tenant IDs,
domain names, admin contacts, and CIPP portal integration.
Attributes:
client_id: Reference to the client
tenant_id: Microsoft tenant ID (UUID)
tenant_name: Tenant name (e.g., "dataforth.com")
default_domain: Default domain (e.g., "dataforthcorp.onmicrosoft.com")
admin_email: Administrator email address
cipp_name: Name in CIPP portal
notes: Additional notes
"""
__tablename__ = "m365_tenants"
# Foreign keys
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
doc="Reference to the client"
)
# Tenant identification
tenant_id: Mapped[str] = mapped_column(
CHAR(36),
nullable=False,
unique=True,
doc="Microsoft tenant ID (UUID)"
)
tenant_name: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Tenant name (e.g., 'dataforth.com')"
)
default_domain: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Default domain (e.g., 'dataforthcorp.onmicrosoft.com')"
)
# Contact information
admin_email: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Administrator email address"
)
# CIPP integration
cipp_name: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Name in CIPP portal"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes"
)
# Indexes
__table_args__ = (
Index("idx_m365_client", "client_id"),
Index("idx_m365_tenant_id", "tenant_id"),
)
def __repr__(self) -> str:
"""String representation of the M365 tenant."""
return f"<M365Tenant(tenant_name='{self.tenant_name}', tenant_id='{self.tenant_id}')>"

263
api/models/machine.py Normal file
View File

@@ -0,0 +1,263 @@
"""
Machine model for technician's machines used for MSP work.
Tracks laptops, desktops, and workstations with their capabilities,
installed tools, MCP servers, and skills.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, Index, Integer, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .session import Session
class Machine(Base, UUIDMixin, TimestampMixin):
"""
Machine model representing technician's machines used for MSP work.
Tracks machine identification, capabilities, installed tools, MCP servers,
skills, and network context. Machines are auto-detected on session start
using hostname, username, platform, and home directory.
Attributes:
hostname: Machine hostname from `hostname` command
machine_fingerprint: SHA256 hash of hostname + username + platform + home_directory
friendly_name: Human-readable name like "Main Laptop" or "Home Desktop"
machine_type: Type of machine (laptop, desktop, workstation, vm)
platform: Operating system platform (win32, darwin, linux)
os_version: Operating system version
username: Username from `whoami` command
home_directory: User home directory path
has_vpn_access: Whether machine can connect to client networks
vpn_profiles: JSON array of available VPN profiles
has_docker: Whether Docker is installed
has_powershell: Whether PowerShell is installed
powershell_version: PowerShell version if installed
has_ssh: Whether SSH is available
has_git: Whether Git is installed
typical_network_location: Typical network location (home, office, mobile)
static_ip: Static IP address if applicable
claude_working_directory: Primary working directory for Claude Code
additional_working_dirs: JSON array of additional working directories
installed_tools: JSON object with tool versions
available_mcps: JSON array of available MCP servers
mcp_capabilities: JSON object with MCP capabilities
available_skills: JSON array of available skills
skill_paths: JSON object mapping skill names to paths
preferred_shell: Preferred shell (powershell, bash, zsh, cmd)
package_manager_commands: JSON object with package manager commands
is_primary: Whether this is the primary machine
is_active: Whether machine is active
last_seen: Last time machine was seen
last_session_id: UUID of last session from this machine
notes: Additional notes about the machine
"""
__tablename__ = "machines"
# Machine identification (auto-detected)
hostname: Mapped[str] = mapped_column(
String(255),
nullable=False,
unique=True,
doc="Machine hostname from `hostname` command"
)
machine_fingerprint: Mapped[Optional[str]] = mapped_column(
String(500),
unique=True,
doc="SHA256 hash: hostname + username + platform + home_directory"
)
# Environment details
friendly_name: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Human-readable name like 'Main Laptop' or 'Home Desktop'"
)
machine_type: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Type of machine: laptop, desktop, workstation, vm"
)
platform: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Operating system platform: win32, darwin, linux"
)
os_version: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Operating system version"
)
username: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Username from `whoami` command"
)
home_directory: Mapped[Optional[str]] = mapped_column(
String(500),
doc="User home directory path"
)
# Capabilities
has_vpn_access: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
doc="Whether machine can connect to client networks"
)
vpn_profiles: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of available VPN profiles"
)
has_docker: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
doc="Whether Docker is installed"
)
has_powershell: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
doc="Whether PowerShell is installed"
)
powershell_version: Mapped[Optional[str]] = mapped_column(
String(20),
doc="PowerShell version if installed"
)
has_ssh: Mapped[bool] = mapped_column(
Boolean,
default=True,
server_default="1",
doc="Whether SSH is available"
)
has_git: Mapped[bool] = mapped_column(
Boolean,
default=True,
server_default="1",
doc="Whether Git is installed"
)
# Network context
typical_network_location: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Typical network location: home, office, mobile"
)
static_ip: Mapped[Optional[str]] = mapped_column(
String(45),
doc="Static IP address if applicable (supports IPv4/IPv6)"
)
# Claude Code context
claude_working_directory: Mapped[Optional[str]] = mapped_column(
String(500),
doc="Primary working directory for Claude Code"
)
additional_working_dirs: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of additional working directories"
)
# Tool versions
installed_tools: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON object with tool versions like {\"git\": \"2.40\", \"docker\": \"24.0\"}"
)
# MCP Servers & Skills
available_mcps: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of available MCP servers"
)
mcp_capabilities: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON object with MCP capabilities"
)
available_skills: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of available skills"
)
skill_paths: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON object mapping skill names to paths"
)
# OS-Specific Commands
preferred_shell: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Preferred shell: powershell, bash, zsh, cmd"
)
package_manager_commands: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON object with package manager commands"
)
# Status
is_primary: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
doc="Whether this is the primary machine"
)
is_active: Mapped[bool] = mapped_column(
Boolean,
default=True,
server_default="1",
doc="Whether machine is currently active"
)
last_seen: Mapped[Optional[datetime]] = mapped_column(
TIMESTAMP,
doc="Last time machine was seen"
)
last_session_id: Mapped[Optional[str]] = mapped_column(
String(36),
doc="UUID of last session from this machine"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes about the machine"
)
# Relationships
sessions: Mapped[list["Session"]] = relationship(
"Session",
back_populates="machine",
doc="Sessions associated with this machine"
)
# Indexes
__table_args__ = (
Index("idx_machines_hostname", "hostname"),
Index("idx_machines_fingerprint", "machine_fingerprint"),
Index("idx_machines_is_active", "is_active"),
Index("idx_machines_platform", "platform"),
)
def __repr__(self) -> str:
"""String representation of the machine."""
return f"<Machine(hostname='{self.hostname}', friendly_name='{self.friendly_name}', platform='{self.platform}')>"

98
api/models/network.py Normal file
View File

@@ -0,0 +1,98 @@
"""
Network model for network segments and VLANs.
Networks represent network segments, VLANs, VPN networks, and other
logical or physical network divisions.
"""
from typing import Optional
from sqlalchemy import CHAR, CheckConstraint, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base, TimestampMixin, UUIDMixin
class Network(Base, UUIDMixin, TimestampMixin):
"""
Network model representing network segments and VLANs.
Tracks network segments including LANs, VPNs, VLANs, isolated networks,
and DMZs with CIDR notation, gateway IPs, and VLAN IDs.
Attributes:
client_id: Reference to the client
site_id: Reference to the site
network_name: Name of the network
network_type: Type of network (lan, vpn, vlan, isolated, dmz)
cidr: Network CIDR notation (e.g., "192.168.0.0/24")
gateway_ip: Gateway IP address
vlan_id: VLAN ID if applicable
notes: Additional notes
created_at: When the network was created
"""
__tablename__ = "networks"
# Foreign keys
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
doc="Reference to the client"
)
site_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("sites.id", ondelete="CASCADE"),
doc="Reference to the site"
)
# Network identification
network_name: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="Name of the network"
)
network_type: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Type: lan, vpn, vlan, isolated, dmz"
)
# Network configuration
cidr: Mapped[str] = mapped_column(
String(100),
nullable=False,
doc="Network CIDR notation (e.g., '192.168.0.0/24')"
)
gateway_ip: Mapped[Optional[str]] = mapped_column(
String(45),
doc="Gateway IP address"
)
vlan_id: Mapped[Optional[int]] = mapped_column(
Integer,
doc="VLAN ID if applicable"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"network_type IN ('lan', 'vpn', 'vlan', 'isolated', 'dmz')",
name="ck_networks_type"
),
Index("idx_networks_client", "client_id"),
Index("idx_networks_site", "site_id"),
)
def __repr__(self) -> str:
"""String representation of the network."""
return f"<Network(network_name='{self.network_name}', cidr='{self.cidr}')>"

View File

@@ -0,0 +1,178 @@
"""
Operation failure model for tracking non-command failures.
Tracks failures from API calls, file operations, network requests, and other
operations (distinct from command execution failures tracked in command_runs).
"""
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, CHAR, CheckConstraint, ForeignKey, Index, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
if TYPE_CHECKING:
from .session import Session
from .work_item import WorkItem
class OperationFailure(Base, UUIDMixin):
"""
Operation failure model for non-command failures.
Tracks failures from API calls, file operations, network requests,
database queries, and external integrations. Used for troubleshooting,
pattern detection, and system reliability monitoring.
Distinct from CommandRun failures which track shell command execution.
This tracks programmatic operations and API interactions.
Attributes:
session_id: Foreign key to sessions table
work_item_id: Foreign key to work_items table
operation_type: Type of operation that failed
operation_description: Detailed description of what was attempted
target_system: Host, URL, or service name that was targeted
error_message: Error message from the failure
error_code: HTTP status, exit code, or error number
failure_category: Category of failure (timeout, authentication, etc.)
stack_trace: Stack trace if available
resolution_applied: Description of how the failure was resolved
resolved: Whether the failure has been resolved
resolved_at: When the failure was resolved
request_data: JSON data of what was attempted
response_data: JSON data of error response
environment_snapshot: JSON snapshot of relevant environment variables/versions
created_at: When the failure occurred
session: Relationship to Session model
work_item: Relationship to WorkItem model
"""
__tablename__ = "operation_failures"
# Foreign keys
session_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
doc="Foreign key to sessions table"
)
work_item_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
doc="Foreign key to work_items table"
)
# Operation details
operation_type: Mapped[str] = mapped_column(
String(100),
nullable=False,
doc="Type of operation: api_call, file_operation, network_request, database_query, external_integration, service_restart"
)
operation_description: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Detailed description of what was attempted"
)
target_system: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Host, URL, or service name that was targeted"
)
# Failure details
error_message: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Error message from the failure"
)
error_code: Mapped[Optional[str]] = mapped_column(
String(50),
doc="HTTP status code, exit code, or error number"
)
failure_category: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Category of failure: timeout, authentication, not_found, permission_denied, etc."
)
stack_trace: Mapped[Optional[str]] = mapped_column(
Text,
doc="Stack trace if available"
)
# Resolution tracking
resolution_applied: Mapped[Optional[str]] = mapped_column(
Text,
doc="Description of how the failure was resolved"
)
resolved: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
nullable=False,
doc="Whether the failure has been resolved"
)
resolved_at: Mapped[Optional[datetime]] = mapped_column(
TIMESTAMP,
doc="When the failure was resolved"
)
# Context data (JSON stored as text)
request_data: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON data of what was attempted"
)
response_data: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON data of error response"
)
environment_snapshot: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON snapshot of relevant environment variables, versions, etc."
)
# Timestamp
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=func.now(),
doc="When the failure occurred"
)
# Relationships
session: Mapped[Optional["Session"]] = relationship(
"Session",
back_populates="operation_failures",
doc="Relationship to Session model"
)
work_item: Mapped[Optional["WorkItem"]] = relationship(
"WorkItem",
doc="Relationship to WorkItem model"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"operation_type IN ('api_call', 'file_operation', 'network_request', 'database_query', 'external_integration', 'service_restart')",
name="ck_operation_failures_type"
),
Index("idx_op_failure_session", "session_id"),
Index("idx_op_failure_type", "operation_type"),
Index("idx_op_failure_category", "failure_category"),
Index("idx_op_failure_resolved", "resolved"),
)
def __repr__(self) -> str:
"""String representation of the operation failure."""
return f"<OperationFailure(type='{self.operation_type}', target='{self.target_system}', resolved={self.resolved})>"

154
api/models/pending_task.py Normal file
View File

@@ -0,0 +1,154 @@
"""
Pending task model for tracking open items across clients and projects.
Tracks tasks that need to be completed, their priority, status, and
assignment information.
"""
from datetime import date, datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import CHAR, CheckConstraint, DATE, ForeignKey, Index, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .client import Client
from .project import Project
from .work_item import WorkItem
class PendingTask(Base, UUIDMixin, TimestampMixin):
"""
Pending task model for open items across all clients and projects.
Tracks tasks that need to be completed with priority, blocking information,
assignment, and due dates. These represent work items that are planned or
in progress but not yet completed.
Attributes:
client_id: Foreign key to clients table
project_id: Foreign key to projects table
work_item_id: Foreign key to work_items table (if task linked to work item)
title: Brief title of the task
description: Detailed description of the task
priority: Task priority (critical, high, medium, low)
blocked_by: Description of what is blocking this task
assigned_to: Name of person assigned to the task
due_date: Due date for the task
status: Task status (pending, in_progress, blocked, completed, cancelled)
completed_at: When the task was completed
client: Relationship to Client model
project: Relationship to Project model
work_item: Relationship to WorkItem model
"""
__tablename__ = "pending_tasks"
# Foreign keys
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
doc="Foreign key to clients table"
)
project_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("projects.id", ondelete="CASCADE"),
doc="Foreign key to projects table"
)
work_item_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="SET NULL"),
doc="Foreign key to work_items table (if task linked to work item)"
)
# Task details
title: Mapped[str] = mapped_column(
String(500),
nullable=False,
doc="Brief title of the task"
)
description: Mapped[Optional[str]] = mapped_column(
Text,
doc="Detailed description of the task"
)
# Priority and blocking
priority: Mapped[Optional[str]] = mapped_column(
String(20),
doc="Task priority: critical, high, medium, low"
)
blocked_by: Mapped[Optional[str]] = mapped_column(
Text,
doc="Description of what is blocking this task"
)
# Assignment
assigned_to: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Name of person assigned to the task"
)
# Scheduling
due_date: Mapped[Optional[date]] = mapped_column(
DATE,
doc="Due date for the task"
)
# Status
status: Mapped[str] = mapped_column(
String(50),
default="pending",
server_default="pending",
nullable=False,
doc="Task status: pending, in_progress, blocked, completed, cancelled"
)
# Completion tracking
completed_at: Mapped[Optional[datetime]] = mapped_column(
TIMESTAMP,
doc="When the task was completed"
)
# Relationships
client: Mapped[Optional["Client"]] = relationship(
"Client",
back_populates="pending_tasks",
doc="Relationship to Client model"
)
project: Mapped[Optional["Project"]] = relationship(
"Project",
back_populates="pending_tasks",
doc="Relationship to Project model"
)
work_item: Mapped[Optional["WorkItem"]] = relationship(
"WorkItem",
doc="Relationship to WorkItem model"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"priority IN ('critical', 'high', 'medium', 'low')",
name="ck_pending_tasks_priority"
),
CheckConstraint(
"status IN ('pending', 'in_progress', 'blocked', 'completed', 'cancelled')",
name="ck_pending_tasks_status"
),
Index("idx_pending_tasks_client", "client_id"),
Index("idx_pending_tasks_status", "status"),
Index("idx_pending_tasks_priority", "priority"),
)
def __repr__(self) -> str:
"""String representation of the pending task."""
return f"<PendingTask(title='{self.title}', status='{self.status}', priority='{self.priority}')>"

View File

@@ -0,0 +1,127 @@
"""
Problem solution model for tracking issues and their resolutions.
This model captures problems encountered during work sessions, the investigation
process, root cause analysis, and solutions applied.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import CHAR, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from api.models.base import Base, UUIDMixin
class ProblemSolution(UUIDMixin, Base):
"""
Track problems and their solutions.
Records issues encountered during work, including symptoms, investigation steps,
root cause analysis, solutions applied, and verification methods.
Attributes:
id: UUID primary key
work_item_id: Reference to the work item
session_id: Reference to the session
problem_description: Detailed description of the problem
symptom: What the user observed/experienced
error_message: Exact error code or message
investigation_steps: JSON array of diagnostic commands/steps taken
root_cause: Identified root cause of the problem
solution_applied: The solution that was implemented
verification_method: How the fix was verified
rollback_plan: Plan to rollback if solution causes issues
recurrence_count: Number of times this problem has occurred
created_at: When the problem was recorded
"""
__tablename__ = "problem_solutions"
# Foreign keys
work_item_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to work item",
)
session_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to session",
)
# Problem details
problem_description: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Detailed description of the problem",
)
symptom: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="What the user observed/experienced",
)
error_message: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Exact error code or message",
)
# Investigation and analysis
investigation_steps: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="JSON array of diagnostic commands/steps taken",
)
root_cause: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Identified root cause of the problem",
)
# Solution details
solution_applied: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="The solution that was implemented",
)
verification_method: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="How the fix was verified",
)
rollback_plan: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Plan to rollback if solution causes issues",
)
# Recurrence tracking
recurrence_count: Mapped[int] = mapped_column(
Integer,
nullable=False,
server_default="1",
doc="Number of times this problem has occurred",
)
# Timestamp
created_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the problem was recorded",
)
# Table constraints
__table_args__ = (
Index("idx_problems_work_item", "work_item_id"),
Index("idx_problems_session", "session_id"),
)
def __repr__(self) -> str:
"""String representation of the problem solution."""
desc_preview = self.problem_description[:50] + "..." if len(self.problem_description) > 50 else self.problem_description
return f"<ProblemSolution(id={self.id}, problem={desc_preview}, recurrence={self.recurrence_count})>"

161
api/models/project.py Normal file
View File

@@ -0,0 +1,161 @@
"""
Project model for individual projects and engagements.
Tracks client projects, internal products, infrastructure work, and development tools.
"""
from datetime import date, datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import DATE, ForeignKey, Index, Numeric, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .client import Client
from .pending_task import PendingTask
from .session import Session
class Project(Base, UUIDMixin, TimestampMixin):
"""
Project model representing individual projects and engagements.
Tracks client projects, internal products, infrastructure work,
websites, development tools, and documentation projects. Each project
belongs to a client and has status, priority, and time tracking.
Attributes:
client_id: Foreign key to clients table
name: Project name
slug: URL-safe slug (directory name)
category: Project category
status: Current status (complete, working, blocked, pending, critical, deferred)
priority: Priority level (critical, high, medium, low)
description: Project description
started_date: Date project started
target_completion_date: Target completion date
completed_date: Actual completion date
estimated_hours: Estimated hours for completion
actual_hours: Actual hours spent
gitea_repo_url: Gitea repository URL if applicable
notes: Additional notes about the project
client: Relationship to Client model
"""
__tablename__ = "projects"
# Foreign keys
client_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("clients.id", ondelete="CASCADE"),
nullable=False,
doc="Foreign key to clients table"
)
# Project identification
name: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="Project name"
)
slug: Mapped[Optional[str]] = mapped_column(
String(255),
unique=True,
doc="URL-safe slug (directory name like 'dataforth-dos')"
)
# Categorization
category: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Project category: client_project, internal_product, infrastructure, website, development_tool, documentation"
)
status: Mapped[str] = mapped_column(
String(50),
default="working",
server_default="working",
doc="Status: complete, working, blocked, pending, critical, deferred"
)
priority: Mapped[Optional[str]] = mapped_column(
String(20),
doc="Priority level: critical, high, medium, low"
)
# Description
description: Mapped[Optional[str]] = mapped_column(
Text,
doc="Project description"
)
# Timeline
started_date: Mapped[Optional[date]] = mapped_column(
DATE,
doc="Date project started"
)
target_completion_date: Mapped[Optional[date]] = mapped_column(
DATE,
doc="Target completion date"
)
completed_date: Mapped[Optional[date]] = mapped_column(
DATE,
doc="Actual completion date"
)
# Time tracking
estimated_hours: Mapped[Optional[float]] = mapped_column(
Numeric(10, 2),
doc="Estimated hours for completion"
)
actual_hours: Mapped[Optional[float]] = mapped_column(
Numeric(10, 2),
doc="Actual hours spent"
)
# Repository
gitea_repo_url: Mapped[Optional[str]] = mapped_column(
String(500),
doc="Gitea repository URL if applicable"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes about the project"
)
# Relationships
client: Mapped["Client"] = relationship(
"Client",
back_populates="projects",
doc="Relationship to Client model"
)
sessions: Mapped[list["Session"]] = relationship(
"Session",
back_populates="project",
doc="Sessions associated with this project"
)
pending_tasks: Mapped[list["PendingTask"]] = relationship(
"PendingTask",
back_populates="project",
doc="Pending tasks associated with this project"
)
# Indexes
__table_args__ = (
Index("idx_projects_client", "client_id"),
Index("idx_projects_status", "status"),
Index("idx_projects_slug", "slug"),
)
def __repr__(self) -> str:
"""String representation of the project."""
return f"<Project(name='{self.name}', slug='{self.slug}', status='{self.status}')>"

118
api/models/project_state.py Normal file
View File

@@ -0,0 +1,118 @@
"""
ProjectState model for tracking current state of projects.
Stores the current phase, progress, blockers, and next actions for each project
to enable quick context retrieval when resuming work.
"""
from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .project import Project
from .session import Session
class ProjectState(Base, UUIDMixin, TimestampMixin):
"""
ProjectState model for tracking current state of projects.
Stores the current phase, progress, blockers, next actions, and key
information about a project's state. Each project has exactly one
ProjectState record that is updated as the project progresses.
Attributes:
project_id: Foreign key to projects (required, unique - one state per project)
current_phase: Current phase or stage of the project
progress_percentage: Integer percentage of completion (0-100)
blockers: JSON array of current blockers preventing progress
next_actions: JSON array of next steps to take
context_summary: Dense overview text of where the project currently stands
key_files: JSON array of important file paths for this project
important_decisions: JSON array of key decisions made for this project
last_session_id: Foreign key to the last session that updated this state
project: Relationship to Project model
last_session: Relationship to Session model
"""
__tablename__ = "project_states"
# Foreign keys
project_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("projects.id", ondelete="CASCADE"),
nullable=False,
unique=True,
doc="Foreign key to projects (required, unique - one state per project)"
)
last_session_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("sessions.id", ondelete="SET NULL"),
doc="Foreign key to the last session that updated this state"
)
# State metadata
current_phase: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Current phase or stage of the project"
)
progress_percentage: Mapped[int] = mapped_column(
Integer,
default=0,
server_default="0",
doc="Integer percentage of completion (0-100)"
)
# State content
blockers: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of current blockers preventing progress"
)
next_actions: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of next steps to take"
)
context_summary: Mapped[Optional[str]] = mapped_column(
Text,
doc="Dense overview text of where the project currently stands"
)
key_files: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of important file paths for this project"
)
important_decisions: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of key decisions made for this project"
)
# Relationships
project: Mapped["Project"] = relationship(
"Project",
doc="Relationship to Project model"
)
last_session: Mapped[Optional["Session"]] = relationship(
"Session",
doc="Relationship to Session model"
)
# Indexes
__table_args__ = (
Index("idx_project_states_project", "project_id"),
Index("idx_project_states_last_session", "last_session_id"),
Index("idx_project_states_progress", "progress_percentage"),
)
def __repr__(self) -> str:
"""String representation of the project state."""
return f"<ProjectState(project_id='{self.project_id}', phase='{self.current_phase}', progress={self.progress_percentage}%)>"

View File

@@ -0,0 +1,73 @@
"""
Schema migration model for tracking Alembic database migrations.
Tracks which database schema migrations have been applied, when, and by whom
for database version control and migration management.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from .base import Base
class SchemaMigration(Base):
"""
Schema migration model for tracking Alembic database migrations.
Records database schema version changes applied via Alembic migrations.
Used to track which migrations have been applied, when they were applied,
and the SQL executed for audit and rollback purposes.
Note: This model does NOT use UUIDMixin as it uses version_id as the
primary key to match Alembic's migration tracking system.
Attributes:
version_id: Alembic migration version identifier (primary key)
description: Description of what the migration does
applied_at: When the migration was applied
applied_by: User or system that applied the migration
migration_sql: SQL executed during the migration
"""
__tablename__ = "schema_migrations"
# Primary key - Alembic version identifier
version_id: Mapped[str] = mapped_column(
String(100),
primary_key=True,
doc="Alembic migration version identifier"
)
# Migration details
description: Mapped[Optional[str]] = mapped_column(
Text,
doc="Description of what the migration does"
)
# Application tracking
applied_at: Mapped[datetime] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=func.now(),
doc="When the migration was applied"
)
applied_by: Mapped[Optional[str]] = mapped_column(
String(255),
doc="User or system that applied the migration"
)
# Migration SQL
migration_sql: Mapped[Optional[str]] = mapped_column(
Text,
doc="SQL executed during the migration"
)
def __repr__(self) -> str:
"""String representation of the schema migration."""
return f"<SchemaMigration(version='{self.version_id}', applied_at='{self.applied_at}')>"

View File

@@ -0,0 +1,144 @@
"""
Security incident model for tracking security events and remediation.
This model captures security incidents, their investigation, and resolution
including BEC, backdoors, malware, and other security threats.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import (
CHAR,
CheckConstraint,
ForeignKey,
Index,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.models.base import Base, TimestampMixin, UUIDMixin
class SecurityIncident(UUIDMixin, TimestampMixin, Base):
"""
Security incident tracking and remediation.
Records security incidents from detection through investigation to resolution,
including details about the incident type, severity, and remediation steps.
Attributes:
id: UUID primary key
client_id: Reference to affected client
service_id: Reference to affected service
infrastructure_id: Reference to affected infrastructure
incident_type: Type of security incident
incident_date: When the incident occurred
severity: Severity level (critical, high, medium, low)
description: Detailed description of the incident
findings: Investigation results and findings
remediation_steps: Steps taken to remediate
status: Current status of incident handling
resolved_at: When the incident was resolved
notes: Additional notes
created_at: Creation timestamp
updated_at: Last update timestamp
"""
__tablename__ = "security_incidents"
# Foreign keys
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
nullable=True,
doc="Reference to affected client",
)
service_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("services.id", ondelete="SET NULL"),
nullable=True,
doc="Reference to affected service",
)
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="SET NULL"),
nullable=True,
doc="Reference to affected infrastructure",
)
# Incident details
incident_type: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
doc="Type of security incident",
)
incident_date: Mapped[datetime] = mapped_column(
nullable=False,
doc="When the incident occurred",
)
severity: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
doc="Severity level",
)
description: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Detailed description of the incident",
)
# Investigation and remediation
findings: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Investigation results and findings",
)
remediation_steps: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Steps taken to remediate the incident",
)
# Status tracking
status: Mapped[str] = mapped_column(
String(50),
nullable=False,
server_default="'investigating'",
doc="Current status of incident handling",
)
resolved_at: Mapped[Optional[datetime]] = mapped_column(
nullable=True,
doc="When the incident was resolved",
)
# Additional information
notes: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
doc="Additional notes and context",
)
# Table constraints
__table_args__ = (
CheckConstraint(
"incident_type IN ('bec', 'backdoor', 'malware', 'unauthorized_access', 'data_breach', 'phishing', 'ransomware', 'brute_force')",
name="ck_security_incidents_type",
),
CheckConstraint(
"severity IN ('critical', 'high', 'medium', 'low')",
name="ck_security_incidents_severity",
),
CheckConstraint(
"status IN ('investigating', 'contained', 'resolved', 'monitoring')",
name="ck_security_incidents_status",
),
Index("idx_incidents_client", "client_id"),
Index("idx_incidents_type", "incident_type"),
Index("idx_incidents_status", "status"),
)
def __repr__(self) -> str:
"""String representation of the security incident."""
return f"<SecurityIncident(id={self.id}, type={self.incident_type}, severity={self.severity}, status={self.status})>"

122
api/models/service.py Normal file
View File

@@ -0,0 +1,122 @@
"""
Service model for applications running on infrastructure.
Services represent applications, databases, web servers, and other software
running on infrastructure components.
"""
from typing import TYPE_CHECKING, Optional
from sqlalchemy import CHAR, CheckConstraint, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .deployment import Deployment
class Service(Base, UUIDMixin, TimestampMixin):
"""
Service model representing applications running on infrastructure.
Tracks applications, services, databases, web servers, and other software
components running on infrastructure with URLs, ports, and status.
Attributes:
infrastructure_id: Reference to the infrastructure hosting this service
service_name: Name of the service (e.g., "Gitea", "PostgreSQL")
service_type: Type of service (e.g., "git_hosting", "database")
external_url: External URL for accessing the service
internal_url: Internal URL for accessing the service
port: Port number the service runs on
protocol: Protocol used (https, ssh, smb, etc.)
status: Current status (running, stopped, error, maintenance)
version: Version of the service
notes: Additional notes
"""
__tablename__ = "services"
# Foreign keys
infrastructure_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("infrastructure.id", ondelete="CASCADE"),
doc="Reference to the infrastructure hosting this service"
)
# Service identification
service_name: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="Name of the service (e.g., 'Gitea', 'PostgreSQL', 'Apache')"
)
service_type: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Type of service (e.g., 'git_hosting', 'database', 'web_server')"
)
# URLs and connectivity
external_url: Mapped[Optional[str]] = mapped_column(
String(500),
doc="External URL for accessing the service"
)
internal_url: Mapped[Optional[str]] = mapped_column(
String(500),
doc="Internal URL for accessing the service"
)
port: Mapped[Optional[int]] = mapped_column(
Integer,
doc="Port number the service runs on"
)
protocol: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Protocol used (https, ssh, smb, etc.)"
)
# Status
status: Mapped[str] = mapped_column(
String(50),
default="running",
server_default="running",
nullable=False,
doc="Status: running, stopped, error, maintenance"
)
# Version
version: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Version of the service"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes"
)
# Relationships
deployments: Mapped[list["Deployment"]] = relationship(
"Deployment",
back_populates="service",
doc="Relationship to Deployment model"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"status IN ('running', 'stopped', 'error', 'maintenance')",
name="ck_services_status"
),
Index("idx_services_infrastructure", "infrastructure_id"),
Index("idx_services_name", "service_name"),
Index("idx_services_type", "service_type"),
)
def __repr__(self) -> str:
"""String representation of the service."""
return f"<Service(service_name='{self.service_name}', status='{self.status}')>"

View File

@@ -0,0 +1,83 @@
"""
Service relationship model for service dependencies and relationships.
Service relationships track how services depend on, proxy through, or
relate to other services in the infrastructure.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import CHAR, CheckConstraint, ForeignKey, Index, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
class ServiceRelationship(Base, UUIDMixin):
"""
Service relationship model representing dependencies and relationships.
Tracks relationships between services including hosting, proxying,
authentication, backend dependencies, and replication.
Attributes:
from_service_id: Reference to the source service in the relationship
to_service_id: Reference to the target service in the relationship
relationship_type: Type of relationship (hosted_on, proxied_by, etc.)
notes: Additional notes about the relationship
created_at: When the relationship was created
"""
__tablename__ = "service_relationships"
# Foreign keys
from_service_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("services.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to the source service in the relationship"
)
to_service_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("services.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to the target service in the relationship"
)
# Relationship details
relationship_type: Mapped[str] = mapped_column(
CHAR(50),
nullable=False,
doc="Type: hosted_on, proxied_by, authenticates_via, backend_for, depends_on, replicates_to"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes about the relationship"
)
# Timestamp
created_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the relationship was created"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"relationship_type IN ('hosted_on', 'proxied_by', 'authenticates_via', 'backend_for', 'depends_on', 'replicates_to')",
name="ck_service_relationships_type"
),
UniqueConstraint("from_service_id", "to_service_id", "relationship_type", name="uq_service_relationship"),
Index("idx_service_rel_from", "from_service_id"),
Index("idx_service_rel_to", "to_service_id"),
)
def __repr__(self) -> str:
"""String representation of the service relationship."""
return f"<ServiceRelationship(from='{self.from_service_id}', to='{self.to_service_id}', type='{self.relationship_type}')>"

215
api/models/session.py Normal file
View File

@@ -0,0 +1,215 @@
"""
Session model for work sessions with time tracking.
Tracks individual work sessions including client, project, machine used,
time tracking, and session documentation.
"""
from datetime import date, datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, DATE, ForeignKey, Index, Integer, Numeric, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from .client import Client
from .database_change import DatabaseChange
from .deployment import Deployment
from .infrastructure_change import InfrastructureChange
from .machine import Machine
from .operation_failure import OperationFailure
from .project import Project
from .work_item import WorkItem
class Session(Base, UUIDMixin, TimestampMixin):
"""
Session model representing work sessions with time tracking.
Tracks individual work sessions including which client, project, and machine
were involved, along with timing information, billability, and session documentation.
Enhanced with machine tracking to understand which machine was used for the work.
Attributes:
client_id: Foreign key to clients table
project_id: Foreign key to projects table
machine_id: Foreign key to machines table (which machine was used)
session_date: Date of the session
start_time: Session start timestamp
end_time: Session end timestamp
duration_minutes: Duration in minutes (auto-calculated or manual)
status: Session status (completed, in_progress, blocked, pending)
session_title: Brief title describing the session
summary: Markdown summary of the session
is_billable: Whether this session is billable
billable_hours: Billable hours if applicable
technician: Name of technician who performed the work
session_log_file: Path to markdown session log file
notes: Additional notes about the session
client: Relationship to Client model
project: Relationship to Project model
machine: Relationship to Machine model
"""
__tablename__ = "sessions"
# Foreign keys
client_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("clients.id", ondelete="SET NULL"),
doc="Foreign key to clients table"
)
project_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("projects.id", ondelete="SET NULL"),
doc="Foreign key to projects table"
)
machine_id: Mapped[Optional[str]] = mapped_column(
String(36),
ForeignKey("machines.id", ondelete="SET NULL"),
doc="Foreign key to machines table (which machine was used)"
)
# Session timing
session_date: Mapped[date] = mapped_column(
DATE,
nullable=False,
doc="Date of the session"
)
start_time: Mapped[Optional[datetime]] = mapped_column(
TIMESTAMP,
doc="Session start timestamp"
)
end_time: Mapped[Optional[datetime]] = mapped_column(
TIMESTAMP,
doc="Session end timestamp"
)
duration_minutes: Mapped[Optional[int]] = mapped_column(
Integer,
doc="Duration in minutes (auto-calculated or manual)"
)
# Status
status: Mapped[str] = mapped_column(
String(50),
default="completed",
server_default="completed",
doc="Session status: completed, in_progress, blocked, pending"
)
# Session details
session_title: Mapped[str] = mapped_column(
String(500),
nullable=False,
doc="Brief title describing the session"
)
summary: Mapped[Optional[str]] = mapped_column(
Text,
doc="Markdown summary of the session"
)
# Billability
is_billable: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
doc="Whether this session is billable"
)
billable_hours: Mapped[Optional[float]] = mapped_column(
Numeric(10, 2),
doc="Billable hours if applicable"
)
# Technician
technician: Mapped[Optional[str]] = mapped_column(
String(255),
doc="Name of technician who performed the work"
)
# Documentation
session_log_file: Mapped[Optional[str]] = mapped_column(
String(500),
doc="Path to markdown session log file"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes about the session"
)
# Relationships
client: Mapped[Optional["Client"]] = relationship(
"Client",
back_populates="sessions",
doc="Relationship to Client model"
)
project: Mapped[Optional["Project"]] = relationship(
"Project",
back_populates="sessions",
doc="Relationship to Project model"
)
machine: Mapped[Optional["Machine"]] = relationship(
"Machine",
back_populates="sessions",
doc="Relationship to Machine model"
)
work_items: Mapped[list["WorkItem"]] = relationship(
"WorkItem",
back_populates="session",
cascade="all, delete-orphan",
doc="Relationship to WorkItem model"
)
operation_failures: Mapped[list["OperationFailure"]] = relationship(
"OperationFailure",
back_populates="session",
cascade="all, delete-orphan",
doc="Relationship to OperationFailure model"
)
deployments: Mapped[list["Deployment"]] = relationship(
"Deployment",
back_populates="session",
cascade="all, delete-orphan",
doc="Relationship to Deployment model"
)
database_changes: Mapped[list["DatabaseChange"]] = relationship(
"DatabaseChange",
back_populates="session",
cascade="all, delete-orphan",
doc="Relationship to DatabaseChange model"
)
infrastructure_changes: Mapped[list["InfrastructureChange"]] = relationship(
"InfrastructureChange",
back_populates="session",
cascade="all, delete-orphan",
doc="Relationship to InfrastructureChange model"
)
# Indexes
__table_args__ = (
Index("idx_sessions_client", "client_id"),
Index("idx_sessions_project", "project_id"),
Index("idx_sessions_date", "session_date"),
Index("idx_sessions_billable", "is_billable"),
Index("idx_sessions_machine", "machine_id"),
)
def __repr__(self) -> str:
"""String representation of the session."""
return f"<Session(title='{self.session_title}', date='{self.session_date}', status='{self.status}')>"

51
api/models/session_tag.py Normal file
View File

@@ -0,0 +1,51 @@
"""
Session tag junction table for many-to-many relationships.
Associates sessions with tags for categorization and filtering.
"""
from sqlalchemy import CHAR, ForeignKey, Index, PrimaryKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base
class SessionTag(Base):
"""
Session tag junction table for many-to-many relationships.
Links sessions to tags, allowing sessions to have multiple tags
and tags to be associated with multiple sessions.
Attributes:
session_id: Reference to the session
tag_id: Reference to the tag
"""
__tablename__ = "session_tags"
# Composite primary key
session_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to the session"
)
tag_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("tags.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to the tag"
)
# Table constraints
__table_args__ = (
PrimaryKeyConstraint("session_id", "tag_id"),
Index("idx_st_session", "session_id"),
Index("idx_st_tag", "tag_id"),
)
def __repr__(self) -> str:
"""String representation of the session tag."""
return f"<SessionTag(session_id='{self.session_id}', tag_id='{self.tag_id}')>"

95
api/models/site.py Normal file
View File

@@ -0,0 +1,95 @@
"""
Site model for client physical locations.
Sites represent physical locations for clients including network configuration,
VPN settings, and gateway information.
"""
from typing import Optional
from sqlalchemy import Boolean, CHAR, ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base, TimestampMixin, UUIDMixin
class Site(Base, UUIDMixin, TimestampMixin):
"""
Site model representing client physical locations.
Tracks physical sites for clients with network configuration including
subnets, VPN settings, gateway IPs, and DNS servers.
Attributes:
client_id: Reference to the client this site belongs to
name: Site name (e.g., "Main Office", "SLC - Salt Lake City")
network_subnet: Network subnet for the site (e.g., "172.16.9.0/24")
vpn_required: Whether VPN is required to access this site
vpn_subnet: VPN subnet if applicable (e.g., "192.168.1.0/24")
gateway_ip: Gateway IP address (IPv4 or IPv6)
dns_servers: JSON array of DNS server addresses
notes: Additional notes about the site
"""
__tablename__ = "sites"
# Foreign keys
client_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to the client this site belongs to"
)
# Site identification
name: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="Site name (e.g., 'Main Office', 'SLC - Salt Lake City')"
)
# Network configuration
network_subnet: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Network subnet for the site (e.g., '172.16.9.0/24')"
)
# VPN configuration
vpn_required: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
nullable=False,
doc="Whether VPN is required to access this site"
)
vpn_subnet: Mapped[Optional[str]] = mapped_column(
String(100),
doc="VPN subnet if applicable (e.g., '192.168.1.0/24')"
)
# Gateway and DNS
gateway_ip: Mapped[Optional[str]] = mapped_column(
String(45),
doc="Gateway IP address (IPv4 or IPv6)"
)
dns_servers: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of DNS server addresses"
)
# Notes
notes: Mapped[Optional[str]] = mapped_column(
Text,
doc="Additional notes about the site"
)
# Indexes
__table_args__ = (
Index("idx_sites_client", "client_id"),
)
def __repr__(self) -> str:
"""String representation of the site."""
return f"<Site(name='{self.name}', client_id='{self.client_id}')>"

69
api/models/tag.py Normal file
View File

@@ -0,0 +1,69 @@
"""
Tag model for categorizing and organizing work items.
Provides flexible tagging system for technologies, clients, infrastructure,
problem types, actions, and services.
"""
from typing import Optional
from sqlalchemy import Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
class Tag(Base, UUIDMixin, TimestampMixin):
"""
Tag model for categorizing and organizing work items.
Provides a flexible tagging system for organizing work by technology,
client, infrastructure, problem type, action, or service. Tags can be
pre-populated or created on-demand, with automatic usage tracking.
Attributes:
name: Tag name (unique)
category: Tag category (technology, client, infrastructure, problem_type, action, service)
description: Description of the tag
usage_count: Number of times this tag has been used (auto-incremented)
"""
__tablename__ = "tags"
# Tag identification
name: Mapped[str] = mapped_column(
String(100),
nullable=False,
unique=True,
doc="Tag name (unique)"
)
# Categorization
category: Mapped[Optional[str]] = mapped_column(
String(50),
doc="Tag category: technology, client, infrastructure, problem_type, action, service"
)
# Description
description: Mapped[Optional[str]] = mapped_column(
Text,
doc="Description of the tag"
)
# Usage tracking
usage_count: Mapped[int] = mapped_column(
Integer,
default=0,
server_default="0",
doc="Number of times this tag has been used (auto-incremented)"
)
# Indexes
__table_args__ = (
Index("idx_tags_category", "category"),
Index("idx_tags_name", "name"),
)
def __repr__(self) -> str:
"""String representation of the tag."""
return f"<Tag(name='{self.name}', category='{self.category}', usage_count={self.usage_count})>"

160
api/models/task.py Normal file
View File

@@ -0,0 +1,160 @@
"""
Task model for hierarchical task tracking.
Tasks represent work items that can be hierarchical, assigned to agents,
and tracked across sessions with dependencies and complexity estimates.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import CHAR, CheckConstraint, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base, TimestampMixin, UUIDMixin
class Task(Base, UUIDMixin, TimestampMixin):
"""
Task model representing hierarchical work items.
Tasks support parent-child relationships for breaking down complex work,
status tracking with blocking reasons, assignment to agents, and
complexity estimation.
Attributes:
parent_task_id: Reference to parent task for hierarchical structure
task_order: Order of this task relative to siblings
title: Task title
description: Detailed task description
task_type: Type of task (implementation, research, review, etc.)
status: Current status (pending, in_progress, blocked, completed, cancelled)
blocking_reason: Reason why task is blocked
session_id: Reference to the session this task belongs to
client_id: Reference to the client
project_id: Reference to the project
assigned_agent: Which agent is handling this task
estimated_complexity: Complexity estimate (trivial to very_complex)
started_at: When the task was started
completed_at: When the task was completed
task_context: Detailed context for this task (JSON)
dependencies: JSON array of dependency task IDs
"""
__tablename__ = "tasks"
# Task hierarchy
parent_task_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("tasks.id", ondelete="CASCADE"),
doc="Reference to parent task for hierarchical structure"
)
task_order: Mapped[int] = mapped_column(
Integer,
nullable=False,
doc="Order of this task relative to siblings"
)
# Task details
title: Mapped[str] = mapped_column(
String(500),
nullable=False,
doc="Task title"
)
description: Mapped[Optional[str]] = mapped_column(
Text,
doc="Detailed task description"
)
task_type: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Type: implementation, research, review, deployment, testing, documentation, bugfix, analysis"
)
# Status tracking
status: Mapped[str] = mapped_column(
String(50),
nullable=False,
doc="Status: pending, in_progress, blocked, completed, cancelled"
)
blocking_reason: Mapped[Optional[str]] = mapped_column(
Text,
doc="Reason why task is blocked (if status='blocked')"
)
# Context references
session_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
doc="Reference to the session this task belongs to"
)
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="SET NULL"),
doc="Reference to the client"
)
project_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("projects.id", ondelete="SET NULL"),
doc="Reference to the project"
)
assigned_agent: Mapped[Optional[str]] = mapped_column(
String(100),
doc="Which agent is handling this task"
)
# Timing
estimated_complexity: Mapped[Optional[str]] = mapped_column(
String(20),
doc="Complexity: trivial, simple, moderate, complex, very_complex"
)
started_at: Mapped[Optional[datetime]] = mapped_column(
doc="When the task was started"
)
completed_at: Mapped[Optional[datetime]] = mapped_column(
doc="When the task was completed"
)
# Context data (stored as JSON text)
task_context: Mapped[Optional[str]] = mapped_column(
Text,
doc="Detailed context for this task (JSON)"
)
dependencies: Mapped[Optional[str]] = mapped_column(
Text,
doc="JSON array of dependency task IDs"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"task_type IN ('implementation', 'research', 'review', 'deployment', 'testing', 'documentation', 'bugfix', 'analysis')",
name="ck_tasks_type"
),
CheckConstraint(
"status IN ('pending', 'in_progress', 'blocked', 'completed', 'cancelled')",
name="ck_tasks_status"
),
CheckConstraint(
"estimated_complexity IN ('trivial', 'simple', 'moderate', 'complex', 'very_complex')",
name="ck_tasks_complexity"
),
Index("idx_tasks_session", "session_id"),
Index("idx_tasks_status", "status"),
Index("idx_tasks_parent", "parent_task_id"),
Index("idx_tasks_client", "client_id"),
Index("idx_tasks_project", "project_id"),
)
def __repr__(self) -> str:
"""String representation of the task."""
return f"<Task(title='{self.title}', status='{self.status}')>"

118
api/models/ticket_link.py Normal file
View File

@@ -0,0 +1,118 @@
"""
Ticket Link model for connecting sessions to external ticketing systems.
This model creates relationships between ClaudeTools sessions and tickets
in external systems like SyncroMSP, Autotask, ConnectWise, etc.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import CHAR, ForeignKey, Index, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
class TicketLink(Base, UUIDMixin):
"""
Links between sessions and external ticketing system tickets.
Creates associations between ClaudeTools work sessions and tickets
in external MSP platforms. Enables automatic time tracking, status
updates, and work documentation in ticketing systems.
Attributes:
id: Unique identifier
session_id: Reference to the ClaudeTools session
client_id: Reference to the client
integration_type: Type of ticketing system (syncro, autotask, connectwise)
ticket_id: External ticket identifier
ticket_number: Human-readable ticket number (e.g., "T12345")
ticket_subject: Subject/title of the ticket
ticket_url: Direct URL to view the ticket
ticket_status: Current status of the ticket
link_type: Type of relationship (related, resolves, documents)
created_at: When the link was created
"""
__tablename__ = "ticket_links"
# Foreign keys
session_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=True,
doc="ClaudeTools session linked to this ticket",
)
client_id: Mapped[Optional[str]] = mapped_column(
CHAR(36),
ForeignKey("clients.id", ondelete="CASCADE"),
nullable=True,
doc="Client this ticket belongs to",
)
# Ticket information
integration_type: Mapped[str] = mapped_column(
String(100),
nullable=False,
doc="Ticketing system type (syncro, autotask, connectwise)",
)
ticket_id: Mapped[str] = mapped_column(
String(255),
nullable=False,
doc="External ticket identifier",
)
ticket_number: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
doc="Human-readable ticket number (T12345)",
)
ticket_subject: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
doc="Subject/title of the ticket",
)
ticket_url: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
doc="Direct URL to view the ticket",
)
ticket_status: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
doc="Current status of the ticket",
)
# Link metadata
link_type: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
doc="Type of relationship (related, resolves, documents)",
)
created_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the link was created",
)
# Indexes
__table_args__ = (
Index("idx_ticket_session", "session_id"),
Index("idx_ticket_client", "client_id"),
Index("idx_ticket_external", "integration_type", "ticket_id"),
)
# Relationships
# session = relationship("Session", back_populates="ticket_links")
# client = relationship("Client", back_populates="ticket_links")
def __repr__(self) -> str:
"""String representation of the ticket link."""
return (
f"<TicketLink(id={self.id!r}, "
f"type={self.integration_type!r}, "
f"ticket={self.ticket_number or self.ticket_id!r}, "
f"link_type={self.link_type!r})>"
)

189
api/models/work_item.py Normal file
View File

@@ -0,0 +1,189 @@
"""
Work item model for tracking session work activities.
Work items represent individual tasks and activities completed during
work sessions, with categorization, timing, and billing tracking.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, CHAR, CheckConstraint, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, UUIDMixin
if TYPE_CHECKING:
from .database_change import DatabaseChange
from .deployment import Deployment
from .infrastructure_change import InfrastructureChange
from .session import Session
class WorkItem(Base, UUIDMixin):
"""
Work item model representing individual work activities during sessions.
Tracks detailed work activities completed during a session including
categorization, status, timing estimates and actuals, affected systems,
and technologies used.
Attributes:
session_id: Reference to the session this work item belongs to
category: Work category (infrastructure, troubleshooting, etc.)
title: Brief title of the work item
description: Detailed description of the work performed
status: Current status of the work item
priority: Priority level (critical, high, medium, low)
is_billable: Whether this work item is billable
estimated_minutes: Estimated time to complete in minutes
actual_minutes: Actual time spent in minutes
affected_systems: JSON array of affected systems
technologies_used: JSON array of technologies used
item_order: Sequence order within the session
created_at: When the work item was created
completed_at: When the work item was completed
"""
__tablename__ = "work_items"
# Foreign keys
session_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
doc="Reference to the session this work item belongs to"
)
# Relationships
session: Mapped["Session"] = relationship(
"Session",
back_populates="work_items",
doc="Relationship to Session model"
)
deployments: Mapped[list["Deployment"]] = relationship(
"Deployment",
back_populates="work_item",
cascade="all, delete-orphan",
doc="Relationship to Deployment model"
)
database_changes: Mapped[list["DatabaseChange"]] = relationship(
"DatabaseChange",
back_populates="work_item",
cascade="all, delete-orphan",
doc="Relationship to DatabaseChange model"
)
infrastructure_changes: Mapped[list["InfrastructureChange"]] = relationship(
"InfrastructureChange",
back_populates="work_item",
cascade="all, delete-orphan",
doc="Relationship to InfrastructureChange model"
)
# Work categorization
category: Mapped[str] = mapped_column(
String(50),
nullable=False,
doc="Work category: infrastructure, troubleshooting, configuration, development, maintenance, security, documentation"
)
title: Mapped[str] = mapped_column(
String(500),
nullable=False,
doc="Brief title of the work item"
)
description: Mapped[str] = mapped_column(
Text,
nullable=False,
doc="Detailed description of the work performed"
)
# Status tracking
status: Mapped[str] = mapped_column(
String(50),
default="completed",
server_default="completed",
nullable=False,
doc="Status: completed, in_progress, blocked, pending, deferred"
)
priority: Mapped[Optional[str]] = mapped_column(
String(20),
doc="Priority level: critical, high, medium, low"
)
# Billing
is_billable: Mapped[bool] = mapped_column(
Boolean,
default=False,
server_default="0",
nullable=False,
doc="Whether this work item is billable"
)
# Time tracking
estimated_minutes: Mapped[Optional[int]] = mapped_column(
Integer,
doc="Estimated time to complete in minutes"
)
actual_minutes: Mapped[Optional[int]] = mapped_column(
Integer,
doc="Actual time spent in minutes"
)
# Context data (stored as JSON text)
affected_systems: Mapped[Optional[str]] = mapped_column(
Text,
doc='JSON array of affected systems (e.g., ["jupiter", "172.16.3.20"])'
)
technologies_used: Mapped[Optional[str]] = mapped_column(
Text,
doc='JSON array of technologies used (e.g., ["docker", "mariadb"])'
)
# Ordering
item_order: Mapped[Optional[int]] = mapped_column(
Integer,
doc="Sequence order within the session"
)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
nullable=False,
server_default=func.now(),
doc="When the work item was created"
)
completed_at: Mapped[Optional[datetime]] = mapped_column(
doc="When the work item was completed"
)
# Constraints and indexes
__table_args__ = (
CheckConstraint(
"category IN ('infrastructure', 'troubleshooting', 'configuration', 'development', 'maintenance', 'security', 'documentation')",
name="ck_work_items_category"
),
CheckConstraint(
"status IN ('completed', 'in_progress', 'blocked', 'pending', 'deferred')",
name="ck_work_items_status"
),
CheckConstraint(
"priority IN ('critical', 'high', 'medium', 'low')",
name="ck_work_items_priority"
),
Index("idx_work_items_session", "session_id"),
Index("idx_work_items_category", "category"),
Index("idx_work_items_status", "status"),
)
def __repr__(self) -> str:
"""String representation of the work item."""
return f"<WorkItem(title='{self.title}', category='{self.category}', status='{self.status}')>"

View File

@@ -0,0 +1,56 @@
"""
Work Item Tag junction table for many-to-many relationship.
This model creates the many-to-many relationship between work items and tags,
allowing flexible categorization and filtering of work items.
"""
from sqlalchemy import CHAR, ForeignKey, Index, PrimaryKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import Base
class WorkItemTag(Base):
"""
Junction table linking work items to tags.
Implements many-to-many relationship between work_items and tags tables.
Allows work items to be tagged with multiple categories for filtering
and organization.
Attributes:
work_item_id: Foreign key to work_items table
tag_id: Foreign key to tags table
"""
__tablename__ = "work_item_tags"
# Composite primary key
work_item_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("work_items.id", ondelete="CASCADE"),
nullable=False,
doc="Work item being tagged",
)
tag_id: Mapped[str] = mapped_column(
CHAR(36),
ForeignKey("tags.id", ondelete="CASCADE"),
nullable=False,
doc="Tag applied to the work item",
)
# Table constraints and indexes
__table_args__ = (
PrimaryKeyConstraint("work_item_id", "tag_id"),
Index("idx_wit_work_item", "work_item_id"),
Index("idx_wit_tag", "tag_id"),
)
# Relationships
# work_item = relationship("WorkItem", back_populates="tags")
# tag = relationship("Tag", back_populates="work_items")
def __repr__(self) -> str:
"""String representation of the work item tag relationship."""
return f"<WorkItemTag(work_item_id={self.work_item_id!r}, tag_id={self.tag_id!r})>"