"""Reports: what was found, written down once and never replied to.""" from __future__ import annotations from sqlalchemy import Boolean, ForeignKey, String, Text from sqlalchemy.orm import Mapped, mapped_column from lembas.db.base import Base, Timestamps, UUIDPrimaryKey # Where a report came from. Not a foreign key to anything -- see `source_id`. SOURCE_SCHEDULE = "schedule" SOURCE_CHAT = "chat" SOURCE_MANUAL = "manual" SOURCES = (SOURCE_SCHEDULE, SOURCE_CHAT, SOURCE_MANUAL) class Report(UUIDPrimaryKey, Timestamps, Base): """A finished piece of work, filed. Deliberately not a `Chat` with one `Message` in it. A report is read top to bottom and never answered, so everything a conversation carries -- a composer, a sidebar row, a title that regenerates itself, a bubble with an avatar and a rewind button -- would be machinery to suppress rather than machinery to use. It is the same line `services/library/` already draws between a note and a chat: a durable artefact is not a turn. It must also be writable with no chat behind it at all, being the fallback destination for a scheduled run whose own chat has gone. `body` is Markdown written by a model and goes through `services/markdown.py` like everything else from an endpoint. Hard rule 6 applies here exactly as it does in a transcript. """ __tablename__ = "reports" owner_id: Mapped[str] = mapped_column( String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) title: Mapped[str] = mapped_column(String(300), nullable=False) # One line for the list page, so a feed of forty reports can be read without # opening any of them. Written by the model beside the body; falls back to # the body's first line when it did not bother. summary: Mapped[str] = mapped_column(String(500), default="") body: Mapped[str] = mapped_column(Text, default="") source: Mapped[str] = mapped_column(String(16), default=SOURCE_MANUAL, nullable=False) # The chat or the schedule this came out of, kept so a report can say where # it was made. Deliberately not a ForeignKey: `migrations.py` compiles the # column type only, so a REFERENCES clause would exist on a fresh database # and not on an upgraded one -- the same reason `Chat.compacted_through_id` # and `Folder.ssh_profile_id` are plain ids. Both are validated on read, and # the row outliving what it points at is normal rather than exceptional: a # report is worth keeping after the chat that produced it has been deleted. source_id: Mapped[str] = mapped_column(String(32), default="") schedule_id: Mapped[str] = mapped_column(String(32), default="") model_id: Mapped[str] = mapped_column(String(300), default="") # NOT NULL with a scalar default so `migrations._add_column_sql` can backfill # it if this column is ever added to a table that already has rows. unread: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Whether its arrival has already been announced. The dot can be shown for # as long as it is unread; the toast and the browser notification must fire # once. Without this the poll would announce the same report every ten # seconds until somebody opened it, which is the shape of notification # nobody leaves switched on. `Chat.unread_notified` exists for exactly this # and this is the same pair. unread_notified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # Why a run produced nothing worth reading. A scheduled report that failed # is still a report -- one that silently did not appear is indistinguishable # from a schedule that never fired. error: Mapped[str] = mapped_column(Text, default="") def __repr__(self) -> str: return f""