diff --git a/docs/api-frontend-gap-analysis.md b/docs/api-frontend-gap-analysis.md new file mode 100644 index 0000000..bcd3f41 --- /dev/null +++ b/docs/api-frontend-gap-analysis.md @@ -0,0 +1,175 @@ +# API ↔ Frontend Gap Analysis — Pi Multi-FX Pedal Web UI + +**Date:** June 14, 2026 +**Scope:** All backend API routes vs. calls from Legacy Jinja2 UI + React SPA + +--- + +## Summary + +| Metric | Count | +|--------|-------| +| Total backend API routes | **76** | +| Routes called by at least one frontend | **57** | +| Routes with NO frontend consumer | **10** (+ 2 alias endpoints) | +| Critical bugs found | **2** | +| Missing features (backend exists, no UI) | **5** | + +--- + +## Changes Applied — Fixed Items + +| # | Item | Status | Files Changed | +|---|------|--------|--------------| +| B1 | IR Install on IRs page broken — `installToneIR` added to irs.js | ✅ Fixed | `irs.js`, `models.js` | +| G6 | Tuner pitch display — note + cents bar + string indicator on dashboard | ✅ Added | `app.js`, `dashboard.html`, `style.css` | +| G1–G3 | Audio profile selector — dropdown with JACK status in Settings > Audio | ✅ Added | `app.js`, `settings.html`, `style.css` | +| G4 | FX chain clickable toggles — each block toggles ON/OFF via `PATCH /api/blocks` | ✅ Added | `app.js`, `style.css` | +| O1 | FX type labels — proper label map (FX_TYPE_LABELS) instead of `replace(/_/g,' ')` | ✅ Added | `app.js` | +| B2 | `installToneIR` removed from models.js (was duplicated, used wrong localStorage key) | ✅ Fixed | `models.js` | + +--- + +## 1. Backend APIs NOT Called by Any Frontend + +These are routes defined in `server.py` that neither the legacy UI (`static/`) nor the React SPA (`ui-dist/`) ever calls. + +### Tier 1 — Full Features Without UI + +| # | Route | Method | Endpoint exists but | Why it matters | +|---|-------|--------|--------------------|---------------| +| G1 | `/api/audio/profile` | GET | No UI to read current audio profile | Users can't see what latency profile is active | +| G2 | `/api/audio/profiles` | GET | No UI to list available profiles | Users can't see what options exist | +| G3 | `/api/audio/profile` | POST | No UI to switch profiles | Users can't switch between standard/low latency! | +| G4 | `PATCH /api/blocks` | PATCH | No UI to enable/disable individual FX blocks | FX chain shown on dashboard is display-only — can't toggle blocks on/off | +| G5 | `PATCH /api/block` | PATCH | No UI to update individual block params | Can't tweak per-block settings (drive, level, mix, etc.) | +| G6 | `GET /api/tuner/pitch` | GET | No tuner pitch display UI | Tuner toggle works but you can't *see* what note you're playing | + +### Tier 2 — Secondary / Alternative Endpoints + +| # | Route | Method | Notes | +|---|-------|--------|-------| +| G7 | `GET /api/routing` | GET | Both UIs only POST routing changes and rely on WS for state — GET is defined but never read separately | +| G8 | `GET /api/channel-mode` | GET | SPA POSTs to set channel mode but never GETs current mode — relies on `/api/state` snapshot | +| G9 | `POST /api/bypass/toggle` | POST | Alias — both UIs call `/api/bypass` directly | + +### Tier 3 — External Content Sources (No UI) + +| # | Route | Method | Notes | +|---|-------|--------|-------| +| G10 | `GET /api/tonedownload/status` | GET | Tone3000 connection status — never queried by UI | +| G11 | `GET /api/tonehub/search` | GET | ToneHub search — never called (Tone3000 preferred) | +| G12 | `GET /api/tonehub/search/irs` | GET | ToneHub IR search — never called | + +### Tier 4 — Unused Alias + +| # | Route | Method | Notes | +|---|-------|--------|-------| +| G13 | `POST /api/volume` | POST | POST alias for volume — legacy UI calls `apiPut('/volume')`, SPA... see below | + +--- + +## 2. Critical Bugs Found + +### Bug B1 — IR Install on IRs Page is Broken + +**File:** `src/web/static/irs.js:197` calls `installToneIR()` but function only exists in `models.js:235` + +**Root cause:** The Tone3000 IR search results on the `/irs` page render "Install" buttons that call `installToneIR()`. This function is defined in `models.js`, which is loaded on the **Models page** (`/models`). The **IRs page** (`/irs`) loads `irs.js`, which does NOT define `installToneIR()` — the function appears in `irs.js` only as a call reference (line 197), not a definition. + +**Impact:** Clicking "Install" on any Tone3000 IR result from the IRs page throws `ReferenceError: installToneIR is not defined`. The button action silently fails. + +**Fix:** Either (a) copy the entire `installToneIR()` function (lines 235-256) from `models.js` into `irs.js`, or (b) extract the shared Tone3000 install logic into a shared module loaded by both pages. + +```bash +# Confirm the gap +grep -n 'function installToneIR' src/web/static/*.js +# → only in models.js:235, NOT in irs.js +grep -n 'installToneIR' src/web/static/irs.js +# → line 197 is a CALL to it (onclick=...), not a definition +``` + +### Bug B2 — `installToneIR` Duplicated Across Files + +**File:** Models page (`/models`) also has IR install logic. + +`installToneIR` is defined in `models.js:235` (not just `installToneModel`). This means the Models page can install IR files via the Tone3000 button embedded in model search results — which makes no sense. The function is cargo-culted but only the `installToneModel` variant is relevant to the Models page. + +--- + +## 3. Underserved Features (Present in Backend, Weak UI) + +| # | Feature | Has UI? | Details | +|---|---------|---------|---------| +| F1 | **Tuner pitch display** | ❌ | `GET /api/tuner/pitch` returns frequency, note name, cents, string — but zero UI renders it. The tuner toggle exists but you get no visual feedback about what note you're playing | +| F2 | **Per-block FX control** | ❌ | `PATCH /api/blocks` (toggle) and `PATCH /api/block` (params) are never called. The dashboard shows an FX chain but it's display-only — can't toggle blocks or tweak knobs from the web UI | +| F3 | **Audio profile switching** | ❌ | `GET/POST /api/audio/profile` and `GET /api/audio/profiles` exist but no UI anywhere. Users must use curl to switch latency profiles | +| F4 | **Channel mode display** | ⚠️ | SPA shows channel mode in the header (STEREO badge or GUITAR/BASS toggle). But `GET /api/channel-mode` is never called — the value comes from the `/api/state` snapshot. Legacy UI has no channel mode awareness at all | +| F5 | **Snapshots** | ⚠️ | Snapshots exist in SPA only (save/recall/rename/delete via modal). Legacy UI has zero snapshot support | + +--- + +## 4. Feature Parity: Legacy UI vs React SPA + +| Feature | Legacy (`static/`) | React SPA (`ui-dist/`) | +|---------|-------------------|----------------------| +| Dashboard + status | ✅ Full | ✅ Full | +| Preset browse + activate | ✅ Full | ✅ Full | +| Preset edit (rename, volume) | ✅ Modal-based | ✅ Inline | +| Preset create | ❌ | ✅ "New Preset" button | +| Preset delete | ✅ With confirm | ✅ With confirm (inline) | +| Model browse + load | ✅ Full | ✅ Full | +| Model upload | ✅ | ✅ | +| Tone3000 search (models) | ✅ | ✅ | +| Tone3000 install (models) | ✅ | ✅ | +| IR browse + load | ✅ Full | ✅ Full | +| IR upload | ✅ | ✅ | +| Tone3000 search (IRs) | ✅ | ❌ Broken (Bug B1) | +| Tone3000 install (IRs) | ❌ Broken (Bug B1) | ✅ | +| Bypass toggle | ✅ | ✅ | +| Volume control | ✅ | ✅ | +| Tuner toggle | ✅ | ✅ | +| **Tuner pitch display** | **❌** | **❌** | +| 4CM routing toggle | ✅ | ✅ | +| **Per-block FX toggle** | **❌** | **❌** | +| **Per-block param edit** | **❌** | **⚠️** (block-params schema fetched) | +| **Audio profile switch** | **❌** | **❌** | +| WiFi scan + connect | ✅ | ✅ | +| Hotspot enable/disable | ✅ | ✅ | +| Bluetooth scan + pair + connect | ✅ | ✅ | +| Bluetooth MIDI enable/disable | ✅ | ✅ | +| Snapshots save/recall | ❌ | ✅ Full (CRUD) | +| Capture/record audio | ❌ | ✅ Full (start/stop/list/play) | +| Channel mode (dual-mono/stereo) | ❌ | ✅ (Settings → Routing) | + +--- + +## 5. Source of Truth Verification + +```bash +# Complete list of backend API routes: +grep -oP '@app\.\w+\(["'"'"']/api/[^"'"'"']+' src/web/server.py | sed "s/@app\.//" | sort + +# Complete list of API calls from legacy JS: +grep -roP "apiGet\('/[^']+|apiPost\('/[^']+|apiPut\('/[^']+|apiDelete\('/[^']+|fetch\('/api[^']+" src/web/static/ | sed "s/.*api\(Get\|Post\|Put\|Delete\)('//;s/fetch('/\/api/" | sort -u + +# Complete list of API calls from React SPA bundle: +grep -oP '`/api/[^`]+`' src/web/ui-dist/assets/index-CxlwZhqE.js | sort -u +``` + +--- + +## 6. Remaining Gaps (Not Addressed) + +These are lower-priority items that still have no frontend consumer: + +| # | Route | Method | Notes | +|---|-------|--------|-------| +| G7 | `GET /api/routing` | GET | Both UIs only POST routing and rely on WS for state — GET exists but is redundant | +| G8 | `GET /api/channel-mode` | GET | SPA POSTs to set mode but gets current state from `/api/state` snapshot — low value | +| G9 | `POST /api/bypass/toggle` | POST | Alias endpoint — both UIs call `/api/bypass` directly | +| G10 | `GET /api/tonedownload/status` | GET | Tone3000 connection status — no UI indicator, minor | +| G11 | `GET /api/tonehub/search` | GET | ToneHub — never wired up in favor of Tone3000 | +| G12 | `GET /api/tonehub/search/irs` | GET | ToneHub IR search — never wired up | +| G13 | `POST /api/volume` | POST | POST alias — legacy calls PUT, SPA calls POST; both work | +| G14 | Per-block param editing (`PATCH /api/block`, `GET /api/block-params/{fx_type}`) | PATCH/GET | Block param sliders still need a UI — tracked in UX review test plan | diff --git a/docs/ux-review-test-plan.md b/docs/ux-review-test-plan.md new file mode 100644 index 0000000..ebbf03e --- /dev/null +++ b/docs/ux-review-test-plan.md @@ -0,0 +1,441 @@ +# 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 `