diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8190455..2fded7059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,23 @@ ## [Unreleased] -## [1.28.18] — 2026-07-10 — Google Health sleep reads its true total +## [1.28.19] — 2026-07-10 — Document and card polish + +Small refinements across a few surfaces. + +In the documents vault, each card now carries the date, size and an "read by AI" +marker directly under the document's title, instead of the meta line sitting off +under the thumbnail. + +The document view gets a larger, easier close button, drops a redundant top-right +shortcut and the inline "read with AI" row under the preview, and gains a button +to ask about the document right next to share and download. + +The mental-wellbeing cards no longer wash the whole tile on hover — only the +title is the tap target now, matching how the medication cards behave. + +The medication API settings render inline instead of behind an expander that +never had anything else beside it. Sleep imported from Google Health could read far too long — a night of about seven and a half hours showing as ten. Google re-scores a night after the fact, diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 7ec6fcb1e..78916a428 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.18 + version: 1.28.19 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/messages/de.json b/messages/de.json index b3a4cd525..93f63d8d9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8317,15 +8317,14 @@ "close": "Ausblenden" }, "ai": { - "blockTitle": "Mit KI auslesen", - "read": "Mit KI auslesen", - "reread": "Erneut lesen", - "reading": "Liest…", "statusAiRead": "Von KI gelesen", "statusSearchable": "Durchsuchbar", "statusIndexing": "Wird durchsuchbar gemacht…", "statusNotYet": "Noch nicht durchsuchbar", - "readDone": "Die KI hat dein Dokument gelesen." + "readDone": "Die KI hat dein Dokument gelesen.", + "read": "Mit KI auslesen", + "reread": "Erneut lesen", + "reading": "Liest…" }, "contentIndex": { "indexPrompt": "Dokumentinhalte durchsuchbar machen.", @@ -8357,6 +8356,7 @@ "errorConsent": "Aktiviere die KI-Zustimmung in den Einstellungen, um über dieses Dokument zu chatten.", "errorNetwork": "Du scheinst offline zu sein. Prüfe deine Verbindung und versuche es erneut.", "openAria": "Über dieses Dokument chatten", + "openButton": "Zu diesem Dokument fragen", "scopeBadge": "Chat über: {title}" } }, diff --git a/messages/en.json b/messages/en.json index ad815d6df..c99619f58 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8317,15 +8317,14 @@ "close": "Hide" }, "ai": { - "blockTitle": "Read with AI", - "read": "Read with AI", - "reread": "Read again", - "reading": "Reading…", "statusAiRead": "Read by AI", "statusSearchable": "Searchable", "statusIndexing": "Making searchable…", "statusNotYet": "Not searchable yet", - "readDone": "The AI read your document." + "readDone": "The AI read your document.", + "read": "Read with AI", + "reread": "Read again", + "reading": "Reading…" }, "contentIndex": { "indexPrompt": "Make document contents searchable.", @@ -8357,6 +8356,7 @@ "errorConsent": "Turn on AI consent in settings to chat about this document.", "errorNetwork": "You appear to be offline. Check your connection and try again.", "openAria": "Chat about this document", + "openButton": "Ask about this document", "scopeBadge": "Chatting about: {title}" } }, diff --git a/messages/es.json b/messages/es.json index 71ef8f355..ccf25e291 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8317,15 +8317,14 @@ "close": "Ocultar" }, "ai": { - "blockTitle": "Leer con IA", - "read": "Leer con IA", - "reread": "Leer de nuevo", - "reading": "Leyendo…", "statusAiRead": "Leído por IA", "statusSearchable": "Buscable", "statusIndexing": "Haciéndolo buscable…", "statusNotYet": "Aún no buscable", - "readDone": "La IA leyó tu documento." + "readDone": "La IA leyó tu documento.", + "read": "Leer con IA", + "reread": "Leer de nuevo", + "reading": "Leyendo…" }, "contentIndex": { "indexPrompt": "Haz que el contenido de los documentos sea buscable.", @@ -8357,6 +8356,7 @@ "errorConsent": "Activa el consentimiento de IA en los ajustes para chatear sobre este documento.", "errorNetwork": "Parece que no tienes conexión. Comprueba tu red e inténtalo de nuevo.", "openAria": "Chatea sobre este documento", + "openButton": "Pregunta sobre este documento", "scopeBadge": "Chateando sobre: {title}" } }, diff --git a/messages/fr.json b/messages/fr.json index e58d77646..e3a275ee0 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8317,15 +8317,14 @@ "close": "Masquer" }, "ai": { - "blockTitle": "Lire avec l’IA", - "read": "Lire avec l’IA", - "reread": "Relire", - "reading": "Lecture…", "statusAiRead": "Lu par l’IA", "statusSearchable": "Consultable", "statusIndexing": "Rendu consultable…", "statusNotYet": "Pas encore consultable", - "readDone": "L’IA a lu votre document." + "readDone": "L’IA a lu votre document.", + "read": "Lire avec l’IA", + "reread": "Relire", + "reading": "Lecture…" }, "contentIndex": { "indexPrompt": "Rendre le contenu des documents consultable.", @@ -8357,6 +8356,7 @@ "errorConsent": "Activez le consentement à l'IA dans les paramètres pour discuter de ce document.", "errorNetwork": "Vous semblez hors ligne. Vérifiez votre connexion et réessayez.", "openAria": "Discuter de ce document", + "openButton": "Poser une question sur ce document", "scopeBadge": "Discussion sur : {title}" } }, diff --git a/messages/it.json b/messages/it.json index 0b4806912..3c7c91572 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8317,15 +8317,14 @@ "close": "Nascondi" }, "ai": { - "blockTitle": "Leggi con l’IA", - "read": "Leggi con l’IA", - "reread": "Leggi di nuovo", - "reading": "Lettura…", "statusAiRead": "Letto dall’IA", "statusSearchable": "Ricercabile", "statusIndexing": "Rendo ricercabile…", "statusNotYet": "Non ancora ricercabile", - "readDone": "L’IA ha letto il tuo documento." + "readDone": "L’IA ha letto il tuo documento.", + "read": "Leggi con l’IA", + "reread": "Leggi di nuovo", + "reading": "Lettura…" }, "contentIndex": { "indexPrompt": "Rendi ricercabile il contenuto dei documenti.", @@ -8357,6 +8356,7 @@ "errorConsent": "Attiva il consenso all'IA nelle impostazioni per chattare su questo documento.", "errorNetwork": "Sembri offline. Controlla la connessione e riprova.", "openAria": "Chatta su questo documento", + "openButton": "Fai una domanda su questo documento", "scopeBadge": "Stai chattando su: {title}" } }, diff --git a/messages/pl.json b/messages/pl.json index d16923fb0..aa22b3209 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8317,15 +8317,14 @@ "close": "Ukryj" }, "ai": { - "blockTitle": "Odczytaj z AI", - "read": "Odczytaj z AI", - "reread": "Odczytaj ponownie", - "reading": "Odczytywanie…", "statusAiRead": "Odczytane przez AI", "statusSearchable": "Wyszukiwalne", "statusIndexing": "Udostępnianie do wyszukiwania…", "statusNotYet": "Jeszcze nie do wyszukania", - "readDone": "AI odczytało Twój dokument." + "readDone": "AI odczytało Twój dokument.", + "read": "Odczytaj z AI", + "reread": "Odczytaj ponownie", + "reading": "Odczytywanie…" }, "contentIndex": { "indexPrompt": "Udostępnij treść dokumentów do wyszukiwania.", @@ -8357,6 +8356,7 @@ "errorConsent": "Włącz zgodę na AI w ustawieniach, aby porozmawiać o tym dokumencie.", "errorNetwork": "Wygląda na to, że jesteś offline. Sprawdź połączenie i spróbuj ponownie.", "openAria": "Porozmawiaj o tym dokumencie", + "openButton": "Zapytaj o ten dokument", "scopeBadge": "Rozmowa o: {title}" } }, diff --git a/package.json b/package.json index a5025a826..f14c617f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.28.18", + "version": "1.28.19", "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 9c4ffa9d6..4eb8b44e0 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.18"; + /* @sw-version-fallback */ "v1.28.19"; 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/document-ai-section.tsx b/src/components/documents/document-ai-section.tsx index da39c1341..1810b2f74 100644 --- a/src/components/documents/document-ai-section.tsx +++ b/src/components/documents/document-ai-section.tsx @@ -118,16 +118,11 @@ export function DocumentAiSection({ data-slot="document-ai-section" className="border-border bg-muted/30 space-y-3 rounded-lg border p-3 md:p-4" > -
-
- -

