# UX Review Test Plan — Pi Multi-FX Pedal Web UI

**Date:** June 14, 2026
**Scope:** Full web UI — React SPA + legacy Jinja2 pages
**Target URL:** http://pedal.local:8080
**Methodology:** Nielsen's 10 Usability Heuristics + Flow Analysis + Visual Cohesion Audit

---

## 1. Scope & Personas

### Personas

| Persona | Goal | Context |
|---------|------|---------|
| **Session Guitarist** | Dial in a tone fast during a live session or recording | On a phone/tablet near the amp, tweaking EQs and effects between takes. Wants minimal taps to hear results. |
| **Preset Tweaker** | Browse, load, save, and edit presets from a laptop | Sitting at a desk building a pedalboard. Wants deep control over FX chain, NAM models, IR cab sims. |
| **Headless Stage User** | Switch presets, toggle bypass, check tuner — with footswitches only | Never touches the web UI during performance. Uses it *only* for setup/config behind the scenes. |
| **Admin / Integrator** | Configure WiFi, Bluetooth MIDI, audio profiles, hotspot | Technical user connecting the pedal to different environments (studio, stage, rehearsal space). |

### Flows to Test

| # | Flow | Steps | Priority |
|---|------|-------|----------|
| F1 | **Load pedal dashboard → check status** | 2 | Critical |
| F2 | **Browse preset banks → load a preset** | 4 | Critical |
| F3 | **Edit preset parameters (name, volume, FX chain)** | 5 | High |
| F4 | **Toggle bypass and adjust master volume** | 3 | Critical |
| F5 | **Browse and install a NAM model from Tone3000** | 6 | High |
| F6 | **Upload and load an IR cab sim file** | 4 | High |
| F7 | **Switch audio latency profile** | 3 | Medium |
| F8 | **Configure and enable WiFi hotspot** | 5 | Low |
| F9 | **Scan for, connect to WiFi network** | 4 | Low |
| F10 | **Bluetooth scan, pair, and view MIDI status** | 5 | Low |
| F11 | **Toggle 4CM routing mode and set breakpoint** | 3 | Medium |
| F12 | **Check tuner display with guitar input** | 2 | Medium |
| F13 | **Save/recall snapshot slots** | 4 | Medium |
| F14 | **Empty state: unload model, view dashboard with no presets** | 2 | High |

### UI Surfaces Reviewed

- [ ] React SPA (`/` — root URL)
- [ ] Legacy Dashboard (`/` — fallback without React build)
- [ ] Presets page (`/presets`)
- [ ] Models page (`/models`)
- [ ] IRs page (`/irs`)
- [ ] Settings > Connection tab
- [ ] Settings > Audio tab
- [ ] Settings > 4CM Routing tab
- [ ] Settings > Network tab (WiFi scan, hotspot)
- [ ] Settings > Bluetooth tab
- [ ] Settings > About tab
- [ ] Preset Edit modal
- [ ] WiFi Connect modal
- [ ] Connection status indicator (global header)

---

## 2. Heuristic Walkthrough

### H1 — Visibility of System Status

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H1.1 | Page load indicator | Navigate to `/` | Loading placeholder or skeleton while API/WS connects | Browser |
| H1.2 | Connection status | Open page when pedal is off | "Disconnected" badge (red) in header | Browser |
| H1.3 | Connection status | Open page when pedal is on | "Connected" badge (green) in header | Browser |
| H1.4 | Preset activation feedback | Click a preset card | Card highlights, dashboard preset name updates, WS broadcast visible | Browser |
| H1.5 | Volume slider feedback | Drag volume slider | Value % text updates in real time | Browser |
| H1.6 | Bypass toggle feedback | Click bypass button | Button text toggles BYPASSED/ACTIVE, color changes | Browser |
| H1.7 | Tuner toggle feedback | Click tuner button | Button toggles ON/OFF, color changes | Browser |
| H1.8 | WebSocket reconnection | Kill server, wait, restart | "Disconnected" → "Connected" after auto-reconnect | Browser |
| H1.9 | Model load progress | Click Load on a NAM model | Loading state on the button, list refreshes after | Browser |
| H1.10 | Tone3000 search feedback | Search Tone3000 for models | "Searching..." spinner shown, results appear, offline warning shown if 503 | Browser |
| H1.11 | Tone3000 install feedback | Install a model from Tone3000 | Button shows "Downloading...", then "✓ Done", list refreshes | Browser |
| H1.12 | File upload feedback | Upload a .nam or .wav file | Input clears, list refreshes with new entry | Browser |
| H1.13 | Audio profile switch | Switch to low latency profile | "restarted: true" in response, brief JACK restart pause | API + Browser |
| H1.14 | RSSI/connection data in settings | Open Network tab | WiFi SSID, IP, signal % shown, visible from refresh | Browser |
| H1.15 | BT status indicators | Open Bluetooth tab | Power, discoverable, adapter name visible | Browser |
| H1.16 | WS client count | Open Settings → Connection | Connected clients count shows active WS sessions | Browser |

