From c4dffd471b5121c2631e3560c72bc8b4d95ad7b8 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Thu, 14 May 2026 21:26:56 +0200 Subject: [PATCH] feat(l4d2-web): add command_history table for RCON console transcript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A row per RCON command execution: (user, server, command, reply, is_error, created_at). Composite index on (user_id, server_id, id) supports the only query shape — "latest N for this user+server", id DESC. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../alembic/versions/0012_command_history.py | 48 +++++++++++++++++++ l4d2web/models.py | 23 +++++++++ 2 files changed, 71 insertions(+) create mode 100644 l4d2web/alembic/versions/0012_command_history.py diff --git a/l4d2web/alembic/versions/0012_command_history.py b/l4d2web/alembic/versions/0012_command_history.py new file mode 100644 index 0000000..fee821e --- /dev/null +++ b/l4d2web/alembic/versions/0012_command_history.py @@ -0,0 +1,48 @@ +"""add command_history table + +Revision ID: 0012_command_history +Revises: 0011_server_hostname +Create Date: 2026-05-14 +""" +from __future__ import annotations + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +revision: str = "0012_command_history" +down_revision: Union[str, Sequence[str], None] = "0011_server_hostname" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "command_history", + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False), + sa.Column( + "server_id", + sa.Integer(), + sa.ForeignKey("servers.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("command", sa.Text(), nullable=False), + sa.Column("reply", sa.Text(), nullable=False, server_default=""), + sa.Column( + "is_error", sa.Boolean(), nullable=False, server_default=sa.text("0") + ), + sa.Column("created_at", sa.DateTime(), nullable=False), + ) + op.create_index( + "ix_cmdhist_user_server_id", + "command_history", + ["user_id", "server_id", "id"], + ) + + +def downgrade() -> None: + op.drop_index("ix_cmdhist_user_server_id", table_name="command_history") + op.drop_table("command_history") diff --git a/l4d2web/models.py b/l4d2web/models.py index 029c90d..f43fd32 100644 --- a/l4d2web/models.py +++ b/l4d2web/models.py @@ -227,3 +227,26 @@ class SteamUserProfile(Base): persona_name: Mapped[str] = mapped_column(String(64), nullable=False) avatar_url: Mapped[str] = mapped_column(Text, nullable=False) fetched_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + + +class CommandHistory(Base): + __tablename__ = "command_history" + __table_args__ = ( + Index("ix_cmdhist_user_server_id", "user_id", "server_id", "id"), + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) + server_id: Mapped[int] = mapped_column( + ForeignKey("servers.id", ondelete="CASCADE"), nullable=False + ) + command: Mapped[str] = mapped_column(Text, nullable=False) + reply: Mapped[str] = mapped_column( + Text, nullable=False, default="", server_default="" + ) + is_error: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=False, server_default=text("0") + ) + created_at: Mapped[datetime] = mapped_column( + DateTime, default=now_utc, nullable=False + )