Files
waggle-os/docs/handoffs/2026-05-27-web-guidelines-review.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

50 KiB
Raw Blame History

Web Interface Guidelines Review — apps/web/src/components/os/apps/

Date: 2026-05-27 · Scope: All 26 OS app surfaces · Source rules: vercel-labs/web-interface-guidelines

~480 findings across 26 files. Most are repeated patterns — a small number of utility/wrapper fixes close hundreds of findings. Read §0 (Systemic patterns) first to plan the work, then §1 (per-file findings) when implementing.


0. Systemic patterns — fix once, close many

These 12 patterns account for ~80% of all findings. Each can be addressed with a single utility/component fix that propagates across the codebase.

# Pattern Approx count Fix once
S-1 Decorative lucide icons missing aria-hidden="true" ~100 Wrap lucide icons in a <Icon hidden /> helper, or add lint rule. Simplest: codemod <X /> (icon-only sibling to text) → <X aria-hidden="true" />
S-2 Icon-only buttons missing aria-label ~40 Audit <Button> usage where children are a single lucide icon; add aria-label prop. Lint rule: jsx-a11y/role-has-required-aria-props
S-3 Loader2 / animate-spin / animate-pulse not honoring prefers-reduced-motion ~25 Add motion-reduce:animate-none to all spinner/pulse classes; OR create a <Spinner /> primitive that applies it
S-4 Hardcoded toLocaleDateString() / toLocaleTimeString() / toLocaleString() / toFixed() instead of Intl.DateTimeFormat + Intl.NumberFormat ~30 Create lib/format.ts with formatDate(d, opts) + formatNumber(n, opts) + formatCurrency(n, ccy) wrappers; codemod replace
S-5 Placeholders + loading states using three-dot ASCII (...) instead of horizontal-ellipsis character () ~30 Codemod find-and-replace across the apps dir. Add ESLint rule no-trailing-three-dots
S-6 Form <Input> / <select> lacking htmlFor/id label association + autocomplete + spellCheck={false} for codes/usernames/secrets ~40 Create <FormField label name autocomplete> wrapper; migrate file-by-file
S-7 Tab buttons missing role="tab" / aria-selected / arrow-key keyboard navigation ~12 (× many buttons each) Create <Tabs /> primitive with the WAI-ARIA tab pattern; replace ad-hoc <button> tab implementations
S-8 Native <select> missing explicit background-color + color for Windows dark mode ~8 Add to global CSS: select { background-color: var(--bg); color: var(--fg); color-scheme: dark light; }
S-9 Modals (3 modals found) missing role="dialog" + aria-modal + focus trap + overscroll-behavior: contain + Escape handler ~3 modals × 5 issues = 15 Create <Modal /> primitive that bundles all 5; replace ad-hoc modal divs
S-10 Destructive actions using confirm() / alert() or no confirmation at all (delete frame, revoke grant, remove integration, etc.) ~10 Replace confirm() with <ConfirmDialog /> primitive; add to destructive-action checklist in PR review
S-11 URL state not reflected for tabs / filters / search query ~8 Adopt nuqs (or similar) for URL-synced state; migrate per-app
S-12 Async updates (loading text, save status, toast result) lacking aria-live="polite" region ~15 Create <StatusRegion /> primitive; migrate per-app. Or: ensure existing toast layer has aria-live on its container

Less systemic but worth a single PR each:

# Pattern Where
S-13 console.error in production paths ChatWindowInstance (×3), AgentsApp (×9), ConnectorsApp (×3)
S-14 Brand names not wrapped in translate="no" (Waggle, Vault, KVARK) Throughout
S-15 <tr> rows used as buttons in FilesApp without role="button" / keyboard activation FilesApp.tsx:492, 531
S-16 Direct fetch without AbortControllersetState after unmount risk DashboardApp, MissionControlApp, CockpitApp, TelemetryApp, BackupApp
S-17 Polling intervals (setInterval) not pausing on document.visibilityState === 'hidden' MissionControlApp (3s), CockpitApp (30s), ApprovalsApp (5s)
S-18 Nested <button> inside <button> (invalid HTML) EventsApp.tsx:84, CapabilitiesApp.tsx:218
S-19 Date.now() / new Date() called during render (hydration mismatch + non-deterministic re-render) RoomApp.tsx:62, ApprovalsApp.tsx:39, EventsApp.tsx:131
S-20 Raw-HTML injection React prop usage (verify escape correctness) MemoryApp.tsx:308
  1. Day 1 — S-1 (icon aria-hidden) + S-2 (icon button aria-label) + S-5 (three-dot → ellipsis char) via codemod. Closes ~170 findings in one PR.
  2. Day 2 — S-3 (motion-reduce) via shared <Spinner /> primitive + S-4 (Intl.* wrappers in lib/format.ts) via codemod. Closes ~55 findings.
  3. Day 3 — S-9 (<Modal /> primitive) + S-10 (<ConfirmDialog />) + S-12 (<StatusRegion />). Closes ~30 findings plus removes confirm()/alert() from the product entirely.
  4. Day 4 — S-7 (<Tabs /> primitive with WAI-ARIA pattern) — propagates to MissionControl, Agents, Memory, UserProfile, Approvals, Connectors, Marketplace, Capabilities, FilesAppTabs. Closes ~12 tab-pattern clusters.
  5. Day 5 — S-6 (<FormField />) for label association + autocomplete + spellCheck. Migrate one app at a time; priority: UserProfile, Vault, Connectors, Settings.
  6. Day 6 — S-13/S-16/S-17 (console.error cleanup + AbortController + visibilityState guards). Touches ~5 apps.
  7. Day 7 — S-11 (URL state via nuqs) per-app. Deep-link enablement.

~7 working days of leverage work closes the bulk of the ~480 findings. Remaining work is genuine per-file polish.


1. Per-file findings

Each section below is reviewed against the full Vercel Web Interface Guidelines ruleset. Format: file:line — issue. Ellipsis character below written as per guideline S-5.

apps/web/src/components/os/apps/SettingsApp.tsx