- {t("documents.ai.blockTitle")} -

-
+ {/* 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. */} +
)} -

{title}

+
+

{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")} + + ) : null} +
-

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

{document.conditionLinks.length > 0 || document.servingClass === "attachment" ? (
diff --git a/src/components/documents/document-detail-sheet.tsx b/src/components/documents/document-detail-sheet.tsx index 8959688b0..924440243 100644 --- a/src/components/documents/document-detail-sheet.tsx +++ b/src/components/documents/document-detail-sheet.tsx @@ -25,6 +25,7 @@ import { Share2, Sparkles, Trash2, + X, } from "lucide-react"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; @@ -49,12 +50,6 @@ import { SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { apiDelete, ApiError, @@ -434,32 +429,24 @@ export function DocumentDetailSheet({ title={title} description={t("documents.detail.title")} contentWidth="3xl" + showCloseButton={false} headerAction={ - doc && aiEnabled ? ( - // Neutral, ghost Coach entry pinned top-right of the document — - // mirrors the Insights `AskCoachIconButton`. Deliberately NOT the - // purple FAB styling: `text-muted-foreground` only. Opens the - // doc-scoped chat drawer; the drawer body shows the calm - // read-it-first hint when the document is not yet indexed. - - - - - - {t("documents.chat.openAria")} - - - ) : undefined + // Enlarged close control rendered in the header's trailing slot — + // the sheet's own X glyph is `size-4` via the primitive; we hide it + // (`showCloseButton={false}`) and render this larger `size-5` X with + // a comfortable min tap target instead. Scoped to this sheet only, + // so the global primitive default is unchanged for other callers. + } footer={ doc ? ( @@ -480,6 +467,19 @@ export function DocumentDetailSheet({ {t("documents.detail.delete")}
+ {aiEnabled ? ( + + ) : null} + {/* Label/value block in the Vorsorge card's next-last grammar. With + no history yet, one calm line replaces the pair — no dashes + pretending a value exists. */} + {last ? ( +
+
+ + {t("mentalHealth.lastResult")} + + + {relativeCalendarDate(last.takenAt, t, formatDate)} + +
+
+ + {t("mentalHealth.lastScore")} + + + {last.totalScore} + {" · "} + {t(`mentalHealth.band.${last.instrument}.${last.severityBand}`)} + +
+
+ ) : ( +
+ {t("mentalHealth.noResultYet")} +
+ )} + {/* Bottom-pinned single primary action — the med-/Vorsorge card slot. Nothing sits below it now: the attribution moved to the header's diff --git a/src/lib/google-health/__tests__/sync-sleep-replace.test.ts b/src/lib/google-health/__tests__/sync-sleep-replace.test.ts index 33af0ffd4..ee2debec9 100644 --- a/src/lib/google-health/__tests__/sync-sleep-replace.test.ts +++ b/src/lib/google-health/__tests__/sync-sleep-replace.test.ts @@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const { updateManyMock } = vi.hoisted(() => ({ - updateManyMock: vi.fn(async (_args: unknown) => ({ count: 0 })), + updateManyMock: vi.fn(async () => ({ count: 0 })), })); vi.mock("@/lib/db", () => ({ @@ -53,7 +53,7 @@ describe("replaceStaleGoogleHealthSleep — bounded replace-by-window", () => { expect(removed).toBe(1); expect(updateManyMock).toHaveBeenCalledTimes(1); - const arg = updateManyMock.mock.calls[0]![0] as { + const arg = (updateManyMock.mock.calls[0]! as unknown[])[0] as { where: Record; data: Record; };