mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
"Read with AI" and the lab scan used to run their model calls inside the
request. At the default response time a read of three calls outlasted the
100 s a proxy in front of the server commonly allows, and the person saw a
cut connection for a read the server was still doing.
Both now run as a background run (`DocumentAiRun`, migration 0366):
- `POST /api/documents/inbound/{id}/index` with `Prefer: respond-async`
answers 202 with a run id once every quick refusal has passed. Without
the header it answers exactly as before, which the iPhone app relies on.
A second request while a read of the same document is queued or running
attaches to that run.
- `POST /api/labs/ocr/extract` always answers 202. Only the web client
calls it.
- `GET /api/ai-runs/{id}` serves the run: the same body the synchronous
route answers with, or the failure code and status it would have given.
Scoped to the caller; another account's run is a 404.
The worker (`document-ai-run`, a long queue with a one-hour expiry, one
job per run under `lockedPass`) claims the run, asks the capability again
for the record and re-checks the wire for the provider it picks, sets the
run's deadline from the person's AI response time and refuses a read that
cannot fit the job's expiry. Sync route and worker run one body per kind,
so the two paths cannot drift. Budget is reserved at enqueue and settled
once by whoever ends the run.
Input and result are sealed with labelled AES-256-GCM, registered for key
rotation; the input is dropped when the run ends. A five-minute reaper
fails a run no worker took within fifteen minutes (and hands its
reservation back), fails a run past its deadline, and deletes a finished
run after an hour. Runs are outside backups and inside the data wipe.
The web client polls through the query-key factory and says, calmly, that
the read continues in the background, and when no worker has picked it up.
84 lines
3.0 KiB
TypeScript
84 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* v1.27.22 (Document vault P2) — content-search indexing hooks.
|
|
*
|
|
* - `useIndexDocument()` populates / refreshes ONE document's blind content
|
|
* index (encrypted extracted text + opaque HMAC token array). Vision decrypts
|
|
* the stored original server-side; text OCR's the image on-device and posts
|
|
* only the text — the same transport split the assist hooks use.
|
|
* - `useReindexAll()` fires the per-user corpus backfill (its own rate bucket);
|
|
* the worker indexes every not-yet-indexed document off-request.
|
|
*
|
|
* Both invalidate the `["documents"]` prefix so the timeline's per-row
|
|
* `hasContentIndex`, the open detail sheet, and the usage coverage gauge all
|
|
* refresh in lockstep.
|
|
*/
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
|
|
import { useAiProviderState } from "@/hooks/use-ai-capability";
|
|
import { useAiRunPhase } from "@/hooks/use-ai-run";
|
|
import { apiPost } from "@/lib/api/api-fetch";
|
|
import { invalidateKeys, queryKeys } from "@/lib/query-keys";
|
|
|
|
import {
|
|
runDocumentIndex,
|
|
type DocumentAiMode,
|
|
type DocumentAiTarget,
|
|
type DocumentIndexResult,
|
|
} from "./document-ai-transport";
|
|
|
|
/**
|
|
* Index / re-index one document for content search. The read runs in the
|
|
* background (v1.40); `runPhase` says so while it does, so the screen can tell
|
|
* the person they may leave: the index and any staged lab values land on the
|
|
* document whether or not anyone is still watching.
|
|
*/
|
|
export function useIndexDocument() {
|
|
const queryClient = useQueryClient();
|
|
const { responseTimeoutMs } = useAiProviderState();
|
|
const { phase, onQueued, onProgress, reset } = useAiRunPhase();
|
|
const mutation = useMutation<
|
|
DocumentIndexResult,
|
|
Error,
|
|
{ mode: DocumentAiMode; target: DocumentAiTarget }
|
|
>({
|
|
mutationFn: ({ mode, target }) =>
|
|
runDocumentIndex({
|
|
mode,
|
|
target,
|
|
responseTimeoutMs,
|
|
background: { queryClient, onQueued, onProgress },
|
|
}),
|
|
onSettled: reset,
|
|
onSuccess: () => {
|
|
void invalidateKeys(queryClient, [queryKeys.documents()]);
|
|
},
|
|
});
|
|
return { ...mutation, runPhase: mutation.isPending ? phase : "idle" };
|
|
}
|
|
|
|
/**
|
|
* Fire the corpus backfill; resolves with how many docs were enqueued.
|
|
*
|
|
* Wire contract (#776): `enqueued` is the NUMBER of live documents the
|
|
* backfill still has to index — the reindex route computes it from the same
|
|
* live-scoped counts the usage gauge reads, and returns an honest 0 when no
|
|
* job was created. The toast interpolates it as a count, so this type is
|
|
* load-bearing: it must match the route's shape exactly (a boolean here once
|
|
* rendered as "Indexing true document(s)").
|
|
*/
|
|
export function useReindexAll() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation<{ enqueued: number }, Error, void>({
|
|
mutationFn: () =>
|
|
apiPost<{ enqueued: number }>(
|
|
"/api/documents/inbound/reindex",
|
|
undefined,
|
|
),
|
|
onSuccess: () => {
|
|
void invalidateKeys(queryClient, [queryKeys.documents()]);
|
|
},
|
|
});
|
|
}
|