SettingsApp.tsx:246 — <select> lacks <label> or aria-label ("Dock Experience" is paragraph, not associated)
SettingsApp.tsx:265 — theme button "Dark" lacks aria-label/aria-pressed for toggle state
SettingsApp.tsx:276 — theme button "Light" lacks aria-label/aria-pressed for toggle state
SettingsApp.tsx:307 — telemetry toggle button lacks aria-label, role="switch", aria-checked
SettingsApp.tsx:321 — confirm()/alert() blocks UI; use accessible dialog with focus mgmt
SettingsApp.tsx:394 — Prompt Shape <select> label wraps HintTooltip but onClick from tooltip can fire htmlFor association
SettingsApp.tsx:415 — Daily Budget Input has no htmlFor link to label (label is plain <label> without for)
SettingsApp.tsx:415 — number input lacks inputMode="decimal" and autocomplete
SettingsApp.tsx:415 — placeholder "No limit" lacks example pattern + ellipsis
SettingsApp.tsx:587 — Enterprise <a> lacks visible focus state class
SettingsApp.tsx:656 — Remove button lacks aria-label (icon-less but ambiguous in repeated list)
SettingsApp.tsx:662 — newGate Input lacks label/aria-label
SettingsApp.tsx:691 — Team Server URL Input: label not associated via htmlFor; should be type="url" with autocomplete="url"
SettingsApp.tsx:696 — Auth Token Input: label not associated; password input lacks autocomplete="current-password" / spellCheck={false}
SettingsApp.tsx:728 — export button does direct DOM manipulation + URL.createObjectURL; loading state missing
SettingsApp.tsx:744 — Import Data button has no onClick handler (dead button)
SettingsApp.tsx:762 — alert() instead of inline error UI; not aria-live
SettingsApp.tsx:777 — file input lacks aria-label/visible label
SettingsApp.tsx:780 — confirm() blocks; use accessible dialog
SettingsApp.tsx:790 — alert() success/error not announced (no aria-live)
SettingsApp.tsx:813 — KVARK Server URL: label not associated; missing type="url"/autocomplete
SettingsApp.tsx:818 — API Token: label not associated; missing autocomplete + spellCheck={false}
SettingsApp.tsx:983 — `as any` cast violates type safety
SettingsApp.tsx:996 — <a> with target="_blank" opens server log JSON in tab — should download or be <button>
SettingsApp.tsx:1015 — Save status uses "Saved" not "Saved…"; lacks aria-live="polite"
SettingsApp.tsx:267 — hardcoded HSL colors bypass theme tokens (hive-950 / token violation)
SettingsApp.tsx:184 — setTimeout for saveMsg can leak if component unmounts
SettingsApp.tsx:185 — "Failed" lacks fix/next-step copy
SettingsApp.tsx:422 — "Save Model Settings" loading state lacks ellipsis
SettingsApp.tsx:518 — "Confirming your payment..." uses three-dot ASCII instead of ellipsis char
SettingsApp.tsx:432 — "Vault" brand name should use translate="no"
SettingsApp.tsx:202 — 9 tab buttons in tablist; arrow-key navigation handler missing (roving tabindex partial only)

apps/web/src/components/os/apps/DashboardApp.tsx

DashboardApp.tsx:91 — direct fetch without abort signal; can setState after unmount
DashboardApp.tsx:132 — Brain Health HintTooltip wraps div tabIndex={0} — non-interactive element shouldn't be focusable without role
DashboardApp.tsx:163 — <h2> "Workspaces" is page section; no <h1> in component scope
DashboardApp.tsx:209 — inline style ws.hue lacks safelist for arbitrary HSL — XSS risk if hue unsanitized
DashboardApp.tsx:214 — <AvatarImage> needs alt text for persona avatar
DashboardApp.tsx:225 — HintTooltip wraps span with tabIndex={0} and role="img" — focusable image-only span is awkward
DashboardApp.tsx:262 — new Date(...).toLocaleDateString() without Intl.DateTimeFormat hardcoded locale
DashboardApp.tsx:194 — grid of workspace cards: no virtualization for large workspace counts (>50)
DashboardApp.tsx:199 — workspace card button missing focus-visible:ring class
DashboardApp.tsx:298 — "Create your first workspace" should be Title Case
DashboardApp.tsx:84 — useState/useEffect with no cleanup for fetch
DashboardApp.tsx:147 — {brainScore}% uses tabular-nums but parent should also balance text
DashboardApp.tsx:266 — ChevronRight decorative — needs aria-hidden="true"

apps/web/src/components/os/apps/MissionControlApp.tsx

MissionControlApp.tsx:63 — setInterval polling every 3s without document.visibilityState check (wastes battery/network)
MissionControlApp.tsx:93 — icon-only refresh button missing aria-label
MissionControlApp.tsx:102 — Rocket icon decorative — needs aria-hidden="true"
MissionControlApp.tsx:121 — tabs lack role="tablist"/role="tab"/aria-selected
MissionControlApp.tsx:123 — tab buttons lack keyboard arrow-key navigation
MissionControlApp.tsx:162 — icon-only pause button missing aria-label
MissionControlApp.tsx:167 — icon-only play button missing aria-label
MissionControlApp.tsx:171 — icon-only stop button missing aria-label
MissionControlApp.tsx:171 — destructive stop action lacks confirmation/undo
MissionControlApp.tsx:156 — animate-pulse not honoring prefers-reduced-motion
MissionControlApp.tsx:218 — new Date(...).toLocaleTimeString() hardcoded — use Intl.DateTimeFormat
MissionControlApp.tsx:177 — duration math `Math.round(s.duration ?? 0) / 60` lacks Intl.NumberFormat
MissionControlApp.tsx:180 — tokenUsage .toLocaleString() — should use Intl.NumberFormat with explicit locale
MissionControlApp.tsx:24 — sessions list .map() — no virtualization for >50 fleet sessions
MissionControlApp.tsx:152 — fleet card lacks key focus-visible class

apps/web/src/components/os/apps/CockpitApp.tsx

CockpitApp.tsx:63 — setInterval(30s) polling without visibility check
CockpitApp.tsx:77 — icon-only refresh button missing aria-label
CockpitApp.tsx:84 — AlertTriangle decorative — needs aria-hidden="true"
CockpitApp.tsx:106 — "Uptime: {Math.round(...)}h" lacks Intl.NumberFormat; needs nbsp before unit
CockpitApp.tsx:130 — $cost.toFixed(4) hardcoded — use Intl.NumberFormat currency
CockpitApp.tsx:153 — new Date(...).toLocaleDateString() hardcoded locale
CockpitApp.tsx:163 — decorative icons in cards lack aria-hidden="true" (repeats x7)
CockpitApp.tsx:200 — empty state "No routines" lacks call-to-action
CockpitApp.tsx:221 — empty state "No connectors" lacks CTA
CockpitApp.tsx:236 — Advanced toggle button lacks aria-expanded
CockpitApp.tsx:300 — audit JSON.stringify dumped to <p> — should be <pre> with formatting; truncate may clip critical data
CockpitApp.tsx:78 — RefreshCw animate-spin not honoring prefers-reduced-motion
CockpitApp.tsx:130 — cost numbers lack tabular-nums for column alignment
CockpitApp.tsx:71 — destructive-red default for unknown health status is misleading (treats unknown as error)

apps/web/src/components/os/apps/TelemetryApp.tsx

