# UI review: information architecture, usability, and best practices Status: Done — 2026-07-11. All fix-plan items implemented and verified live except C2 (a11y lint enforcement — no ESLint/svelte-check is configured in `web/` at all, so there's nothing to promote from warn to error; flagged below instead of silently adding lint infra). Verification also surfaced an unrelated pre-existing bug (Knowledge page search results never render) — spun off as a separate task, not fixed here. ## Scope Systematic review of `web/src/` (Svelte 5 + shadcn-svelte + Tailwind v4 control-room UI): all 13 pages, the 11 shared components, the sidebar/routing shell (`App.svelte`), and cross-cutting patterns (filtering, loading/empty states, live-event wiring, accessibility). Read in full, not sampled. Grounded in what's actually in the code — no speculative "best practice" items without a concrete file:line instance. Not implementation. Findings and a proposed fix plan only, mirroring [`2026-07-11-nomos-agent-code-review.md`](2026-07-11-nomos-agent-code-review.md)'s structure — implement on a later "proceed." ## Findings ### A. Information architecture **A1. Entity detail has two competing UI patterns for the same content.** [`Entities.svelte:16-19,155`](../../web/src/pages/Entities.svelte) opens entity detail as an in-page `EntitySheet` slide-over (no URL change, no sidebar state change). [`Knowledge.svelte:57-59`](../../web/src/pages/Knowledge.svelte) and [`Graph.svelte:464`](../../web/src/pages/Graph.svelte) instead navigate via `location.hash = '#/entity/' + slug`, which `App.svelte`'s router resolves to a full-page `EntityDetail` route — but `'entity'` isn't in `navItems` ([`App.svelte:68-79`](../../web/src/App.svelte)), so landing there leaves the sidebar with nothing highlighted and the header showing the raw slug instead of a section name. Same underlying view (`EntityDetailContent.svelte`), three different entry points, two different navigation models, one of which produces an orphaned page state. A user who reaches an entity via Knowledge or Graph has no way back to "where they were" via the sidebar — only browser back. **A2. Two chat entry points with no visual link between them.** The sidebar's "Tasks" section (board → `Chat.svelte` detail, `isActive={page === 'tasks' || page === 'chat'}`, [`App.svelte:127`](../../web/src/App.svelte)) and the footer's "Chat drawer" button ([`App.svelte:160-163`](../../web/src/App.svelte), opens a `Sheet` wrapping the same `Chat` component) are both valid, intentional ways to reach chat — but nothing in the UI explains they're different modes (drawer = overlay on current page, keeps your place; Tasks = full navigation). A first-time user has no way to know which one preserves their current page. Low-severity, but worth a tooltip/label distinction. **A3. Overview's KPI cards don't drill down.** [`Overview.svelte`](../../web/src/pages/Overview.svelte) shows "Pending approvals," "Open signals," and fleet-health counts as static cards. The header badges for the same data (`approvalsPending`, `openSignals`, [`App.svelte:185-194`](../../web/src/App.svelte)) ARE clickable and navigate to Ops/Signals — so the pattern exists in the app, just not on the page whose entire purpose is summarizing this data. A dashboard card showing a count that doesn't lead anywhere is a standard drill-down gap. ### B. Usability / interaction consistency **B1. Table-row click targets lack keyboard/screen-reader support in one place but not others.** [`Entities.svelte:117-120`](../../web/src/pages/Entities.svelte) makes an entire `Table.Row` clickable via a bare `onclick`, with no `role`, `tabindex`, or `onkeydown` — unreachable and inoperable via keyboard, and screen readers get no indication the row is interactive. This is a regression against the codebase's own established pattern: `Tasks.svelte` wraps its cards in real `