""" Pydantic schemas for terminal session API. """ from datetime import datetime from typing import List, Optional, Literal, Any from pydantic import BaseModel, Field # ============================================================================ # Command History Schemas # ============================================================================ class CommandHistoryItem(BaseModel): """A single command from history.""" id: int command: str created_at: datetime host_name: Optional[str] = None username: Optional[str] = None execution_count: Optional[int] = None class Config: from_attributes = True class CommandHistoryResponse(BaseModel): """Response containing command history.""" commands: List[CommandHistoryItem] total: int host_id: Optional[str] = None query: Optional[str] = None class UniqueCommandItem(BaseModel): """A unique command with usage stats.""" command: str command_hash: str last_used: datetime execution_count: int class UniqueCommandsResponse(BaseModel): """Response containing unique commands.""" commands: List[UniqueCommandItem] total: int host_id: str # ============================================================================ # Terminal Session Schemas # ============================================================================ class TerminalSessionRequest(BaseModel): """Request to create a new terminal session.""" mode: Literal["embedded", "popout"] = Field( default="embedded", description="Terminal display mode: embedded in drawer or popout window" ) class TerminalSessionHost(BaseModel): """Host information included in session response.""" id: str name: str ip: str status: str bootstrap_ok: bool class TerminalSessionResponse(BaseModel): """Response after creating a terminal session.""" session_id: str url: str websocket_url: str expires_at: datetime ttl_seconds: int mode: str host: TerminalSessionHost reused: bool = False # True if an existing session was reused token: Optional[str] = None # Only provided for new sessions class TerminalSessionStatus(BaseModel): """Current status of a terminal session.""" session_id: str status: str host_id: str host_name: str mode: str = "embedded" created_at: datetime expires_at: datetime last_seen_at: Optional[datetime] = None remaining_seconds: int age_seconds: int = 0 last_seen_seconds: int = 0 class TerminalSessionList(BaseModel): """List of active terminal sessions.""" sessions: list[TerminalSessionStatus] total: int max_per_user: int class ActiveSessionInfo(BaseModel): """Info about an active session for limit error response.""" session_id: str host_id: str host_name: str mode: str age_seconds: int last_seen_seconds: int class SessionLimitError(BaseModel): """Rich error response when session limit is exceeded.""" error: str = "SESSION_LIMIT" message: str max_active: int current_count: int active_sessions: List[ActiveSessionInfo] suggested_actions: List[str] can_reuse: bool = False reusable_session_id: Optional[str] = None class HeartbeatRequest(BaseModel): """Request to update session heartbeat.""" pass # Empty body, session_id comes from path class HeartbeatResponse(BaseModel): """Response from heartbeat endpoint.""" session_id: str status: str last_seen_at: datetime remaining_seconds: int healthy: bool = True class CloseBeaconRequest(BaseModel): """Request from sendBeacon to close session.""" reason: str = "client_close" class SessionMetrics(BaseModel): """Terminal session service metrics.""" sessions_created: int sessions_reused: int sessions_closed_user: int sessions_gc_expired: int sessions_gc_idle: int session_limit_hits: int active_processes: int allocated_ports: int gc_running: bool