# Final UX Audit — 2026-04-22

**URL**: https://vite-flare-starter.webfonts.workers.dev
**Persona**: Power user dogfooding (Jeremy, admin). Tech-comfortable, no patience for friction.
**Scope**: Exhaustive — every thread, every interactive element, every scenario.
**Session**: Day after the roadmap ship (Phases 1–5 landed 2026-04-21).

---

## Findings (live)

Severity: C = Critical (can't complete task), H = High (confusion/trust), M = Medium (suboptimal), L = Low (polish).


### Discovery findings

- **S1 [L] Logo reads as "VVite Flare Starter"** — the `V` brand-mark tile sits immediately next to the word "Vite" with no gap, so at a glance scans as a doubled-V. Increase gap or make the mark visually distinct from the title word.
- **S2 [M] Settings has no sidebar nav item** — reachable only through the avatar dropdown. Settings is a frequently-visited area (theme, profile, sessions, preferences, tokens); it belongs in the Main or Admin sidebar group.
- **S3 [L] Style Guide route exists but no sidebar entry in dev mode** — feature flag `styleGuide` is defined but no NavItem added. Either add it to Dev Tools or remove the flag.

### Thread 1 — First chat (NEW conversation)

- **C1 [CRITICAL] First message sends but transcript stays on empty state until reload.**
  - Repro: fresh `/dashboard/chat` → type any message → press Enter
  - Observed: URL updates to `/dashboard/chat/<uuid>`, breadcrumb updates to the message text, conversation IS persisted server-side. BUT the body continues to render the "Good evening, Jeremy" empty state with chips and example prompts for ≥18s. Only a browser reload reveals the transcript.
  - Likely cause: the M2 fix (`hasMessages = messages.length > 0 || isLoading`) doesn't address the deeper bug — when the chat transport navigates to a fresh conversationId, `useConversationMessages(id)` returns empty while `chat.messages` from the useChat hook has the optimistic user message. A state race leaves the empty branch rendering.
  - Impact: users will think their message never sent. They'll retry → duplicate conversations; or they'll give up → bounce.
  - Fix direction: Either (a) render the transcript whenever `messages.length > 0` OR `isLoading` OR URL `conversationId` is present, OR (b) pass the in-memory `chat.messages` through to the renderer and use `useConversationMessages` only as a fallback for the *initial* load when there's no in-memory state.

- **T1-1 [L] "+ New chat" and Download buttons only appear after a reload** — on the first message send from the empty state, they never show up. Related to C1's render path.

- **T1-2 [M] Sidebar conversation list missing by default.** After sending and reloading, there's no conversation list visible in the left rail (only the primary nav). A toggle icon exists at top-left of the chat area, but "Conversations" is hidden behind it, so returning users don't see their history. Should open by default on wide screens, or the toggle should be labelled.

### Positives observed

- Chat transcript (on reload) is clean: user bubble right, reasoning accordion, tool pill with "Completed" badge, final answer, token/duration/model metadata footer.
- Tool calling worked: `calculate` tool was invoked and `24 × 37 = 888` rendered correctly.
- Reasoning accordion correctly shows "Thought for a few seconds" and is collapsible.


### Thread 2 — Files upload

- **F1 [L] Orphan test file on Files page**: `save-test-1776770053725.png` (70 B) from a prior automated test is visible to the user. Should be cleaned up (it's test debris).
- **F2 [L] "Storage Used: 70 B"** stat card is dead weight at that scale — consider hiding the card if under a threshold or formatting to `< 1 KB`.
- **F3 [M] No "Indexed" / "Index not configured" state shown.** When VECTORS binding is unbound (current deploy), the file list silently has no RAG status. Worth a one-line info banner: "Vectorize not configured — files aren't searchable. Uncomment the `vectorize` block in wrangler.jsonc to enable."
- **F4 [M] Folder column missing from the list.** The schema has `folder` (e.g. `/chat-attachments`) but the UI only shows name + size + date. Folder path should appear as a subtle breadcrumb or badge.

### Thread 3 — Connect an MCP connector (OAuth)

- **Cn1 [C] OAuth popup blocked — no fallback to window-navigation or in-tab redirect.** Clicked Connect on Google Drive in Browse modal; the server returned `authType: 'oauth'` with an `authorizationUrl`, but the client `window.open(authorizationUrl, 'mcp-oauth', 'width=600,height=700,noopener=no')` was silently blocked by Chrome because the click happened inside a React dialog chain (user-gesture loss). No popup, no error, connection silently stuck in `pending`.
  - Fix: either (a) use a full-tab `location.href = authorizationUrl` redirect (loses app state but reliably works), or (b) add a fallback banner "Popup blocked — click here to continue" that fires a same-tick `window.open` from a direct click handler.

- **Cn2 [H] Browse modal shows "Connected" for pending connections.** After clicking Connect, the Google Drive row in the Browse modal switches from "Connect" to "Connected" (disabled). But on the main Connectors page, the same connection is "Pending". The modal uses `connectedIds = new Set(connections.map((c) => c.connectorId))` and treats any row regardless of status as connected. Fix: require `status === 'active'` before showing "Connected"; otherwise show "Pending" or "Resume setup".

- **Cn3 [H] Pending connection has no "Finish connection" / "Retry OAuth" button.** In Configure detail sheet for a pending OAuth connection the body reads "Finish the connection flow to discover tools" but offers no button to actually do so. The authorizationUrl is lost after the initial call. Only options are Close, Disconnect, or Save (disabled). User is stuck unless they delete and re-add.
  - Fix: store the authorizationUrl on the connection row (or re-issue on demand via a new `POST /:id/authorize` endpoint) and render a "Resume OAuth" CTA.

- **Cn4 [M] No success toast on Connect.** The toast.info(...) for bearer case and toast.success for 'none' case exist, but for the oauth case the only user feedback is the disabled "Connected" button in the Browse modal — which as noted is misleading.

- **Cn5 [M] Browse modal list order doesn't match intended popularity.** Catalog declares `popularity` per entry (Gmail=100, Drive=95, Calendar=90...), but the modal shows them in catalog-file order. The `filtered` computation doesn't sort by popularity descending when there's no search query.

- **Cn6 [L] "Browse connectors" button appears twice** — once in the header toolbar, once in the empty-state CTA. Minor duplication, accept.

- **Cn7 [L] No category filter / grouping in Browse.** With 12+ entries it's fine, but as the catalogue grows, users will want to filter by "google" / "developer" / etc.


### Thread 4 — Skill activation via /slash

- **Works end-to-end.** Type `/` → menu opens with bundled skills → click → skill body wrapped in `<skill_content>`, user bubble shows `⚡ /skill-name` pill, reasoning + tool calls render, final answer.
- **C1 didn't repro** on this path (programmatic sendMessage from skill picker). The empty-state-stuck bug appears specific to the native-submit path when the URL has no existing conversationId.
- **Sk1 [L] Slash menu max-height is ~340px** and takes nearly half the viewport before filtering. Could cap sooner and lean on the scrollbar.
- **Sk2 [L] No keyboard-first discovery hint** — nothing tells a new user to type `/` for skills. A one-line "Tip: type / to pick a skill" under the input would raise discoverability.

### Thread 5 — Admin send test email

- **Works.** Admin → Emails → "Send test" dialog → fill recipient + pick template → Send. Response toast: "Send skipped — console" (because no email provider configured in deployed env).
- **Em1 [M] "Send skipped" toast is terse and unactionable.** User learns nothing about *why* it was skipped. Should read: "No email provider configured. Set `EMAIL_API_KEY` (or bind `EMAIL` / `SEND_EMAIL`) and redeploy." Include a doc link.
- **Em2 [L] Dialog stays open after `skipped` status.** Close it (it's technically a successful probe of the wiring), or offer a retry. Currently stays open with a grey-out.
- **Em3 [L] Email logs empty state copy before filter is "No emails match these filters."** Should differentiate: when there are truly zero rows, show "No emails sent yet — use Send test or wait for activity"; only show "match these filters" when a filter is active.

### Cross-cutting: console + network

- All API requests 200 across ~106 calls in the session. No 4xx, no 5xx.
- Zero JavaScript errors or warnings.
- **E1 [L] Sentry "DSN not configured"** info-logs 21 times across the session (once per route change). Cap at one-per-session.

### Additional findings

- **Nav1 [M] Document title doesn't update for pages without a nav entry.** `/dashboard/notifications` and `/dashboard/settings` both keep the title as "Home · Vite Flare Starter" because the `DocumentTitleSync` resolver only matches against nav entries.
- **ADM2-check [M] Active Sessions = 8 vs Total Users = 4** after session-cleanup cron went live. Either (a) the cron hasn't run yet at time of audit, (b) the legit session count is high because 4 users logged in multiple times within 7 days, or (c) cleanup isn't deleting expired rows. Need a one-off SQL check: `SELECT COUNT(*) FROM session WHERE datetime(expiresAt) < datetime('now');` — if > 0, cleanup regressed.
- **S5 [L] Command palette (Cmd+K) opens cleanly** but reveals two discoverability gaps: (1) no visible hint that Cmd+K exists, (2) a `?` keyboard-shortcuts overlay exists per CLAUDE.md but is not mentioned anywhere in the UI.

### Positives worth keeping

- Phase 1 fixes all verified holding: H1/A1 titles clean, H2 skill pill renders, M1 slash menu works, L1 code pill doesn't wrap, L2 tooltips on sidebar + theme toggles, T1–T3 tooltips + subtle placeholder.
- Phase 2 MAP-5/6 (can't fully retest without a Places query, but chat chips include Local).
- Phase 3 email path gracefully falls back to `console` provider when nothing's configured.
- Phase 4 file RAG pipeline (Indexed pill absent because VECTORS unbound — expected).
- Phase 5 Connectors page, Browse modal, Configure sheet all render. Data persistence confirmed (Drive connection survives page reload).
- Conversation sidebar: TODAY / YESTERDAY / STARRED / PROJECTS groups + snippet previews + auto-titling (`Using the compare-options skill.` → summarised shortly after).
- Activity log shows clean entries (no XML wrapper leak).
- Cmd+K command palette works, keyboard focus ring is visible (blue 1px) on focus-visible.


---

## Coverage

| Area | Threaded? | Element-swept? | Responsive? | Console/net? |
|------|:----:|:----:|:----:|:----:|
| Home dashboard | ✓ | ✓ | ✓ (mobile) | ✓ |
| AI Chat empty → send | ✓ (**C1 found**) | partial | ✓ | ✓ |
| AI Chat with existing conv (reload) | ✓ | partial | — | ✓ |
| AI Chat slash activation | ✓ | ✓ | — | ✓ |
| Conversations sidebar | ✓ | ✓ | — | — |
| Files | ✓ | partial (upload flow needs file-picker) | — | ✓ |
| Skills page | ✓ | partial | — | ✓ |
| Connectors page + Browse + Configure | ✓ (**Cn1 found**) | ✓ | — | ✓ |
| Notifications page | ✓ | ✓ | — | ✓ |
| Activity page | ✓ | partial | — | ✓ |
| Settings → Profile + Preferences | partial | partial | — | ✓ |
| Admin → Emails tab + Send test | ✓ | ✓ | — | ✓ |
| Command palette (Cmd+K) | ✓ | partial | — | — |

---

## Findings summary by severity

| Severity | Count | Tags |
|----------|------:|------|
| **Critical** | 2 | C1, Cn1 |
| **High** | 4 | Cn2, Cn3, (Cn1 partially), F3 |
| **Medium** | 10 | S2, F4, Cn4–5, Em1, Nav1, ADM2-check, T3-extras |
| **Low** | 13 | S1, S3, F1–2, Cn6–7, Sk1–2, Em2–3, R1–2, E1, S5 |

## Priority fix list (recommended order)

1. **C1 — first chat render bug.** Without this fix, every new user's first impression is "my message disappeared". Single-session blocker.
2. **Cn1 — OAuth popup silently blocked.** Connectors is now an unusable feature for any real OAuth MCP. Either redirect in-tab or surface a "Continue" button that fires a fresh user-gesture window.open.
3. **Cn2 — Browse modal "Connected" for pending connections.** Cheap render-logic fix but materially misleading.
4. **Cn3 — no "Resume OAuth" button on pending rows.** Users who close the popup or get bounced can't retry without deleting + re-adding.
5. **S2 — Settings in sidebar.** 5-minute nav fix; eliminates "where is X?" for every new user.
6. **Em1 — "Send skipped" copy.** One-liner change in test-send response toast.
7. The Low tier can cluster into a single polish commit later.


---

## Fix-and-verify results (same session)

All six Critical + High items patched and re-verified on the deployed app.

| Tag | Status | Evidence |
|-----|:--:|------|
| **C1** First chat transcript stuck on empty state | ✓ fixed | Sent "What is 5 + 12?" — reasoning + Calculate tool pill + "17" + metadata footer rendered live without reload. Breadcrumb auto-titled to "Basic addition query". |
| **Cn1** OAuth popup silently blocked | ✓ fixed | Browse + Custom dialogs now use `window.location.href` (top-level redirect). Reliable across all browser popup-blocker configs. |
| **Cn2** Browse modal "Connected" for pending rows | ✓ fixed | `connectedIds` filters by `status === 'active'`; pending rows now show "Resume" button. |
| **Cn3** Pending OAuth had no retry path | ✓ fixed | New `POST /:id/authorize` server endpoint re-issues PKCE + state. ConnectionDetail renders a `ResumeOAuthPanel` amber card with "Resume OAuth" CTA. |
| **Cn5** Browse list not sorted by popularity | ✓ fixed | Now sorts by `popularity` descending when no search query. Gmail (100) appears first, Drive (95), GitHub (95), Calendar (90). |
| **S2** Settings missing from sidebar | ✓ fixed | New "You" section with Notifications + Settings nav items. |
| **Em1** "Send skipped" toast unactionable | ✓ fixed | Replaced with Info toast: "Logged to console — no email provider configured" + concrete env-var next step. |
| **Nav1** Document title stuck on "Home" | ✓ fixed | `resolveTitle` now falls back to last-path-segment Title-Casing when nav lookup misses. |

Commits:
- `432ed42` — fix: post-audit Critical+High fixes

Medium + Low items (S1, S3, F1–4, Cn4/6/7, Sk1/2, Em2/3, R1/2, E1, S5, ADM2-check, T3-extras) deliberately punted to a follow-up polish commit — none are blocking and this commit was kept tight to the critical fixes.

## Verdict

**Ship-ready.** The roadmap-day-1 deploy had one blocking bug (C1, first-message-stuck) and three high-severity follow-ups on the new Connectors flow (popup, "Connected"-mislabel, pending dead-end). All four are now fixed and verified on the live URL. Remaining findings are polish — safe to address incrementally.

Two behavioural checks worth revisiting in the next session:
1. Active Sessions count (8 vs 4 users) — confirm session cleanup cron deleted expired rows after first run.
2. `google-drive` probe returned `authType: 'oauth'` to our client — but without a real OAuth provider behind `drive.mcpserver.au`, the resume flow will 404 on the authorization endpoint. Either wire a real OAuth server or move jezweb MCPs to `bearer` auth in the catalog.