TelemetryApp.tsx:33 — no AbortController; setState after unmount possible
TelemetryApp.tsx:51 — loading spinner not announced (no aria-live, no aria-busy)
TelemetryApp.tsx:51 — Loader2 animate-spin not honoring prefers-reduced-motion
TelemetryApp.tsx:69 — BarChart3 icon decorative — needs aria-hidden="true"
TelemetryApp.tsx:82 — large numbers .toLocaleString() — use Intl.NumberFormat with explicit locale
TelemetryApp.tsx:89 — $cost.toFixed(4) — use Intl.NumberFormat currency
TelemetryApp.tsx:107 — .map index as React key — use stable workspaceId
TelemetryApp.tsx:113 — progress bar lacks role="progressbar"/aria-valuenow/aria-valuemax
TelemetryApp.tsx:115 — cost numbers lack tabular-nums
TelemetryApp.tsx:130 — .map index as key — use t.name
TelemetryApp.tsx:131 — decorative Zap icon needs aria-hidden="true"
TelemetryApp.tsx:107 — byWorkspace .map() unbounded — no virtualization at >50

apps/web/src/components/os/apps/BackupApp.tsx

BackupApp.tsx:19 — direct fetch w/o AbortController; setState after unmount
BackupApp.tsx:53 — decorative Archive icon needs aria-hidden="true"
BackupApp.tsx:58 — Loader2 animate-spin not honoring prefers-reduced-motion
BackupApp.tsx:59 — "Creating..." uses three-dot ASCII — should use ellipsis char
BackupApp.tsx:63 — lastResult feedback lacks aria-live="polite"; success/error indistinguishable to AT
BackupApp.tsx:82 — .map index as key — use b.timestamp
BackupApp.tsx:87 — new Date(b.timestamp).toLocaleString() hardcoded — use Intl.DateTimeFormat
BackupApp.tsx:93 — "Restore" button has no onClick handler (dead button)
BackupApp.tsx:93 — "Restore" is destructive — needs confirmation/undo
BackupApp.tsx:42 — formatSize hardcoded units — should use Intl.NumberFormat with notation: "compact"
BackupApp.tsx:64 — lastResult.includes('success') string-match brittle
BackupApp.tsx:84 — decorative CheckCircle2 icon needs aria-hidden="true"
BackupApp.tsx:14 — backups list .map() no virtualization for >50 backups

apps/web/src/components/os/apps/EventsApp.tsx

EventsApp.tsx:53 — icon-only abort button (StopCircle) — has text "Cancel" so OK; redundant tooltip
EventsApp.tsx:69 — Loader2 animate-spin not honoring prefers-reduced-motion
EventsApp.tsx:79 — formatTimestamp uses toLocaleTimeString — should be Intl.DateTimeFormat
EventsApp.tsx:84 — nested <button> inside <button> (StepCard expand wraps Cancel button) — invalid HTML/a11y bug
EventsApp.tsx:98 — <pre> JSON.stringify without word break — overflows on long content
EventsApp.tsx:187 — animate-pulse not honoring prefers-reduced-motion
EventsApp.tsx:203 — inline style paddingLeft uses arbitrary computed value — fine but lacks safe area handling
EventsApp.tsx:213 — ChevronRight decorative needs aria-hidden="true"
EventsApp.tsx:216 — Circle decorative icon needs aria-hidden="true"
EventsApp.tsx:233 — formatTimestamp again hardcoded format
EventsApp.tsx:281 — GitBranch decorative needs aria-hidden="true"
EventsApp.tsx:298 — tabs lack role="tablist"/role="tab"/aria-selected/aria-controls
EventsApp.tsx:298 — tab buttons lack arrow-key keyboard navigation
EventsApp.tsx:319 — sidebar lacks <nav>/role="navigation"
EventsApp.tsx:346 — filter pill buttons lack aria-pressed
EventsApp.tsx:363 — autoScroll toggle lacks role="switch"/aria-checked
EventsApp.tsx:418 — filteredSteps.map() unbounded — no virtualization for >50 events
EventsApp.tsx:435 — stepsByTime nested map() unbounded — no virtualization
EventsApp.tsx:131 — rootNode.timestamp falls back to new Date().toISOString() — hydration mismatch risk if SSR
EventsApp.tsx:328 — tooltip text uses straight apostrophe — should use curly quote
EventsApp.tsx:367 — "Auto-scroll ON/OFF" should use semantic switch + visual ON/OFF reflects via aria-checked

apps/web/src/components/os/apps/ChatApp.tsx

ChatApp.tsx:106 — icon button (Code toggle) missing aria-label
ChatApp.tsx:178 — icon button (ThumbsUp) missing aria-label
ChatApp.tsx:186 — icon button (ThumbsDown) missing aria-label
ChatApp.tsx:204 — autoFocus inside menu items applied to every loop iteration; misuse of autoFocus
ChatApp.tsx:262 — "Allow once" not Title Case; use "Allow Once"
ChatApp.tsx:282 — "Show/Hide details" not Title Case
ChatApp.tsx:346 — button missing aria-label / no aria-expanded for popover trigger
ChatApp.tsx:359 — <div onClick> overlay used as backdrop dismiss — not a button/no keyboard handler
ChatApp.tsx:393 — TTL option buttons rely on hover/color only; "15 min" should use &nbsp; (non-breaking)
ChatApp.tsx:678 — "New Session" button uses Title Case OK but icon-only button at 714 missing aria-label
ChatApp.tsx:714 — chevron toggle button missing aria-label (icon-only)
ChatApp.tsx:722 — persona picker button missing aria-haspopup/aria-expanded
ChatApp.tsx:873 — model picker button missing aria-haspopup/aria-expanded
ChatApp.tsx:962 — pin toggle button missing aria-expanded
ChatApp.tsx:975 — PinOff icon button missing aria-label
ChatApp.tsx:986 — shimmer animation not gated by prefers-reduced-motion
ChatApp.tsx:1033 — large messages.map without virtualization (could exceed 50 items in long sessions)
ChatApp.tsx:1035 — onDoubleClick on <div> without keyboard equivalent for context rail
ChatApp.tsx:1067 — copy icon button missing aria-label
ChatApp.tsx:1077 — pin icon button missing aria-label
ChatApp.tsx:1151 — paperclip button missing aria-label
ChatApp.tsx:1154 — textarea lacks <label>/aria-label (placeholder only)
ChatApp.tsx:1159 — placeholder doesn't end with ellipsis char (uses three-dot ASCII)
ChatApp.tsx:1163 — Send icon button missing aria-label
ChatApp.tsx:696 — hardcoded date format (toLocaleDateString without locale options)
ChatApp.tsx:1159 — "Message Waggle..." uses three-dot ASCII and "Waggle" should be wrapped translate="no"
ChatApp.tsx:1166 — submit button has disabled state but no spinner indicator during request
ChatApp.tsx:907 — "Agent Profile" toggle button missing aria-expanded
ChatApp.tsx:282 — "Show/Hide details" — placeholders should be specific; use "Show JSON" / "Hide JSON"
ChatApp.tsx:175 — decorative ToolStatusIcon icons inside ToolCard not marked aria-hidden
ChatApp.tsx:243 — decorative AlertTriangle missing aria-hidden
ChatApp.tsx:1054 — decorative Sparkles missing aria-hidden

apps/web/src/components/os/apps/ChatWindowInstance.tsx

