mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +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
@@ -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