Files
ss-tools/specs/031-maintenance-banner/ux_reference.md
busya d9669698b8 feat(031): Maintenance Banner for Dashboards — full spec→plan→tasks package
Spec: 5 user stories (P1-P3), 17 FRs, 7 SCs, 12 edge cases, RBAC matrix
Plan: 7 research decisions, 15 semantic contracts (C1-C4)
Data: 4 SQLAlchemy models (Banner entity enforces one-chart-per-dashboard)
UX: async-only API, banner template with optional variables, admin settings UI
Tasks: 64 tasks across 8 phases, 12 reused frontend components
Workflow: component reuse scan mandated in speckit.plan/tasks + semantics-svelte
Constitution: filled with 7 architecture principles from ADRs
2026-05-22 16:20:39 +03:00

14 KiB
Raw Blame History

UX Reference: Maintenance Banner for Dashboards

Feature Branch: 031-maintenance-banner Created: 2026-05-21 Status: Draft

1. User Persona & Context

  • Who is the user?:

    • ETL Operator: System administrator or DevOps engineer running ETL pipelines (Airflow, cron, dbt). Does not interact with the ss-tools UI directly — API only.
    • ss-tools Administrator: Platform administrator managing maintenance mode settings through the web UI.
    • Dashboard Viewer: Business user viewing dashboards in Superset. Sees the maintenance banner but does not trigger it.
  • What is their goal?:

    • Operator: Automatically warn dashboard users that data is being refreshed and may be inaccurate.
    • Administrator: Control the maintenance process, manually remove banners if needed, configure exclusions.
    • Dashboard viewer: Understand why dashboard data appears incomplete.
  • Context:

    • Operator calls REST API from a pipeline (Airflow DAG, shell script, CI/CD).
    • Administrator works in the ss-tools web interface (SvelteKit SPA, desktop).
    • Dashboard viewer sees the result in Superset (third-party web application).

2. The "Happy Path" Narrative

Maintenance start (Operator → ss-tools → Superset)
An Airflow DAG finishes clearing staging tables and triggers: curl -X POST https://ss-tools/api/maintenance/start -d '{"tables":["raw.sales","raw.inventory"],"start_time":"2026-05-21T22:00:00+03:00","end_time":"2026-05-22T02:00:00+03:00"}'. The API immediately returns {"task_id":"task-abc","maintenance_id":"m-abc123","status":"pending"}. The operator polls GET /api/tasks/task-abc. Within 60 seconds, the task completes, and 12 dashboards in Superset display an orange banner at the very top. If the operator doesn't know the end time, they can omit end_time — the banner will show «End: TBD» instead of a specific time. Dashboard viewers see the warning immediately upon opening the page.

Maintenance end (Operator → ss-tools → Superset)
The ETL pipeline completes successfully. The Airflow DAG calls curl -X POST https://ss-tools/api/maintenance/m-abc123/end. The API returns {"task_id":"task-end-abc","status":"pending"}. Upon task completion, banners disappear from all dashboards. In the ss-tools UI, the administrator sees the event marked as "Completed".

Manual override (Administrator → ss-tools UI → Superset)
An administrator notices the ETL has stalled and banners remain past the scheduled end time. They navigate to ss-tools → Maintenance → see the list of active events → click "Remove All". Within 10 seconds, all banners are removed.

3. Interface Mockups

API Interaction

# ===== Start maintenance =====
# With end_time:
$ curl -X POST https://ss-tools/api/maintenance/start \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "tables": ["raw.sales", "raw.inventory"],
    "start_time": "2026-05-21T22:00:00+03:00",
    "end_time": "2026-05-22T02:00:00+03:00",
    "message": "Scheduled data mart refresh"
  }'

# Without end_time (banner shows «Окончание: уточняется»):
$ curl -X POST https://ss-tools/api/maintenance/start \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "tables": ["raw.sales"],
    "start_time": "2026-05-21T22:00:00+03:00"
  }'

# Success response — async (HTTP 202):
{
  "task_id": "task-maint-abc123",
  "maintenance_id": "m-ev-20260521-abc123",
  "status": "pending"
}

# Poll task status:
$ curl https://ss-tools/api/tasks/task-maint-abc123 \
  -H "Authorization: Bearer $JWT"

# Task completed response:
{
  "task_id": "task-maint-abc123",
  "status": "completed",
  "result": {
    "maintenance_id": "m-ev-20260521-abc123",
    "affected_dashboards": 12,
    "dashboards": [
      {"id": 42, "title": "Daily Sales Overview", "chart_id": 902},
      {"id": 57, "title": "Inventory Health Check", "chart_id": 903}
    ],
    "unmatched_tables": []
  }
}

