import enum from datetime import datetime, timezone from sqlalchemy import ( Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, UniqueConstraint, ) from sqlalchemy.orm import relationship from .connection import Base def _utcnow() -> datetime: """Naive UTC timestamp for TIMESTAMP WITHOUT TIME ZONE columns. asyncpg rejects timezone-aware datetimes for naive TIMESTAMP columns, so we normalise to naive UTC at the application layer. """ return datetime.now(timezone.utc).replace(tzinfo=None) class UserRole(str, enum.Enum): admin = "admin" moderator = "moderator" participant = "participant" class MeetingStatus(str, enum.Enum): active = "active" ended = "ended" class MeetingRole(str, enum.Enum): """Role a user holds *within a specific meeting* (distinct from the global account role). The meeting creator is the host.""" host = "host" moderator = "moderator" participant = "participant" class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String(50), unique=True, index=True, nullable=False) email = Column(String(255), unique=True, index=True, nullable=False) hashed_password = Column(String(255), nullable=False) role = Column(Enum(UserRole), default=UserRole.participant, nullable=False) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=_utcnow) meetings = relationship( "Meeting", back_populates="host", foreign_keys="Meeting.host_id", ) microphone_configs = relationship("MicrophoneConfig", back_populates="user") memberships = relationship( "MeetingMember", back_populates="user", cascade="all, delete-orphan", ) class Meeting(Base): __tablename__ = "meetings" id = Column(Integer, primary_key=True, index=True) title = Column(String(200), nullable=False) description = Column(String(500), nullable=True) host_id = Column(Integer, ForeignKey("users.id"), nullable=False) invite_token = Column(String(64), unique=True, index=True, nullable=True) created_at = Column(DateTime, default=_utcnow) ended_at = Column(DateTime, nullable=True) status = Column(Enum(MeetingStatus), default=MeetingStatus.active) host = relationship("User", back_populates="meetings", foreign_keys=[host_id]) microphone_configs = relationship("MicrophoneConfig", back_populates="meeting") speech_logs = relationship("SpeechLog", back_populates="meeting") members = relationship( "MeetingMember", back_populates="meeting", cascade="all, delete-orphan", ) class MeetingMember(Base): """Per-meeting membership with a meeting-scoped role.""" __tablename__ = "meeting_members" __table_args__ = ( UniqueConstraint("meeting_id", "user_id", name="uq_meeting_member"), ) id = Column(Integer, primary_key=True, index=True) meeting_id = Column(Integer, ForeignKey("meetings.id"), nullable=False, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) role = Column( Enum(MeetingRole), default=MeetingRole.participant, nullable=False ) joined_at = Column(DateTime, default=_utcnow) meeting = relationship("Meeting", back_populates="members") user = relationship("User", back_populates="memberships") class MicrophoneConfig(Base): __tablename__ = "microphone_configs" id = Column(Integer, primary_key=True, index=True) meeting_id = Column(Integer, ForeignKey("meetings.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) name = Column(String(100), nullable=False) priority = Column(Integer, default=5) is_chairman = Column(Boolean, default=False) is_muted = Column(Boolean, default=False) created_at = Column(DateTime, default=_utcnow) meeting = relationship("Meeting", back_populates="microphone_configs") user = relationship("User", back_populates="microphone_configs") speech_logs = relationship("SpeechLog", back_populates="microphone") class SpeechLog(Base): __tablename__ = "speech_logs" id = Column(Integer, primary_key=True, index=True) meeting_id = Column(Integer, ForeignKey("meetings.id"), nullable=False) microphone_id = Column( Integer, ForeignKey("microphone_configs.id"), nullable=False ) start_time = Column(DateTime, default=_utcnow) end_time = Column(DateTime, nullable=True) peak_db = Column(Float, default=-100.0) avg_db = Column(Float, default=-100.0) meeting = relationship("Meeting", back_populates="speech_logs") microphone = relationship("MicrophoneConfig", back_populates="speech_logs") class SystemDiagnostic(Base): __tablename__ = "system_diagnostics" id = Column(Integer, primary_key=True, index=True) timestamp = Column(DateTime, default=_utcnow, index=True) cpu_usage = Column(Float, default=0.0) memory_usage = Column(Float, default=0.0) audio_latency_ms = Column(Float, default=0.0) buffer_health = Column(Float, default=100.0) packet_loss = Column(Float, default=0.0)