# UX Audit — Vite Flare Starter — 2026-04-19

**Tester:** Claude Opus 4.7 (1M) via Chrome MCP (Jez's real logged-in session)
**Live URL:** https://vite-flare-starter.webfonts.workers.dev
**Viewport:** 1440×900 window / 1440×651 app render (Chrome chrome eats ~249px — real-user condition)
**Persona:** A developer evaluating vite-flare-starter as the base for their next Cloudflare app. Mid-tech comfort, 30-minute evaluation slot, MacBook + Chrome, has seen ~3 other Cloudflare starters this week. Decisive but suspicious — will bail if the anchor feature (AI Chat) has rough edges or if the included modules feel like half-finished demos.
**Prior audit:** `.jez/artifacts/ux-audit-2026-04-18.md` (3 medium, 4 low, all within Jez's own dogfood). Today's pass is independent and exhaustive within the evaluator persona.

## Summary

Nothing critical. The app is coherent, polished, and the AI Chat / Conversations / Activity core is strong enough that an evaluator would keep it on the shortlist.

Two **Medium** findings worth queueing now:
- **Security tab shows a Change Password form to OAuth-only users** (M3) — Jeremy signed in with Google and has no password, but the form is shown with no hint that it won't work. Either hide it or explain the path to Google Account password management.
- **Active Sessions show "Unknown IP" for every session** (M4) — the whole "revoke access to any session" feature loses its purpose when every row is indistinguishable.

Minor bug: **opening Cmd+K while the Keyboard Shortcuts dialog is open stacks a second modal** (M5) — opening one dialog should close the other.

Most of the remaining findings are polish items around the dashboard, the settings surface, and copy / label consistency. Two prior-session findings (L7 phantom overlay, L6 model count) are resolved or narrowed below.

## Coverage

| Area | Status | Notes |
|---|---|---|
| Landing (signed out + signed in view) | Walked | Thread 1 |
| Sign in / sign up | Walked | Thread 1 |
| Dashboard home | Walked | 3 action cards + 11 info cards inventoried |
| AI Chat | Walked exhaustively | Thread 2 — empty state, stream, sidebar, search, model switch, export, reload |
| Conversations (sidebar, search, auto-title, auto-summary) | Walked | Thread 2 |
| Settings → Profile | Walked | Thread 3 — inspected fields |
| Settings → Organization | Walked | Placeholder-only, correctly no fake data |
| Settings → Security | Walked | **M3 filed** |
| Settings → Sessions | Walked | **M4 + L8 filed** |
| Settings → API Tokens | Walked (empty state only, did not create) | Good empty state |
| Settings → AI | Walked | Chat preferences surface is well-designed |
| Settings → Settings (preferences/themes) | Walked | **L9 filed** |
| Activity | Walked | **L10 filed** — rows not clickable |
| Files | Walked (empty state only) | Upload not exercised |
| Notifications bell | Walked | Good empty state |
| Extract | Not walked today | Deferred — no change since prior audit |
| Components (dev tool page) | Not walked today | Deferred |
| Admin | Not walked today | Deferred |
| Keyboard shortcuts (`?`, Cmd+K) | Walked | **M5 filed** — stacked modals bug |
| Command palette | Walked (Cmd+K) | Lists navigation targets; confirmed feature works |
| Responsive sweep | **Not walked today** | Would need device-resize tooling — carrying forward |
| Second-user / ownership | Not walked today | Would need a 2nd account |
| Destructive flows (delete account, revoke session, remove token) | Not attempted | By design — avoid polluting live account |

---

## Findings

### Critical
*None yet.*

### High
*None yet.*

### Medium

#### M3. Security tab offers a Change Password form to OAuth-only users
**Where:** `src/client/modules/settings/pages/SettingsPage.tsx` → Security tab → `src/server/modules/settings/routes.ts`
**Observed:** Jeremy signed in with Google OAuth and has no password on the better-auth `account` row (providerId='google'). The Security tab unconditionally renders `Current Password / New Password / Confirm New Password` with a "Change Password" submit. If Jez types anything and clicks Change Password, the server will reject it (there is no password to compare against) and the user gets an opaque failure.
**Why it matters:** This is the security surface — ambiguity here makes the app look broken or insecure. "The change password button didn't work" is a message a dev-evaluator tells colleagues. OAuth-only accounts should either see a calmly-worded "You signed in with Google. Manage your password at your Google Account." block, or a "Set a password" flow if the starter wants to support upgrading from OAuth-only to OAuth+email-login.
**Fix:** In the Security tab, query `GET /api/auth/accounts` (or inspect `session.providerId`) and:
- If the user has a `credential` account row → show the current form (unchanged).
- If they don't → replace the form with a short explainer + link to Google Account security settings, OR a "Create a password" form that sets one for the first time.

#### M4. Active Sessions list shows "Unknown IP" for every row
**Where:** `src/server/modules/settings/sessions` (or wherever session rows are materialised for the API), `src/client/modules/settings/components/SessionsTab.tsx`
**Observed:** Every session row in Active Sessions reads "Chrome 146 on macOS 10.15.7 · Unknown IP · Last active N days ago". 4+ rows, all identical except for the "N days ago".
**Why it matters:** The point of the Sessions tab is to spot a compromised session and revoke it. When every row says "Unknown IP" you can't tell which is the current device, which is the old laptop, and which is (if any) a rogue. You also can't tell them apart from each other when deciding what to revoke. This is the exact feature that *has* to feel trustworthy on a starter that pitches auth as a bundled capability.
**Likely root cause:** On Cloudflare Workers, `request.headers.get('cf-connecting-ip')` (or `c.req.header('cf-connecting-ip')`) should be captured and stored on session create. Either the code isn't capturing it, or the database column exists but isn't rendered.
**Fix (two parts):**
1. Ensure the session creation path reads `cf-connecting-ip` and writes it to `session.ipAddress` (better-auth supports the column natively).
2. Render the IP. For bonus UX, geo-resolve it via `request.cf.country` / `request.cf.city` at session-create time and store `ipLabel = "Sydney, AU · 124.171.x.x"`.
Also: flag the current session with a "This device" / "Current session" pill so users know which one they're on.

#### M5. Pressing `?` then Cmd+K stacks a second modal instead of replacing the first
**Where:** `src/client/components/KeyboardShortcuts.tsx` + `src/client/components/CommandPalette.tsx`
**Observed:** Pressed `?` (global shortcut) → Keyboard Shortcuts dialog opened. Without closing it, pressed Cmd+K → Command Palette opened **on top of** the Keyboard Shortcuts dialog. Two stacked dialogs, the back one partially obscured. Pressing Escape once closed the Command Palette, second Escape closed Keyboard Shortcuts. Not a crash, but feels broken.
**Why it matters:** The two modals advertise related functionality (navigate, run commands, see shortcuts) and should coordinate — not race. First-time evaluators will bump into this because the shortcuts are right next to each other on the keyboard.
**Fix:** Two options, either works:
- **A** — When opening any app-level modal (Command Palette, Keyboard Shortcuts, any future global modal), close all other app-level modals first. One-at-a-time policy.
- **B** — Merge the two: put "Keyboard Shortcuts" as an item inside the Command Palette under a "Help" section. Then `?` and Cmd+K both land in the same place.

#### M1. Authenticated users are not redirected off `/sign-in` and `/sign-up`
**Where:** `src/client/App.tsx:56-57`, `src/client/modules/auth/SignInPage.tsx`, `SignUpPage.tsx`
**Observed:** Logged in as Jeremy, navigating to `/sign-up` shows the sign-up form ("Create an account / Continue with Google"). Same on `/sign-in`. Either page should detect an active session and redirect to `/dashboard`.
**Why it matters:** Returning users who click a bookmarked landing link, a nav "Get Started" CTA, or follow a stale URL get dumped on a confusing sign-up page. They're already signed in — they just see "Already have an account? Sign in" and think the starter has an auth bug.
**Fix:** Guard both routes with a `PublicOnlyRoute` wrapper that mirrors `ProtectedRoute` — if `useSession()` returns a user, redirect to `/dashboard`. One component, both routes.

#### M2. Public landing shows "Get Started" and "Sign In" CTAs to authenticated users
**Where:** `src/client/layouts/PublicLayout.tsx` (header) and `src/client/pages/LandingPage.tsx` (hero + "Ready to build?" section)
**Observed:** Top-nav shows both "Sign In" (→ `/sign-in`) and "Get Started" (→ `/sign-up`) even when I'm logged in. Hero shows a second "Get Started" (→ `/sign-up`) and "View on GitHub". The "Ready to build?" section shows "Try the Demo" (→ `/sign-up`) and "Star on GitHub".
**Why it matters:** For a signed-in evaluator, every CTA points away from the app. A correct auth-aware landing for returning users collapses these to a single "Open Dashboard →" button next to the theme toggle.
**Fix:** In `PublicLayout` header and `LandingPage` CTAs, call `useSession()` and swap the two auth buttons for one "Open Dashboard" link when `data?.user` exists. Keep "View on GitHub" and "Star on GitHub" as-is — those are always useful.

### Low

#### L1. User dropdown over-itemises the Settings tabs as separate menu rows
**Where:** User avatar dropdown (bottom-left of sidebar) — `src/client/layouts/DashboardLayout.tsx`
**Observed:** Menu has 6 rows: Profile, Security, Settings, Notifications, Admin Panel, Sign out. Profile / Security / Settings / Notifications are all tabs on `SettingsPage`. Four rows in the dropdown that all go to the same page feels over-itemised; new users can't tell whether they're separate modules or one page with tabs.
**Fix:** Collapse the four to a single "Settings" row (goes to `/dashboard/settings?tab=profile`). If deep-linking to tabs matters, expose them as in-page tabs only.
**Alternate:** Keep the four rows but visually group them ("Account" separator) and keep Admin Panel / Sign out in their own sections with shadcn `DropdownMenuSeparator` between.

#### L2. Two `Get Started` links at landing-page level share the same label
**Where:** `src/client/layouts/PublicLayout.tsx` header, `src/client/pages/LandingPage.tsx` hero
**Observed:** The header has a solid-button "Get Started" and the hero has an identical solid-button "Get Started". Both `href="/sign-up"`. An evaluator reading the hero doesn't know the header button does the same thing. Minor visual redundancy.
**Fix:** Keep the hero CTA. In the header, either remove the "Get Started" button for unauthenticated users (Sign In is enough) or change its label to "Create account" to differentiate.

#### L3. Conversation header still shows generic "AI Chat" after auto-title lands
**Where:** Chat page header — left of "New chat" button
**Observed:** When a new conversation is created, the breadcrumb/header shows "AI Chat" forever — even after the summariser has auto-titled the conversation to "Year query and Sydney fact" (visible in the conversation sidebar within ~2s of the response finishing). The conversation title never replaces "AI Chat" in the main pane header.
**Why it matters:** Users with multiple tabs open across conversations can't tell them apart from the tab title or the in-app header. claude.ai, ChatGPT, and t3.chat all render the conversation title in the header as soon as it exists.
**Fix:** Render `{conversation.title ?? 'AI Chat'}` in the header component. Falls back gracefully during streaming.

#### L4. Token usage grows ~5k per turn — probably no caching
**Where:** Response metadata footer (`@cf/moonshotai/kimi-k2.5 · 7,756 tokens`)
**Observed:** Turn 1 reported 7,756 tokens for "what year is it and give a Sydney fact". Turn 2 on Sonnet reported 12,806 tokens for a 1-sentence follow-up ("what 4.x version are you"). Growth ~5k per turn on a near-empty conversation suggests the full tool schema + system prompt re-serialises each call without Anthropic's `cache_control` markers.
**Why it matters:** At this rate, long sessions cost ~10x more than they need to on paid providers. For a starter that advertises cost efficiency, this is worth showing off.
**Fix:** On the Anthropic / OpenRouter paths, add `cache_control: { type: 'ephemeral' }` to the system prompt + tool list blocks. Anthropic's prompt caching is free to enable and typically cuts 80%+ of input tokens on multi-turn chats. Workers AI models can't cache but are already free — not a concern there.

#### L5. Export button only surfaces via `title` tooltip, no `aria-label`, single format only
**Where:** Chat header — download icon next to "New chat"
**Observed:** Icon button has `title="Export as Markdown"` but no `aria-label`. Screen readers may not announce it. CLAUDE.md says JSON + Markdown are supported but only Markdown is exposed in this UI — either it's not wired through or the format choice is hidden behind a modifier key/menu that isn't discoverable.
**Fix:** Add `aria-label="Export conversation as Markdown"`. If JSON export is intentional, change the icon to a dropdown offering "Markdown" / "JSON" / "JSON with tool traces".

#### L6. Model picker advertises "16 models across 8 providers" — RESOLVED (my miscount)
**Where:** Model picker dropdown + CLAUDE.md
**Outcome:** Config in `src/shared/config/models.ts` enables 16 models (4 Workers AI + 12 OpenRouter). Copy says 16. I miscounted in the audit — omitted `qwen/qwen3.6-plus` from my enumerated list. Server `listModels()` returns all 16 with no filtering. No code change needed. Closing.

#### L7. Fixed z=2147483646 transparent overlay in the body — RESOLVED (not an app bug)
**Where:** `document.body > #claude-agent-glow-border` and `document.body > #claude-agent-stop-container`
**Outcome:** Confirmed it's the Claude-in-Chrome browser extension (ids give it away). Not app code. Closing this as non-issue — leaving the note here so future audits don't re-discover it.

#### L8. No "current session" pill on the active session row (Sessions tab)
**Where:** Settings → Sessions
**Observed:** Every session row looks identical. There's no way to tell which row is "the browser I'm in right now" vs "a session from three days ago I probably want to kill". Pair this with M4 and the Sessions tab is effectively non-functional for its stated purpose.
**Fix:** Detect `session.token === currentSession.token` and render a small pill ("This device" / "Current session") on that row. Also disable the Revoke button on the current-session row and suggest "Sign out" instead — revoking the session you're on mid-click is a bad UX.

#### L9. "Settings → Settings" tab — circular label
**Where:** Settings page tablist, 7th (last) tab
**Observed:** The Settings page has 7 tabs: Profile, Organization, Security, Sessions, API Tokens, AI, **Settings**. The last tab, literally titled "Settings", holds the color-theme picker and display preferences. The breadcrumb reads "Settings → Settings" on that tab, which is disorienting.
**Fix:** Rename the last tab to **"Preferences"** or **"Appearance"** (the latter is more accurate — it's color themes and display). "Display" would also work.

#### L10. Activity Log rows aren't clickable / don't link back to their resource
**Where:** `/dashboard/activity` — each activity row is a `<div>` with `cursor: auto`
**Observed:** An activity entry ("Create conversation · 'What year is it and give a Sydney fact' · 33 minutes ago") names a specific conversation but provides no way to click through to it. To jump back, you have to open AI Chat, scan the sidebar, find it. Two-click round trip for a feature that should be one click.
**Fix:** If the entity exists and the route is known (`entityType === 'conversation'` → `/dashboard/chat/<entityId>`), render the row as `<a>` with cursor-pointer. Resolve unknown entity types to no-op rows to preserve the log-only use case.

#### L11. Dashboard info cards look interactive but aren't
**Where:** `/dashboard` — the 11 info cards below the 3 action cards (AI SDK v6, 60+ Agent Tools, Image Processing, Video Processing, Semantic Search, Business Modules, Auth + Admin, UI Library, ...)
**Observed:** The info cards share visual styling with the 3 action cards at the top (AI Chat / Extract / Settings). An evaluator instinctively hovers or clicks — nothing happens. Cards are pure reference.
**Fix:** Either:
- **A** — Make them link targets: each card → a demo page, a docs anchor, or an in-app example. Good for a marketing/evaluation starter.
- **B** — Visually distinguish "action" vs "reference" cards (remove the hover state from info cards; change border/bg; smaller type). Good if the intention is "this is capability inventory, not clickable".

#### L12. Dashboard advertises "Image Processing" and "Video Processing" but neither has a nav page
**Where:** `/dashboard` info cards + `src/shared/config/nav.ts`
**Observed:** Dashboard prominently lists "Image Processing" (resize, crop, bg removal, face detection, FLUX/GPT gen) and "Video Processing" (clip, frame extraction, audio extraction) as capabilities. But the sidebar nav has Home, AI Chat, Extract, Activity, Files, Components — nothing for Image or Video. A user clicking "Image Processing" on the dashboard has nowhere to go. Most likely these surfaces only exist as agent tools inside chat, but the dashboard presents them like first-class features.
**Fix:** Either add a nav entry for each (even a simple demo page that shows what the tools can do), or reword the dashboard cards so they explicitly say "Available as AI tools — ask the chat to resize an image".

---

## Thread Results

### Thread 1 — First Contact (landing → sign-in → dashboard)

**Path walked:** `/` → inspect hero → inspect feature section → visit `/sign-up` → visit `/sign-in` → `/dashboard` → user dropdown.

**What worked:**
- Landing hero is clear and honest: "Build AI apps at the edge" + one-sentence pitch + two CTAs (Get Started / View on GitHub) + trust badges (MIT / TypeScript / Cloudflare Ready). An evaluator knows what the starter is in under 3 seconds.
- Feature section lists 11 cards — AI Agent, Conversation Persistence, Image/Video, Auth+Admin, Skills, MCP, Business Modules, Semantic Search, DataTable+Editor, Human-in-the-Loop, 59 UI Components. Comprehensive.
- Tech stack section reinforces the positioning without jargon.
- Console is clean — only a Sentry "DSN not configured" info message. No errors.
- Dashboard welcome uses the real name ("Welcome, Jeremy") — warm touch.

**Persona friction moments** (in audit order):
- *After logging in, what do I do?* Dashboard has 3 action cards (AI Chat, Extract, Settings) and 9 pure-info cards. An evaluator would be unsure: are the info cards also clickable? Do they have demos?
- *Where is Admin?* It's not in the sidebar. Only found it by clicking my name at the bottom-left.
- *Why does the user-dropdown have "Profile / Security / Settings / Notifications" as separate items when they're all tabs on one SettingsPage?* Feels over-itemised.
- *Why are there two "Get Started" buttons on the landing when I'm already signed in?* I clicked one expecting "go to dashboard" — got a sign-up form instead (see F3 below).

**Questions at thread end (as persona):**
- *Did it end clearly?* Dashboard is confident but dense. Fine.
- *Would I come back?* Yes, probably. Dashboard conveys capability.
- *One thing twice-as-easy?* A "Last used / Continue" card at the top of the dashboard for returning users, replacing one of the info cards.

### Thread 2 — AI Chat (the anchor feature)

**Path walked:** `/dashboard/chat` → inspect empty state → send message → observe stream → open conversation sidebar → search ("harbour") → switch model (Kimi → Sonnet 4.6) → send follow-up → check export button.

**What worked:**
- Time-aware greeting ("Good afternoon, Jeremy") — warm first touch.
- 4 category chips (Write / Research / Code / Plan) + 4 concrete starter prompts underneath. Good new-user affordance without bloating the empty state.
- Default model is Kimi K2.5 (Workers AI, free) — cost-sensible default.
- Streaming works; collapsed "Thought for a few seconds" reasoning section — sensible default (expanded would be noise for most questions).
- Response footer metadata line: `@cf/moonshotai/kimi-k2.5 · 7,756 tokens · 3.6s` — genuine differentiator vs generic chat UIs. The evaluator persona sees costs at a glance.
- URL updates with the conversation UUID immediately on first message — refresh/bookmark works from the first turn.
- Conversation sidebar groups by Projects / Starred / Today / Yesterday / Last 7 days. Auto-title ("Year query and Sydney fact") and auto-summary ("Stated it is 2026 and described the Harbour Bridge's distinctive 'Coathanger' nickname.") generated within seconds.
- FTS search works: typing "harbour" returns the new conversation with a "Message match" label and the matched sentence. Yesterday's M1 (FTS falls back to title) is resolved — search against message body is live.
- Model picker groups by tier: "FREE · WORKERS AI" header, with a clear visual distinction. Reasoning-only models flagged ("no tools, reasoning"). 15 distinct models listed (CLAUDE.md says 16 across 10 providers — off by one somewhere, worth a sanity check).
- Switch from Kimi → Sonnet 4.6 succeeded on the first try; response came in 3.5s. OpenRouter path works.
- Input placeholder swaps from "Ask anything, or drop a file…" to "Reply to the AI…" once the chat has history. Subtle but thoughtful.
- Network: only `/api/chat` + `/api/auth/get-session`. No stray 4xx/5xx. Console clean.

**Persona friction moments:**
- The conversation header just says "AI Chat" — no conversation title. Once the summariser has generated "Year query and Sydney fact", it should render in place of "AI Chat" so the user knows which conversation they're in. (Covered by F4 below.)
- 7,756 tokens for "what year is it" and 12,806 tokens for a 1-sentence follow-up. Growth of ~5k per turn suggests no aggressive prompt caching or context trimming. For a starter that pitches "60+ tools", every unused tool schema accounts for context weight. (Covered by F5.)
- The Export icon exposes only "Export as Markdown" via `title` attr (no `aria-label`). CLAUDE.md advertises JSON + Markdown export; the UI only surfaces one. (Covered by F6.)
- `<button>` with only a `title=` and no `aria-label` — screen readers may miss it.

**Questions at thread end (as persona):**
- *Did it end clearly?* Yes — streamed response, persisted, searchable.
- *Would I come back?* Yes. Sidebar + search + auto-title is competitive with paid chat apps.
- *One thing twice-as-easy?* Show the auto-generated conversation title in the main header once it's ready — right now "AI Chat" is the breadcrumb for every conversation.

## Scenario Battery

### Keyboard-only
- `?` opens the Keyboard Shortcuts dialog. ✓
- `Cmd+K` opens the Command Palette. ✓
- **`?` followed by Cmd+K stacks both dialogs (M5).**
- `Escape` closes the front-most modal, once per press. ✓
- Tabbing through the settings tablist moves focus visibly. ✓ (Not fully audited — partial check.)

### Returning-user with a signed-in session
- Landing at `/sign-in` when signed in → renders the sign-in form instead of redirecting (**M1**). Same on `/sign-up`.
- Landing at `/` when signed in → renders public marketing page with "Sign In / Get Started" CTAs instead of an "Open Dashboard" button (**M2**).

### Wrong turn
- Clicking a "Get Started" button on the landing page as an already-signed-in user sends me to `/sign-up` — a dead end for me (**M1 / M2 in combination**). Fix required.

### Destructive actions
- Not executed (by design on a live account): Delete Account, Revoke Session, Delete API Token. Carrying forward to a dedicated dogfood pass with a throwaway account.

### Deferred scenarios
- Heavy data volume (pagination on Activity / Conversations at 100s of rows) — not attempted.
- Second user (ownership / access control) — needs a second account.
- Interrupted workflow (close tab mid-chat, reopen) — AI Chat URL persistence is confirmed (Thread 2), but full interrupt-recovery wasn't stressed.

## Console + Network Inventory

### Console
- Clean across every page walked today. One Sentry DSN-not-configured info log on landing, no errors or warnings otherwise. Carried over from prior audits.

### Network
- `/api/auth/get-session` on every route transition — expected (session check).
- `/api/chat` streaming via `text/event-stream` — works cleanly.
- No 4xx/5xx observed in any traversal today.
- No unexpected third-party requests.

## What Works Well

1. **AI Chat is the anchor and it's competitive.** Empty-state with category chips + starter prompts, time-aware greeting, streaming with collapsed reasoning, per-message metadata footer (model · tokens · duration), auto-title + auto-summary within seconds, FTS search across conversation bodies, sidebar grouping by Projects / Starred / Today / Yesterday / Last 7 days. This is the part of the starter that would make a developer keep the tab open.
2. **Cost transparency.** Every message shows `model · tokens · duration`. Genuine differentiator.
3. **Default to Workers AI.** `@cf/moonshotai/kimi-k2.5` is the default — free, fast enough, and reasoning-capable. Good for evaluation without an OpenRouter key.
4. **Tasteful empty states across the app.** Files, API Tokens, Notifications — all use the same icon + headline + subtitle + CTA pattern. Coherent.
5. **URL persistence.** Conversation UUID lands in the URL on first message, so refresh/bookmark works from turn 1.
6. **Model picker grouping + reasoning flags.** "FREE · WORKERS AI" section header, reasoning-only models marked "no tools, reasoning". Clear at a glance.
7. **AI Preferences surface.** The Settings → AI tab (Preferred name, Response style, Tone, Confirmation mode, About you) gives an evaluator a concrete reason to return — personalisation is a real user value, not just a demo widget.
8. **Good restraint on fake data.** Organization tab has placeholder-only values ("Acme Corporation", "+61 2 1234 5678") — correctly aligned with the "no fabricated testimonials / contact details" rule.
9. **Command Palette + nav-driven sidebar.** Both read from the same `nav.ts` config. Adding a page in one place flows to both surfaces.

## Priority Recommendations

In the order I'd ship them.

| # | Finding | Effort | Why first |
|---|---|---|---|
| 1 | **M3** — Hide/reword the Change Password form for OAuth-only users | Small | Security surface; looks broken today. One query + a conditional render. |
| 2 | **M4** — Capture and render session IPs (`cf-connecting-ip`, optionally geo-label) | Small–Medium | Whole Sessions tab is currently non-functional for its stated purpose. |
| 3 | **M1 + M2** — Auth-aware landing and `PublicOnlyRoute` guard | Small | Returning users hit this on their second visit. Quick, high-visibility win. |
| 4 | **M5** — One-at-a-time policy for app-level modals | Small | Stacked modals is a clear bug. Minutes of work. |
| 5 | **L3** — Conversation title in the chat header once auto-title lands | Small | Noticeable in daily use; easy fix. |
| 6 | **L4** — Add Anthropic prompt caching on the OpenRouter/Anthropic paths | Medium | Token cost growth is real on multi-turn chats; shipping this would let the starter demonstrate cost optimisation live. |
| 7 | **L8** — "Current session" pill on the active session row | Small | Pairs with M4. |
| 8 | **L9** — Rename the "Settings" tab to "Preferences" or "Appearance" | Trivial | Remove circular label. |
| 9 | **L10** — Link activity rows back to their resource when entity type is known | Small | Dashboard-to-resource flow is currently two clicks; should be one. |
| 10 | **L11 / L12** — Dashboard info cards either link somewhere or stop looking interactive; make it clear that Image/Video "processing" lives inside AI Chat, not a separate page | Medium | Signals polish; addresses two related issues. |
| 11 | **L1 / L2** — Collapse the Profile/Security/Settings/Notifications user-dropdown rows into one "Settings" entry; remove redundant "Get Started" in the header CTA | Small | Copy/UX cleanup. |
| 12 | **L5** — Add `aria-label` to Export button; expose JSON/Markdown format choice | Small | Accessibility + discoverability. |
| 13 | **L6** — Resolve the 15-vs-16 model count mismatch between copy and `models.ts` | Trivial | Pick one. |

**Prior finding resolved this pass**: L7 (phantom overlay) is the Claude-in-Chrome extension injecting `#claude-agent-glow-border` / `#claude-agent-stop-container` — not app code. Closed, not actionable.

**Deferred to a next pass**:
- Responsive sweep (would need a device-resize tooling setup).
- Destructive flows on a throwaway account.
- Heavy-data pagination behaviour.
- Extract, Components, Admin surfaces.