# Partial success — async (HTTP 202):
{
  "task_id": "task-maint-def456",
  "maintenance_id": "m-ev-20260521-def456",
  "status": "pending"
}
# Task result on completion (via GET /api/tasks/{task_id}):
{
  "task_id": "task-maint-def456",
  "status": "completed",
  "result": {
    "maintenance_id": "m-ev-20260521-def456",
    "status": "partial",
    "affected_dashboards": 10,
    "failed_dashboards": [
      {"id": 99, "title": "Archived Report", "error": "Dashboard not found in Superset"}
    ],
    "unmatched_tables": ["raw.unknown_table"]
  }
}

# No dashboards found — always async (HTTP 202, result in task):
# API response:
{
  "task_id": "task-maint-noop",
  "maintenance_id": "m-ev-20260521-noop",
  "status": "pending"
}
# Task result on completion:
{
  "task_id": "task-maint-noop",
  "status": "completed",
  "result": {
    "maintenance_id": "m-ev-20260521-noop",
    "status": "no_match",
    "affected_dashboards": 0,
    "unmatched_tables": ["raw.archived"]
  }
}

# ===== End maintenance =====
$ curl -X POST https://ss-tools/api/maintenance/m-ev-20260521-abc123/end \
  -H "Authorization: Bearer $JWT"

# Success response — async (HTTP 202):
{
  "task_id": "task-end-abc123",
  "status": "pending"
}
# Task result on completion:
{
  "task_id": "task-end-abc123",
  "status": "completed",
  "result": {
    "maintenance_id": "m-ev-20260521-abc123",
    "removed_from": 12,
    "failed_removals": []
  }
}

# Already completed (HTTP 200 — sync, no task needed):
{
  "maintenance_id": "m-ev-20260521-abc123",
  "status": "already_completed",
  "message": "Maintenance was already completed at 2026-05-22T02:05:00+03:00"
}

# ===== Bulk removal =====
$ curl -X POST https://ss-tools/api/maintenance/end-all \
  -H "Authorization: Bearer $JWT"

# Success response — async (HTTP 202):
{
  "task_id": "task-endall-xyz",
  "status": "pending"
}
# Task result on completion:
{
  "task_id": "task-endall-xyz",
  "status": "completed",
  "result": {
    "closed_events": 3,
    "removed_from": 25,
    "failed_removals": []
  }
}

# ===== Settings =====
$ curl https://ss-tools/api/maintenance/settings \
  -H "Authorization: Bearer $JWT"

# Response:
{
  "target_environment_id": "prod",
  "display_timezone": "Europe/Moscow",
  "banner_template": "<div style=\"background:#FFF3E0;...\">## ⚠️ Технические работы\n\n{message}\n\n**Начало:** {start_time}\n**Конец:** {end_time}\n\n*Данные могут быть неполными.*\n</div>",
  "dashboard_scope": "published_only",
  "excluded_dashboard_ids": [99, 101],
  "forced_dashboard_ids": [10, 42],
  "updated_at": "2026-05-20T15:00:00+03:00"
}

$ curl -X PUT https://ss-tools/api/maintenance/settings \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"dashboard_scope": "all", "excluded_dashboard_ids": [99]}'

# Response: (settings object, same shape as GET)

UI Layout & Flow

Screen 1: Maintenance Banners Management (/maintenance)

  • Layout: Single-page management panel. Top section — settings, bottom section — events table.
  • Key Elements:
    • Settings Panel (collapsible):
      • Dashboard Scope: Radio group: "Published only" / "Drafts only" / "All".
      • Excluded Dashboards: Multi-select search by dashboard title. Removable badge chips.
      • Forced Dashboards: Multi-select search by dashboard title. Removable badge chips.
      • "Save Settings" button (Primary, blue).
    • Active Events Table:
      • Columns: Event ID, Tables (truncated list + tooltip), Affected Dashboards, Start Time, End Time, Status (badge: 🟢 Active / Completed / Failed), Actions.
      • Actions: "Remove Banners" button (Warning, orange) per active event.
      • Above table: "Remove All Banners" button (Danger, red) with confirmation dialog.
    • Completed Events Table (collapsible, hidden by default):
      • Same columns + "Completed At" timestamp. No action buttons.
    • Contract Mapping:
      • @UX_STATE: idle (table loaded), loading (skeleton), action_in_progress (remove button shows spinner, others disabled), error (error toast, retry available), empty (no active events — placeholder "No active maintenance events").
      • @UX_FEEDBACK: Toast notifications: success (green "Banners removed from N dashboards"), error (red "Removal error: ..."), warning (yellow "Partial success: X/Y processed").
      • @UX_RECOVERY: On removal error — "Retry" button per failed dashboard. On network error — auto-retry after 3s with countdown display.
      • @UX_REACTIVITY: $state for activeEvents, completedEvents, settings; $derived for filtered/sorted views; $effect for auto-refresh (30s polling).

