# 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 `<pre>` 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 `<label>` elements
- Confirmation dialog is focus-trapped while open
- Key display uses `<code>` element for screen reader pronunciation of the key string

## 4. Interaction Flows

*(existing sections 4 through end remain unchanged)*

### 4.1 Maintenance Start (External Tool via API Key)

**Using API Key authentication:**

```bash
# 1. Admin generates key once (via Settings → System → API Keys)
# 2. Admin stores key in CI/CD variable: MAINTENANCE_API_KEY=ssk_M7xqa...
# 3. External tool uses X-API-Key header

curl -X POST https://ss-tools.example.com/api/maintenance/start \
  -H "X-API-Key: $MAINTENANCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tables": ["public.messages"],
    "start_time": "2026-05-25T22:00:00Z",
    "end_time": "2026-05-26T02:00:00Z",
    "environment_id": "ss-dev",
    "message": "Scheduled ETL refresh"
  }'

# Response: {"task_id":"abc123","maintenance_id":"m-xyz","status":"pending"}
```

**Auth decision flow:**
1. Request arrives with `X-API-Key: ssk_...`
2. System checks `Authorization: Bearer <jwt>` — absent
3. System resolves API key: hash incoming value, lookup in DB by `key_hash`
4. Key found, `active=true`, `expires_at` not past → APIKeyPrincipal created
5. Permissions checked: key has `maintenance:start` → allowed
6. Task dispatched; audit log records `"actor": "api_key: Airflow Production ETL"`

**If auth fails:**
- Invalid key → `401 {"detail":"Invalid API key"}`
- Expired key → `401 {"detail":"API key has expired"}`
- Revoked key → `401 {"detail":"API key has been revoked"}`
- Insufficient permission → `403 {"detail":"API key lacks maintenance:start permission"}`
- Environment mismatch → `400 {"detail":"API key restricted to ss-dev, got ss-prod"}`

### 4.2 Banner Appearance in Superset

*(existing content unchanged — banner rendering details)*

### 4.3 Maintenance End

*(existing content unchanged)*

### 4.4 Dashboard Hub Indicator

*(existing content unchanged)*

### 4.5 Error Recovery

*(existing content unchanged — add API key error path)*

**API Key authentication errors:**
- **401 Invalid key**: External tool should check that `$MAINTENANCE_API_KEY` matches the current key. If key was rotated, update the CI/CD variable.
- **401 Expired key**: Admin must generate a new key (Settings → API Keys → Generate). Update CI/CD variable.
- **403 Permission denied**: Admin must update the key's permissions. Or generate a new key with the required rights.
- **400 Environment mismatch**: The key is scoped to a specific env. Either change the key's environment or use a different key.

## 5. Visual Design

### 5.1 API Key Section (in MaintenanceSettingsPanel)

- Section title: `<details><summary>` collapsible, matching other settings sections
- Key list table: inline Tailwind table (`min-w-full divide-y`), matches EventsTable pattern
- Prefix column: `<code>` element, monospace font (`font-mono text-sm`)
- Status: `<span class="inline-block w-2 h-2 rounded-full">` green (`bg-green-500`) or red (`bg-red-400`)
- Revoke button: `<button class="text-red-500 hover:text-red-700">✕</button>`
- Generate button: `<Button variant="secondary" size="sm">` from `$lib/ui/Button.svelte`

### 5.2 Reveal Panel (Read-Once Key Display)

- Background: yellow-50 border yellow-400 (warning style)
- Warning icon: `⚠️` before title
- Key code block: `<pre class="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm break-all">`
- Copy button: positioned overlapping the code block (top-right corner)
- "Copied!" indicator: absolute positioned, green text, `transition-opacity duration-300`

### 5.3 Revoke Confirmation

- Inline expand panel below the key row (not a modal)
- Red-tinted background (`bg-red-50 border-red-200`)
- Two buttons: `[Cancel]` (grey) and `[Revoke Key]` (red outline)

## 6. Localization

API key UI strings (EN/RU):

| EN | RU |
|----|----|
| API Keys | API-ключи |
| Generate New Key | Создать новый ключ |
| Revoke Key | Отозвать ключ |
| Copy to clipboard | Скопировать |
| Copied! | Скопировано! |
| This is the ONLY time the key will be shown | Ключ показывается ЕДИНСТВЕННЫЙ раз |
| Key has been revoked | Ключ отозван |
| Invalid API key | Неверный API-ключ |
| API key has expired | API-ключ истёк |
| API key lacks {permission} permission | API-ключ не имеет права {permission} |
