"""Database models and session management.""" from datetime import datetime from typing import Optional, List import json from sqlalchemy import ( create_engine, Column, Integer, String, Boolean, DateTime, Float, ForeignKey, Text, event ) from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session from sqlalchemy.pool import StaticPool from backend.config import settings, DATA_DIR # Create engine if settings.DATABASE_URL.startswith("sqlite"): engine = create_engine( settings.DATABASE_URL, connect_args={"check_same_thread": False}, poolclass=StaticPool, ) else: engine = create_engine(settings.DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() # Database Models class User(Base): """Admin user model.""" __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String(50), unique=True, index=True, nullable=False) hashed_password = Column(String(255), nullable=False) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) last_login = Column(DateTime, nullable=True) class Server(Base): """IPMI Server model.""" __tablename__ = "servers" id = Column(Integer, primary_key=True, index=True) name = Column(String(100), nullable=False) host = Column(String(100), nullable=False) # IP address port = Column(Integer, default=623) username = Column(String(100), nullable=False) encrypted_password = Column(String(255), nullable=False) # Encrypted password # Server type (dell, hpe, etc.) vendor = Column(String(50), default="dell") # Fan control settings manual_control_enabled = Column(Boolean, default=False) third_party_pcie_response = Column(Boolean, default=True) fan_curve_data = Column(Text, nullable=True) # JSON string auto_control_enabled = Column(Boolean, default=False) # Panic mode settings panic_mode_enabled = Column(Boolean, default=True) panic_timeout_seconds = Column(Integer, default=60) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) last_seen = Column(DateTime, nullable=True) is_active = Column(Boolean, default=True) # Relationships sensor_data = relationship("SensorData", back_populates="server", cascade="all, delete-orphan") fan_data = relationship("FanData", back_populates="server", cascade="all, delete-orphan") class FanCurve(Base): """Fan curve configuration.""" __tablename__ = "fan_curves" id = Column(Integer, primary_key=True, index=True) server_id = Column(Integer, ForeignKey("servers.id"), nullable=False) name = Column(String(100), default="Default") curve_data = Column(Text, nullable=False) # JSON array of {temp, speed} points sensor_source = Column(String(50), default="cpu") # cpu, inlet, exhaust, etc. is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class SensorData(Base): """Historical sensor data.""" __tablename__ = "sensor_data" id = Column(Integer, primary_key=True, index=True) server_id = Column(Integer, ForeignKey("servers.id"), nullable=False) sensor_name = Column(String(100), nullable=False) sensor_type = Column(String(50), nullable=False) # temperature, voltage, fan, power value = Column(Float, nullable=False) unit = Column(String(20), nullable=True) timestamp = Column(DateTime, default=datetime.utcnow) server = relationship("Server", back_populates="sensor_data") class FanData(Base): """Historical fan speed data.""" __tablename__ = "fan_data" id = Column(Integer, primary_key=True, index=True) server_id = Column(Integer, ForeignKey("servers.id"), nullable=False) fan_number = Column(Integer, nullable=False) fan_id = Column(String(20), nullable=False) # IPMI fan ID (0x00, 0x01, etc.) speed_rpm = Column(Integer, nullable=True) speed_percent = Column(Integer, nullable=True) is_manual = Column(Boolean, default=False) timestamp = Column(DateTime, default=datetime.utcnow) server = relationship("Server", back_populates="fan_data") class SystemLog(Base): """System event logs.""" __tablename__ = "system_logs" id = Column(Integer, primary_key=True, index=True) server_id = Column(Integer, ForeignKey("servers.id"), nullable=True) event_type = Column(String(50), nullable=False) # panic, fan_change, error, warning, info message = Column(Text, nullable=False) details = Column(Text, nullable=True) timestamp = Column(DateTime, default=datetime.utcnow) class AppSettings(Base): """Application settings storage.""" __tablename__ = "app_settings" id = Column(Integer, primary_key=True, index=True) key = Column(String(100), unique=True, nullable=False) value = Column(Text, nullable=True) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def get_db() -> Session: """Get database session.""" db = SessionLocal() try: yield db finally: db.close() def init_db(): """Initialize database tables.""" Base.metadata.create_all(bind=engine) # Create default admin if no users exist db = SessionLocal() try: # Check if setup is complete setup_complete = db.query(AppSettings).filter(AppSettings.key == "setup_complete").first() if not setup_complete: AppSettings(key="setup_complete", value="false") db.add(AppSettings(key="setup_complete", value="false")) db.commit() finally: db.close() def is_setup_complete(db: Session) -> bool: """Check if initial setup is complete.""" setting = db.query(AppSettings).filter(AppSettings.key == "setup_complete").first() if setting: return setting.value == "true" return False def set_setup_complete(db: Session, complete: bool = True): """Mark setup as complete.""" setting = db.query(AppSettings).filter(AppSettings.key == "setup_complete").first() if setting: setting.value = "true" if complete else "false" else: setting = AppSettings(key="setup_complete", value="true" if complete else "false") db.add(setting) db.commit()