# UX Reference: Maintenance Banner for Dashboards **Feature Branch**: `031-maintenance-banner` **Created**: 2026-05-21 **Status**: Draft **Updated**: 2026-05-25 — Added API Key management UX flow ## 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". ## 3. API Key Management UX Flow **Location:** Settings → System Settings → "API Keys" section (NOT in Maintenance panel). API keys are centralized infrastructure, reusable across tools. They live in the System Settings page alongside other cross-cutting configurations (environments, auth, etc.). Maintenance is just the first consumer. ### 3.1 Issuing a New API Key **Step 1: Navigate to Settings → System → API Keys** The System Settings page contains an "API Keys" section showing a table of existing keys (or empty state if none): ``` ┌─ API Keys ─────────────────────────────────────────────────┐ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Name Prefix Env Permissions Actv │ │ │ │─────────────────────────────────────────────────────│ │ │ │ Airflow ETL ssk_M7xqaP… ss-dev start,end 🟢 ✕ │ │ │ │ GitLab CI ssk_K9yFbZ… ss-dev s/e/a 🟢 ✕ │ │ │ │ Old staging ssk_T1vLmC… ss-dev start 🔴 · │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ No API keys yet. Generate one for external tool access. │ ← empty state │ │ │ [+ Generate New Key] │ └──────────────────────────────────────────────────────────────┘ ``` - **Permission notation:** `s` = maintenance:start, `e` = maintenance:end, `a` = maintenance:end_all - **Status:** 🟢 = active, 🔴 = revoked - **Revoke button:** `✕` on active keys; `·` (dot — no action) on revoked keys - **Empty state:** dashed border, grey text, centered **Step 2: Fill in the generation form** Clicking `[+ Generate New Key]` expands an inline form (not a modal — matches the existing settings pattern): ``` ┌─ Generate API Key ──────────────────────────────────────┐ │ │ │ Name ────────────────────────────────────────────────── │ │ ┌───────────────────────────────────────────────────┐ │ │ │ Airflow Production ETL │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ Environment ────────────────────────────────────────── │ │ ┌──────────────────────────────────────▼─────────────┐ │ │ │ ss-dev │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ Permissions ────────────────────────────────────────── │ │ ☑ maintenance:start Start new maintenance events │ │ ☑ maintenance:end End a specific event │ │ ☐ maintenance:end_all End ALL active events at once │ │ │ │ Expires (optional) ─────────────────────────────────── │ │ ┌──────────────────────┐ 📅 │ │ │ 2027-01-01 │ │ │ └──────────────────────┘ │ │ │ │ [Cancel] [Generate Key] │ └───────────────────────────────────────────────────────────┘ ``` **Step 3: Copy the raw key (ONCE!)** After successful generation, the form is replaced by a **read-once reveal panel**: ``` ┌─ ⚠️ API Key Generated — Copy It Now! ───────────────────┐ │ │ │ This is the ONLY time the full key will be displayed. │ │ Store it securely (password manager, CI/CD variable). │ │ It CANNOT be retrieved later. If lost, you must │ │ revoke this key and generate a new one. │ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 │ │ │ │ [📋 Copy] │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ Name: Airflow Production ETL │ │ Prefix: ssk_M7xqaP… │ │ Environment: ss-dev │ │ Permissions: maintenance:start, maintenance:end │ │ Created: 2026-05-25 19:45 UTC │ │ Expires: 2027-01-01 │ │ │ │ [I have copied the key — dismiss] │ └───────────────────────────────────────────────────────────┘ ``` **UX rules for the reveal panel:** - Warning text in **red/bold** — user must understand this is one-time - Key displayed in monospace `
` with a visual "code block" background
- `[📋 Copy]` button copies to clipboard; on click → shows "Copied!" for 1.5s (green checkmark icon)
- `[I have copied the key — dismiss]` — replaces the panel with the updated key list
- Panel CANNOT be reopened — the raw key is gone from memory
- If user closes without copying: key is irrecoverable. They must revoke and regenerate. This is intentional.

**Step 4: Key appears in the list**

After dismissal, the key list is updated with the new entry (prefix only):

```
Name                   Prefix       Env     Perms  Status
─────────────────────────────────────────────────────────
Airflow Production ETL ssk_M7xqaP… ss-dev  s/e    🟢  ✕   ← NEW
GitLab CI/CD           ssk_K9yFbZ… ss-dev  s/e/a  🟢  ✕
```

### 3.2 Revoking an API Key

**Step 1: Click `✕` on a key row**

Triggers a confirmation dialog (native `confirm()` or inline confirmation):

```
┌─ Revoke API Key? ───────────────────────────────────────┐
│                                                          │
│  Are you sure you want to revoke:                        │
│                                                          │
│    "Airflow Production ETL" (ssk_M7xqaP…)                │
│                                                          │
│  → All external tools using this key will immediately    │
│    receive 401 Unauthorized responses.                   │
│                                                          │
│  → The key entry will be preserved for audit but         │
│    marked as INACTIVE.                                   │
│                                                          │
│                    [Cancel]    [Revoke Key]               │
└──────────────────────────────────────────────────────────┘
```

**Step 2: Key becomes inactive**

After confirmation, the row updates:

```
Airflow Production ETL ssk_M7xqaP… ss-dev  s/e    🔴  ·   ← REVOKED
```

- `🔴` = inactive status
- `✕` replaced by `·` (no action — already revoked)
- Row is slightly greyed out (opacity 0.6)

### 3.3 State Machine

```
                ┌──────────────┐
                │    IDLE       │←────────────────────────────┐
                │ (key list)    │                              │
                └──────┬───────┘                              │
                       │                                       │
          [+Generate]  │                                       │
                       ▼                                       │
                ┌──────────────┐    submit form     ┌─────────┐│
                │   FORM        │ ────────────────→ │ REVEAL   ││
                │ (name,env,    │                    │ ⚠️ once  ││
                │  perm,expiry) │                    └────┬─────┘│
                └──────┬───────┘                         │      │
                       │           [Cancel]              │      │
                       └─────────────────────────────────┤      │
                                                         │dismiss│
                                                         ▼      │
                ┌──────────────┐    click [✕]     ┌──────────┐ │
                │   IDLE        │ ───────────────→ │ CONFIRM   │ │
                │ (list updated)│                   │ "Revoke?" │ │
                └──────────────┘                   └─────┬─────┘ │
                       ↑                                 │       │
                       │        [Cancel]                  │       │
                       └─────────────────────────────────┤       │
                                                         │[Revoke]│
                                                         ▼       │
                                                ┌──────────────┐ │
                                                │   IDLE        │─┘
                                                │ (key.inactive) │
                                                └──────────────┘