ChatWindowInstance.tsx:91 — useEffect dependency array missing currentPersona (would loop) but eslint react-hooks/exhaustive-deps suppressed silently
ChatWindowInstance.tsx:145 — console.error in production code path
ChatWindowInstance.tsx:172 — console.error in production code path
ChatWindowInstance.tsx:199 — console.error in production code path
ChatWindowInstance.tsx:210 — useEffect deps array empty but uses workspaceId via fetchTeam closure — stale closure risk
ChatWindowInstance.tsx:216 — toast description splits "/" path — display name not localized via Intl

apps/web/src/components/os/apps/AgentsApp.tsx

AgentsApp.tsx:58 — console.error in production code path
AgentsApp.tsx:83 — console.error in production code path
AgentsApp.tsx:92 — console.error in production code path
AgentsApp.tsx:100 — console.error in production code path
AgentsApp.tsx:108 — console.error in production code path
AgentsApp.tsx:117 — console.error in production code path
AgentsApp.tsx:125 — console.error in production code path
AgentsApp.tsx:138 — console.error in production code path
AgentsApp.tsx:146 — console.error in production code path
AgentsApp.tsx:174 — decorative Bot icon missing aria-hidden
AgentsApp.tsx:186 — decorative Bot icon inside tab missing aria-hidden
AgentsApp.tsx:197 — decorative Users icon inside tab missing aria-hidden
AgentsApp.tsx:202 — "New Persona" toggle button missing aria-expanded
AgentsApp.tsx:208 — X icon button (cancel) needs aria-label, currently relies on text adjacent
AgentsApp.tsx:226 — decorative AlertCircle missing aria-hidden
AgentsApp.tsx:228 — "Retry" button has icon + text but icon missing aria-hidden
AgentsApp.tsx:231 — icon-only dismiss button (X) missing aria-label
AgentsApp.tsx:239 — decorative Search icon missing aria-hidden
AgentsApp.tsx:243 — placeholder "Search personas..." uses three-dot ASCII not ellipsis char
AgentsApp.tsx:243 — placeholder "Search groups..." uses three-dot ASCII not ellipsis char
AgentsApp.tsx:240 — Input lacks aria-label / <label>
AgentsApp.tsx:235 — role="tabpanel" missing aria-labelledby tying to active tab
AgentsApp.tsx:182 — tabs lack onKeyDown for arrow-key navigation (WAI-ARIA tab pattern)

apps/web/src/components/os/apps/MemoryApp.tsx

MemoryApp.tsx:122 — decorative Search icon missing aria-hidden
MemoryApp.tsx:123 — Input missing aria-label / <label>
MemoryApp.tsx:126 — placeholder "Search memories..." uses three-dot ASCII not ellipsis char
MemoryApp.tsx:138 — filter toggle button missing aria-expanded
MemoryApp.tsx:152 — decorative frame-type emoji icons should be aria-hidden
MemoryApp.tsx:167 — range input missing aria-label (only visual label sibling without htmlFor)
MemoryApp.tsx:178 — frames.map can exceed 50 items — sidebar should virtualize
MemoryApp.tsx:200 — hardcoded date format (toLocaleDateString without options) — use Intl.DateTimeFormat
MemoryApp.tsx:217 — decorative Loader2 missing aria-hidden
MemoryApp.tsx:223 — decorative Brain icon missing aria-hidden
MemoryApp.tsx:245 — tabs use aria-pressed (button) instead of role="tab"/aria-selected pattern
MemoryApp.tsx:289 — Edit3 icon-only button missing aria-label
MemoryApp.tsx:292 — Trash2 destructive action (delete frame) lacks confirmation or undo
MemoryApp.tsx:303 — toLocaleString called without locale — hardcoded date format
MemoryApp.tsx:308 — raw-HTML injection React prop usage — verify renderSimpleMarkdown escapes correctly (comment states yes, but high-risk surface)
MemoryApp.tsx:285 — decorative frame-type emoji missing aria-hidden
MemoryApp.tsx:321 — decorative Brain icon missing aria-hidden
MemoryApp.tsx:86 — URL doesn't reflect view tab state (deep-link missing)

apps/web/src/components/os/apps/VoiceApp.tsx

VoiceApp.tsx:4 — decorative Mic icon missing aria-hidden
VoiceApp.tsx:7 — "voice commands" — should end with period or follow consistent copy style; minor copy issue

apps/web/src/components/os/apps/UserProfileApp.tsx

UserProfileApp.tsx:84 — useEffect deps array empty but uses adapter.getProfile — should declare or comment intent
UserProfileApp.tsx:174 — any-typed setProfile callback violates avoid-any rule
UserProfileApp.tsx:192 — any-typed setProfile callback violates avoid-any rule
UserProfileApp.tsx:217 — role="tablist" but tab buttons missing onKeyDown arrow handlers
UserProfileApp.tsx:226 — decorative tab icons missing aria-hidden
UserProfileApp.tsx:232 — decorative CheckCircle2 missing aria-hidden
UserProfileApp.tsx:239 — role="tabpanel" missing aria-labelledby/id
UserProfileApp.tsx:254 — decorative Sparkles missing aria-hidden
UserProfileApp.tsx:301 — <label> missing htmlFor + Input missing id (label not associated)
UserProfileApp.tsx:302 — Input missing autoComplete="name" + spellCheck={false} consideration
UserProfileApp.tsx:306 — <label> missing htmlFor + Input missing id (label not associated)
UserProfileApp.tsx:307 — Input missing autoComplete="organization-title"
UserProfileApp.tsx:311 — <label> missing htmlFor + Input missing id
UserProfileApp.tsx:312 — Input missing autoComplete="organization"
UserProfileApp.tsx:316 — <label> missing htmlFor + select missing id
UserProfileApp.tsx:317 — native <select> in dark mode missing explicit background-color/color
UserProfileApp.tsx:326 — <label> missing htmlFor + textarea missing id
UserProfileApp.tsx:327 — placeholder "Brief professional bio..." uses three-dot ASCII
UserProfileApp.tsx:334 — submit-style button stays enabled correctly but no aria-live feedback for saveMsg (line 521)
UserProfileApp.tsx:351 — placeholder "Paste at least 50..." uses three-dot ASCII
UserProfileApp.tsx:356 — decorative Loader2/Sparkles missing aria-hidden
UserProfileApp.tsx:397 — color inputs missing <label htmlFor>
UserProfileApp.tsx:425 — <label> missing htmlFor + Input missing id
UserProfileApp.tsx:430 — <label> missing htmlFor + Input missing id
UserProfileApp.tsx:445 — placeholder "Paste your brand guidelines..." uses three-dot ASCII
UserProfileApp.tsx:501 — <label> missing htmlFor + select missing id
UserProfileApp.tsx:502 — native <select> in dark mode missing explicit background-color/color
UserProfileApp.tsx:521 — saveMsg announcement lacks aria-live="polite"
UserProfileApp.tsx:217 — tabs missing tabIndex management for arrow-key navigation (only static tabIndex set)
UserProfileApp.tsx:140 — switch statement field name "name" overrides closure name shadowing (lint risk; works but error-prone)

