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:
97
api/models/__init__.py
Normal file
97
api/models/__init__.py
Normal 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
111
api/models/api_audit_log.py
Normal 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
147
api/models/backup_log.py
Normal 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
69
api/models/base.py
Normal 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
186
api/models/billable_time.py
Normal 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
120
api/models/client.py
Normal 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
140
api/models/command_run.py
Normal 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})>"
|
||||
124
api/models/context_snippet.py
Normal file
124
api/models/context_snippet.py
Normal 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})>"
|
||||
135
api/models/conversation_context.py
Normal file
135
api/models/conversation_context.py
Normal 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
231
api/models/credential.py
Normal 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})>"
|
||||
95
api/models/credential_audit_log.py
Normal file
95
api/models/credential_audit_log.py
Normal 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})>"
|
||||
88
api/models/credential_permission.py
Normal file
88
api/models/credential_permission.py
Normal 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})>"
|
||||
152
api/models/database_change.py
Normal file
152
api/models/database_change.py
Normal 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
115
api/models/decision_log.py
Normal 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
167
api/models/deployment.py
Normal 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}')>"
|
||||
145
api/models/environmental_insight.py
Normal file
145
api/models/environmental_insight.py
Normal 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})>"
|
||||
)
|
||||
127
api/models/external_integration.py
Normal file
127
api/models/external_integration.py
Normal 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})>"
|
||||
)
|
||||
184
api/models/failure_pattern.py
Normal file
184
api/models/failure_pattern.py
Normal 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
99
api/models/file_change.py
Normal 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
108
api/models/firewall_rule.py
Normal 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}')>"
|
||||
198
api/models/infrastructure.py
Normal file
198
api/models/infrastructure.py
Normal 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}')>"
|
||||
165
api/models/infrastructure_change.py
Normal file
165
api/models/infrastructure_change.py
Normal 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})>"
|
||||
56
api/models/infrastructure_tag.py
Normal file
56
api/models/infrastructure_tag.py
Normal 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})>"
|
||||
130
api/models/integration_credential.py
Normal file
130
api/models/integration_credential.py
Normal 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
86
api/models/m365_tenant.py
Normal 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
263
api/models/machine.py
Normal 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
98
api/models/network.py
Normal 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}')>"
|
||||
178
api/models/operation_failure.py
Normal file
178
api/models/operation_failure.py
Normal 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
154
api/models/pending_task.py
Normal 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}')>"
|
||||
127
api/models/problem_solution.py
Normal file
127
api/models/problem_solution.py
Normal 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
161
api/models/project.py
Normal 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
118
api/models/project_state.py
Normal 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}%)>"
|
||||
73
api/models/schema_migration.py
Normal file
73
api/models/schema_migration.py
Normal 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}')>"
|
||||
144
api/models/security_incident.py
Normal file
144
api/models/security_incident.py
Normal 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
122
api/models/service.py
Normal 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}')>"
|
||||
83
api/models/service_relationship.py
Normal file
83
api/models/service_relationship.py
Normal 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
215
api/models/session.py
Normal 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
51
api/models/session_tag.py
Normal 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
95
api/models/site.py
Normal 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
69
api/models/tag.py
Normal 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
160
api/models/task.py
Normal 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
118
api/models/ticket_link.py
Normal 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
189
api/models/work_item.py
Normal 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}')>"
|
||||
56
api/models/work_item_tag.py
Normal file
56
api/models/work_item_tag.py
Normal 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})>"
|
||||
Reference in New Issue
Block a user