Phase 5: xlsx/xls/xlsm/csv ingestion, per-cell citations, and XlsxView component
Phase 5 makes spreadsheets first-class documents: upload, extract, cite by cell address, and export with citations baked back into ExcelJS comments on the source cells. The extractors are pure side-effect-free pipelines with their own unit tests, which makes them the most portable piece of this PR.
The backend gets a new backend/src/lib/extractors/ directory with two pure modules: xlsx.ts (ExcelJS, numfmt-formatted display values, formula preservation, merged range handling) and csv.ts (a handwritten RFC 4180 parser). Both are side-effect-free. Ten new Vitest unit tests cover them. .xls files are normalized to .xlsx via libreoffice-convert before extraction - that adds an OS-level LibreOffice dependency to any Docker image.
routes/documents.ts now accepts .xlsx, .xls, .xlsm, and .csv. Spreadsheets skip the PDF conversion pipeline entirely. tools/shared/documentReading.ts gains an xlsx/csv branch that calls the extractor and flattener, and appends cell-address citation guidance to the LLM input when the file type is a spreadsheet. normalizeCitation preserves Sheet!Cell strings in the page field; expandCitationToEntries routes them correctly.
exportToExcel.ts writes per-cell ExcelJS comments containing the citation list on export - citations re-attach to their source cells in the downloaded file, which closes the loop between AI output and the original spreadsheet.
The frontend gets XlsxView.tsx (413 lines): sheet tabs, sticky column-letter header and row-number gutter, a read-only formula bar showing the cell address chip and formula/value, numfmt-formatted display, click-to-select, and citation jump with a 2.5-second yellow highlight.
freeTierGuard.ts is deleted in this PR. It was blocking real documents from reaching free-tier Gemini, defeating its own purpose in a fork that wanted Gemma-default routing. A redesign is parked in CLAUDE.md "Future capabilities" gated on the connector framework, which will clarify what counts as customer data. Gemma 4 31B is added as the new default model across models.ts and ModelToggle.tsx.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?