apps/web/src/components/os/apps/TimelineApp.tsx

TimelineApp.tsx:48 — hardcoded date format — should use Intl.DateTimeFormat
TimelineApp.tsx:59 — hardcoded date format — should use Intl.DateTimeFormat
TimelineApp.tsx:123 — decorative Clock icon missing aria-hidden
TimelineApp.tsx:130 — cost format uses toFixed instead of Intl.NumberFormat currency
TimelineApp.tsx:149 — decorative Filter icon missing aria-hidden
TimelineApp.tsx:150 — <select> missing <label> / aria-label
TimelineApp.tsx:150 — native <select> missing explicit background-color/color (dark mode)
TimelineApp.tsx:166 — decorative Loader2 missing aria-hidden
TimelineApp.tsx:170 — decorative Clock missing aria-hidden
TimelineApp.tsx:182 — pluralization "event/events" — should use Intl.PluralRules
TimelineApp.tsx:188 — filtered.map may exceed 500 items (limit param) — should virtualize
TimelineApp.tsx:193 — row button missing aria-expanded for expand/collapse state
TimelineApp.tsx:200 — decorative event Icon missing aria-hidden
TimelineApp.tsx:215 — cost format uses toFixed instead of Intl.NumberFormat
TimelineApp.tsx:225 — chevron animation not gated by prefers-reduced-motion (rotate-90 transition-transform)
TimelineApp.tsx:218 — long output truncated with slice(500) but lacks "show more" affordance
TimelineApp.tsx:135 — range filter state not reflected in URL (deep-link missing)

apps/web/src/components/os/apps/ConnectorsApp.tsx

ConnectorsApp.tsx:81 — console.error in production code
ConnectorsApp.tsx:100 — console.error in production code
ConnectorsApp.tsx:108 — console.error in production code
ConnectorsApp.tsx:134 — icon-only Loader2 button area lacks aria-label / aria-busy on loading state
ConnectorsApp.tsx:140 — decorative AlertTriangle icon missing aria-hidden="true"
ConnectorsApp.tsx:144 — decorative RefreshCw icon missing aria-hidden="true"
ConnectorsApp.tsx:161 — decorative Plug icon missing aria-hidden="true"
ConnectorsApp.tsx:171 — decorative Server icon missing aria-hidden="true"
ConnectorsApp.tsx:177 — filter buttons missing role="radio"/aria-pressed + visible focus state
ConnectorsApp.tsx:196 — icon-only refresh button missing aria-label="Refresh connectors"
ConnectorsApp.tsx:203 — decorative Zap icon missing aria-hidden="true"
ConnectorsApp.tsx:231 — expand/collapse button lacks aria-expanded + aria-controls
ConnectorsApp.tsx:248 — decorative CheckCircle2 missing aria-hidden="true"
ConnectorsApp.tsx:249 — decorative ChevronDown/ChevronRight missing aria-hidden="true"
ConnectorsApp.tsx:267 — using array index as React key
ConnectorsApp.tsx:290 — email Input lacks <label>, name, autocomplete="email", type="email"
ConnectorsApp.tsx:294 — token Input lacks <label>, name, autocomplete="off"; password type but spellCheck not disabled
ConnectorsApp.tsx:295 — placeholder doesn't end with ellipsis char
ConnectorsApp.tsx:297 — Connect button while connecting lacks aria-busy / loading state ending in ellipsis char
ConnectorsApp.tsx:299 — decorative Loader2/Plug icons missing aria-hidden="true"
ConnectorsApp.tsx:152-186 — tab buttons missing keyboard arrow navigation typical for role="tablist"
ConnectorsApp.tsx:271 — external link missing visible "(opens in new tab)" cue / decorative ExternalLink missing aria-hidden
ConnectorsApp.tsx:282 — Disconnect destructive action lacks confirmation/undo
ConnectorsApp.tsx:158,168,178,212,232,283,298 — "transition-colors" OK but hover state on buttons missing focus-visible:ring-* class
ConnectorsApp.tsx:184 — filter values "all/connected/available" not reflected in URL (deep-linking)
ConnectorsApp.tsx:265 — empty state for filtered (zero results) not handled

apps/web/src/components/os/apps/MarketplaceApp.tsx

MarketplaceApp.tsx:46 — empty catch swallows error silently
MarketplaceApp.tsx:58 — empty catch swallows error silently
MarketplaceApp.tsx:141 — decorative Store icon missing aria-hidden="true"
MarketplaceApp.tsx:149 — tab buttons missing focus-visible:ring-* and keyboard arrow-nav for role="tablist"
MarketplaceApp.tsx:167 — decorative Search icon missing aria-hidden="true"
MarketplaceApp.tsx:168 — search Input lacks <label> or aria-label, type="search", autocomplete="off"
MarketplaceApp.tsx:171 — placeholder uses three-dot ASCII not ellipsis char
MarketplaceApp.tsx:174 — decorative Loader2 missing aria-hidden="true"
MarketplaceApp.tsx:180 — tabpanel lacks aria-labelledby pointing to active tab
MarketplaceApp.tsx:183 — decorative Loader2 missing aria-hidden="true"
MarketplaceApp.tsx:184 — "Loading packages..." should use ellipsis char
MarketplaceApp.tsx:184 — loading text lacks aria-live="polite"
MarketplaceApp.tsx:190 — decorative Package missing aria-hidden="true"
MarketplaceApp.tsx:192 — straight quotes around query; should use curly quotes
MarketplaceApp.tsx:197 — large list rendered via .map without virtualization (>50 risk)
MarketplaceApp.tsx:202 — decorative Package missing aria-hidden="true"
MarketplaceApp.tsx:207 — decorative CheckCircle2 missing aria-hidden="true"
MarketplaceApp.tsx:218 — destructive "Remove" lacks confirmation/undo
MarketplaceApp.tsx:218 — icon-only sized button labeled "Remove" OK but Trash2 needs aria-hidden
MarketplaceApp.tsx:225 — Install button while installing lacks aria-busy
MarketplaceApp.tsx:230 — decorative Loader2/Download missing aria-hidden="true"
MarketplaceApp.tsx:144 — "{total} packages available" lacks aria-live to announce changes after search

apps/web/src/components/os/apps/CapabilitiesApp.tsx