Screen 2: Dashboard Hub — Maintenance Indicator (modification to existing /dashboards)

  • Layout: New indicator added to each dashboard row (alongside existing Git/LLM status badges).
  • Key Elements:
    • Maintenance Badge: Orange badge "⚠️ Maintenance" in the dashboard row.
    • Tooltip: On hover — start and end times, source event ID.
    • Contract Mapping:
      • @UX_STATE: no_maintenance (no badge), maintenance_active (orange badge), loading (grey skeleton badge).
      • @UX_REACTIVITY: $derived from maintenance state data, refreshed alongside dashboard polling.

Screen 3: Superset Dashboard — Maintenance Markdown Chart (result in third-party application)

  • Layout: Markdown chart at the very top of the dashboard (position (0,0) in layout), full width.

  • Appearance:

    ⚠️ Maintenance in progress
    
    Scheduled data mart refresh
    Start: May 21, 2026, 10:00 PM (MSK)
    End:   May 22, 2026,  2:00 AM (MSK)
    
    Data may be incomplete or temporarily unavailable.
    
  • Стиль: Markdown с настраиваемым шаблоном (banner_template в настройках). Переменные {start_time}, {end_time}, {message} опциональны — шаблон работает с любым набором. По умолчанию: оранжевый фон с рамкой.

  • Примеры шаблонов:

    • По умолчанию: <div style="background:#FFF3E0;...">## ⚠️ Технические работы\n\n{message}\n\n**Начало:** {start_time}\n**Конец:** {end_time}\n\n*Данные могут быть неполными.*</div>
    • Минимальный: > ⚠️ Ведутся технические работы. Данные могут быть неполными. — без переменных, просто статичный текст
    • Тёмная тема: <div style="background:#FFEBEE;color:#B71C1C;padding:12px">🚧 **{start_time} {end_time}**{message}</div>
    • Только время: ⚠️ Работы: {start_time} — {end_time} — без message

4. The "Error" Experience

Philosophy: API returns maximally detailed responses so the caller can make informed decisions. UI provides explicit recovery paths.

Scenario A: Table not found in any dashboard

  • User Action (Operator): Passes a table name with a typo: raw.sales_typo.
  • System Response: {"status": "no_match", "affected_dashboards": 0, "message": "No dashboards found referencing tables: raw.sales_typo"}.
  • Recovery: Operator corrects the table name and retries.

Scenario B: Some dashboards no longer exist in Superset

  • User Action (Operator): Initiates maintenance, but 3 of 15 dashboards have been deleted from Superset.
  • System Response: HTTP 207 Multi-Status with failed_dashboards field containing id and reason for each problematic dashboard. Remaining 12 dashboards successfully receive banners.
  • Recovery: Operator reviews failed_dashboards and adjusts the exclusion list if needed.

Scenario C: Superset API network timeout

  • User Action (Operator): Initiates maintenance, but Superset responds slowly.
  • System Response: Timeout after 30 seconds. Returns HTTP 207 with status: "partial", listing processed and unprocessed dashboards.
  • Recovery: Operator can re-call /maintenance/start with the same parameters — the system only processes dashboards that don't yet have an active banner.

Scenario D: Multiple maintenance events on one dashboard

  • User Action: Two ETL processes simultaneously update tables affecting dashboard D.
  • System Response: Dashboard D shows a single markdown chart with aggregated text: "⚠️ Multiple data refresh processes in progress. ..."
  • Recovery: The banner is only removed when ALL maintenance events for D are completed. Premature /end for one event updates the banner text but keeps it visible.

Scenario E: Insufficient permissions

  • User Action: API call without operator or admin role.
  • System Response: HTTP 403 {"detail": "Insufficient permissions. Required role: operator or admin"}.
  • Recovery: Request role assignment from administrator.

5. Tone & Voice

  • Style: Technical, concise, informative. No alarmism, but clear indication of possible data issues.
  • Terminology:
    • "Maintenance" (not "downtime" or "outage" — data is still accessible, just potentially stale)
    • "Dashboard" (not "report" or "view")
    • "Banner" (used in API/UI/internal docs for the markdown chart)
    • "Dataset" (not "datasource")
    • "Table" (not "source" or "relation")
  • Default banner template:
    ⚠️ Maintenance in progress
    {message}
    Start: {start_time_formatted}
    End:   {end_time_formatted}
    Data may be incomplete or temporarily unavailable.
    
    If end_time is not provided, «End: TBD» (or localized equivalent) is shown. If message is not provided, the first body line is omitted.
  • Dashboard Hub tooltip: "Maintenance active: {start} — {end}"