Files
ss-tools/specs/031-maintenance-banner/spec.md
busya ec6421de35 rename ss-tools to superset-tools across the entire project
- Replace all occurrences of 'ss-tools' with 'superset-tools' in 104 files
- Rename git bundle file ss-tools.bundle → superset-tools.bundle
- Update .gitignore pattern accordingly
- Preserve variable names (hasSsTools etc.) and code identifiers
2026-06-16 11:15:19 +03:00

33 KiB
Raw Blame History

Feature Specification: Maintenance Banner for Dashboards

Feature Branch: 031-maintenance-banner
Created: 2026-05-21
Status: Draft
Updated: 2026-05-25 — Added API Key Authentication (US6, FR-018FR-022); Revised FR-001 to require explicit environment_id with API key scoping rules; Revised FR-002, FR-008, FR-009 for environment_id consistency

Input: User description: "Service for adding a maintenance banner to dashboards during ETL table updates. External tool passes names of tables being updated to an API method in superset-tools; superset-tools finds dashboards with datasets referencing those tables (both table-based and virtual SQL Jinja datasets) and adds a static text-chart at the top of the dashboard with maintenance information. Support passing start and end times. Banner removal — centrally from superset-tools or externally via API."

Clarifications

Session 2026-05-21

  • Q: Which Superset environment should /api/maintenance/start target (superset-tools manages multiple)? → A: The caller specifies the target environment via the environment_id field in the request body. This is REQUIRED. The external tool or API key scope determines which environment is used. Revision (2026-05-25): Original decision had the system auto-targeting a single pre-configured "production" environment. This was revised when API Key environment scoping was added — scoped keys can only operate within their assigned environment, and the environment_id field became explicit and required.
  • Q: Should /api/maintenance/start be synchronous (blocking) or asynchronous (task-based)? → A: Async — returns {task_id, status: "pending"} immediately; operator polls GET /api/tasks/{task_id} or uses WebSocket for progress (matches existing TaskManager pattern).
  • Q: How should table names from the API be matched against dataset references? → A: Exact schema.table match only (e.g., "raw.sales" matches only "raw.sales", not unqualified "sales"). Case-insensitive.
  • Q: Should duplicate /start calls (same tables, same times) create a new MaintenanceEvent or be idempotent? → A: Idempotent by (tables, start_time, end_time, environment_id) key. Same tuple = already_active. Changed end_time (or adding it when previously omitted) = new event. Omitted end_time treated as None in the key. Revision (2026-05-25): Added environment_id to idempotency key — same tables in different environments are distinct events.
  • Q: When /end is called on event M1 but dashboard D is also affected by active event M2, should the banner be removed? → A: No — banner stays on D but is rebuilt with updated text (only M2's info). Removal occurs only when the LAST active event affecting D is ended.

Session 2026-05-25 — API Key Authentication

  • Q: How should external tools (Airflow, CI/CD) authenticate without browser-based JWT login? → A: Static API Key in X-API-Key header. Key is bound to one environment and limited to maintenance permissions (start/end/end_all). Key is generated once via admin UI; raw value shown only at creation time. Stored as SHA-256 hash.
  • Q: Should API keys have scoped permissions? → A: Yes — each key has a permissions list containing fine-grained operations: maintenance:start, maintenance:end, maintenance:end_all. If environment_id is set on the key, the key may only operate on that environment.
  • Q: Should API keys be refreshable? → A: No. If compromise is suspected, admin revokes the key (sets active = false) and generates a new one. Re-keying is intentional — not automatic.
  • Q: Where should API key management live in the UI? → A: Centralized in System Settings (/settings → System), NOT embedded in the Maintenance panel. API keys are cross-cutting infrastructure — future tools (migration API, translation API) will also consume them. Maintenance is the first consumer. The keys table, generation form, and revocation UI are reusable components shared across all tools.

User Scenarios & Testing (mandatory)

User Story 1 — External tool initiates maintenance banner display (Priority: P1)

An ETL operator triggers a table refresh. The external tool (e.g., Airflow DAG) calls an superset-tools API endpoint, passing:

  • List of table names being updated (e.g., ["raw.sales", "raw.inventory"])
  • Maintenance start time
  • Planned end time
  • (optional) custom message text

superset-tools finds all dashboards in Superset whose datasets reference the given tables (both table-based datasets and virtual/SQL Jinja datasets whose SQL query references the tables). For each affected dashboard, superset-tools adds a markdown chart at the very top with the maintenance warning.

Why this priority: This is the core use case — without it, the feature has no value. It enables automatic notification of dashboard users about potentially stale data during ETL.

Independent Test: Can be tested by calling the API with a table name known to be used in a test dashboard, then verifying a markdown chart appears at the top of that dashboard in Superset.

Acceptance Scenarios:

  1. Given dashboard D in the ss-dev environment uses a dataset referencing table raw.sales, When external tool calls POST /api/maintenance/start with {"tables": ["raw.sales"], "start_time": "2026-05-21T22:00:00Z", "end_time": "2026-05-22T02:00:00Z", "environment_id": "ss-dev"}, Then API returns {task_id: "t-...", status: "pending"} immediately, and upon task completion dashboard D in Superset shows a markdown chart at the top: "⚠️ Maintenance in progress: 2026-05-21 22:00 — 2026-05-22 02:00 UTC. Data may be incomplete."

  2. Given dashboard D uses a virtual SQL dataset with query SELECT * FROM raw.sales JOIN raw.inventory ON ..., When external tool passes table raw.inventory, Then dashboard D also receives the banner (since the table is found in the dataset SQL text).

  3. Given dashboard D already has a banner from a previous maintenance event, When a new POST /api/maintenance/start request arrives referencing a different table that also affects D, Then the banner is updated (time and text replaced) rather than duplicated.

  4. Given table raw.archived is not used in any dashboard, When external tool passes {"tables": ["raw.archived"]}, Then API returns {task_id, maintenance_id, status: "pending"} immediately. Upon task completion, the task result contains {status: "no_match", affected_dashboards: 0, unmatched_tables: ["raw.archived"]}. No dashboard is modified.

  5. Given an active MaintenanceEvent E already exists with tables ["raw.sales"], When external tool calls /start again with the same tables, Then API returns {maintenance_id: E.id, status: "already_active"} without creating a duplicate event.

  6. Given external tool does not know the end time, When it calls /start with {"tables": ["raw.sales"], "start_time": "2026-05-21T22:00:00Z"} (no end_time), Then API returns 202 {task_id}, and the banner shows «Окончание: уточняется».


User Story 2 — External tool removes maintenance banner upon completion (Priority: P1)

The ETL process completes. The external tool calls an superset-tools API endpoint to remove banners. superset-tools removes markdown charts from all dashboards affected by the given maintenance event.

Why this priority: Critical for restoring normal dashboard appearance after maintenance. Users must not see stale warnings.

Independent Test: After US1, call POST /api/maintenance/{id}/end and verify charts disappear from dashboards.

Acceptance Scenarios:

  1. Given maintenance event M added banners to dashboards D1, D2 (no other active events affect D1 or D2), When external tool calls POST /api/maintenance/{maintenance_id}/end, Then banners are removed from D1 and D2, and maintenance event M is marked as completed.

  2. Given maintenance event M is already completed, When external tool calls POST /api/maintenance/{maintenance_id}/end again, Then API returns {"status": "already_completed", "message": "Maintenance M was already completed at ..."} (idempotent).

  3. Given dashboard D is affected by active events M1 and M2, When external tool calls /end on M1, Then the banner on D is NOT removed — instead, the banner text is rebuilt to show only M2's information. M1 is marked as completed. The banner is fully removed only when M2 also ends.


User Story 3 — Administrator manages banners via superset-tools UI (Priority: P2)

An administrator opens the superset-tools web interface, sees a list of active maintenance events and affected dashboards. They can selectively or bulk-remove banners without waiting for the external tool.

Why this priority: Administrators need manual override capability in case of ETL tool failure or emergency banner removal.

Independent Test: Navigate to maintenance management page, view active events, click "Remove All" and verify banners disappear.

Acceptance Scenarios:

  1. Given active maintenance events M1 (affects D1, D2) and M2 (affects D3), When administrator opens the "Maintenance Banners" page, Then they see a table with M1, M2, affected dashboard counts, start/end times, and action buttons.

  2. Given administrator is on the management page, When they select M1 and click "Remove Banners", Then markdown charts are removed from D1, D2, and M1 status changes to "Completed".

  3. Given administrator is on the management page, When they click "Remove All", Then banners are removed from all dashboards across all active maintenance events.


User Story 4 — Maintenance mode scope configuration (Priority: P3)

Administrator configures which dashboards are affected by automatic banner placement:

  • Published only, drafts only, or both
  • List of dashboards excluded from maintenance mode (hard ban — banner is never added)
  • List of dashboards forcibly included (banner always added, even if no table match is found)

Why this priority: Configuration flexibility allows adaptation to specific processes, but the system works with sensible defaults.

Independent Test: Configure exclusion of dashboard D1, then trigger maintenance on a table used in D1 — banner must NOT appear on D1, but must appear on D2.

Acceptance Scenarios:

  1. Given default settings (scope = published_only, excluded = [], forced = []), When maintenance affects draft dashboard D, Then banner is NOT added to D.

  2. Given administrator added dashboard D to the excluded list, When maintenance affects a table used in D, Then banner is NOT added to D, and logs record the reason: "Dashboard D is in exclusion list".

  3. Given administrator added dashboard D to the forced list, When maintenance does NOT affect any table in D, Then banner IS STILL added to D (forced inclusion).

  4. Given scope = all (published + draft), When maintenance affects tables in D1 (published) and D2 (draft), Then banner is added to BOTH dashboards.


User Story 5 — Maintenance status indicator in dashboard grid (Priority: P3)

The Dashboard Hub table shows an indicator when a dashboard has an active maintenance banner. Users can see this information without opening Superset.

Why this priority: Improves operator UX but does not block core functionality.

Independent Test: After US1, the dashboard grid shows a maintenance badge in the status column.

Acceptance Scenarios:

  1. Given dashboard D has an active maintenance banner, When a user opens the Dashboard Hub, Then dashboard D's row shows an orange badge "⚠️ Maintenance" with a tooltip showing start and end times.

  2. Given the maintenance banner is removed from dashboard D, When the user refreshes the Dashboard Hub page, Then the badge disappears.


User Story 6 — External tool authenticates via API Key (Priority: P1)

External tools (Airflow DAGs, CI/CD pipelines, monitoring scripts) need a simple, token-based authentication mechanism to start and end maintenance events. Instead of the browser-based JWT authentication flow (login → get token → call API), external callers present a static API Key in the X-API-Key HTTP header. The API Key is bound to a specific Superset environment and limited to maintenance operations only. The API key is a read-once secret — shown only at creation, never stored in plaintext.

Why this priority: This is the authentication enabler for the core P1 use cases (US1, US2). Without API keys, every external script must manage JWT login sessions — brittle and insecure for CI/CD contexts. This is as fundamental as US1.

Independent Test: Generate an API key via the superset-tools admin UI, then call POST /api/maintenance/start with X-API-Key: <key> header (no JWT cookie). Verify the maintenance event is created and the banner appears on the dashboard.

Acceptance Scenarios:

  1. Given an administrator has generated an API key ssk_aB3x... for the ss-dev environment with permission maintenance:start, When an external tool calls POST /api/maintenance/start with header X-API-Key: ssk_aB3x... (no other auth), Then the request is authenticated as the API key principal, and the maintenance event is created. The task creator metadata records api_key: ci-gitlab-deploy.

  2. Given an API key has only maintenance:start permission, When the same key is used to call POST /api/maintenance/{id}/end, Then the request is rejected with 403 Forbidden{"detail": "API key lacks maintenance:end permission"}.

  3. Given an API key bound to the ss-dev environment, When the external tool specifies environment_id: ss-prod in the request body, Then the request is rejected with 400 Bad Request{"detail": "API key is restricted to environment ss-dev"}.

  4. Given an administrator revokes an API key (sets active = false), When the now-revoked key is used, Then the request is rejected with 401 Unauthorized{"detail": "API key has been revoked"}.

  5. Given an API key has an expires_at date in the past, When the key is used after expiration, Then the request is rejected with 401 Unauthorized{"detail": "API key has expired"}.

  6. Given an administrator generates a new API key via POST /api/admin/api-keys, When the key is created, Then the response returns the RAW key value (visible once only — never stored or retrievable again). The database stores only the SHA-256 hash of the key. Subsequent GET /api/admin/api-keys calls return the key's name, prefix (first 11 chars for identification), environment_id, permissions, active status, and created_at — but NOT the raw key.

  7. Given a failed CI/CD pipeline, When an API key with maintenance:end_all permission calls POST /api/maintenance/end-all, Then all active banners are removed across the key's bound environment.

  8. Given a request arrives with BOTH a valid JWT session cookie AND a valid X-API-Key header, When the system processes the request, Then the JWT session takes precedence — the user's identity is used, and the API key is logged but ignored. This prevents session fixation attacks when the header is accidentally present.

  9. Given the administrator navigates to Settings → System, When they open the "API Keys" section, Then they see the centralized key management table (NOT hidden inside the Maintenance panel). The same API key infrastructure will serve future tools (migration, translation) without code changes to the UI.


Edge Cases

  • Multiple maintenance events conflict: If two ETL processes simultaneously update different tables affecting the same dashboard — the banner must aggregate information from all active events. When one event ends, the banner text is rebuilt from remaining active events rather than removed. Full banner removal occurs only when ALL events affecting that dashboard are completed.

  • Dashboard with no datasets (empty dashboard): Must not receive a banner, even if tables are listed in the maintenance event.

  • Table referenced in multiple datasets within one dashboard: Banner is added only once per dashboard, regardless of how many dataset references the table has within it.

  • Network error contacting Superset API: If some dashboards are processed successfully and others fail due to Superset errors, the API returns a partial success response listing the problematic dashboards.

  • Very large table list (100+): API must process Superset queries in batches and return progress or a warning about extended processing time.

  • Dashboard deleted from Superset between discovery and banner placement: Handle 404 from Superset API, log the event, and continue processing the rest.

  • Virtual dataset with Jinja and SQL: Table names may appear both in plain SQL (FROM/JOIN) and inside Jinja {% set %} blocks (e.g., dictionary mapping dataset types to table names). Extraction uses global pattern matching on raw text (not Jinja-stripping first) to capture ALL schema.table occurrences, then sqlparse filters out false positives inside SQL string literals. Unqualified table references are not matched.

  • Dashboard present in both excluded and forced lists: Forced takes priority (explicit inclusion overrides exclusion).

  • Duplicate /start call with identical parameters: Idempotency key is (tables, start_time, end_time, environment_id). Same tuple → "already_active". Same tables + start_time + env but different end_time → new event. Different environment_id → different event (even with same tables). Omitted end_time is None in the key. The message field is NOT part of the idempotency key.

  • Message HTML escaping: The message field in the API request is plain text only. All HTML special characters (<, >, &, ", ') are escaped before rendering in the Superset markdown chart. Raw HTML/CSS injection via the message field is rejected at the API boundary (400).

  • Time zones: Maintenance start and end times must be correctly displayed in the banner text respecting timezone (accept ISO 8601 with timezone, display in local timezone or UTC as configured).

  • Tabbed dashboards: The banner is added to the root dashboard layout only. If a Superset dashboard uses tabs, the banner may not be visible when viewing individual tabs — this is a known limitation. Future enhancement may add per-tab banner placement.

  • API key raw value leak: The raw key (ssk_...) is returned exactly ONCE — in the response to POST /api/admin/api-keys. If the caller fails to capture it, the admin must revoke and regenerate. This is by design: storing plaintext API keys is a CWE-312 vulnerability.

  • API key concurrent revocation and use: If an admin revokes a key while a task is already in-flight (authenticated and dispatched), the task completes normally. Revocation takes effect for NEW requests only. This is intentional — in-flight operations should not be interrupted.

  • API key rate limiting (future): Not implemented in v1. Abuse prevention is the admin's responsibility via key rotation. Rate limiting may be added as a cross-cutting concern.

Requirements (mandatory)

Functional Requirements

  • FR-001: System MUST provide API endpoint POST /api/maintenance/start accepting: list of table names (tables: str[], max 100 entries, sorted set for idempotency), start time (start_time: datetime — required, in the future ±1h grace period), end time (end_time: datetime | None — optional, must be > start_time if provided), optional message text (message: str | None), and target environment_id (environment_id: str — required, identifies the Superset environment to scan/modify). Always returns 202 {task_id, maintenance_id, status: "pending"} immediately after validation and event creation.

    environment_id scoping rules (2026-05-25 update):

    • When called with JWT session auth, environment_id must be explicitly provided by the caller (operator selects the target environment in the UI or script).
    • When called with API Key, the key's environment_id scope is enforced: if the key is scoped to a specific environment (e.g., ss-dev), the request's environment_id must match (or be omitted — in which case the key's environment is used as default). A scoped key cannot target a different environment — 400 Bad Request is returned.
    • When the API key has environment_id = "*" (all environments), the caller may specify any environment.
    • If no environment_id can be resolved (none in request body, none in API key scope, none in settings), the request is rejected with 400 Bad Request — "environment_id is required".

    Sync short-circuits only for already_active (idempotency) and validation errors (400). When end_time is omitted, the banner displays «Окончание: уточняется».

  • FR-002: System MUST, upon receiving a maintenance start request, find all dashboards in the target Superset environment (specified by environment_id in the request) whose datasets reference the given tables. The environment_id is explicit and required — scoped by the caller or the API key's environment.

  • FR-003: System MUST support finding tables in two dataset types using case-insensitive exact schema.table matching:

    • Table-based (physical tables): match on {schema}.{table_name} format (e.g., "raw.sales" matches dataset with schema=raw, table_name=sales).
    • Virtual/SQL (is_sqllab_view = true): extract ALL fully-qualified schema.table references from the raw SQL+Jinja text using global pattern matching (regex), then filter false positives via sqlparse tokenizer. This captures table names in FROM/JOIN clauses, Jinja {% set %} blocks, CTE bodies, and subqueries. Jinja is NOT pre-stripped — table names inside {% set %} blocks are preserved. Unqualified references are NOT matched.
  • FR-004: System MUST add a markdown chart (Superset viz_type: "markdown") at the top of each affected dashboard with maintenance information.

  • FR-005: The banner text is rendered from banner_template (configured in MaintenanceSettings). For a single active event, variables {start_time}, {end_time}, {message} are directly substituted. For multiple active events, aggregated text is built by rendering each event's info as a sub-block (e.g., "Event N: {start} {end}"), and the combined text is substituted as {message}. Variables not present in the template are replaced with an empty string. The message field from the API request is plain text — HTML special characters are escaped before substitution.

  • FR-006: System MUST provide API endpoint POST /api/maintenance/{maintenance_id}/end for removing banners from dashboards affected by a specific maintenance event. Returns {task_id} immediately; removal runs asynchronously.

  • FR-007: System MUST provide API endpoint POST /api/maintenance/end-all for bulk removal of all active banners from all dashboards. Returns {task_id} immediately; removal runs asynchronously.

  • FR-008: System MUST persist a maintenance event history in the local database with fields: id, task_id, environment_id (snapshot), table list, start time, end time, optional message, status (active/completed/failed), timestamps. Affected dashboard tracking uses the normalized MaintenanceDashboardState relationship, not an inline dashboard ID list.

  • FR-009: System MUST support maintenance mode settings via GET/PUT /api/maintenance/settings:

    • target_environment_id: str — the default Superset environment to scan/modify (used when no explicit environment_id is provided in the API request). Overridden by environment_id in the request body or API key scope.
    • display_timezone: str — timezone for banner text display (default: "UTC")
    • banner_template: str — markdown template with optional variables {start_time}, {end_time}, {message}. Variables are substituted at render time; missing variables are omitted. Default template includes all three with ⚠️ header and styling (see UX reference).
    • dashboard_scope: "published_only" | "draft_only" | "all" (default: "published_only")
    • excluded_dashboard_ids: int[] — dashboards NEVER receiving banners
    • forced_dashboard_ids: int[] — dashboards ALWAYS receiving banners (overrides excluded per FR-009-b)
  • FR-009-b: When a dashboard is present in BOTH forced_dashboard_ids and excluded_dashboard_ids, forced inclusion takes priority — the banner IS placed.

  • FR-010: System MUST provide a UI management page for maintenance mode with:

    • Table of active and completed maintenance events
    • Selective and bulk banner removal
    • Settings configuration form (scope, excluded, forced)
  • FR-011: System MUST display an active banner indicator in the Dashboard Hub table as a badge with a tooltip.

  • FR-012: System MUST handle multiple maintenance events on the same dashboard: aggregate information and remove the banner only after ALL affecting events are completed.

  • FR-013: System MUST, on partial success, return a detailed response listing successfully processed and problematic dashboards.

  • FR-014: All dashboard-modifying operations via Superset API MUST require the operator or admin role (per RBAC ADR-0005).

  • FR-015: System MUST enforce RBAC per this matrix:

Endpoint / UI viewer operator admin API Key
GET /api/maintenance/dashboard-banners
GET /api/maintenance/events
POST /api/maintenance/start
POST /api/maintenance/{id}/end
POST /api/maintenance/end-all
GET /api/maintenance/settings
PUT /api/maintenance/settings
/maintenance UI page (read-only)
GET /api/admin/api-keys
POST /api/admin/api-keys
DELETE /api/admin/api-keys/{id}
  • FR-016: System MUST provide GET /api/maintenance/events returning active and completed event lists with affected dashboard counts.

  • FR-017: System MUST provide GET /api/maintenance/dashboard-banners returning per-dashboard banner state for the Dashboard Hub indicator.

API Key Authentication Requirements

  • FR-018: System MUST authenticate external API callers via a static bearer token in the X-API-Key HTTP header, as an ALTERNATIVE to the browser-based JWT (Authorization: Bearer) flow. When both JWT cookie and X-API-Key header are present, JWT takes precedence (the API key is logged but ignored).

  • FR-019: The APIKey entity stores the SHA-256 hash of the key. The raw key (format: ssk_<43 random chars>) is a once-only output — returned in the POST /api/admin/api-keys response at creation time and NEVER retrievable again. Storing plaintext is prohibited (CWE-312).

  • FR-020: Each API key MUST be bound to:

    • One environment_id (or "*" for all environments — admin-only). If set, the caller's environment_id in the request body must match or be omitted (in which case the key's environment is used as default).
    • A permissions list (subset of ["maintenance:start", "maintenance:end", "maintenance:end_all"]). If the key lacks the required permission for the attempted operation, the request is rejected with 403 Forbidden.
    • An active flag — revoked keys are rejected with 401 Unauthorized.
    • An optional expires_at timestamp — expired keys are rejected with 401 Unauthorized.
  • FR-021: System MUST provide admin endpoints for API key lifecycle management. API key management is a cross-cutting infrastructure concern — the UI lives in the central Settings page (/settings → System), NOT embedded in the Maintenance panel. This enables future tools (migration API, translation API, etc.) to reuse the same API key infrastructure without coupling to maintenance.

    • GET /api/admin/api-keys — list all keys (returns name, prefix, environment_id, permissions, active, created_at, expires_at; NEVER returns raw key or hash)
    • POST /api/admin/api-keys — generate new key (accepts name, environment_id, permissions, optional expires_at; returns {id, raw_key, prefix, ...} — the raw key is the ONLY output)
    • DELETE /api/admin/api-keys/{id} — revoke key (sets active = false; does not delete the row, preserving audit trail)
    • @RATIONALE Centralized: API keys are a platform-level auth primitive, not a maintenance feature. Maintenance is the first consumer. Future features (migration triggers, translation pipeline, data export API) will reuse the same key table, auth dependency, and Settings UI — zero code changes needed.
  • FR-022: The maintenance endpoints (/start, /{id}/end, /end-all) MUST accept EITHER JWT (existing) OR X-API-Key header. When using API key, the principal is identified by the key's name field in logs and task audit records (e.g., "api_key: ci-gitlab-deploy").

Key Entities

  • MaintenanceEvent: A record of a maintenance event. Contains: id, task_id (links to TaskManager task), environment_id (snapshot of target Superset environment at creation), table list, start time, end time, optional message, affected dashboard IDs, status (active/completed/failed), created_at, updated_at.

  • MaintenanceDashboardBanner (NEW): The canonical "one chart per dashboard" entity. Unique constraint on (environment_id, dashboard_id, status='active'). Contains: id, environment_id, dashboard_id, chart_id (Superset markdown chart ID), banner_text (current aggregated markdown), updated_at. Only ONE active banner chart exists per dashboard regardless of how many MaintenanceEvents affect it.

  • MaintenanceDashboardState: Links a maintenance event to a dashboard. Contains: id, maintenance_event_id, dashboard_id, banner_id (FK → MaintenanceDashboardBanner), status (pending_apply/active/removing/removed/apply_failed/removal_failed), created_at, updated_at. Does NOT own chart_id directly — references the shared MaintenanceDashboardBanner.

  • MaintenanceSettings: Maintenance mode configuration. Contains: id, target_environment_id (str), display_timezone (str, default "UTC"), banner_template (str — markdown with optional {start_time}, {end_time}, {message} variables; default has all three), dashboard_scope (enum), excluded_dashboard_ids (int[]), forced_dashboard_ids (int[]), updated_at.

  • Dashboard (existing Superset entity): Used for the dashboard → datasets → tables relationship. Has published/draft status.

  • Dataset (existing Superset entity): Two types: table-based (direct table reference) and virtual/SQL (is_sqllab_view = true, contains SQL query).

  • APIKey (NEW): Service-to-service authentication token for external tools. Contains:

    • id: str (UUID, PK) — unique identifier
    • key_hash: str — SHA-256 hash of the raw key (plaintext NEVER stored)
    • prefix: str — first 11 characters of raw key ("ssk_" + 7 chars) for identification in the admin list
    • name: str — human-readable label (e.g., "Airflow ETL Pipeline", "GitLab CI/CD")
    • environment_id: str | None — bound Superset environment; "*" for all (admin-only)
    • permissions: JSON (str[]) — fine-grained permissions (["maintenance:start", "maintenance:end", "maintenance:end_all"])
    • active: bool — revocation flag; false = key rejected
    • created_at: datetime
    • expires_at: datetime | None — optional automatic expiry
    • last_used_at: datetime | None — last request timestamp (for audit)

    Invariant: Raw key (ssk_...) is returned EXACTLY ONCE — in the POST /api/admin/api-keys response. After creation, only the hash exists in the database. The key is NOT refreshable; compromise requires revocation + regeneration.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: POST /api/maintenance/start returns {task_id} within 1 second (immediate async dispatch). The background task finds all affected dashboards (up to 50) and places banners within 60 seconds.

  • SC-002: After task completion, the banner appears on each dashboard in Superset within the same 60-second window (including markdown chart creation via Superset API).

  • SC-003: POST /api/maintenance/{id}/end returns {task_id} within 1 second; the background task removes banners from all affected dashboards (up to 50) within 30 seconds.

  • SC-004: SQL table name extraction from virtual datasets correctly identifies all fully-qualified schema.table references embedded in Jinja {% set %} blocks, plain SQL FROM/JOIN, CTE bodies, and subqueries, while filtering out string-literal false positives (date literals like '2026.04.30') — with ≥ 95% accuracy on a test corpus of production virtual dataset SQL+Jinja samples.

  • SC-005: Administrator can remove banners from all dashboards via UI in fewer than 3 clicks.

  • SC-006: Conflicting maintenance events on the same dashboard do not produce duplicate banners — exactly one markdown chart per dashboard at all times.

  • SC-007: On Superset API network errors, the system correctly returns a partial result without losing information about already-processed dashboards.

  • SC-008: API key authentication adds ≤ 5ms overhead per request (SHA-256 hash lookup against indexed DB column). Key generation and listing are sub-100ms operations.

UX Reference

See ux_reference.md for detailed interaction flows, UI mockups, and error recovery paths.