# Oscar UX Audit Report

**Date:** 2026-06-29 (UTC)
**Tool:** Playwright (chromium-headless-shell 1228)
**Target:** https://oscar-platform.pages.dev
**Routes audited:** 13 desktop + 5 mobile = **18 routes**

---

## Summary

| Severity | Count |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 0 |
| Low | 18 |
| Info | 0 |

| Category | Count |
|---|---|
| a11y | 18 |
| console | 0 |
| visual | 0 |
| layout | 0 |
| interaction | 0 |
| network | 0 |
| semantic | 0 |

**Verdict:** No critical/high/medium issues. All 18 findings are the same single Low-severity a11y issue appearing on every page.

---

## Findings detail

### Each page has exactly 1 unlabeled interactive element

**Severity:** Low
**Category:** Accessibility
**Affected routes:** All 18 (every audited route × viewport)

**Description:** On every page, there is exactly one `<button>` element that lacks an accessible label (no text content, no `aria-label`, no `title`).

**Diagnosis:** It is the **Mantine `Modal.CloseButton`** (the X icon button in the modal header). When `withCloseButton={true}` is set on a `<Modal>`, Mantine renders a CloseButton that:
- Has the `m_220c80f2` class
- Contains an `<svg>` icon
- Has **NO `aria-label`** by default
- Is in the DOM and visible whenever any Modal with `withCloseButton=true` is rendered

**Evidence:**

```html
<button class="mantine-focus-auto mantine-active m_220c80f2 m_606cb269
                mantine-Modal-close m_86a44da5 mantine-CloseButton-root
                m_87cf2631 mantine-UnstyledButton-root"
        data-variant="subtle" type="button">
  <svg viewBox="0 0 15 15" fill="none" …>…</svg>
</button>
```

Source confirmation: in `node_modules/@mantine/core/esm/components/Modal/ModalCloseButton.mjs` — no `aria-label` set.

**App-level impact:** 4 Modal locations across the app, each contributing 1 unlabeled button.

| File | Modal | withCloseButton |
|---|---|---|
| `app/src/onboarding/Onboarding.tsx` | Onboarding tour | true (default) |
| `app/src/search/GlobalSearch.tsx` | Global search | false (explicit) |
| `app/src/views/Blog.tsx` | AI draft brief | default true |
| `app/src/views/Blog.tsx` | Post editor | default true |

Since one modal is rendered at app mount (Onboarding tour, even when hidden by `keepMounted`), the unlabeled close button persists across every page in the audit.

**Recommendation (not applied per audit scope):** pass `closeButtonProps={{ 'aria-label': 'Close' }}` to each `<Modal>`. Better: hoist a shared `<OscarModal>` wrapper component that adds aria-label by default.

---

## Per-route state

All routes passed these checks with 0 issues:

| Route | Layout | Console errors | Network errors | Headings | Images alt | Buttons labeled |
|---|---|---|---|---|---|---|
| `/` | OK | 0 | 0 | proper | 0 missing | (1 unlabeled — Modal close) |
| `/projects` | OK | 0 | 0 | proper | 0 missing | " |
| `/voice` | OK | 0 | 0 | proper | 0 missing | " |
| `/media` | OK | 0 | 0 | proper | 0 missing | " |
| `/calendar` | OK | 0 | 0 | proper | 0 missing | " |
| `/map` | OK | 0 | 0 | proper | 0 missing | " |
| `/comms` | OK | 0 | 0 | proper | 0 missing | " |
| `/workflows` | OK | 0 | 0 | proper | 0 missing | " |
| `/surveys` | OK | 0 | 0 | proper | 0 missing | " |
| `/notes` | OK | 0 | 0 | proper | 0 missing | " |
| `/reports` | OK | 0 | 0 | proper | 0 missing | " |
| `/blog` | OK | 0 | 0 | proper | 0 missing | " |
| `/settings` | OK | 0 | 0 | proper | 0 missing | " |
| mobile `/` | OK | 0 | 0 | proper | 0 missing | " |
| mobile `/projects` | OK | 0 | 0 | proper | 0 missing | " |
| mobile `/voice` | OK | 0 | 0 | proper | 0 missing | " |
| mobile `/workflows` | OK | 0 | 0 | proper | 0 missing | " |
| mobile `/surveys` | OK | 0 | 0 | proper | 0 missing | " |

Checks performed on each route:
- ✅ Page loads (HTTP 200)
- ✅ No console errors
- ✅ No network failures (>500 errors)
- ✅ Body has a background color (not transparent)
- ✅ No horizontal overflow on viewport
- ✅ At least 1 semantic heading (h1)
- ✅ Exactly 1 h1 element on page (where h1 present)
- ✅ All `<img>` tags have `alt` attribute
- ✅ All buttons have accessible name (text/aria-label/title) — **fails** because of Modal close button
- ✅ Main content area is not empty (>30 chars)

---

## Performance observations

| Metric | Value |
|---|---|
| Average page load time | ~1.8s (warm cache) |
| Cold start (lazy views) | <500ms after first nav |
| Console errors across all routes | 0 |
| Network failures | 0 |
| Lazy chunk splits working | ✅ |

---

## Visual evidence

Screenshots captured for every route:

**Desktop (1280×800):**
- `01-home.png` through `13-settings.png`

**Mobile (390×844, iPhone 14 viewport):**
- `mobile-01-home.png`
- `mobile-02-projects.png`
- `mobile-03-voice.png`
- `mobile-08-workflows.png`
- `mobile-09-surveys.png`

**Saved at:** `/workspace/audit-output/` (18 PNG + `report.json`)

---

## Conclusion

The Oscar Platform passes **all non-a11y UX checks**: no crashes, no console errors, no broken layouts, no failed network requests, no empty pages, and correct heading hierarchy.

The one persistent issue is a **single low-severity accessibility gap**: the Mantine `Modal.CloseButton` lacks an `aria-label`. This affects every page because the Onboarding modal is mounted at app start (and 3 other modals are lazy-loaded per route).

**Severity rationale:** Low because:
- It's an icon-only close button, screen readers usually skip it or announce "Close button"
- The button's role (`button`) is announced
- Most users (mouse/touch) see the X icon and understand
- Screen-reader users navigating via Tab will encounter it; announcing "Close" would be ideal

**Fix would be trivial** (one prop on each Modal) but is out of audit scope.

