"""Nexus — Domain Models SQLAlchemy ORM models for all database tables. """ import datetime from datetime import timezone import uuid from sqlalchemy import ( Column, Integer, BigInteger, Float, String, Boolean, DateTime, Text, ForeignKey, Index, JSON ) from sqlalchemy.dialects.mysql import MEDIUMTEXT from sqlalchemy.orm import declarative_base, relationship def _utcnow(): """Callable default for DateTime columns — evaluated per-row, not at module import""" return datetime.datetime.now(timezone.utc) Base = declarative_base() # ────────────────────────────────────────────── # Platform & Node (资产组织层) # ────────────────────────────────────────────── class Platform(Base): """Asset type template — defines what kind of asset this is""" __tablename__ = "platforms" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), unique=True, nullable=False, comment="平台名称: Linux服务器/Windows/MySQL") category = Column(String(50), nullable=False, comment="分类: host/database/device") type = Column(String(50), nullable=False, comment="类型: linux/windows/mysql/switch") default_protocols = Column(JSON, nullable=True, comment="默认协议: [{name:'ssh',port:22,primary:true}]") charset = Column(String(20), default="utf-8", comment="默认字符集") created_at = Column(DateTime, default=_utcnow) assets = relationship("Server", back_populates="platform") class Node(Base): """Tree-structured asset grouping""" __tablename__ = "nodes" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False, comment="节点名称") parent_id = Column(Integer, ForeignKey("nodes.id", ondelete="SET NULL"), nullable=True, comment="父节点") sort_order = Column(Integer, default=0, comment="排序权重") created_at = Column(DateTime, default=_utcnow) parent = relationship("Node", remote_side=[id], back_populates="children") children = relationship("Node", back_populates="parent") assets = relationship("Server", back_populates="node") __table_args__ = ( Index('idx_nodes_parent_id', 'parent_id'), ) # ────────────────────────────────────────────── # Server (核心资产表 — 保持原名,扩展字段) # ────────────────────────────────────────────── class Server(Base): """Sub-server configuration / Asset""" __tablename__ = "servers" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), unique=True, nullable=False, comment="服务器名称") domain = Column(String(255), nullable=False, comment="域名或IP") port = Column(Integer, default=22, comment="SSH端口") username = Column(String(100), default="root", comment="SSH用户名") auth_method = Column(String(20), default="key", comment="认证方式: key/password") password = Column(String(255), nullable=True, comment="SSH密码(加密存储)") ssh_key_path = Column(String(500), nullable=True, comment="SSH私钥路径") # Text columns: RSA 4096 private key PEM is ~3.2 KB; Fernet-encrypted ~4.3 KB. # String(500) would cause DataError in MySQL strict mode for large keys. ssh_key_private = Column(Text, nullable=True, comment="加密后的私钥内容(Fernet)") ssh_key_public = Column(Text, nullable=True, comment="公钥内容") ssh_key_configured = Column(Boolean, default=False, comment="是否已配置SSH Key") agent_port = Column(Integer, default=8601, comment="Agent API端口") agent_api_key = Column(String(255), nullable=True, comment="Agent API密钥") preset_id = Column(Integer, nullable=True, comment="关联的密码预设ID(SSH认证)") ssh_key_preset_id = Column(Integer, nullable=True, comment="关联的密钥预设ID(SSH认证)") description = Column(Text, nullable=True, comment="备注说明") target_path = Column(String(500), nullable=True, comment="推送目标路径") category = Column(String(100), nullable=True, comment="服务器分类(旧字段,保留兼容)") # ── 新增:资产组织字段 ── platform_id = Column(Integer, ForeignKey("platforms.id", ondelete="SET NULL"), nullable=True, comment="平台类型ID") node_id = Column(Integer, ForeignKey("nodes.id", ondelete="SET NULL"), nullable=True, comment="所属节点ID") protocols = Column(JSON, nullable=True, comment="实例级协议覆盖: [{name:'ssh',port:2222}]") extra_attrs = Column(JSON, nullable=True, comment="扩展属性: {os:'ubuntu',arch:'x86_64',cpu:8,mem:'32G'}") # Status (from Agent heartbeat) is_online = Column(Boolean, default=False, comment="是否在线") last_heartbeat = Column(DateTime, nullable=True, comment="最后心跳时间") system_info = Column(Text, nullable=True, comment="系统信息JSON(Agent上报)") agent_version = Column(String(20), nullable=True, comment="Agent版本") # ── 新增:连接状态 ── connectivity = Column(String(20), default="unknown", comment="连接状态: ok/err/unknown") last_checked_at = Column(DateTime, nullable=True, comment="上次连接测试时间") # Metadata created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) # Relationships sync_logs = relationship("SyncLog", back_populates="server", cascade="all, delete-orphan") platform = relationship("Platform", back_populates="assets") node = relationship("Node", back_populates="assets") __table_args__ = ( Index('idx_servers_is_online', 'is_online'), Index('idx_servers_category', 'category'), Index('idx_servers_platform_id', 'platform_id'), Index('idx_servers_node_id', 'node_id'), ) class ServerMetricSample(Base): """Per-server CPU/mem samples every heartbeat flush (~10min).""" __tablename__ = "server_metric_samples" id = Column(Integer, primary_key=True, autoincrement=True) server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False) recorded_at = Column(DateTime, nullable=False, comment="采样时刻 UTC naive") is_online = Column(Boolean, nullable=False, default=False, comment="该采样时刻是否在线") cpu_pct = Column(Integer, nullable=True, comment="CPU %") mem_pct = Column(Integer, nullable=True, comment="内存 %") __table_args__ = ( Index("idx_server_metric_server_recorded", "server_id", "recorded_at"), ) class FleetMetricSample(Base): """Fleet-wide CPU/mem/disk averages sampled every heartbeat flush (~10min).""" __tablename__ = "fleet_metric_samples" id = Column(Integer, primary_key=True, autoincrement=True) recorded_at = Column(DateTime, nullable=False, comment="采样时刻 UTC naive") online_count = Column(Integer, default=0, comment="在线子机数") sample_count = Column(Integer, default=0, comment="参与均值计算的子机数") cpu_avg = Column(Integer, nullable=True, comment="CPU 平均 %") mem_avg = Column(Integer, nullable=True, comment="内存平均 %") disk_avg = Column(Integer, nullable=True, comment="磁盘平均 %") __table_args__ = ( Index("idx_fleet_metric_recorded_at", "recorded_at"), ) class WatchPinSession(Base): """One 8h monitoring session when admin pins a server to a watch slot.""" __tablename__ = "watch_pin_sessions" id = Column(Integer, primary_key=True, autoincrement=True) admin_id = Column(Integer, ForeignKey("admins.id", ondelete="CASCADE"), nullable=False) server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False) slot_index = Column(Integer, nullable=False, comment="槽位 0-3") started_at = Column(DateTime, nullable=False, comment="UTC naive") ended_at = Column(DateTime, nullable=True, comment="UTC naive") end_reason = Column(String(16), nullable=True, comment="expired|manual|replaced") __table_args__ = ( Index("idx_watch_pin_sess_admin_started", "admin_id", "started_at"), Index("idx_watch_pin_sess_server_started", "server_id", "started_at"), ) class WatchPin(Base): """Active watch slot (max 4 per admin); removed when expired or manual delete.""" __tablename__ = "watch_pins" id = Column(Integer, primary_key=True, autoincrement=True) admin_id = Column(Integer, ForeignKey("admins.id", ondelete="CASCADE"), nullable=False) session_id = Column(Integer, ForeignKey("watch_pin_sessions.id", ondelete="CASCADE"), nullable=False) slot_index = Column(Integer, nullable=False, comment="槽位 0-3") server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False) pinned_at = Column(DateTime, nullable=False, comment="UTC naive") expires_at = Column(DateTime, nullable=False, comment="UTC naive") monitoring_enabled = Column( Boolean, nullable=False, default=True, comment="是否开启实时探针(关闭时保留槽位、暂停倒计时)", ) ttl_hours = Column( Integer, nullable=False, default=8, comment="遗留字段(小时),新逻辑用 ttl_minutes", ) ttl_minutes = Column( Integer, nullable=False, default=30, comment="监测窗口时长(分钟) 30|60|120|480|1440", ) __table_args__ = ( Index("uq_watch_pins_admin_slot", "admin_id", "slot_index", unique=True), Index("uq_watch_pins_admin_server", "admin_id", "server_id", unique=True), Index("idx_watch_pins_expires", "expires_at"), ) class WatchProbeRecord(Base): """Per-probe sample (5s) while server is pinned — success or failure.""" __tablename__ = "watch_probe_records" id = Column(Integer, primary_key=True, autoincrement=True) session_id = Column(Integer, ForeignKey("watch_pin_sessions.id", ondelete="CASCADE"), nullable=False) server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False) admin_id = Column(Integer, ForeignKey("admins.id", ondelete="CASCADE"), nullable=False) recorded_at = Column(DateTime, nullable=False, comment="UTC naive") source = Column(String(16), nullable=False, comment="redis|ssh|mixed") probe_status = Column(String(24), nullable=False, comment="ok|ssh_timeout|...") duration_ms = Column(Integer, nullable=False, default=0) is_online = Column(Boolean, nullable=False, default=False) cpu_pct = Column(Integer, nullable=True) mem_pct = Column(Integer, nullable=True) disk_pct = Column(Integer, nullable=True) disk_mount = Column(String(255), nullable=True, default="/") load_1 = Column(Float, nullable=True) load_5 = Column(Float, nullable=True) load_15 = Column(Float, nullable=True) net_bytes_sent = Column(BigInteger, nullable=True) net_bytes_recv = Column(BigInteger, nullable=True) disk_read_bytes = Column(BigInteger, nullable=True) disk_write_bytes = Column(BigInteger, nullable=True) net_up_bps = Column(BigInteger, nullable=True) net_down_bps = Column(BigInteger, nullable=True) disk_read_bps = Column(BigInteger, nullable=True) disk_write_bps = Column(BigInteger, nullable=True) processes_json = Column(JSON, nullable=True, comment="Top5 processes snapshot") error = Column(String(255), nullable=True) __table_args__ = ( Index("idx_watch_probe_sess_recorded", "session_id", "recorded_at"), Index("idx_watch_probe_server_recorded", "server_id", "recorded_at"), Index("idx_watch_probe_admin_recorded", "admin_id", "recorded_at"), Index("idx_watch_probe_status_recorded", "probe_status", "recorded_at"), ) class SyncLog(Base): """Push operation log""" __tablename__ = "sync_logs" id = Column(Integer, primary_key=True, autoincrement=True) server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False) source_path = Column(String(500), nullable=False, comment="源目录") target_path = Column(String(500), nullable=False, comment="目标目录") trigger_type = Column(String(20), comment="触发类型: manual/schedule/batch") operator = Column(String(100), nullable=True, comment="操作人用户名") status = Column(String(20), comment="pending/running/success/failed") sync_mode = Column(String(20), default="incremental", comment="同步模式: incremental/full/overwrite/checksum") files_total = Column(Integer, default=0) files_transferred = Column(Integer, default=0) files_skipped = Column(Integer, default=0) bytes_transferred = Column(Integer, default=0) duration_seconds = Column(Integer, default=0) diff_summary = Column(Text, nullable=True, comment="rsync输出摘要") push_permission = Column(String(120), nullable=True, comment="推送后权限策略 chown=… chmod=…") error_message = Column(Text, nullable=True) started_at = Column(DateTime, default=_utcnow) finished_at = Column(DateTime, nullable=True) server = relationship("Server", back_populates="sync_logs") __table_args__ = ( Index('idx_sync_logs_srv_start', 'server_id', 'started_at'), ) class Admin(Base): """Admin user for web login""" __tablename__ = "admins" id = Column(Integer, primary_key=True, autoincrement=True) username = Column(String(100), unique=True, nullable=False) password_hash = Column(String(255), nullable=False) email = Column(String(255), nullable=True) totp_secret = Column(String(64), nullable=True) totp_enabled = Column(Boolean, default=False) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) last_login = Column(DateTime, nullable=True) token_version = Column(Integer, default=0, comment="令牌版本(重用时递增使旧token失效)") class AdminUiPreference(Base): """Per-admin UI state (search history, etc.) — JSON payload keyed by context.""" __tablename__ = "admin_ui_preferences" admin_id = Column(Integer, ForeignKey("admins.id", ondelete="CASCADE"), primary_key=True) context = Column(String(50), primary_key=True, comment="UI 上下文,如 servers_search") payload = Column(JSON, nullable=False, comment='JSON: {"history":[],"last":null}') updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) class LoginAttempt(Base): """Login attempt log for brute-force protection""" __tablename__ = "login_attempts" id = Column(Integer, primary_key=True, autoincrement=True) username = Column(String(100), nullable=False) ip_address = Column(String(45), nullable=False) attempted_at = Column(DateTime, default=_utcnow) success = Column(Boolean, default=False) __table_args__ = ( Index('idx_login_attempts_user_time', 'username', 'attempted_at'), ) class AdminRefreshToken(Base): """Refresh token hash persisted for login sessions (Redis + MySQL dual store).""" __tablename__ = "admin_refresh_tokens" id = Column(Integer, primary_key=True, autoincrement=True) admin_id = Column(Integer, nullable=False, comment="管理员 ID") token_hash = Column(String(64), nullable=False, unique=True, comment="SHA-256(refresh token)") expires_at = Column(DateTime, nullable=False, comment="过期时间 UTC") created_at = Column(DateTime, default=_utcnow) __table_args__ = ( Index("idx_admin_refresh_admin_expires", "admin_id", "expires_at"), ) class Setting(Base): """Key-value system settings (including brand config)""" __tablename__ = "settings" key = Column(String(100), primary_key=True) value = Column(Text, nullable=True) updated_at = Column(DateTime, default=_utcnow) class PasswordPreset(Base): """SSH password preset — encrypted storage""" __tablename__ = "password_presets" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False, comment="预设名称") username = Column(String(100), default="root", comment="SSH用户名") encrypted_pw = Column(String(500), nullable=False, comment="加密后的密码") created_at = Column(DateTime, default=_utcnow) class SshKeyPreset(Base): """SSH key preset — encrypted private key storage""" __tablename__ = "ssh_key_presets" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False, comment="预设名称") username = Column(String(100), default="root", comment="SSH用户名") encrypted_private_key = Column(Text, nullable=False, comment="加密后的私钥内容(Fernet)") public_key = Column(Text, nullable=True, comment="公钥内容(不加密)") created_at = Column(DateTime, default=_utcnow) class PendingServer(Base): """Servers awaiting successful SSH credential polling before promotion to servers table.""" __tablename__ = "pending_servers" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False, comment="显示名称") domain = Column(String(255), nullable=False, comment="IP或域名") port = Column(Integer, default=22, comment="SSH端口") agent_port = Column(Integer, default=8601, comment="Agent端口") target_path = Column(String(500), nullable=True, comment="推送目标路径") category = Column(String(100), nullable=True, comment="分类") platform_id = Column(Integer, nullable=True, comment="平台ID") node_id = Column(Integer, nullable=True, comment="节点ID") attempts = Column(Integer, default=0, comment="轮询尝试次数") last_error = Column(Text, nullable=True, comment="最后一次失败摘要") last_attempt_at = Column(DateTime, nullable=True, comment="最后尝试时间") created_at = Column(DateTime, default=_utcnow) __table_args__ = ( Index("idx_pending_servers_domain", "domain"), ) class PushSchedule(Base): """Scheduled task — cron-triggered file push OR script execution.""" __tablename__ = "push_schedules" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False, comment="调度名称") # ── Schedule type ── # 'push': rsync file sync (default, original behaviour) # 'script': execute a shell command / script via Agent schedule_type = Column(String(20), default="push", comment="调度类型: push | script") # ── Push-specific fields (schedule_type = 'push') ── source_path = Column(String(500), nullable=True, comment="推送源目录(push 类型必填)") target_path = Column(String(500), nullable=True, comment="推送目标路径(留空则各机 target_path)") sync_mode = Column(String(20), default="incremental", comment="同步模式: incremental/full/overwrite/checksum") # ── Script-specific fields (schedule_type = 'script') ── script_id = Column(Integer, ForeignKey("scripts.id", ondelete="SET NULL"), nullable=True, comment="脚本库引用(script 类型,可留空使用 script_content)") script_content = Column(Text, nullable=True, comment="临时命令内容(script 类型,不保存到脚本库)") exec_timeout = Column(Integer, default=60, comment="执行超时(秒)") long_task = Column(Boolean, default=False, comment="长任务 nohup 模式") # ── Common fields ── server_ids = Column(Text, nullable=False, comment="JSON数组: 目标服务器ID列表") run_mode = Column(String(10), default="once", comment="运行模式: once(一次性) | cron(循环)") cron_expr = Column(String(100), nullable=True, comment="cron表达式 (run_mode=cron时必填)") fire_at = Column(DateTime, nullable=True, comment="一次性执行时间 (run_mode=once时必填)") enabled = Column(Boolean, default=True) last_run_at = Column(DateTime, nullable=True) created_at = Column(DateTime, default=_utcnow) __table_args__ = ( Index('idx_push_schedules_enabled', 'enabled'), ) class PushRetryJob(Base): """Failed push retry queue""" __tablename__ = "push_retry_jobs" id = Column(Integer, primary_key=True, autoincrement=True) server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False) server_name = Column(String(100), comment="冗余字段便于列表显示") operator = Column(String(100), nullable=True, comment="操作人用户名") source_path = Column(String(500), nullable=False) target_path = Column(String(500), nullable=False) status = Column(String(20), default="pending") retry_count = Column(Integer, default=0) max_retries = Column(Integer, default=3) next_retry_at = Column(DateTime) last_error = Column(Text) created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow) server = relationship("Server") __table_args__ = ( Index('idx_push_retry_pending', 'status', 'next_retry_at'), ) class AuditLog(Base): """Audit log — record all key operations""" __tablename__ = "audit_logs" id = Column(Integer, primary_key=True, autoincrement=True) admin_username = Column(String(100), nullable=True, comment="操作人用户名") action = Column(String(50), nullable=False, comment="操作类型") target_type = Column(String(50), nullable=True, comment="目标类型") target_id = Column(Integer, nullable=True, comment="目标ID") detail = Column(Text, nullable=True, comment="操作详情JSON") ip_address = Column(String(45), nullable=True, comment="操作IP") created_at = Column(DateTime, default=_utcnow) __table_args__ = ( Index('idx_audit_logs_action_time', 'action', 'created_at'), Index('idx_audit_logs_created_at', 'created_at'), ) class Script(Base): """Script library — reusable shell scripts""" __tablename__ = "scripts" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False, comment="脚本名称") category = Column(String(50), default="ops", comment="分类: ops/deploy/check/cleanup") content = Column(Text, nullable=False, comment="Shell命令内容") description = Column(Text, nullable=True, comment="说明") created_by = Column(String(100), nullable=True, comment="创建人") created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow) class ScriptExecution(Base): """Script execution log — results per server""" __tablename__ = "script_executions" id = Column(Integer, primary_key=True, autoincrement=True) script_id = Column(Integer, ForeignKey("scripts.id", ondelete="SET NULL"), nullable=True, comment="关联脚本(手动输入时为null)") command = Column(Text, nullable=False, comment="实际执行的命令") server_ids = Column(Text, nullable=False, comment="JSON数组: 目标服务器ID列表") credential_id = Column(Integer, ForeignKey("db_credentials.id", ondelete="SET NULL"), nullable=True, comment="DB凭据替换$DB_*变量") status = Column(String(20), default="pending", comment="pending/running/completed/failed") results = Column( Text().with_variant(MEDIUMTEXT(), "mysql"), nullable=True, comment="JSON: 每台服务器执行结果{server_id: {stdout,stderr,exit_code}}", ) operator = Column(String(100), nullable=True, comment="操作人用户名") started_at = Column(DateTime, default=_utcnow) completed_at = Column(DateTime, nullable=True) script = relationship("Script") __table_args__ = ( Index('idx_script_exec_script_id', 'script_id'), ) class ServerBatchJob(Base): """Server batch job history — persisted after Redis live state (install Agent, health check, etc.).""" __tablename__ = "server_batch_jobs" id = Column(Integer, primary_key=True, autoincrement=False, comment="与 Redis job_id 一致") op = Column(String(50), nullable=False, comment="category/health-check/install-agent/...") label = Column(String(200), nullable=False, comment="展示标题") server_ids = Column(Text, nullable=False, comment="JSON 数组: 目标服务器 ID") status = Column(String(20), default="running", comment="running/completed/failed/partial") results = Column(Text, nullable=True, comment="JSON 数组: 每台服务器结果") operator = Column(String(100), nullable=True, comment="操作人") params = Column(Text, nullable=True, comment="JSON: 额外参数") started_at = Column(DateTime, default=_utcnow) completed_at = Column(DateTime, nullable=True) __table_args__ = ( Index('idx_server_batch_jobs_op_time', 'op', 'started_at'), Index('idx_server_batch_jobs_started_at', 'started_at'), ) class DbCredential(Base): """Database credentials — encrypted storage for $DB_* variable substitution""" __tablename__ = "db_credentials" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False, comment="凭据名称") db_type = Column(String(20), default="mysql", comment="数据库类型: mysql/postgresql/etc") host = Column(String(255), default="localhost", comment="数据库主机") port = Column(Integer, default=3306, comment="数据库端口") username = Column(String(100), nullable=False, comment="数据库用户名") encrypted_password = Column(String(500), nullable=False, comment="加密后的密码") database = Column(String(100), nullable=True, comment="数据库库名") created_at = Column(DateTime, default=_utcnow) # ────────────────────────────────────────────── # Web SSH 会话 & 命令日志 # ────────────────────────────────────────────── class AlertLog(Base): """Persistent alert & recovery history.""" __tablename__ = "alert_logs" id = Column(Integer, primary_key=True, autoincrement=True) server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False) server_name = Column(String(100), nullable=True, comment="冗余服务器名,便于列表显示") alert_type = Column(String(20), nullable=False, comment="cpu|mem|disk|time_drift|system") value = Column(String(50), nullable=True, comment="指标值(百分比或描述)") is_recovery = Column(Boolean, default=False, comment="True=恢复通知") created_at = Column(DateTime, default=_utcnow) server = relationship("Server") __table_args__ = ( Index("idx_alert_logs_server_id", "server_id"), Index("idx_alert_logs_created_at", "created_at"), Index("idx_alert_logs_type", "alert_type"), ) class SshSession(Base): """Web SSH session lifecycle tracking""" __tablename__ = "ssh_sessions" id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="UUID") server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False, comment="服务器ID") admin_id = Column(Integer, ForeignKey("admins.id", ondelete="SET NULL"), nullable=True, comment="操作人ID") remote_addr = Column(String(45), nullable=True, comment="客户端IP") status = Column(String(20), default="active", comment="active/closed") started_at = Column(DateTime, default=_utcnow) closed_at = Column(DateTime, nullable=True) server = relationship("Server") admin = relationship("Admin") command_logs = relationship("CommandLog", back_populates="session", cascade="all, delete-orphan") __table_args__ = ( Index('idx_ssh_sessions_server_id', 'server_id'), Index('idx_ssh_sessions_admin_id', 'admin_id'), ) class CommandLog(Base): """SSH command log — record every command executed in terminal""" __tablename__ = "command_logs" id = Column(Integer, primary_key=True, autoincrement=True) session_id = Column(String(36), ForeignKey("ssh_sessions.id", ondelete="CASCADE"), nullable=True, comment="SSH会话ID") server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False, comment="服务器ID") admin_id = Column(Integer, ForeignKey("admins.id", ondelete="SET NULL"), nullable=True, comment="操作人ID") command = Column(String(2000), nullable=False, comment="执行的命令") remote_addr = Column(String(45), nullable=True, comment="客户端IP") created_at = Column(DateTime, default=_utcnow) session = relationship("SshSession", back_populates="command_logs") server = relationship("Server") admin = relationship("Admin") __table_args__ = ( Index('idx_cmdlog_srv_time', 'server_id', 'created_at'), Index('idx_cmdlog_session_id', 'session_id'), ) # ────────────────────────────────────────────── # Quick Commands (终端快捷命令) # ────────────────────────────────────────────── class QuickCommand(Base): """Terminal quick commands — persistent across browsers, shareable across users""" __tablename__ = "quick_commands" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(100), nullable=False, comment="命令名称") cmd = Column(String(500), nullable=False, comment="命令内容") is_builtin = Column(Boolean, default=False, comment="系统内置标记(不可删除/编辑)") sort_order = Column(Integer, default=0, comment="排序权重") created_by = Column(String(100), nullable=True, comment="创建人") created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)