**Grep for verification:**
```bash
# Loading states in source
grep -n "loading\|spinner\|skeleton\|Loading" src/web/static/*.js src/web/static/style.css

# Connection status toggle
grep -n "connection-status\|Connected\|Disconnected" src/web/static/*.js src/web/templates/base.html

# Button disabled states on async actions
grep -n "\.disabled\|disabled =" src/web/static/*.js
```

---

### H2 — Match Between System and Real World

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H2.1 | FX type labels in UI | View FX chain, preset edit modal | Labels use real-world names (e.g., "Noise Gate" not "noise_gate") | Source + Browser |
| H2.2 | Model/IR list readability | View Models or IRs page | File names appear as human-readable labels | Browser |
| H2.3 | 4CM routing description | Enable 4CM in Settings | Plain language: "Input 1 (Guitar) → Pre blocks → Send \| Input 2 (Return) → Post blocks → Output" | Browser |
| H2.4 | Tuner note display | Enable tuner with guitar plugged in | Shows musical note names (A, A#, Bb...) not frequencies only | Browser |
| H2.5 | Preset meta labels | View preset card | Shows "Bank 0 · Program 2" — accessible musician language | Browser |
| H2.6 | Audio profile names | View profiles list | "Standard", "Low" — musician-friendly not technical | Browser |
| H2.7 | NAM architecture info | View model details | Shows "featherwave-v2" or architecture name, not internal paths | Browser |

**Grep for verification:**
```bash
# Check for snake_case / internal names leaking into UI
grep -n "fx_type\|replace.*_/g\|FXType" src/web/static/presets.js | head -20
# presets.js line 92: const label = block.fx_type.replace(/_/g, ' '); — detects raw fx_type
```

---

### H3 — User Control and Freedom

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H3.1 | Modal close options | Open preset edit modal | Close on ✕ button, close on overlay click, close on Cancel button | Browser |
| H3.2 | Modal close on Escape | Open modal, press Escape | Modal closes | Browser |
| H3.3 | Delete preset confirmation | Click Delete in preset modal | `confirm()` dialog appears before deletion | Browser |
| H3.4 | Cancel WiFi connect | Open WiFi modal, click Cancel | Modal closes, no connection attempted | Browser |
| H3.5 | Back navigation | Navigate to any page, press Back | Browser back works, returns to previous page | Browser |
| H3.6 | Tab navigation persists | Switch to Network tab, navigate away and back | Active tab remembered via URL hash | Browser |
| H3.7 | Tuner toggle graceful | Toggle tuner ON while playing | Audio mutes or tuner activates without loud pop/clip | Browser + Audio |
| H3.8 | Bypass toggle graceful | Toggle bypass ON while playing | Clean DI signal, no pop/clip | Browser + Audio |
| H3.9 | Unload model without issues | Click Unload on loaded model | Model unloads, dashboard shows "None loaded", audio still flows | Browser |

---

### H4 — Consistency and Standards

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H4.1 | Navigation active state | Click each nav link | Active link highlighted, consistent style across all pages | Browser |
| H4.2 | Button style consistency | Compare primary/secondary/danger across pages | Same styles everywhere (colors, padding, font size) | Browser |
| H4.3 | Toggle button pattern | Compare bypass, tuner, 4CM toggles | Same ON/OFF pattern, same active/inactive colors | Browser |
| H4.4 | Slider pattern | Compare volume sliders on Dashboard vs Settings | Same visual, same behavior, synced values | Browser |
| H4.5 | Connection status in header | Navigate through all pages | Connection indicator always present, same position | Browser |
| H4.6 | Page header pattern | Visit Presets, Models, IRs, Settings pages | Consistent page-header with h1, same spacing | Browser |
| H4.7 | Card structure | Compare all card components | Uniform header/body/footer pattern — identical padding, border, radius | Browser |
| H4.8 | Danger button meaning | Compare Delete preset, Unload model/IR | All red danger buttons are destructive actions | Browser |
| H4.9 | Tab pattern (Settings) | Switch between all 6 settings tabs | Underline active indicator, same transition | Browser |

---

### H5 — Error Prevention

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H5.1 | Disabled submit during load | Click Load on model rapidly | Button disabled after first click, no duplicate load | Browser |
| H5.2 | Disabled submit during upload | Upload a file, click multiple times | Button disabled during upload | Source |
| H5.3 | Delete preset confirmation | Click Delete | Confirm dialog: "Delete this preset?" | Browser |
| H5.4 | File type validation on upload | Try uploading a .txt file | Rejected at client or server level | Browser |
| H5.5 | Audio profile double-switch | Switch to same profile twice | `restarted: false`, no unnecessary JACK restart | API |
| H5.6 | 4CM breakpoint bounds | Drag breakpoint slider min/max | Clamped 0–16 | Browser |
| H5.7 | Volume clamping | Set volume > 100% or < 0% via API | Clamped to 0.0–1.0 | API |
| H5.8 | Empty preset name | Save preset with empty name | Should default to "Preset" or reject with validation | Browser |
| H5.9 | Hotspot password length | Try password > 63 chars | Maxlength on input field prevents it | Browser |

**Code verification:**
```bash
grep -n "confirm\|showModal\|are you sure" src/web/static/*.js
grep -n "\.disabled" src/web/static/models.js src/web/static/irs.js
```

---

### H6 — Recognition Rather Than Recall

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H6.1 | Current preset name visible | Load any page | Preset name clearly shown in Dashboard or in header | Browser |
| H6.2 | Currently loaded model/IR | View Models/IRs page | "CURRENT" badge visible on active model/IR | Browser |
| H6.3 | Active preset card highlighted | Browse presets | The active preset card has a distinct border/background | Browser |
| H6.4 | Bank/program location | View preset card | "Bank N · Slot N" visible without clicking | Browser |
| H6.5 | Navigation labels visible | All pages | All 5 nav links visible, no hamburger hiding | Browser |
| H6.6 | FX chain status on dashboard | Connected, preset loaded | Blocks visible with ON/OFF badges | Browser |
| H6.7 | Modal title shows preset name | Click to edit preset | "Edit: [Preset Name]" — user knows what they're editing | Browser |
| H6.8 | Routing mode visible from dashboard | Any page | Badge shows "Mono" or "4CM" | Browser |
| H6.9 | Tone3000 installed state | Revisit after install | Previously installed models show ✓ Installed badge (localStorage) | Browser |

---

### H7 — Flexibility and Efficiency

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H7.1 | Keyboard submit on search | Type in Tone3000 search, press Enter | Triggers search | Browser |
| H7.2 | FAQ: Keyboard shortcuts | Test Tab order through form fields | Logical focus order, all interactive elements reachable | Browser |
| H7.3 | Bulk actions | Models/IRs pages | No bulk select/load/delete — note as missing efficiency | Browser |
| H7.4 | Auto-refresh on preset save | Save preset edit | Modal closes, preset list auto-refreshes | Browser |
| H7.5 | Auto-refresh after model/IR upload | Upload model/IR | List auto-refreshes after upload completes | Browser |
| H7.6 | Trending option on Tone3000 | Click "Trending" | Fetches trending models/IRs without typing a query | Browser |
| H7.7 | Tab navigation in Settings | URL hash (`#network`, `#bluetooth`) | Deep-linkable settings tabs | Browser |
| H7.8 | WS-driven live updates | Change volume on another tab | Dashboard and Settings sliders sync in real time | Browser |

---

### H8 — Aesthetic and Minimalist Design

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H8.1 | Content-to-chrome ratio | View Dashboard | Essential info above the fold: preset, bypass, volume, FX chain | Browser |
| H8.2 | Visual hierarchy | View any page | Most important element draws eye first (preset name, status) | Browser |
| H8.3 | Information density | Dashboard page | Not overcrowded — cards with clear sections | Browser |
| H8.4 | Typography consistency | Compare h1/h2 across pages | Same font sizes, weights, colors for each heading level | Browser |
| H8.5 | Line length readability | Any card body | Text doesn't span full viewport — contained by max-width | Browser |
| H8.6 | Dark theme cohesion | All pages | Consistent dark palette, no jarring color changes | Browser |
| H8.7 | Redundant elements | Check for duplicate info | No data shown in multiple places unnecessarily | Browser |
| H8.8 | Empty states visual | Unload model, clear presets | Helpful empty states, not just blank areas | Browser |
| H8.9 | Modal sizing | Open preset edit modal | Appropriate width, doesn't overwhelm on mobile | Browser |

---

### H9 — Help Users Recognize, Diagnose, and Recover from Errors

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H9.1 | API error → user message | Trigger a 500 on preset load | User-facing error, not raw JSON or console-only | Browser |
| H9.2 | Tone3000 offline state | Disconnect internet, search | "⚠ Offline — search unavailable" warning shown | Browser |
| H9.3 | Upload error handling | Upload corrupt .nam file | Alert with error message: "Upload error: ..." | Browser |
| H9.4 | Model load failure | Try to load corrupt model | Alert: "Failed to load model: ..." | Browser |
| H9.5 | Preset load failure | Access empty slot via direct URL | 404 — "Preset not found" | Browser |
| H9.6 | WiFi connect error | Wrong password on WiFi modal | Error message shown in modal, modal stays open | Browser |
| H9.7 | JACK restart failure | Kill JACK, switch profile | API returns 500 with "Failed to start JACK — rolled back" | API |
| H9.8 | API error on Dashboard | Kill backend, load dashboard | Silent catch with console.warn — check for user-facing message | Browser |
| H9.9 | Empty preset slot styling | Presets page, empty slot | Shows "— Empty —" with reduced opacity | Browser |

**Code verification:**
```bash
# Error handling patterns — check for user-facing messages vs silent console
grep -n "\.catch\|catch(" src/web/static/*.js | grep -v "console.warn\|console.error"
grep -n "alert(" src/web/static/*.js
grep -n "text-muted\|network-error\|Error:" src/web/static/*.js
```

---

### H10 — Help and Documentation

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| H10.1 | Tooltips on controls | Hover over any button | No tooltips currently — add recommendation | Browser |
| H10.2 | Upload hints | Check upload sections | "Upload .nam model files to the pedal's model directory." — clear | Browser |
| H10.3 | Tone3000 hints | Check Tone3000 section | "Search and download NAM models directly from the Tone3000 community." | Browser |
| H10.4 | Empty state guidance | No models/presets/IRs | Actionable: "No .nam models found" + upload option visible | Browser |
| H10.5 | About page | Navigate to About tab | Version, platform, repo link available | Browser |
| H10.6 | Help within the app | Check for question marks, help links | None found — add recommendation for critical flows | Browser |
| H10.7 | IR details context | View IR list | Shows "1024 taps · 21.3ms @ 48000Hz" — helpful technical detail | Browser |

---

## 3. Flow Friction Map

Each flow from Section 1 tested step-by-step. Score 1-5:

| Flow | Step | Friction | Issue Ref | Notes |
|------|------|----------|-----------|-------|
| **F1 — Load dashboard** | Navigate to `pedal.local:8080` | 1 | — | React SPA loads (or legacy fallback) |
| **F1** | Check connection status | 1 | — | Color-coded badge in header |
| **F1** | View current state | 1 | — | WS pushes state on connect |
| **F2 — Browse presets** | Click Presets nav | 1 | — | |
| **F2** | Wait for bank list | 2 | H1-like | Loading indicator present |
| **F2** | Click preset card | 1 | — | |
| **F2** | Preset loads, dashboard updates | 1 | — | WS pushes update |
| **F3 — Edit preset** | Click preset card to open modal | 1 | — | |
| **F3** | Change name | 1 | — | Text input clear |
| **F3** | Adjust volume | 1 | — | Slider works |
| **F3** | View FX chain in modal | 2 | H2, H6 | Labels use `replace(/_/g,' ')` — see notes |
| **F3** | Click Save | 1 | — | Modal closes, list refreshes |
| **F3** | Click Delete | 1 | — | Confirm dialog appears |
| **F4 — Bypass & volume** | Click bypass button | 1 | — | Immediate toggle |
| **F4** | Verify bypassed audio | 1 | — | Clean DI signal |
| **F4** | Drag volume slider | 1 | — | Real-time update |
| **F5 — Tone3000 browse** | Go to Models page | 1 | — | |
| **F5** | Type search query | 1 | — | Enter triggers search |
| **F5** | Wait for results | 3 | H1 | "Searching..." spinner, but no timeout info |
| **F5** | Click Install on result | 2 | H1 | Button shows "Downloading..." |
| **F5** | Verify model appears in list | 1 | — | Auto-refreshes |
| **F6 — Upload IR** | Go to IRs page | 1 | — | |
| **F6** | Click file input | 1 | — | |
| **F6** | Select .wav file | 1 | — | File type restricted |
| **F6** | Click Upload | 2 | H1 | No upload progress bar — just button state |
| **F6** | Verify in list | 1 | — | Refreshes |
| **F7 — Audio profile switch** | Go to Settings > Audio | 1 | — | |
| **F7** | Profiles are visible | 2 | — | No explicit "switch profile" button in UI — needs API |
| **F7** | Switch profile | 2 | H10 | No in-UI profile switcher visible in Audio tab |
| **F8 — Hotspot config** | Go to Network tab | 1 | — | |
| **F8** | Edit SSID/password | 1 | — | Input fields ready |
| **F8** | Click Enable Hotspot | 2 | H1 | No "enabling..." state |
| **F8** | Verify clients listed | 2 | H1 | Auto-polls or manual refresh? |
| **F9 — WiFi scan** | Go to Network tab | 1 | — | |
| **F9** | Click Scan | 2 | H1 | "Scanning..." button state, results appear |
| **F9** | Click Connect on network | 1 | — | Modal opens |
| **F9** | Enter password | 1 | — | |
| **F9** | Click Connect | 2 | H1/H9 | Modal error if fails |
| **F10 — BT scan** | Go to Bluetooth tab | 1 | — | |
| **F10** | Scan for devices | 2 | H1 | "Scanning (12s)..." — shows expected duration |
| **F10** | Pair with device | 2 | H1 | No pairing feedback beyond spinner |
| **F11 — 4CM routing** | Go to Settings > 4CM Routing | 1 | — | |
| **F11** | Toggle 4CM ON | 1 | — | Breakpoint row enables |
| **F11** | Drag breakpoint | 1 | — | Description updates live |
| **F12 — Tuner** | Toggle tuner ON | 1 | — | |
| **F12** | Check note detection | 1 | — | Fast poll endpoint |
| **F13 — Snapshots** | Go to snapshot section | 3 | — | No snapshot UI on dashboard — check React SPA |
| **F13** | Save snapshot to slot | 2 | — | Via API or in-app? |
| **F14 — Empty states** | Unload all models | 1 | H6 | "None loaded" shown clearly |
| **F14** | Delete all presets | 1 | H9 | "No presets found. Create one from the Dashboard." |

---

## 4. Visual & Interaction Checklist

| Check | Pass? | Issues | Notes |
|-------|-------|--------|-------|
| Consistent spacing / grid | ✅ | — | Cards share `--radius`, `--border`, `padding: 10-14px` |
| Typography hierarchy | ✅ | — | h1=1.1rem, h2=0.9-0.95rem, body=0.85rem — clear hierarchy |
| Color consistency | ✅ | — | Single accent `#4fc3f7`, consistent danger/success |
| Interactive states | ⚠️ | See below | Hover, active states defined; missing focus-visible styles |
| Responsive layout | ✅ | — | 82% max-width mobile, 600px/720px breakpoints |
| Loading states | ⚠️ | H1-series | Most pages have loading indicators; Tone3000 spinner lacks progress |
| Empty states | ✅ | — | Clear messages: "No models found", "None loaded" |
| Error states | ⚠️ | H9-series | API errors shown via alert(); silent catches on Dashboard WS |
| Edge cases (long text, rapid click) | ⚠️ | H5, H8 | Long preset names handled with ellipsis; Button disabled states present |
| Touch targets ≥ 48px | ⚠️ | — | `.btn` padding 8px 16px — may be borderline on mobile |
| Dark theme readability | ✅ | — | High contrast: `#e0e0e0` on `#0d0d0d` background |
| Smooth transitions | ✅ | — | `transition: background 0.2s, border-color 0.2s` on interactive elements |

---

## 5. Responsive & Mobile UX Checks

| # | Test | Viewport | Expected | Method |
|---|------|----------|----------|--------|
| R1 | Dashboard on mobile | 375px wide | Status row stacks (grid → 1 column), nav links compact | Browser |
| R2 | Presets grid on mobile | 375px wide | 2-column grid intact, cards shrink appropriately | Browser |
| R3 | Settings tabs on mobile | 375px wide | Horizontal scrollable tabs with hidden scrollbar | Browser |
| R4 | Modal on mobile | 375px wide | Modal fills width with padding, scrollable body | Browser |
| R5 | Touch targets on mobile | 375px, touch | Buttons have `touch-action: manipulation`, no 300ms delay | Source |
| R6 | Nav overflow | Very long nav labels | `overflow-x: auto` with hidden scrollbar | Browser |
| R7 | Header sticky on scroll | Scroll down on any page | Header stays at top (`position: sticky; z-index: 100`) | Browser |
| R8 | Input fields on mobile | Focus any input | No viewport zoom (`user-scalable=no` set) — intentional? | Browser |
| R9 | Content max-width on large screen | 1920px wide | `--max-width: 720px` — content centered, not stretched | Browser |

---

## 6. Real-Time & WebSocket UX

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| WS1 | Multiple tabs sync | Open 2 browser tabs, change volume on tab 1 | Tab 2 slider and value update within 1s | Browser |
| WS2 | Bypass sync across tabs | Toggle bypass on tab 1 | Tab 2 button text and color update | Browser |
| WS3 | Preset change broadcast | Activate preset via /presets page | Dashboard preset card updates on all connected clients | Browser |
| WS4 | Reconnection UX | Unplug network cable, wait 10s, reconnect | "Disconnected" appears, then auto-reconnects showing "Connected" | Browser |
| WS5 | WS reconnection backoff | Keep pedal disconnected for 1 min | Reconnect delay increases from 1s to max 15s | Browser |
| WS6 | Concurrency: 3 tabs + API | Open 3 browser tabs + curl API calls | All clients receive broadcasts, no dropped messages | Multi-browser |
| WS7 | Initial state on WS connect | Open fresh browser tab | Receives `{"type": "connected", "state": {...}}` with full current state | Browser |

---

## 7. Accessibility Quick Checks

| # | Test | Expected | Method |
|---|------|----------|--------|
| A1 | Tab navigation through nav links | All 5 nav links focused in order | Browser |
| A2 | Tab navigation through preset grid | Each preset card focusable | Browser |
| A3 | Tab navigation through modal | Input, slider, Save, Delete, Cancel in logical order | Browser |
| A4 | Focus-visible styles on interactive | Visible focus ring on buttons, links, inputs | Browser |
| A5 | Color contrast: text on bg | `#e0e0e0` on `#1a1a1a` — passes WCAG AA | Source |
| A6 | Color contrast: accent on bg | `#4fc3f7` on `#222` — passes WCAG AA | Source |
| A7 | Alt text on Tone3000 thumbnails | `alt=""` (decorative) — acceptable | Source |
| A8 | ARIA labels on toggle buttons | No ARIA — buttons have text content (ON/OFF) so passable | Source |
| A9 | Form labels associated with inputs | File inputs have no explicit `<label>` — just adjacent text | Source |

---

## 8. React SPA-Specific Tests (if mounted at root)

The React SPA at `src/web/ui-dist/` replaces the legacy dashboard at `/`.

| # | Test | Action | Expected | Method |
|---|------|--------|----------|--------|
| SPA1 | SPA loads at root | GET `/` | Returns React SPA `index.html` (not legacy dashboard) | Browser |
| SPA2 | Fallback if no ui-dist | Delete ui-dist dir, restart | Falls back to legacy dashboard at `/` | Browser |
| SPA3 | SPA navigation | Click through the SPA | Client-side routing, no full page reloads | Browser |
| SPA4 | SPA ↔ legacy pages | Navigate from SPA root to `/presets` | Full page navigation to legacy page — smooth transition? | Browser |
| SPA5 | Favicon loads | Check browser tab icon | SVG favicon at `/ui/favicon.svg` | Browser |
| SPA6 | JS bundle errors | Open browser console on SPA | No console errors, no uncaught exceptions | Browser |

---

## 9. Key UX Observations from Source Code (Pre-Findings)

These are issues visible from code review alone. Confirm in browser during execution.

| # | Observation | Heuristic | Source |
|---|-------------|-----------|--------|
| O1 | FX type labels use `block.fx_type.replace(/_/g, ' ')` — simple replace, no proper label map. Some types may render with underscores if unmatched. | H2 | `presets.js:92` |
| O2 | WS `preset_changed` handler updates name/meta but doesn't re-render FX chain on dashboard. Block bypass states may visually lag. | H1 | `app.js:62-69` |
| O3 | `toggle4cm()` reads button state from DOM classList, not from server — race risk if another client or WS event flips state between read and POST. | H5 | `app.js:142-143` |
| O4 | No keyboard shortcut support (Ctrl+S for save, / for search). | H7 | Whole codebase |
| O5 | No undo for any action — delete is permanent after confirm. | H3 | `presets.js:132` |
| O6 | Signal strength icons for WiFi use the same emoji for all levels (`📶` — same Unicode codepoint, different on each platform). | H8 | `network.js:83-88` |
| O7 | SN: signal icon `📶` vs `📶` — same char repeated, 4 levels collapse to same visual. | H8 | `network.js:23-26` |
| O8 | BT scan timeout (12s) displayed to user — good. No cancel option on scan. | H3 + H1 | `bluetooth.js:72` |
| O9 | No feedback when JACK restarts during profile switch — audio dropout with no visual indicator. | H1 | `server.py:1402-1443` |
| O10 | Volume slider on Dashboard and Settings page both update via REST per `oninput` — may cause excessive API calls during drag. | H7 | `app.js:227-230` |
| O11 | Connection status `_handlers` check (`pedalWS._handlers ? '1' : '0'`) looks for handler registration, not actual WS count. | H1 | `app.js:261` |

---

## 10. Pre-Completion Checklist

Before marking a section complete, verify:

- [ ] Browser tested: each test executed and result recorded
- [ ] JS console checked for errors after each interaction
- [ ] WS messages confirmed where applicable (via browser console: `pedalWS._handlers`)
- [ ] Responsive view tested for mobile flows (R1-R9)
- [ ] Code findings (O1-O11) confirmed in live browser session
- [ ] Screenshots captured for visual issues (use `browser_vision`)
- [ ] Cross-device sync tested for at least one flow (WS1)

---

## 11. Test Annotation Legend

| Symbol | Meaning |
|--------|---------|
| ✅ | Passes — expected behavior confirmed |
| ❌ | Fails — bug discovered (create Gitea issue) |
| ⚠️ | Non-critical / minor friction (note in recommendations) |
| ⏭️ | Skipped — explain why (e.g., "no pedal hardware connected") |
| 🔄 | Needs retest after fix |

---

*Generated by Hermes Agent UX Review Skill — based on Nielsen's 10 Usability Heuristics and ux-review methodology.*
