On a host whose CPU cannot run the page renderer (x64 without AVX2, arm64 musl without LSE/RCpc), every AI read of a PDF for a provider that only takes images failed with "Scanning PDFs needs a Claude vision provider", which says nothing about the cause. Most medical PDFs are produced digitally and carry every word in their text layer, which pdf.js reads without rendering. `prepareVisionInput` now answers a text read (`mode: "text"`) when the renderer is unavailable on this host, the provider cannot take the PDF whole and the PDF has a usable text layer. Summary, suggest and the background summary send that text through their existing text mode; "Read with AI" and the index backfill write it as the local text index it is, with no provider call; the automatic index leaves it to its local path. `planVisionReads` does the same for the extraction and the lab scan: text reads by page group, with anchored provenance, and the pages without a text layer reported as `extractionIncomplete` on a document and as `unreadPages` on a lab scan. A supplement label PDF is read from its text layer too. A scan on such a host now answers `documents.inbound.pdfRendererUnavailable` and `labs.ocr.pdfRendererUnavailable` (422): this server's processor cannot run the page renderer, so a scanned PDF can only be read by a provider that takes PDF files directly, or as photos. The `pdfNeedsAnthropic` codes are no longer emitted, since with a working renderer every provider reads a PDF; the recorded index outcome keeps its `pdf-needs-anthropic` value with corrected copy. The extraction path no longer folds a render failure of one document into the capability error: like `prepareVisionInput` since #776 it reports "The PDF pages couldn't be rendered for scanning." (`extractFailed`), and the lab scan answers `labs.ocr.extractFailed`. Hosts that render, and providers that take PDFs whole, are unchanged. Refs #1311
src/components/
React components. The UI primitives are shadcn/ui (new-york style, zinc base) over Radix + Tailwind 4 + Lucide; everything above the primitives is feature-grouped by surface. Components are server components unless they need a hook, state, or browser API, in which case they open with "use client". Charts use Recharts 3 — replacing the chart library requires explicit approval.
Primitives and shell
ui/— shadcn/ui primitives (button, dialog, sheet, table, …). Generated by the shadcn CLI; keep edits minimal and on-style.layout/— the app shell: sidebar, topbar, bottom nav.providers.tsx,version-poller.tsx— top-level context providers and the self-healing stale-shell poller.i18n/— translation context + locale switcher.
Feature groups
insights/— insight status, assessment cards, Coach surface, advisor cards (the largest group).medications/— medication cards, the schedule wizard, intake timeline, detail history.settings/— the section components behind each settings page.measurements/,mood/,dashboard/— entry forms, lists, dashboard tiles.charts/— Recharts wrappers.monitoring/— Umami / GlitchTip client bootstrap.gamification/,targets/,comparison/,doctor-report/,clinician/,onboarding/,admin/— the remaining per-surface groups.
Conventions
Kebab-case filenames; React component exports stay PascalCase regardless. (A handful of PascalCase-filename outliers under medications/ + onboarding/ are pre-existing drift, noted in ../../CLAUDE.md.) Every t() call needs a matching key in messages/en.json — guarded by i18n-call-site-coverage.test.ts. Data reads go through TanStack Query with keys from @/lib/query-keys and unwrap (await res.json()).data. Tests live in __tests__/.