Files
ss-tools/backend/alembic/versions/k5l6m7n8o9p0_maintenance_settings_and_banner_snapshot.py
busya 093f7f600f fix(maintenance): harden banner lifecycle and guarded migrations
- Guard maintenance Alembic operations for create_all-only tables on clean DBs
- Add guarded verification_runs.fanout_plan_id backfill migration
- Improve maintenance banner rendering, chart management, orchestration, and API routes
- Expand assistant maintenance tool and edge-case coverage

Tests: cd backend && source .venv/bin/activate && python -m pytest -q tests/test_maintenance_api.py tests/test_maintenance_service.py tests/api/test_assistant_tool_maintenance.py tests/api/test_maintenance_routes_edge.py (77 passed)
2026-08-09 08:22:16 +03:00

74 lines
3.4 KiB
Python

# #region Alembic.MaintenanceSettingsAndBannerSnapshot [C:3] [TYPE Module] [SEMANTICS alembic,maintenance,banner-height,timezone,snapshot]
# @ingroup Alembic
# @BRIEF Add maintenance_settings.banner_height and maintenance_dashboard_banners.original_position_json;
# migrate default timezone 'UTC' -> 'Europe/Moscow' for the untouched default settings row.
# @LAYER Database
# @RELATION DEPENDS_ON -> [Models.Maintenance.MaintenanceModels]
# @INVARIANT banner_height is nullable — NULL/0 means auto-estimate (unchanged behavior).
# @INVARIANT original_position_json is nullable — legacy banners (pre-release) have no snapshot
# and keep the surgical removal path.
# @INVARIANT The timezone UPDATE touches only rows still equal to 'UTC' — a deliberate user choice
# of 'UTC' is indistinguishable from the old default and therefore preserved as-is;
# rows already customized to another zone are never overwritten.
# @RATIONALE The banner is removed by restoring the pre-mutation position_json verbatim instead of
# rebuilding the layout structure, which broke dashboards (y-shift drift, unreverted
# ROOT->TABS -> ROOT->GRID normalization). The snapshot column stores that JSON.
# @REJECTED Restoring only the banner keys (surgical removal) was rejected as the primary path —
# it cannot revert structural normalization and drifts y-coordinates. Dropping the
# snapshot after restore was rejected — keeping it is harmless and aids debugging.
"""add banner_height + original_position_json, migrate timezone default
Revision ID: k5l6m7n8o9p0
Revises: j1k2l3m4n5o6
Create Date: 2026-08-04 10:30:00.000000
"""
from collections.abc import Sequence
import sqlalchemy as sa
from sqlalchemy import inspect
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "k5l6m7n8o9p0"
down_revision: str | Sequence[str] | None = "j1k2l3m4n5o6"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _table_exists(table: str) -> bool:
"""Check if a table exists in the current database connection."""
return inspect(op.get_bind()).has_table(table)
def upgrade() -> None:
"""Add the two columns and migrate the untouched timezone default."""
# maintenance_settings / maintenance_dashboard_banners are create_all()-only tables on a
# fresh install (no base migration creates them); guard so clean-DB upgrade is a no-op and
# the ORM schema initialization creates them with the final shape.
if _table_exists("maintenance_settings"):
op.add_column(
"maintenance_settings",
sa.Column("banner_height", sa.Integer(), nullable=True),
)
op.execute(
"UPDATE maintenance_settings SET display_timezone = 'Europe/Moscow' "
"WHERE id = 'default' AND display_timezone = 'UTC'"
)
if _table_exists("maintenance_dashboard_banners"):
op.add_column(
"maintenance_dashboard_banners",
sa.Column("original_position_json", sa.Text(), nullable=True),
)
def downgrade() -> None:
"""Drop the two columns (timezone values are left as-is)."""
if _table_exists("maintenance_dashboard_banners"):
op.drop_column("maintenance_dashboard_banners", "original_position_json")
if _table_exists("maintenance_settings"):
op.drop_column("maintenance_settings", "banner_height")
# #endregion Alembic.MaintenanceSettingsAndBannerSnapshot