diff --git a/CHANGELOG.md b/CHANGELOG.md index 0457aef8a..585c9f2ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +## [1.28.22] — 2026-07-10 — Full sync survives dense histories; quieter document chrome + +A full Google Health sync no longer fails on sample-dense accounts. Collecting +the rollup bookkeeping for a multi-year heart-rate history overflowed the call +stack, which aborted the metrics pass mid-cycle — exactly on the accounts with +the most data. Reported by a self-hoster whose sync log made the diagnosis +immediate; the new sync diagnostics from the previous release confirmed every +other read on that account healthy. + +The documents vault gets quieter chrome: the "read by AI" tag no longer takes +its own line on each card — a small glyph rides inline on the date-and-size row +instead. The document view drops the status bar under the preview entirely; +when reading is automatic and nothing needs review, the content starts straight +with the document's fields. + ## [1.28.21] — 2026-07-10 — Sleep history heals itself; plateau context; sync diagnostics Three follow-ups to the recent sleep fix, plus better Google Health diagnostics. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index e4f0d512a..e53b7cc15 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.28.21 + version: 1.28.22 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/e2e/documents-ai.spec.ts b/e2e/documents-ai.spec.ts index 37a97ef0f..f027d7a36 100644 --- a/e2e/documents-ai.spec.ts +++ b/e2e/documents-ai.spec.ts @@ -345,10 +345,6 @@ test.describe("document vault — AI assist + content search", () => { await expect(read).toBeVisible(); await expect(read).toHaveText(/Read with AI/); - // The status pill starts at the calm "not searchable yet". - const status = sheet.locator('[data-slot="content-search-status"]'); - await expect(status).toHaveAttribute("data-state", "none"); - await read.click(); await expect(page.getByText("The AI read your document.")).toBeVisible(); expect(indexCalls).toBe(1); @@ -366,11 +362,13 @@ test.describe("document vault — AI assist + content search", () => { const sheet = page.getByRole("dialog"); await expect(sheet).toBeVisible(); - // The detail status pill reflects the AI-read source — asserted on every - // project (this is the provenance contract; it holds on desktop + mobile). - const status = sheet.locator('[data-slot="content-search-status"]'); - await expect(status).toHaveAttribute("data-state", "ai-read"); - await expect(status).toHaveText(/Read by AI/); + // v1.28.22 — the provenance marker moved out of the sheet onto the vault + // card's meta row (an inline glyph beside date · size). The sheet carries + // no status chrome; the CARD is the provenance contract now. + const marker = page.locator( + `[data-document-id="${CONTENT_DOC_ID}"] [data-slot="document-card-ai-read"]`, + ); + await expect(marker.first()).toBeAttached(); }); // ── (f) Text-mode refuses a non-image before any OCR/upload ────────────── diff --git a/e2e/documents-auto-read.spec.ts b/e2e/documents-auto-read.spec.ts index 702258ada..b7b67deba 100644 --- a/e2e/documents-auto-read.spec.ts +++ b/e2e/documents-auto-read.spec.ts @@ -252,19 +252,21 @@ test.describe("automatic AI reading — vault per-document contract", () => { page, }) => { // Auto-read reads every upload with no per-document tap, so the manual - // Read / Suggest actions are redundant and collapse away. The section - // header and the searchable status pill stay. + // Read / Suggest actions are redundant and the WHOLE block collapses + // (v1.28.22 — no status chrome; provenance lives on the vault card). await mockDocumentCapability(page, { egress: "external", autoRead: true }); await page.goto(`/documents?doc=${AI_PROBE_DOC_ID}`); const sheet = page.getByRole("dialog"); await expect(sheet).toBeVisible(); - const section = sheet.locator('[data-slot="document-ai-section"]'); - await expect(section).toBeVisible(); + // v1.28.22 — with auto-read ON and nothing pending the WHOLE block + // collapses (no status chrome; provenance lives on the vault card), so the + // sheet content starts straight with the document fields. + await expect(sheet.locator("form")).toBeVisible(); await expect( - section.locator('[data-slot="content-search-status"]'), - ).toBeVisible(); + sheet.locator('[data-slot="document-ai-section"]'), + ).toHaveCount(0); await expect(sheet.locator('[data-slot="document-read-ai"]')).toHaveCount( 0, ); diff --git a/messages/de.json b/messages/de.json index 68ef6f2ce..036e7e84a 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8325,9 +8325,6 @@ }, "ai": { "statusAiRead": "Von KI gelesen", - "statusSearchable": "Durchsuchbar", - "statusIndexing": "Wird durchsuchbar gemacht…", - "statusNotYet": "Noch nicht durchsuchbar", "readDone": "Die KI hat dein Dokument gelesen.", "read": "Mit KI auslesen", "reread": "Erneut lesen", diff --git a/messages/en.json b/messages/en.json index ae42d7fdc..f4ec0ff34 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8325,9 +8325,6 @@ }, "ai": { "statusAiRead": "Read by AI", - "statusSearchable": "Searchable", - "statusIndexing": "Making searchable…", - "statusNotYet": "Not searchable yet", "readDone": "The AI read your document.", "read": "Read with AI", "reread": "Read again", diff --git a/messages/es.json b/messages/es.json index ef1762112..ce059ab9b 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8325,9 +8325,6 @@ }, "ai": { "statusAiRead": "Leído por IA", - "statusSearchable": "Buscable", - "statusIndexing": "Haciéndolo buscable…", - "statusNotYet": "Aún no buscable", "readDone": "La IA leyó tu documento.", "read": "Leer con IA", "reread": "Leer de nuevo", diff --git a/messages/fr.json b/messages/fr.json index 505c3c1f0..4e00eb909 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8325,9 +8325,6 @@ }, "ai": { "statusAiRead": "Lu par l’IA", - "statusSearchable": "Consultable", - "statusIndexing": "Rendu consultable…", - "statusNotYet": "Pas encore consultable", "readDone": "L’IA a lu votre document.", "read": "Lire avec l’IA", "reread": "Relire", diff --git a/messages/it.json b/messages/it.json index 41d9e6c1a..a1b859871 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8325,9 +8325,6 @@ }, "ai": { "statusAiRead": "Letto dall’IA", - "statusSearchable": "Ricercabile", - "statusIndexing": "Rendo ricercabile…", - "statusNotYet": "Non ancora ricercabile", "readDone": "L’IA ha letto il tuo documento.", "read": "Leggi con l’IA", "reread": "Leggi di nuovo", diff --git a/messages/pl.json b/messages/pl.json index 5a4ea06a4..2e5596471 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8325,9 +8325,6 @@ }, "ai": { "statusAiRead": "Odczytane przez AI", - "statusSearchable": "Wyszukiwalne", - "statusIndexing": "Udostępnianie do wyszukiwania…", - "statusNotYet": "Jeszcze nie do wyszukania", "readDone": "AI odczytało Twój dokument.", "read": "Odczytaj z AI", "reread": "Odczytaj ponownie", diff --git a/package.json b/package.json index 9cd9c5c6b..4f021210b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.28.21", + "version": "1.28.22", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index 5722ae20f..4e0974795 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.28.21"; + /* @sw-version-fallback */ "v1.28.22"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/components/documents/__tests__/document-ai-panels.test.tsx b/src/components/documents/__tests__/document-ai-panels.test.tsx index fcdd6757e..505a1a4cd 100644 --- a/src/components/documents/__tests__/document-ai-panels.test.tsx +++ b/src/components/documents/__tests__/document-ai-panels.test.tsx @@ -6,7 +6,6 @@ import type { DocumentSuggestionDto } from "@/lib/validations/inbound-documents" import { AiUnavailableHint, AssistSuggestionReview, - ContentSearchStatus, DocumentSummaryPanel, } from "../document-ai-panels"; @@ -172,48 +171,3 @@ describe("", () => { expect(html).toContain("read the document"); }); }); - -describe("", () => { - it("shows a calm not-searchable-yet pill before the auto-index lands", () => { - const html = render( - , - ); - expect(html).toContain('data-slot="content-search-status"'); - expect(html).toContain('data-state="none"'); - expect(html).toContain("Not searchable yet"); - // A status, never a chore button. - expect(html).not.toContain(" { - const aiRead = render( - , - ); - expect(aiRead).toContain('data-state="ai-read"'); - expect(aiRead).toContain("Read by AI"); - expect(aiRead).toContain("text-primary"); - - const local = render( - , - ); - expect(local).toContain('data-state="searchable"'); - expect(local).toContain("Searchable"); - expect(local).not.toContain("Read by AI"); - }); - - it("reflects a running read as an indexing state", () => { - const html = render( - , - ); - expect(html).toContain('data-state="indexing"'); - expect(html).toContain("Making searchable…"); - }); -}); diff --git a/src/components/documents/__tests__/document-ai-section.test.tsx b/src/components/documents/__tests__/document-ai-section.test.tsx index 87955d85c..e8768e73d 100644 --- a/src/components/documents/__tests__/document-ai-section.test.tsx +++ b/src/components/documents/__tests__/document-ai-section.test.tsx @@ -80,9 +80,9 @@ describe("", () => { ); expect(html).toContain('data-slot="document-read-ai"'); expect(html).toContain("Read again"); - // The status pill reflects the AI-read provenance. - expect(html).toContain('data-state="ai-read"'); - expect(html).toContain("Read by AI"); + // v1.28.22 — no status pill any more; the re-read label alone carries the + // provenance in this block (the marker lives on the vault card). + expect(html).not.toContain('data-state="ai-read"'); }); it("shows the calm settings pointer (no 422 actions) when no provider is available", () => { @@ -98,8 +98,9 @@ describe("", () => { // Every AI action must be absent — never a 422-guaranteed tap. expect(html).not.toContain('data-slot="assist-suggest"'); expect(html).not.toContain('data-slot="document-read-ai"'); - // But the searchable status pill still renders — auto-index is honest here. - expect(html).toContain('data-slot="content-search-status"'); + // v1.28.22 — the status pill is gone entirely (provenance lives on the + // vault card's meta row); only the calm hint renders here. + expect(html).not.toContain('data-slot="content-search-status"'); }); it("keeps a locally-indexed document honestly searchable without a provider", () => { @@ -112,8 +113,10 @@ describe("", () => { contentIndexSource: "local-pdf", }), ); - expect(html).toContain('data-state="searchable"'); - expect(html).toContain("Searchable"); + // v1.28.22 — the searchable pill is gone; only the calm provider hint + // renders for a locally-indexed document without a provider. + expect(html).not.toContain('data-state="searchable"'); + expect(html).toContain('data-slot="assist-unavailable"'); expect(html).not.toContain("Read by AI"); }); @@ -163,11 +166,10 @@ describe("", () => { expect(html).not.toContain('data-slot="assist-suggest"'); expect(html).not.toContain("Summarise"); expect(html).not.toContain("Show extracted text"); - // The header and the searchable status pill stay. The scoped chat entry - // now lives in the detail-sheet header (a neutral Coach icon), not in this - // section, so this block no longer owns a chat slot. - expect(html).toContain('data-slot="document-ai-section"'); - expect(html).toContain('data-slot="content-search-status"'); + // v1.28.22 — with auto-read ON and nothing pending the WHOLE block + // collapses: no chrome, no status pill; the sheet content starts straight + // with the document fields under the preview. + expect(html).toBe(""); }); it("shows the manual AI action row when auto-read is off", () => { diff --git a/src/components/documents/document-ai-panels.tsx b/src/components/documents/document-ai-panels.tsx index 28e764770..8f6ab97f9 100644 --- a/src/components/documents/document-ai-panels.tsx +++ b/src/components/documents/document-ai-panels.tsx @@ -12,12 +12,8 @@ * user applies it (and the title lands in the editable field, not on disk). * - `DocumentSummaryPanel` — the transient summary / extracted-text panel with * the persistent "not saved · not a diagnosis" note. - * - `ContentSearchStatus` — the per-document searchable pill reflecting the - * auto-index: "Read by AI" (provider read the original), "Searchable" - * (locally indexed), "Making searchable…" (a read is running), or the calm - * "not searchable yet". A STATUS, never a chore button. */ -import { Loader2, ScanSearch, Sparkles } from "lucide-react"; +import { Sparkles } from "lucide-react"; import { Check, FileText, X } from "lucide-react"; import Link from "next/link"; @@ -26,8 +22,6 @@ import { Skeleton } from "@/components/ui/skeleton"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import { - isAiReadSource, - type DocumentContentIndexSourceValue, type DocumentSuggestionDto, type DocumentSummaryMode, } from "@/lib/validations/inbound-documents"; @@ -270,77 +264,3 @@ export function DocumentSummaryPanel({ ); } - -/** - * The per-document searchable status pill. Reflects the auto-index (indexing is - * automatic on upload, so this is a state, never a to-do): a document is "Read - * by AI" when a provider read the original, "Searchable" when it is only locally - * indexed, "Making searchable…" while a read runs, and a calm "not searchable - * yet" otherwise. The AI-read pill is highlighted (primary) — that read is the - * richer one and worth surfacing. - */ -export function ContentSearchStatus({ - hasContentIndex, - source, - isPending, -}: { - hasContentIndex: boolean; - source: DocumentContentIndexSourceValue | null; - /** A read/index is running right now. */ - isPending: boolean; -}) { - const { t } = useTranslations(); - - if (isPending) { - return ( - - - {t("documents.ai.statusIndexing")} - - ); - } - - if (!hasContentIndex) { - return ( - - - {t("documents.ai.statusNotYet")} - - ); - } - - if (isAiReadSource(source)) { - return ( - - - {t("documents.ai.statusAiRead")} - - ); - } - - return ( - - - {t("documents.ai.statusSearchable")} - - ); -} diff --git a/src/components/documents/document-ai-section.tsx b/src/components/documents/document-ai-section.tsx index 1810b2f74..7497ae834 100644 --- a/src/components/documents/document-ai-section.tsx +++ b/src/components/documents/document-ai-section.tsx @@ -7,8 +7,9 @@ * are pinned by static-render tests. * * Indexing is AUTOMATIC on upload, so this block never nags a "please index" - * chore. It shows the document's searchable status (auto-indexed) as a pill, and - * offers AI reading as a capability the user reaches for when they want more: + * chore. It carries NO header or status chrome (the AI-read provenance marker + * lives on the vault card's meta row) and offers AI reading as a capability the + * user reaches for when they want more: * * - "Read with AI" (prominent) runs a provider read of the document — the * richer pass that also refreshes the searchable index. Labelled "Read @@ -16,11 +17,13 @@ * - "Suggest details" drafts a title / type / date to review before saving. * - "Summarise" / "Show text" surface a transient, session-only read-out. * + * With auto-read ON and nothing pending to review the block collapses entirely — + * the sheet content starts straight with the document fields under the preview. + * * Presentational: the sheet owns the mutations + capability probe and passes * their state + handlers down. When `aiEnabled` is false the actions collapse to - * the calm "set up an AI provider" pointer (never an error) — the searchable - * status pill stays, honestly reflecting any local auto-index, and the manual - * form below stays fully usable. + * the calm "set up an AI provider" pointer (never an error) and the manual form + * below stays fully usable. */ import { FileText, ScanText, WandSparkles } from "lucide-react"; @@ -37,7 +40,6 @@ import { import { AiUnavailableHint, AssistSuggestionReview, - ContentSearchStatus, DocumentSummaryPanel, } from "./document-ai-panels"; import type { DocumentDescribeResult } from "./use-document-assist"; @@ -113,23 +115,26 @@ export function DocumentAiSection({ ? t("documents.ai.reread") : t("documents.ai.read"); + // v1.28.22 — the block carries NO header/status chrome any more (the + // provenance pill moved to the vault card's meta row; auto-read makes an + // in-sheet "read by AI" banner redundant). With auto-read ON and nothing + // pending to review, nothing below would render — collapse the whole box so + // the content under the preview starts straight with the fields. + if ( + aiEnabled && + autoReadEnabled && + !suggestErrorKey && + !suggestion && + !summaryOutput + ) { + return null; + } + return (
- {/* v1.28.19 — the "read with AI" block title + wand were redundant with - the read-provenance pill (auto-read already surfaces "read by AI"), so - only the provenance pill leads the block now. The pill itself stays — - it is the read-state contract the recipient/owner relies on. */} -
- -
- {aiEnabled ? ( <> {!autoReadEnabled ? ( diff --git a/src/components/documents/document-card.tsx b/src/components/documents/document-card.tsx index 65462b344..b4c37bf91 100644 --- a/src/components/documents/document-card.tsx +++ b/src/components/documents/document-card.tsx @@ -133,22 +133,22 @@ export function DocumentCard({ )}

{title}

-

- {date} · {size} - {showFilename ? ` · ${document.filename}` : ""} -

- {aiRead ? ( - // Discreet AI-read indicator — same visual as the detail sheet's - // ContentSearchStatus ai-read pill (bg-primary/10 text-primary, - // Sparkles). Kept compact for the dense grid card. - - - {t("documents.ai.statusAiRead")} +

+ + {date} · {size} + {showFilename ? ` · ${document.filename}` : ""} - ) : null} + {aiRead ? ( + // AI-read marker rides INLINE on the meta row — a bare glyph, + // no pill line (the tag ate a whole row in the dense grid). + + ) : null} +