# Performance Tests

The Azaion UI is a thin SPA; the dominant performance concerns are bundle size, auth-refresh transparency, SSE responsiveness, and UI reflection of server-confirmed state changes. Server-side throughput is OUT of scope here — this file covers the UI's observable timing only.

### NFT-PERF-01: Initial JS bundle ≤ 2 MB gzipped

**Summary**: The sum of gzipped initial-route JS chunks in `dist/` stays within the architecture's stated budget.
**Traces to**: AC-11, O13
**Metric**: gzipped byte total of initial JS entries.
**Profile**: static

**Preconditions**:
- `bun run build` has produced `dist/`.

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Run `vite build` (or read the build manifest if already built) | `dist/` produced |
| 2 | Walk Vite's `manifest.json` to enumerate entry chunks (non-async) | list of initial chunks |
| 3 | Gzip-size each chunk (Node `zlib.gzipSync(content, {level:9})` or equivalent) | per-chunk size |
| 4 | Sum sizes | total bytes |

**Pass criteria**: total ≤ 2 097 152 bytes (2 MB). Documentary today — no CI gate (AC-11 status: "target, not currently enforced"). Test exists so the gate flips to blocking the day CI wires it up.
**Duration**: ≤ 60 s.
**Expected result source**: `results_report.md` row 40.

---

### NFT-PERF-02: Auth refresh — exactly one network round trip per cycle

**Summary**: A single 401-triggered refresh round consists of exactly one `POST /api/admin/auth/refresh` plus one retry of the original request.
**Traces to**: AC-01, AC-23
**Metric**: count of `/api/admin/auth/refresh` requests per refresh event.
**Profile**: fast

**Preconditions**:
- Authenticated session.

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Issue an authenticated request that returns 401 once, then 200 on retry | network log captured |
| 2 | Count refresh requests fired in the cycle | exactly 1 |

**Pass criteria**: refresh count == 1 per cycle (`results_report.md` row 12 — exact).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 12.

---

### NFT-PERF-03: SSE bearer-rotation reconnect ≤ 5 s

**Summary**: When the bearer rotates while N SSE streams are open, all streams close and reopen with the new token within 5 s.
**Traces to**: AC-24
**Metric**: per-EventSource time from `close()` observed to next `OPEN` readyState (after reconnect with new token).
**Profile**: fast — `quarantined` until SSE refresh-reconnect is implemented (Step 8 hardening)

**Preconditions**:
- Two EventSources open (live-GPS + annotation-status).

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Trigger a refresh that rotates the bearer | new bearer in memory |
| 2 | For each EventSource: time from old `close` to new `OPEN` | dt_i (ms) |
| 3 | Inspect new URLs for new token in query string | new `?token=` value |

**Pass criteria**: `max(dt_i) ≤ 5 000 ms`; both streams close+open exactly once (`results_report.md` row 13).
**Duration**: ≤ 30 s.
**Expected result source**: `results_report.md` row 13.

---

### NFT-PERF-04: Live-GPS SSE opens within 5 s of flight select

**Summary**: After clicking a flight in the Header, the live-GPS EventSource reaches `OPEN` quickly.
**Traces to**: AC-08
**Metric**: time from select-flight click to EventSource `readyState === 1` (OPEN).
**Profile**: e2e (suite live-gps simulator emits events at 1 Hz)

**Preconditions**:
- Authenticated; flight selectable.

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Click a flight | one EventSource constructed to `^/api/flights/[0-9]+/live-gps(\?|$)` |
| 2 | Wait for `OPEN` | dt (ms) |

**Pass criteria**: `dt ≤ 5 000 ms` (`results_report.md` row 34).
**Duration**: ≤ 10 s.
**Expected result source**: `results_report.md` row 34.

---

### NFT-PERF-05: Live-GPS SSE closes within 1 s of deselect

**Summary**: Deselecting the flight tears down the live-GPS stream promptly.
**Traces to**: AC-08
**Metric**: time from deselect to `CLOSED`.
**Profile**: e2e

**Preconditions**:
- Continuation of NFT-PERF-04.

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Deselect the flight | EventSource closes |
| 2 | Wait for `CLOSED` | dt (ms) |

**Pass criteria**: `dt ≤ 1 000 ms` and no remaining open live-GPS sources (`results_report.md` row 35).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 35.

---

### NFT-PERF-06: Annotation-status SSE unsubscribes within 1 s on page unmount

**Summary**: Navigating away from `/annotations` closes the status-events SSE within 1 s.
**Traces to**: AC-09
**Metric**: time from unmount to `CLOSED`.
**Profile**: fast

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Mount `/annotations` then unmount | EventSource transitions |
| 2 | Measure dt | ms |

**Pass criteria**: `dt ≤ 1 000 ms` (`results_report.md` row 25).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 25.

---

### NFT-PERF-07: Bulk-validate UI reflects new status within 2 s

**Summary**: After a successful bulk-validate, every selected row shows `Validated` quickly.
**Traces to**: AC-07
**Metric**: time from server 200 to last DOM row update.
**Profile**: fast

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Select N items, click Validate | request issued |
| 2 | Stub responds 200 | UI updates begin |
| 3 | Wait for all N rows to show `Validated` | dt (ms) |

**Pass criteria**: `dt ≤ 2 000 ms` (`results_report.md` row 37).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 37.

---

### NFT-PERF-08: Panel-width persistence debounce ≤ 1 s after resize-end

**Summary**: A drag-end on a resizable panel triggers a single PUT within 1 s (debounced).
**Traces to**: AC-21
**Metric**: time from `mouseup` (drag-end) to outbound PUT; PUT count per drag.
**Profile**: fast — `quarantined` until Step 4 writer is added

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Drag and release a divider | event captured |
| 2 | Wait for PUT or 1 s timeout | dt (ms); count |

**Pass criteria**: exactly 1 PUT within ≤ 1 000 ms; URL = `/api/annotations/settings/user`; body contains `panelWidths` (`results_report.md` row 64).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 64.

---

### NFT-PERF-09: Settings save error surfaces within 2 s

**Summary**: A 500 on settings save produces an error toast and resets the `saving` flag within 2 s.
**Traces to**: AC-27
**Metric**: time from server 500 to error visibility + state reset.
**Profile**: fast — `quarantined` until Step 4 try/finally fix

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Trigger save, stub responds 500 after T ms | failure delivered |
| 2 | Wait for error toast and `saving === false` | dt (ms) |

**Pass criteria**: `dt ≤ 2 000 ms` (`results_report.md` row 68).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 68.

---

### NFT-PERF-10: First Contentful Paint on `/flights` ≤ 3 s on mid-range edge hardware

**Summary**: A warm-cache load of the default authenticated route renders the main pane within 3 s.
**Traces to**: AC-11 (target), H2 (edge deploys)
**Metric**: `performance.getEntriesByName('first-contentful-paint')[0].startTime`.
**Profile**: e2e — documentary (no enforcement today; no AC binds a hard FCP budget)

**Preconditions**:
- Warm browser cache (second visit).
- Edge-profile container: 2 vCPU, 4 GB RAM (the hardware-assessment phase confirms the figure).

**Steps**:

| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Navigate to `/flights` post-login | navigation completes |
| 2 | Read FCP entry | ms |

**Pass criteria**: FCP ≤ 3 000 ms (row 98 — threshold_max).
**Duration**: ≤ 30 s.
**Expected result source**: `results_report.md` row 98.
