Files
HealthLog/src/app/api/day/notable/route.ts
T
Marc-André Bombeck 0924285cd6 feat: serve the day, its index, the timeline and life events
Replace the day, timeline and life-event stubs with their implementations
(#613) and drop the not-implemented helper, its error code and its OpenAPI
response with the last stub.

loadDay reads one local day for every reader: the readings live inside the
record's own day window (never the UTC rollups), through the canonical-source
engine the mood sheet already uses (resolveDayReadings in linked-context.ts),
with the person's usual range over the 30 days before; the record sections
from their own tables, each in the storage form it speaks (an instant in the
local window, a date string with its own zone, a calendar date). A switched-off
module's section is not read; a section the grant does not cover is named
not_shared and not read.

The day, the day index and the new visit preparation (GET /api/day/notable)
are admitted on the readings and narrowed per section through
actingDomainVisibility, so a scoped grant sees exactly its sections. The
timeline and its readiness stay whole-record reads and narrow their lanes the
same way.

Notable days are deterministic keys: a three-month high or low with history
behind it, a first reading, and in the visit preparation a regular reading
that stopped. The timeline lays out spans and points from the owning tables,
standing items without a date, and series by zoom: months from the rollup
tier inside its five-year window and from one cached live query before it.
The readiness inventory rates each lane with one link per gap.

Life events are created, edited and soft-deleted as MANAGE verbs with audit
rows, encrypted at rest, and join the sharing guard and the handler matrix.
get_day reads a day for the Coach and the MCP endpoint without life events,
notes or document names; its step carries the date it read.
2026-10-08 14:31:13 +02:00

61 lines
2.2 KiB
TypeScript

/**
* `GET /api/day/notable?from=&to=` — what changed and what stood out in a
* window, for the preparation of the next visit (v1.42, #613). Without
* `from` the window starts at the last completed visit the caller may see;
* without `to` it ends today. The shape is `DayNotableWindowResponse` in
* `src/lib/day/contract.ts`.
*
* Observations and changes only, as keys and the record's own names: no
* value judgement, no cause. Same admission and section narrowing as the day.
*/
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
import { resolveDayAccess } from "@/lib/day/sections";
import { loadSinceLastVisit } from "@/lib/day/visit-preparation";
import { dayNotableQuerySchema } from "@/lib/day/wire-schemas";
import { prisma } from "@/lib/db";
import { annotate } from "@/lib/logging/context";
import { actingDomainVisibility } from "@/lib/sharing/acting-domains";
import { userDayKey } from "@/lib/tz/format";
import { resolveUserTimezone } from "@/lib/tz/resolver";
export const GET = apiHandler(async (request: Request) => {
const { user, grantId } = await requireRecordAuth("read", "measurements");
const params = new URL(request.url).searchParams;
const parsed = dayNotableQuerySchema.safeParse({
from: params.get("from") ?? undefined,
to: params.get("to") ?? undefined,
});
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, { errorCode: "day.invalid" });
}
const [access, tz] = await Promise.all([
actingDomainVisibility(prisma, grantId).then((domainVisible) =>
resolveDayAccess({
recordId: user.id,
domainVisible,
owner: grantId === null,
}),
),
resolveUserTimezone(user.id),
]);
const window = await loadSinceLastVisit({
recordId: user.id,
access,
tz,
today: userDayKey(new Date(), tz),
from: parsed.data.from,
to: parsed.data.to,
});
annotate({
action: { name: "day.notable.read" },
meta: {
anchor: window.anchor,
observations: window.observations.length,
changes: window.changes.length,
delegated: grantId !== null,
},
});
return apiSuccess(window);
});