CapabilitiesApp.tsx:38 — useEffect missing dependency array entries (exhaustive-deps); log.length omitted intentionally but eslint will flag
CapabilitiesApp.tsx:40 — decorative Loader2 missing aria-hidden="true"
CapabilitiesApp.tsx:48 — decorative CheckCircle2/X missing aria-hidden="true"
CapabilitiesApp.tsx:53 — new Date().toLocaleDateString() should use Intl.DateTimeFormat with explicit locale
CapabilitiesApp.tsx:202 — decorative Package missing aria-hidden="true"
CapabilitiesApp.tsx:205 — decorative TrustIcon missing aria-hidden="true"
CapabilitiesApp.tsx:218 — skill chip uses three-dot ASCII; tile is a button-in-button violation (button at 194 wraps buttons at 212)
CapabilitiesApp.tsx:230 — decorative CheckCircle2 missing aria-hidden="true"
CapabilitiesApp.tsx:238 — decorative Loader2/Download missing aria-hidden="true"
CapabilitiesApp.tsx:253 — modal backdrop <div onClick> closes modal — should be <button> or document-level handler; missing role="dialog" + aria-modal
CapabilitiesApp.tsx:253 — missing onKeyDown for Escape to close
CapabilitiesApp.tsx:253 — modal missing focus trap + initial focus management
CapabilitiesApp.tsx:253 — missing overscroll-behavior: contain on modal
CapabilitiesApp.tsx:259 — inner div needs role="document" or stopPropagation works but should also handle keydown
CapabilitiesApp.tsx:264 — decorative Package missing aria-hidden="true"
CapabilitiesApp.tsx:283 — tabIndex={0} on a span — should be <button> if interactive
CapabilitiesApp.tsx:298-305 — button-in-button (if HintTooltip wraps a button)
CapabilitiesApp.tsx:313 — decorative CheckCircle2 missing aria-hidden="true"
CapabilitiesApp.tsx:327 — decorative Download missing aria-hidden="true"
CapabilitiesApp.tsx:341 — icon-only view-mode toggle "grid" missing aria-label and aria-pressed
CapabilitiesApp.tsx:345 — decorative Grid3X3 missing aria-hidden="true"
CapabilitiesApp.tsx:347 — icon-only view-mode toggle "list" missing aria-label and aria-pressed
CapabilitiesApp.tsx:351 — decorative List missing aria-hidden="true"
CapabilitiesApp.tsx:357 — tablist lacks keyboard arrow-key navigation
CapabilitiesApp.tsx:369 — tab buttons lack visible focus-visible:ring-*
CapabilitiesApp.tsx:378 — decorative Store icon missing aria-hidden="true"
CapabilitiesApp.tsx:386 — decorative Search missing aria-hidden="true"
CapabilitiesApp.tsx:387 — search Input lacks <label>/aria-label, type="search", autocomplete="off"
CapabilitiesApp.tsx:390 — placeholder uses three-dot ASCII not ellipsis char
CapabilitiesApp.tsx:397 — decorative Loader2 missing aria-hidden="true"
CapabilitiesApp.tsx:401 — active tab not reflected in URL (deep-linking)
CapabilitiesApp.tsx:413 — decorative Package missing aria-hidden="true"
CapabilitiesApp.tsx:434 — "29 services" — hardcoded number should be derived from connectors data
CapabilitiesApp.tsx:464 — decorative FlaskConical missing aria-hidden="true"
CapabilitiesApp.tsx:467 — close button missing aria-label
CapabilitiesApp.tsx:469 — decorative X missing aria-hidden="true"
CapabilitiesApp.tsx:471 — truncation marker uses three-dot ASCII
CapabilitiesApp.tsx:191 — `(pack.trust)` allows undefined index; falls back fine but type is unsound

apps/web/src/components/os/apps/VaultApp.tsx

VaultApp.tsx:94 — useRef<ReturnType<typeof setTimeout>>() called without initial value (TS2554 in strict mode)
VaultApp.tsx:112 — `as any` cast — use proper type
VaultApp.tsx:211 — decorative Loader2 missing aria-hidden="true"
VaultApp.tsx:217 — tablist with single tab visible — odd UX (only "Secrets")
VaultApp.tsx:225 — decorative Key missing aria-hidden="true"
VaultApp.tsx:241 — decorative Shield missing aria-hidden="true"
VaultApp.tsx:243 — icon-only refresh button missing aria-label
VaultApp.tsx:244 — decorative RefreshCw missing aria-hidden="true"
VaultApp.tsx:248 — decorative Shield missing aria-hidden="true"
VaultApp.tsx:268 — decorative Lock missing aria-hidden="true"
VaultApp.tsx:277 — opacity-0/group-hover reveal pattern hides actions from keyboard users — needs focus-within: opacity-100
VaultApp.tsx:282 — icon-only Pencil button — HintTooltip provides label but explicit aria-label safer
VaultApp.tsx:283 — decorative Pencil missing aria-hidden="true"
VaultApp.tsx:287 — icon-only Reveal/Hide button missing explicit aria-label
VaultApp.tsx:288 — decorative Eye/EyeOff missing aria-hidden="true"
VaultApp.tsx:292 — icon-only Delete destructive button lacks confirmation/undo
VaultApp.tsx:293 — decorative Trash2 missing aria-hidden="true"
VaultApp.tsx:279 — revealed value rendered inline; missing aria-live="polite" announcement on reveal
VaultApp.tsx:310 — secret-name Input lacks <label> / autocomplete="off" / name attribute
VaultApp.tsx:312 — placeholder doesn't end with ellipsis char
VaultApp.tsx:314 — icon-only dropdown toggle missing aria-label / aria-expanded / aria-controls
VaultApp.tsx:316 — decorative ChevronDown missing aria-hidden="true"
VaultApp.tsx:321 — suggestion dropdown lacks role="listbox" + option roles + keyboard nav
VaultApp.tsx:342 — native <select> missing explicit background-color and color for dark mode
VaultApp.tsx:342 — select lacks <label>/aria-label
VaultApp.tsx:353 — decorative User missing aria-hidden="true"
VaultApp.tsx:354 — username Input lacks <label>, autocomplete="username", spellCheck={false}
VaultApp.tsx:354 — placeholder doesn't end with ellipsis char
VaultApp.tsx:360 — password Input lacks <label>, autocomplete="off" / "new-password", spellCheck={false}
VaultApp.tsx:361 — placeholder doesn't end with ellipsis char
VaultApp.tsx:364 — submit button uses disabled-while-empty pattern; "Add to Vault" lacks loading aria-busy
VaultApp.tsx:367 — "Add to Vault"/"Update in Vault" copy good; loading state should append ellipsis char
VaultApp.tsx:163 — reveal timer state mutation race if component unmounts (no cleanup on unmount)

apps/web/src/components/os/apps/FilesApp.tsx

