mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
chore(release): v1.28.22 — full sync survives dense histories; quieter document chrome
Google Health: the fullSync rollup-defer tracker collected its touched keys via a single spread push — one argument PER READING, so a multi-year heart-rate history (six figures of samples) overflowed the call stack and aborted the metrics collection mid-cycle, exactly on the accounts with the most data. A plain loop replaces the spread. Reported by a self-hoster; the v1.28.21 sync diagnostics confirmed every other read on that account healthy in one log line. Documents: the vault card's read-by-AI tag no longer takes its own line — a small glyph rides inline on the date-and-size row. The detail view drops the status bar under the preview entirely; with automatic reading on and nothing to review the whole block collapses, so the content starts straight with the document's fields. The provenance contract moves with it: the e2e specs now pin the card marker instead of the sheet pill, and the unmounted status-pill component is removed rather than left dead.
This commit is contained in:
1 parent
94ad48527e
commit
869f5e561b
18 files changed
+95
-209
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 ──────────────
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+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.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/*`
|
||||
|
||||
@@ -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("<DocumentSummaryPanel>", () => {
|
||||
expect(html).toContain("read the document");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<ContentSearchStatus>", () => {
|
||||
it("shows a calm not-searchable-yet pill before the auto-index lands", () => {
|
||||
const html = render(
|
||||
<ContentSearchStatus
|
||||
hasContentIndex={false}
|
||||
source={null}
|
||||
isPending={false}
|
||||
/>,
|
||||
);
|
||||
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("<button");
|
||||
});
|
||||
|
||||
it("highlights an AI-read document distinctly from a locally-indexed one", () => {
|
||||
const aiRead = render(
|
||||
<ContentSearchStatus hasContentIndex source="vision" isPending={false} />,
|
||||
);
|
||||
expect(aiRead).toContain('data-state="ai-read"');
|
||||
expect(aiRead).toContain("Read by AI");
|
||||
expect(aiRead).toContain("text-primary");
|
||||
|
||||
const local = render(
|
||||
<ContentSearchStatus
|
||||
hasContentIndex
|
||||
source="local-pdf"
|
||||
isPending={false}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<ContentSearchStatus hasContentIndex={false} source={null} isPending />,
|
||||
);
|
||||
expect(html).toContain('data-state="indexing"');
|
||||
expect(html).toContain("Making searchable…");
|
||||
});
|
||||
});
|
||||
@@ -80,9 +80,9 @@ describe("<DocumentAiSection>", () => {
|
||||
);
|
||||
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("<DocumentAiSection>", () => {
|
||||
// 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("<DocumentAiSection>", () => {
|
||||
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("<DocumentAiSection>", () => {
|
||||
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", () => {
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<span
|
||||
data-slot="content-search-status"
|
||||
data-state="indexing"
|
||||
className="text-muted-foreground inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs"
|
||||
>
|
||||
<Loader2
|
||||
className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none"
|
||||
aria-hidden
|
||||
/>
|
||||
{t("documents.ai.statusIndexing")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasContentIndex) {
|
||||
return (
|
||||
<span
|
||||
data-slot="content-search-status"
|
||||
data-state="none"
|
||||
className="text-muted-foreground inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs"
|
||||
>
|
||||
<ScanSearch className="size-3.5 shrink-0" aria-hidden />
|
||||
{t("documents.ai.statusNotYet")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAiReadSource(source)) {
|
||||
return (
|
||||
<span
|
||||
data-slot="content-search-status"
|
||||
data-state="ai-read"
|
||||
className="bg-primary/10 text-primary inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
>
|
||||
<Sparkles className="size-3.5 shrink-0" aria-hidden />
|
||||
{t("documents.ai.statusAiRead")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
data-slot="content-search-status"
|
||||
data-state="searchable"
|
||||
className="text-muted-foreground inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs"
|
||||
>
|
||||
<ScanSearch className="size-3.5 shrink-0" aria-hidden />
|
||||
{t("documents.ai.statusSearchable")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
data-slot="document-ai-section"
|
||||
className="border-border bg-muted/30 space-y-3 rounded-lg border p-3 md:p-4"
|
||||
>
|
||||
{/* 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}
|
||||
isPending={indexPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{aiEnabled ? (
|
||||
<>
|
||||
{!autoReadEnabled ? (
|
||||
|
||||
@@ -133,22 +133,22 @@ export function DocumentCard({
|
||||
)}
|
||||
<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")}
|
||||
<p className="text-muted-foreground flex min-w-0 items-center gap-1 text-xs">
|
||||
<span className="truncate">
|
||||
{date} · {size}
|
||||
{showFilename ? ` · ${document.filename}` : ""}
|
||||
</span>
|
||||
) : 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).
|
||||
<Sparkles
|
||||
data-slot="document-card-ai-read"
|
||||
role="img"
|
||||
aria-label={t("documents.ai.statusAiRead")}
|
||||
className="text-primary size-3 shrink-0"
|
||||
/>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
|
||||
@@ -543,7 +543,15 @@ export async function upsertGoogleHealthMeasurements(
|
||||
// of the cycle. The incremental path keeps the inline per-day hook here.
|
||||
if (opts.deferRollup) {
|
||||
const tracker = rollupDeferStorage.getStore();
|
||||
if (tracker) tracker.keys.push(...touched);
|
||||
// Plain loop, never `push(...touched)`: `touched` carries ONE entry PER
|
||||
// READING, and a full-history batch on a sample-dense account (a
|
||||
// multi-year heart-rate series is six figures of rows) blows the call
|
||||
// stack when spread into a single call — the engine caps argument counts.
|
||||
// The RangeError aborted the whole metrics collection on exactly the
|
||||
// accounts with the most data.
|
||||
if (tracker) {
|
||||
for (const t of touched) tracker.keys.push(t);
|
||||
}
|
||||
return { imported, touched };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user