# Data Model: Maintenance Banner for Dashboards **Feature Branch**: `031-maintenance-banner` **Created**: 2026-05-21 **Status**: Phase 1 — Design ## Entity-Relationship Overview ``` ┌──────────────────────┐ ┌─────────────────────────────┐ │ MaintenanceSettings │ │ MaintenanceEvent │ │ (single row) │ │ │ │ ────────────────── │ │ id: str (PK) │ │ target_environment │◄──────│ task_id: str (FK→Task) │ │ display_timezone │ │ tables: JSON (str[]) │ │ dashboard_scope │ │ start_time: datetime │ │ excluded_ids: JSON │ │ end_time: datetime │ │ forced_ids: JSON │ │ message: str? │ │ updated_at │ │ status: enum(pending|applying │ └──────────────────────┘ │ |active|partial|ending │ │ |completed|failed) │ │ created_at / updated_at │ └──────────┬────────────────────┘ │ 1:N ▼ ┌─────────────────────────────┐ │ MaintenanceDashboardState │ │ │ │ id: str (PK) │ │ event_id: str (FK→Event) │ │ dashboard_id: int │ │ banner_id: str (FK→Banner) │ │ status: enum(pending_apply │ │ |active|removing │ │ |removed|apply_failed │ │ |removal_failed) │ │ created_at / updated_at │ └──────────┬──────────────────┘ │ N:1 ▼ ┌─────────────────────────────┐ │ MaintenanceDashboardBanner │ │ (ONE chart per dashboard) │ │ │ │ id: str (PK) │ │ environment_id: str │ │ dashboard_id: int │ │ chart_id: int? (Superset) │ │ banner_text: text │ │ status: enum(active|removed)│ │ UNIQUE (env_id, dash_id, │ │ status=active) │ │ created_at / updated_at │ └─────────────────────────────┘ ``` ## SQLAlchemy Models ### MaintenanceEvent ```python # backend/src/models/maintenance.py from sqlalchemy import Column, String, DateTime, JSON, Enum, Text from sqlalchemy.orm import relationship from src.models.base import Base import enum class MaintenanceEventStatus(str, enum.Enum): PENDING = "pending" APPLYING = "applying" ACTIVE = "active" PARTIAL = "partial" # some dashboards succeeded, some failed ENDING = "ending" COMPLETED = "completed" FAILED = "failed" class MaintenanceEvent(Base): __tablename__ = "maintenance_events" id = Column(String, primary_key=True, index=True) task_id = Column(String, nullable=True, index=True) # Loose reference — no FK to avoid circular dependency environment_id = Column(String, nullable=False) # Target env at event creation (snapshot) tables = Column(JSON, nullable=False) start_time = Column(DateTime(timezone=True), nullable=False) end_time = Column(DateTime(timezone=True), nullable=True) # nullable — "Окончание: уточняется" when absent message = Column(Text, nullable=True) status = Column( Enum(MaintenanceEventStatus), nullable=False, default=MaintenanceEventStatus.PENDING, index=True ) created_at = Column(DateTime(timezone=True), server_default="now()") updated_at = Column(DateTime(timezone=True), server_default="now()", onupdate="now()") dashboard_states = relationship( "MaintenanceDashboardState", back_populates="maintenance_event", cascade="all, delete-orphan" ) ``` ### MaintenanceDashboardBanner ```python class MaintenanceDashboardBannerStatus(str, enum.Enum): ACTIVE = "active" REMOVED = "removed" class MaintenanceDashboardBanner(Base): __tablename__ = "maintenance_dashboard_banners" id = Column(String, primary_key=True, index=True) environment_id = Column(String, nullable=False) dashboard_id = Column(Integer, nullable=False, index=True) chart_id = Column(Integer, nullable=True) # Superset markdown chart ID banner_text = Column(Text, nullable=True) # Current aggregated markdown status = Column( Enum(MaintenanceDashboardBannerStatus), nullable=False, default=MaintenanceDashboardBannerStatus.ACTIVE ) created_at = Column(DateTime(timezone=True), server_default="now()") updated_at = Column(DateTime(timezone=True), server_default="now()", onupdate="now()") dashboard_states = relationship("MaintenanceDashboardState", back_populates="banner") __table_args__ = ( Index("ix_banner_unique_active", "environment_id", "dashboard_id", unique=True, postgresql_where=(status == "active")), ) ``` ### MaintenanceDashboardState ```python class MaintenanceDashboardStateStatus(str, enum.Enum): PENDING_APPLY = "pending_apply" ACTIVE = "active" REMOVING = "removing" REMOVED = "removed" APPLY_FAILED = "apply_failed" REMOVAL_FAILED = "removal_failed" class MaintenanceDashboardState(Base): __tablename__ = "maintenance_dashboard_states" id = Column(String, primary_key=True, index=True) event_id = Column(String, ForeignKey("maintenance_events.id"), nullable=False, index=True) dashboard_id = Column(Integer, nullable=False, index=True) banner_id = Column(String, ForeignKey("maintenance_dashboard_banners.id"), nullable=True) status = Column( Enum(MaintenanceDashboardStateStatus), nullable=False, default=MaintenanceDashboardStateStatus.PENDING_APPLY ) created_at = Column(DateTime(timezone=True), server_default="now()") updated_at = Column(DateTime(timezone=True), server_default="now()", onupdate="now()") maintenance_event = relationship("MaintenanceEvent", back_populates="dashboard_states") banner = relationship("MaintenanceDashboardBanner", back_populates="dashboard_states") __table_args__ = ( Index("ix_mds_dashboard_status", "dashboard_id", "status"), ) ``` ### MaintenanceSettings ```python class DashboardScope(str, enum.Enum): PUBLISHED_ONLY = "published_only" DRAFT_ONLY = "draft_only" ALL = "all" class MaintenanceSettings(Base): __tablename__ = "maintenance_settings" id = Column(String, primary_key=True, default="default") target_environment_id = Column(String, nullable=False) display_timezone = Column(String, nullable=False, default="UTC") banner_template = Column(Text, nullable=False, default=( '