mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
Record why a document's index attempt failed and say so
A content-index attempt that failed (raster failure, a scan with no text layer, a provider error) recorded its reason only in the server's observability stream. In the vault, hasContentIndex false looked identical for a document nobody ever tried and one that failed three times, and the detail sheet had nothing to say about either. Apply the summaryState pattern to the index. Two additive columns on InboundDocument (migration 0333) record when the last attempt finished and why it produced no index, as a closed reason set: local-empty, local-unsupported, decrypt-error, raster-failed, provider-error, pdf-needs-anthropic, empty-transcription. The writer sits at the choke point the auto job and the corpus backfill share, and in the manual index route; a success clears the reason. The provider path's specific failure now survives the local fallback, since "no text" is less actionable than "the PDF would not render". rasterizePdf names its failure class so a build that cannot rasterize at all is distinguished from a PDF that would not render. New guard in both index paths: an empty provider transcription never becomes a successful empty index. The detail sheet renders the reason in plain words (muted, six locales). The DTO and OpenAPI carry the two fields; the disaster recovery backup and restore carry them verbatim like their summary siblings. Refs #776
This commit is contained in:
1 parent
45dc93932a
commit
893db887e9
29 files changed
+786
-33
No files matched your search
@@ -213,3 +213,44 @@ describe("<DocumentAiSection>", () => {
|
||||
expect(html).toContain("Summarise");
|
||||
});
|
||||
});
|
||||
|
||||
describe("<DocumentAiSection> index outcome (#776)", () => {
|
||||
it("explains a failed index attempt in plain words, muted", () => {
|
||||
const html = render(
|
||||
base({ hasContentIndex: false, lastIndexOutcome: "local-empty" }),
|
||||
);
|
||||
expect(html).toContain('data-slot="document-index-outcome"');
|
||||
expect(html).toContain("No readable text was found");
|
||||
expect(html).toContain("text-muted-foreground");
|
||||
});
|
||||
|
||||
it("also explains it on the auto-read unread branch", () => {
|
||||
const html = render(
|
||||
base({
|
||||
autoReadEnabled: true,
|
||||
hasContentIndex: false,
|
||||
lastIndexOutcome: "raster-failed",
|
||||
}),
|
||||
);
|
||||
expect(html).toContain('data-slot="document-index-outcome"');
|
||||
expect(html).toContain("couldn't be rendered");
|
||||
});
|
||||
|
||||
it("says nothing once the document IS indexed (a past failure is history)", () => {
|
||||
const html = render(
|
||||
base({
|
||||
hasContentIndex: true,
|
||||
contentIndexSource: "vision",
|
||||
lastIndexOutcome: "provider-error",
|
||||
}),
|
||||
);
|
||||
expect(html).not.toContain('data-slot="document-index-outcome"');
|
||||
});
|
||||
|
||||
it("says nothing when no attempt ever ran (never tried is not a failure)", () => {
|
||||
const html = render(
|
||||
base({ hasContentIndex: false, lastIndexOutcome: null }),
|
||||
);
|
||||
expect(html).not.toContain('data-slot="document-index-outcome"');
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,8 @@ function doc(overrides: Partial<InboundDocumentDto> = {}): InboundDocumentDto {
|
||||
servingClass: "inline",
|
||||
hasContentIndex: false,
|
||||
contentIndexSource: null,
|
||||
lastIndexAttemptAt: null,
|
||||
lastIndexOutcome: null,
|
||||
hasThumbnail: false,
|
||||
createdAt: "2025-10-05T08:00:00.000Z",
|
||||
updatedAt: "2025-10-05T08:00:00.000Z",
|
||||
|
||||
@@ -85,6 +85,8 @@ function doc(
|
||||
servingClass: "inline",
|
||||
hasContentIndex: true,
|
||||
contentIndexSource: "vision",
|
||||
lastIndexAttemptAt: null,
|
||||
lastIndexOutcome: null,
|
||||
hasThumbnail: false,
|
||||
createdAt: "2026-08-01T10:00:00.000Z",
|
||||
updatedAt: "2026-08-01T10:00:00.000Z",
|
||||
|
||||
@@ -54,6 +54,8 @@ function doc(id: string, title: string): InboundDocumentDto {
|
||||
servingClass: "inline",
|
||||
hasContentIndex: false,
|
||||
contentIndexSource: null,
|
||||
lastIndexAttemptAt: null,
|
||||
lastIndexOutcome: null,
|
||||
hasThumbnail: false,
|
||||
createdAt: "2025-10-05T08:00:00.000Z",
|
||||
updatedAt: "2025-10-05T08:00:00.000Z",
|
||||
|
||||
@@ -42,6 +42,8 @@ function doc(overrides: Partial<InboundDocumentDto> = {}): InboundDocumentDto {
|
||||
servingClass: "inline",
|
||||
hasContentIndex: false,
|
||||
contentIndexSource: null,
|
||||
lastIndexAttemptAt: null,
|
||||
lastIndexOutcome: null,
|
||||
hasThumbnail: false,
|
||||
createdAt: "2026-03-11T08:00:00.000Z",
|
||||
updatedAt: "2026-03-11T08:00:00.000Z",
|
||||
|
||||
@@ -38,6 +38,7 @@ import { cn } from "@/lib/utils";
|
||||
import {
|
||||
isAiReadSource,
|
||||
type DocumentContentIndexSourceValue,
|
||||
type DocumentIndexOutcomeValue,
|
||||
type DocumentSuggestionDto,
|
||||
type DocumentSummaryMode,
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
@@ -74,6 +75,7 @@ export function DocumentAiSection({
|
||||
onCloseSummary,
|
||||
hasContentIndex,
|
||||
contentIndexSource,
|
||||
lastIndexOutcome = null,
|
||||
indexPending,
|
||||
onIndex,
|
||||
}: {
|
||||
@@ -111,11 +113,24 @@ export function DocumentAiSection({
|
||||
onCloseSummary: () => void;
|
||||
hasContentIndex: boolean;
|
||||
contentIndexSource: DocumentContentIndexSourceValue | null;
|
||||
/**
|
||||
* Refs #776 — why the last index attempt produced no index (null: never
|
||||
* tried, or the document IS indexed). Rendered as a muted plain-words line
|
||||
* beside the read action so a failed read is explainable, not a mystery.
|
||||
*/
|
||||
lastIndexOutcome?: DocumentIndexOutcomeValue | null;
|
||||
indexPending: boolean;
|
||||
onIndex: () => void;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const aiRead = hasContentIndex && isAiReadSource(contentIndexSource);
|
||||
// Refs #776 — only an un-indexed document explains itself; once an index
|
||||
// exists the last failure is history. Muted per the text-colour rule: this
|
||||
// is meta about the document, not content.
|
||||
const indexOutcomeText =
|
||||
!hasContentIndex && lastIndexOutcome
|
||||
? t(`documents.detail.index.outcome.${lastIndexOutcome}`)
|
||||
: null;
|
||||
const readLabel = indexPending
|
||||
? t("documents.ai.reading")
|
||||
: aiRead
|
||||
@@ -233,6 +248,15 @@ export function DocumentAiSection({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{indexOutcomeText ? (
|
||||
<p
|
||||
data-slot="document-index-outcome"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
{indexOutcomeText}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{suggestErrorKey ? (
|
||||
<p role="alert" className="text-destructive text-sm">
|
||||
{suggestErrorKey}
|
||||
|
||||
@@ -875,6 +875,7 @@ export function DocumentDetailSheet({
|
||||
}}
|
||||
hasContentIndex={doc.hasContentIndex}
|
||||
contentIndexSource={doc.contentIndexSource}
|
||||
lastIndexOutcome={doc.lastIndexOutcome}
|
||||
indexPending={indexDoc.isPending}
|
||||
onIndex={runIndex}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user