FilesApp.tsx:39 — divider div used as h-px bar — fine but heading <h4> at 41 lacks scroll-margin-top
FilesApp.tsx:47 — new Date().toLocaleDateString() — use Intl.DateTimeFormat with explicit locale
FilesApp.tsx:387 — "Server unreachable" banner missing role="status" / aria-live="polite"
FilesApp.tsx:452 — decorative Folder icon missing aria-hidden="true"
FilesApp.tsx:453 — <input> rename/new-folder field lacks <label>/aria-label
FilesApp.tsx:457 — placeholder "New folder name..." should use ellipsis char
FilesApp.tsx:459 — autoFocus usage acceptable here (single primary input in dialog)
FilesApp.tsx:472 — decorative Folder missing aria-hidden="true"
FilesApp.tsx:492 — <tr> with onClick — table row is not a button; keyboard activation broken (no Enter/Space handlers, no role="button"/tabIndex)
FilesApp.tsx:497 — inline ternary returns a value but isn't used; comma side-effect pattern flagged by ESLint
FilesApp.tsx:503 — decorative Icon missing aria-hidden="true"
FilesApp.tsx:505 — rename <input> missing <label>/aria-label, autocomplete="off", spellCheck={false}
FilesApp.tsx:518 — sizes & dates not using tabular-nums (font-variant-numeric)
FilesApp.tsx:519 — new Date().toLocaleDateString() — use Intl.DateTimeFormat
FilesApp.tsx:526 — grid view large lists may need virtualization >50
FilesApp.tsx:531 — grid <button> contains draggable + onDoubleClick; button doesn't natively get double-click semantics
FilesApp.tsx:536 — same comma-side-effect ternary issue
FilesApp.tsx:541 — decorative Icon missing aria-hidden="true"
FilesApp.tsx:561 — decorative CheckSquare missing aria-hidden="true"
FilesApp.tsx:579 — decorative XSquare missing aria-hidden="true"
FilesApp.tsx:599 — modal backdrop <motion.div onClick> closes — missing role="dialog" + aria-modal + aria-labelledby
FilesApp.tsx:599 — missing overscroll-behavior: contain
FilesApp.tsx:603 — close button missing aria-label
FilesApp.tsx:603 — decorative XIcon missing aria-hidden="true"
FilesApp.tsx:606 — decorative Folder missing aria-hidden="true"
FilesApp.tsx:611 — decorative Folder missing aria-hidden="true"
FilesApp.tsx:613 — paths use font-mono — should be translate="no"
FilesApp.tsx:640 — context-menu lacks role="menu" + role="menuitem" semantics + arrow-key navigation
FilesApp.tsx:644-660 — menu item icons missing aria-hidden="true"
FilesApp.tsx:651 — destructive Delete in context menu lacks confirmation
FilesApp.tsx:670 — properties dialog missing role="dialog"/aria-modal/aria-labelledby
FilesApp.tsx:679 — path display missing translate="no"
FilesApp.tsx:681 — close button missing aria-label
FilesApp.tsx:687 — field icons missing aria-hidden="true" throughout
FilesApp.tsx:698-699 — new Date().toLocaleString() — use Intl.DateTimeFormat
FilesApp.tsx:708 — path display missing translate="no"
FilesApp.tsx:494 — draggable row lacks aria-grabbed/aria-dropeffect alternatives and screen-reader keyboard DnD alternative
FilesApp.tsx:339-342 — drag handlers don't disable text selection or set inert on dragged element
FilesApp.tsx:526 — hard-coded grid-cols-4 doesn't adapt to width
FilesApp.tsx:283 — keyboard handler handleBulkDelete destructive without confirmation
FilesApp.tsx:496 — onClick on <tr> uses ctrlKey/metaKey but no shiftKey range select
FilesApp.tsx:587 — status bar count not announced via aria-live for selection changes
FilesApp.tsx:690 — "({propertiesFile.size.toLocaleString()} bytes)" — Intl.NumberFormat
FilesApp.tsx:445 — hidden file input lacks accessible name (id+label)

apps/web/src/components/os/apps/FilesAppTabs.tsx

FilesAppTabs.tsx:48 — tablist missing aria-orientation="horizontal" (default) and roving tabindex/keyboard arrow navigation
FilesAppTabs.tsx:57 — tab buttons lack tabIndex={active ? 0 : -1} for roving tabindex pattern
FilesAppTabs.tsx:57 — tab buttons lack focus-visible:ring-* class (no visible focus state)
FilesAppTabs.tsx:71 — decorative Icon missing aria-hidden="true"
FilesAppTabs.tsx:40 — active tab not synced to URL (deep-linking missing for tab state)

apps/web/src/components/os/apps/LauncherApp.tsx

LauncherApp.tsx:282 — icon button (refresh) missing aria-label
LauncherApp.tsx:283 — decorative RefreshCw icon needs aria-hidden="true"
LauncherApp.tsx:289-307 — status banners (error/lastResult) should have aria-live="polite" for async updates
LauncherApp.tsx:320 — textarea lacks <label> or aria-label (only adjacent text label)
LauncherApp.tsx:320 — textarea missing spellCheck attribute for prompt content
LauncherApp.tsx:361 — loading state "Detecting installed tools…" good but missing aria-live region
LauncherApp.tsx:441 — title attr used for Stop button hint; should also have aria-label for screen readers
LauncherApp.tsx:282,425,439,452,467,482 — buttons rely on shadcn focus-visible defaults (verify component provides)
LauncherApp.tsx:393 — animate-pulse needs to respect prefers-reduced-motion
LauncherApp.tsx:23 — Loader2 spin animation needs prefers-reduced-motion guard
LauncherApp.tsx:323 — placeholder "Paste a task or question. Leave blank to launch the tool bare." doesn't end with ellipsis char

apps/web/src/components/os/apps/WaggleDanceApp.tsx

WaggleDanceApp.tsx:54 — icon-only refresh button missing aria-label
WaggleDanceApp.tsx:55 — decorative RefreshCw icon needs aria-hidden="true"
WaggleDanceApp.tsx:65 — filter buttons missing aria-pressed for toggle state
WaggleDanceApp.tsx:84 — "Loading signals…" lacks aria-live="polite"
WaggleDanceApp.tsx:98 — signal button (text "button") works, but selected state needs aria-pressed/aria-current
WaggleDanceApp.tsx:126 — `toLocaleTimeString([], ...)` — should use Intl.DateTimeFormat
WaggleDanceApp.tsx:162 — `new Date(...).toLocaleString()` should use Intl.DateTimeFormat
WaggleDanceApp.tsx:106 — Icon (decorative) missing aria-hidden="true"
WaggleDanceApp.tsx:168 — <pre> JSON dump can be very long; consider scroll region with aria-label
WaggleDanceApp.tsx:65,98 — filter/list buttons lack visible focus-visible:ring styles
WaggleDanceApp.tsx:79 — two-pane layout uses fixed w-1/2 without min-w-0 on children → flex truncation risk

apps/web/src/components/os/apps/RoomApp.tsx

RoomApp.tsx:23 — decorative lucide icons need aria-hidden="true" throughout
RoomApp.tsx:85 — animate-pulse on status dot needs prefers-reduced-motion guard
RoomApp.tsx:128 — animate-spin needs prefers-reduced-motion guard
RoomApp.tsx:62 — `Date.now()` inside formatElapsed called during render → hydration mismatch risk + non-deterministic re-renders
RoomApp.tsx:211 — "Recently completed" button uses ▼/▶ Unicode arrows — missing aria-expanded for collapsible
RoomApp.tsx:211 — collapsible toggle button missing aria-controls referencing the panel
RoomApp.tsx:163 — h3 heading hierarchy starts at h3 — verify parent surfaces an h1/h2; otherwise non-hierarchical
RoomApp.tsx:178 — main scrollable region lacks aria-label / role
RoomApp.tsx:211 — button uses outline-none default; relies on browser focus ring — should add focus-visible:ring-*

