from __future__ import annotations from datetime import datetime from sqlalchemy import JSON, DateTime, Index, String, Text, 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 TimelineEntry(Base): """One thing that happened to the infrastructure: an alert firing, a playbook run, a change to Steward's settings. Each row stands on its own, so it outlives the record it points to (`ref_type` / `ref_id`) once that record's retention removes it. `type` is a key of steward.timeline.catalog. Whether a type is shown, and which area it sorts under, are settings applied when the timeline is read; `area` is the area the event itself named when it was written, used unless Settings → Timeline gives its type one.""" __tablename__ = "timeline_entries" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=time_ordered_id) at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) # When something that lasted (a run) ended. until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) type: Mapped[str] = mapped_column(String(64), nullable=False) area: Mapped[str] = mapped_column(String(32), nullable=False) # The hosts it happened on, by name, as every other record keyed by host. hosts: Mapped[list[str]] = mapped_column(ARRAY(String(128)), nullable=False, default=list) # What it happened to, when that is not a host: a container, a rule's resource. subject: Mapped[str | None] = mapped_column(String(255), nullable=True) summary: Mapped[str] = mapped_column(Text, nullable=False) detail: Mapped[dict | None] = mapped_column(JSON, nullable=True) # A username, " via MCP", or "system". actor: Mapped[str] = mapped_column(String(64), nullable=False, default="system") ref_type: Mapped[str | None] = mapped_column(String(32), nullable=True) ref_id: Mapped[str | None] = mapped_column(String(64), nullable=True) __table_args__ = ( Index("ix_timeline_entries_at", "at", "id"), Index("ix_timeline_entries_hosts", "hosts", postgresql_using="gin"), Index("ix_timeline_entries_type", "type"), # One entry per source record, so a backfill can run over rows the # live writers already covered. Index("uq_timeline_entries_ref", "ref_type", "ref_id", unique=True, postgresql_where=text("ref_id IS NOT NULL")), )