```

### 3.4 Error & Edge Case Handling

| Scenario | UX Response |
|----------|-------------|
| Network failure during generation | Toast error: "Failed to generate API key. Check your connection and try again." Form stays open — retry possible. |
| User closes REVEAL panel without copying | Key is **gone forever**. Admin must revoke (now-inactive key) and regenerate. The warning text in the panel explicitly states this. |
| Double-click on [Revoke] | Second click → 404 from API. UI handles: "Key already revoked" inline message on the row. No dialog. |
| Revoke already-inactive key | UI prevents: no ✕ button on inactive rows (rendered as `·`). |
| Empty name field submission | Client-side validation: "Name is required" red text below the input. [Generate Key] button disabled until filled. |
| No permissions selected | Client-side validation: "Select at least one permission" red text. Button disabled. |
| Key generation API returns 403 | (Admin-only endpoint). If somehow triggered by non-admin: Toast "Forbidden: admin privileges required." |

### 3.5 Component States

| Component | State | Visual |
|-----------|-------|--------|
| Key list | **idle** | Table with rows |
| Key list | **empty** | Dashed border box: "No API keys yet. Generate one for external tool access." + button |
| Key list | **loading** | 3 skeleton rows (`animate-pulse bg-gray-200 h-8 w-full rounded`) |
| Key list | **error** | Toast with error message; list shows stale data |
| Generation form | **idle** | Collapsed — only `[+ Generate New Key]` visible |
| Generation form | **open** | Form fields expanded |
| Generation form | **submitting** | Button shows spinner: `[⠋ Generating...]` |
| Generation form | **error** | Toast with error; form stays open |
| Reveal panel | **visible** | Warning + code block + copy button |
| Reveal panel | **copied** | Copy button shows "✅ Copied!" for 1.5s |
| Revoke dialog | **confirming** | Button shows `[⠋ Revoking...]` |
| Row | **active** | 🟢 green circle + ✕ button |
| Row | **inactive** | 🔴 red circle + `·` dot (no button) |
| Row | **revoking** | 🔄 spinner replacing the ✕ button |

### 3.6 Accessibility

- All buttons have `aria-label` (e.g., `"Revoke Airflow Production ETL key"`)
- Reveal panel uses `role="alert"` for screen reader announcement
- Copy button announces "Copied" via `aria-live="polite"`
- Form fields have associated `