mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Merge pull request #463 from MBombeck/release/v1.28.19
release/v1.28.19 — document and card polish
This commit is contained in:
16 files changed
+154
-125
No files matched your search
+17
-1
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+5
-5
@@ -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}"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -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}"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -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}"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -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}"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -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}"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -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}"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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/*`
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<WandSparkles
|
||||
className="text-foreground size-5 shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
<p className="text-sm font-semibold">
|
||||
{t("documents.ai.blockTitle")}
|
||||
</p>
|
||||
</div>
|
||||
{/* 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. */}
|
||||
<div className="flex items-center justify-end">
|
||||
<ContentSearchStatus
|
||||
hasContentIndex={hasContentIndex}
|
||||
source={contentIndexSource}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* whole card is clickable through an invisible overlay button; the checkbox
|
||||
* floats above it.
|
||||
*/
|
||||
import { Download, X } from "lucide-react";
|
||||
import { Download, Sparkles, X } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -24,7 +24,10 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { InboundDocumentDto } from "@/lib/validations/inbound-documents";
|
||||
import {
|
||||
isAiReadSource,
|
||||
type InboundDocumentDto,
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
import { DOCUMENT_KIND_ICONS } from "./document-kind-meta";
|
||||
import type { UploadQueueItem } from "./use-document-upload";
|
||||
import { documentDateKey, formatBytes } from "./vault-utils";
|
||||
@@ -84,6 +87,8 @@ export function DocumentCard({
|
||||
const size = formatBytes(document.byteSize, locale);
|
||||
const showFilename =
|
||||
document.filename !== null && document.filename !== title;
|
||||
const aiRead =
|
||||
document.hasContentIndex && isAiReadSource(document.contentIndexSource);
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -126,7 +131,25 @@ export function DocumentCard({
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
<p className="min-w-0 flex-1 truncate text-sm font-medium">{title}</p>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<p className="truncate text-sm font-medium">{title}</p>
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
{date} · {size}
|
||||
{showFilename ? ` · ${document.filename}` : ""}
|
||||
</p>
|
||||
{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.
|
||||
<span
|
||||
data-slot="document-card-ai-read"
|
||||
className="bg-primary/10 text-primary mt-1 inline-flex w-fit items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
>
|
||||
<Sparkles className="size-3 shrink-0" aria-hidden />
|
||||
{t("documents.ai.statusAiRead")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
// Explicit click handling instead of onCheckedChange: the mouse
|
||||
@@ -145,10 +168,6 @@ export function DocumentCard({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
{date} · {size}
|
||||
{showFilename ? ` · ${document.filename}` : ""}
|
||||
</p>
|
||||
{document.conditionLinks.length > 0 ||
|
||||
document.servingClass === "attachment" ? (
|
||||
<div className="mt-auto flex flex-wrap items-center gap-1.5 pt-1">
|
||||
|
||||
@@ -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.
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-slot="document-chat-open"
|
||||
aria-label={t("documents.chat.openAria")}
|
||||
onClick={() => setDocChatOpen(true)}
|
||||
className="text-muted-foreground hover:text-foreground min-h-11 min-w-11 sm:size-8"
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("documents.chat.openAria")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : 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.
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-slot="document-detail-close"
|
||||
aria-label={t("common.close")}
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="text-muted-foreground hover:text-foreground min-h-11 min-w-11 sm:size-9"
|
||||
>
|
||||
<X className="size-5" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
footer={
|
||||
doc ? (
|
||||
@@ -480,6 +467,19 @@ export function DocumentDetailSheet({
|
||||
{t("documents.detail.delete")}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{aiEnabled ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDocChatOpen(true)}
|
||||
data-slot="document-chat-open"
|
||||
aria-label={t("documents.chat.openAria")}
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden />
|
||||
<span className="hidden sm:inline">
|
||||
{t("documents.chat.openButton")}
|
||||
</span>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShareOpen(true)}
|
||||
|
||||
@@ -732,8 +732,6 @@ export function MedicationDetailTabs({
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.erweitert.group.externalApi")}
|
||||
dataSlot="api-group-external-api"
|
||||
collapsible
|
||||
defaultOpen={false}
|
||||
>
|
||||
<div className="py-3">
|
||||
<ApiTokensRow
|
||||
|
||||
@@ -109,58 +109,59 @@ export function InstrumentCard({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Body is the detail target: clicking the header / last-result block
|
||||
opens this instrument's detail (chart + dated history), like a
|
||||
medication card. The Start button below stays a separate action.
|
||||
The short instrument titles clear the top-right info control that
|
||||
floats over the header's right padding. */}
|
||||
{/* Only the HEADER region is the navigable open-target — mirrors the
|
||||
medication card header (focus-visible ring only, NO hover background
|
||||
wash, no whole-card click). The last-result block below is passive;
|
||||
the Start button stays a separate action. The short instrument titles
|
||||
clear the top-right info control that floats over the header's right
|
||||
padding. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenDetail}
|
||||
data-slot="instrument-card-open"
|
||||
aria-label={`${title} — ${t("mentalHealth.openDetail")}`}
|
||||
className="focus-visible:ring-ring/60 hover:bg-accent/40 flex cursor-pointer flex-col gap-3 rounded-t-xl text-left transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
||||
className="focus-visible:ring-ring block w-full rounded-t-xl text-left focus-visible:ring-2 focus-visible:outline-none"
|
||||
>
|
||||
<MedicationCardHeader
|
||||
name={title}
|
||||
dose=""
|
||||
categoryLabel={t(`mentalHealth.instrumentSub.${key}`)}
|
||||
/>
|
||||
|
||||
{/* 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 ? (
|
||||
<div className="min-h-[2.75rem] w-full space-y-1.5 px-4 text-sm md:px-6">
|
||||
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
|
||||
<span className="min-w-0 flex-shrink truncate font-medium">
|
||||
{t("mentalHealth.lastResult")}
|
||||
</span>
|
||||
<span className="text-foreground text-right">
|
||||
{relativeCalendarDate(last.takenAt, t, formatDate)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
|
||||
<span className="min-w-0 flex-shrink truncate font-medium">
|
||||
{t("mentalHealth.lastScore")}
|
||||
</span>
|
||||
<span
|
||||
className="text-foreground text-right"
|
||||
data-slot="instrument-card-last-score"
|
||||
>
|
||||
{last.totalScore}
|
||||
{" · "}
|
||||
{t(`mentalHealth.band.${last.instrument}.${last.severityBand}`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground flex min-h-[2.75rem] items-center px-4 text-sm md:px-6">
|
||||
{t("mentalHealth.noResultYet")}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 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 ? (
|
||||
<div className="min-h-[2.75rem] w-full space-y-1.5 px-4 text-sm md:px-6">
|
||||
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
|
||||
<span className="min-w-0 flex-shrink truncate font-medium">
|
||||
{t("mentalHealth.lastResult")}
|
||||
</span>
|
||||
<span className="text-foreground text-right">
|
||||
{relativeCalendarDate(last.takenAt, t, formatDate)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
|
||||
<span className="min-w-0 flex-shrink truncate font-medium">
|
||||
{t("mentalHealth.lastScore")}
|
||||
</span>
|
||||
<span
|
||||
className="text-foreground text-right"
|
||||
data-slot="instrument-card-last-score"
|
||||
>
|
||||
{last.totalScore}
|
||||
{" · "}
|
||||
{t(`mentalHealth.band.${last.instrument}.${last.severityBand}`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground flex min-h-[2.75rem] items-center px-4 text-sm md:px-6">
|
||||
{t("mentalHealth.noResultYet")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent className="flex h-full flex-col">
|
||||
{/* Bottom-pinned single primary action — the med-/Vorsorge card slot.
|
||||
Nothing sits below it now: the attribution moved to the header's
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user