apps/web/src/components/os/apps/ApprovalsApp.tsx

ApprovalsApp.tsx:126 — `confirm(...)` browser dialog — destructive action needs custom modal with proper focus management
ApprovalsApp.tsx:146,155 — tab buttons missing role="tab"/aria-selected for tablist semantics
ApprovalsApp.tsx:178,233 — tab panels missing role="tabpanel" / aria-labelledby
ApprovalsApp.tsx:166 — icon-only refresh button missing aria-label (HintTooltip provides visual but not accessible name)
ApprovalsApp.tsx:171 — decorative RefreshCw icon needs aria-hidden="true"
ApprovalsApp.tsx:171 — animate-spin needs prefers-reduced-motion guard
ApprovalsApp.tsx:273 — revoke icon-only button missing aria-label (only HintTooltip)
ApprovalsApp.tsx:179,235 — pending/grants tab panels should announce updates via aria-live="polite"
ApprovalsApp.tsx:39 — `formatRelative` reads Date.now() during render → hydration mismatch / stale UI; no minute-tick refresh
ApprovalsApp.tsx:146,155 — tab buttons rely on browser focus only; need focus-visible:ring
ApprovalsApp.tsx:90 — 5s polling interval on pending — consider visibility/idle pause
ApprovalsApp.tsx:250 — "Revoke all" destructive action style is text-only — should be a clear button
ApprovalsApp.tsx:111,121,131 — error toast generic ("Failed to send response") — should include fix/next step

apps/web/src/components/os/apps/ScheduledJobsApp.tsx

ScheduledJobsApp.tsx:126 — decorative Clock icon missing aria-hidden="true"
ScheduledJobsApp.tsx:130 — "New" button has icon + text label (OK), but icon needs aria-hidden
ScheduledJobsApp.tsx:141 — Loader2 animate-spin needs prefers-reduced-motion guard
ScheduledJobsApp.tsx:156,167,186 — labels not associated with inputs via htmlFor/id (visual labels only)
ScheduledJobsApp.tsx:168 — native <select> missing explicit color-scheme/background for dark mode
ScheduledJobsApp.tsx:188 — same: native <select> needs explicit background-color and color for dark mode
ScheduledJobsApp.tsx:158 — "Nightly memory consolidation" placeholder doesn't end with ellipsis char
ScheduledJobsApp.tsx:231,246,255 — icon-only buttons rely on HintTooltip only — need aria-label for screen readers
ScheduledJobsApp.tsx:243 — `new Date(job.lastRun).toLocaleDateString()` should use Intl.DateTimeFormat
ScheduledJobsApp.tsx:252 — Loader2 animate-spin needs prefers-reduced-motion guard
ScheduledJobsApp.tsx:127 — h2 heading — verify parent h1 exists for hierarchy
ScheduledJobsApp.tsx:159,200 — Input components rely on shadcn defaults — verify autocomplete attribute presence
ScheduledJobsApp.tsx:130,232,247,256 — buttons lack focus-visible:ring (rely on browser default)
ScheduledJobsApp.tsx:88,97,116 — error toasts lack remediation guidance
ScheduledJobsApp.tsx:128 — "{jobs.length} jobs" — should be "1 job" / "n jobs" pluralization

apps/web/src/components/os/apps/TeamGovernanceApp.tsx

TeamGovernanceApp.tsx:7-13 — decorative Shield/Lock icons missing aria-hidden="true"
TeamGovernanceApp.tsx:17,23,30 — decorative Users/Shield/Crown icons missing aria-hidden="true"
TeamGovernanceApp.tsx:38 — "$49/mo" hardcoded format — should use Intl.NumberFormat for currency and non-breaking space "$49&nbsp;/mo per seat"
TeamGovernanceApp.tsx:10 — h2 heading — verify parent h1 exists
TeamGovernanceApp.tsx:38 — upgrade hint text only — no CTA button to upgrade (missed engagement)
TeamGovernanceApp.tsx:5 — empty state has no skip/back link; primarily decorative gate screen

2. Out-of-scope / acceptable patterns (don't action)

  • autoFocus on FilesApp.tsx:459 (rename / new-folder dialog) and ScheduledJobsApp.tsx:162 (job-create form first input) — single primary input in a dialog, desktop-only context, explicitly allowed by guideline.
  • EventsApp.tsx:53 StopCircle "Cancel" button — has visible text; redundant tooltip is acceptable.
  • RoomApp.tsx:166 " · " mid-dot separator — fine, not a copy violation.
  • Per-app transition-colors use — list-only transition (not transition: all), guideline-compliant.
  • LauncherApp.tsx:404 truncation with title attr on path — min-w-0 parent present, no finding.

3. Verification gates (run after each fix PR)

  1. npm run lint — confirm no new ESLint errors.
  2. npx tsc --noEmit --project apps/web/tsconfig.json — confirm types still clean.
  3. npm run test -- --run apps/web — confirm unit tests still pass.
  4. npm run test:visual — confirm visual regression snapshots still pass (or update intentionally).
  5. Manual a11y spot-check on the touched app: tab through with keyboard only, verify focus visible at every stop. Use Chrome DevTools → Accessibility Tree to spot-check aria-* propagation.

4. Suggested PR breakdown (matches §0 fix order)

PR Title Files Approx findings closed
PR-A chore(a11y): aria-hidden + aria-label codemod ~26 app files ~140
PR-B chore(copy): ellipsis codemod (three-dot → single char) ~12 app files ~30
PR-C feat(ui): <Spinner /> primitive with motion-reduce + migrate ~10 app files ~25
PR-D feat(lib): Intl wrappers in lib/format.ts + codemod ~12 app files ~30
PR-E feat(ui): <Modal /> + <ConfirmDialog /> + <StatusRegion /> + migrate ~6 app files ~30
PR-F feat(ui): <Tabs /> WAI-ARIA primitive + migrate ~9 app files ~12 tab clusters
PR-G feat(ui): <FormField /> wrapper + migrate UserProfile, Vault, Connectors, Settings, FilesApp ~40
PR-H fix: console.error cleanup + AbortController + visibilityState AgentsApp, ChatWindowInstance, ConnectorsApp, Dashboard, MissionControl, Cockpit, Telemetry, Backup, Approvals ~30
PR-I feat: URL state via nuqs for tabs/filters/search per-app ~8
PR-J fix: nested button + Date.now()-in-render + raw-HTML escape audit EventsApp, CapabilitiesApp, RoomApp, ApprovalsApp, MemoryApp ~10

~7 working days. Closes ~360 of ~480 findings (75%). Remaining 25% is genuine per-file polish.


Generated by web-design-guidelines skill review on 2026-05-27 against HEAD 9902906.