from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text from sqlalchemy.dialects.postgresql import ARRAY from sqlalchemy.orm import Mapped, mapped_column from .base import Base from steward.core.ids import time_ordered_id class Ticket(Base): """A known problem, open until someone resolves it: attached to hosts and to what it is about (a container, a monitor, a resource), or to nothing. Numbered, so a person can say "#12".""" __tablename__ = "tickets" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) title: Mapped[str] = mapped_column(String(200), nullable=False) body: Mapped[str | None] = mapped_column(Text, nullable=True) # An area key of Settings → Timeline, as a timeline entry names one. area: Mapped[str] = mapped_column(String(32), nullable=False) hosts: Mapped[list[str]] = mapped_column(ARRAY(String(128)), nullable=False, default=list) # What it is about when that is not a host: an alert rule's resource, a # container ("/"), a monitor's name. subject: Mapped[str | None] = mapped_column(String(255), nullable=True) status: Mapped[str] = mapped_column(String(16), nullable=False, default="open") opened_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) # A username, or " via MCP"; the id is who may be named its author. opened_by: Mapped[str] = mapped_column(String(64), nullable=False) opened_by_id: Mapped[str | None] = mapped_column(String(36), nullable=True) resolved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) resolved_by: Mapped[str | None] = mapped_column(String(64), nullable=True) resolution: Mapped[str | None] = mapped_column(Text, nullable=True) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) __table_args__ = ( Index("ix_tickets_status", "status", "updated_at"), Index("ix_tickets_hosts", "hosts", postgresql_using="gin"), ) class TicketLink(Base): """What a ticket is linked to: an alert rule or a metric anomaly, by id.""" __tablename__ = "ticket_links" ticket_id: Mapped[int] = mapped_column(Integer, ForeignKey("tickets.id", ondelete="CASCADE"), primary_key=True) ref_type: Mapped[str] = mapped_column(String(32), primary_key=True) ref_id: Mapped[str] = mapped_column(String(64), primary_key=True) __table_args__ = (Index("ix_ticket_links_ref", "ref_type", "ref_id"),) class TicketNote(Base): """One line of a ticket's log: a comment, or it being opened, edited, resolved or reopened.""" __tablename__ = "ticket_notes" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=time_ordered_id) ticket_id: Mapped[int] = mapped_column(Integer, ForeignKey("tickets.id", ondelete="CASCADE"), nullable=False) at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) # comment | opened | edited | resolved | reopened kind: Mapped[str] = mapped_column(String(16), nullable=False) actor: Mapped[str] = mapped_column(String(64), nullable=False) body: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = (Index("ix_ticket_notes_ticket", "ticket_id", "at"),)