"""Durable last-run state per scheduled job Revision ID: 031 Revises: 030 Create Date: 2026-08-08 00:00:00.000000 Job run state lived only in an in-memory dict in ``app.scheduler``, so every process restart wiped it. Admin → Jobs could then only report "Active" with no indication of whether a job had ever run, or how it ended — which is exactly the information an operator opens that page for. One row per job, upserted on ``job_name``. Not history: ``system_events`` already grows unbounded with no retention job, and a second append-only operational table would repeat that debt. The table starts empty; each job populates its row the next time it finishes. No backfill from ``system_events`` — that table only records warning/error outcomes and uses a different status vocabulary, so seeding from it would invent successful runs that never happened. """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = "031" down_revision: Union[str, None] = "030" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "job_run_state", sa.Column("id", sa.Integer(), nullable=False), sa.Column("job_name", sa.String(length=64), nullable=False), sa.Column("status", sa.String(length=32), nullable=False), sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), sa.Column("finished_at", sa.DateTime(timezone=True), nullable=False), sa.Column("processed", sa.Integer(), nullable=True), sa.Column("total", sa.Integer(), nullable=True), sa.Column("message", sa.Text(), nullable=True), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("job_name", name="uq_job_run_state_job_name"), ) def downgrade() -> None: op.drop_table("job_run_state")