mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(nutrients): refresh the correlation scan after a water or nutrient write
Water, caffeine and the nutrient channels feed the correlation scan behind /api/insights/correlations and the Coach read line on the metric pages, but the water and nutrient batch writes evicted only the dashboard snapshot. After logging water both could serve the read taken before it until the cell went stale. Both writes now sweep the record's analytics cells, as measurement and mood writes do.
This commit is contained in:
1 parent
d430981caa
commit
442d97cc66
6 files changed
+33
-29
No files matched your search
@@ -47,7 +47,7 @@ vi.mock("@/lib/idempotency", () => ({
|
||||
}));
|
||||
vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() }));
|
||||
vi.mock("@/lib/cache/invalidate", () => ({
|
||||
invalidateUserDashboardSnapshot: vi.fn(),
|
||||
invalidateUserHealthContext: vi.fn(),
|
||||
}));
|
||||
vi.mock("next/headers", () => ({
|
||||
headers: vi.fn(async () => ({ get: () => null })),
|
||||
@@ -65,7 +65,7 @@ import { getSession } from "@/lib/auth/session";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { invalidateUserDashboardSnapshot } from "@/lib/cache/invalidate";
|
||||
import { invalidateUserHealthContext } from "@/lib/cache/invalidate";
|
||||
import { nutrientEntryResultSchema } from "@/lib/validations/nutrients";
|
||||
|
||||
const SESSION_OK = {
|
||||
@@ -317,10 +317,11 @@ describe("POST /api/nutrients/batch — write semantics", () => {
|
||||
expect(prisma.nutrientIntakeDay.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// v1.30.3 — a landed row must evict the dashboard snapshot so
|
||||
// the water/nutrient tile reflects the new total on the very next read,
|
||||
// matching the manual water POST's own posture (`nutrients/water/route.ts`).
|
||||
it("evicts the dashboard snapshot when a row lands", async () => {
|
||||
// v1.30.3 — a landed row must evict the record's analytics cells so
|
||||
// the water/nutrient tile and the correlation scan reflect the new total
|
||||
// on the very next read, matching the manual water POST's own posture
|
||||
// (`nutrients/water/route.ts`).
|
||||
it("evicts the analytics cells when a row lands", async () => {
|
||||
const day = recentDay();
|
||||
const res = await POST(
|
||||
postReq({
|
||||
@@ -328,11 +329,11 @@ describe("POST /api/nutrients/batch — write semantics", () => {
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(invalidateUserDashboardSnapshot).toHaveBeenCalledTimes(1);
|
||||
expect(invalidateUserDashboardSnapshot).toHaveBeenCalledWith("user-1");
|
||||
expect(invalidateUserHealthContext).toHaveBeenCalledTimes(1);
|
||||
expect(invalidateUserHealthContext).toHaveBeenCalledWith("user-1");
|
||||
});
|
||||
|
||||
it("does NOT evict the dashboard snapshot when nothing lands (every entry skipped)", async () => {
|
||||
it("does NOT evict the analytics cells when nothing lands (every entry skipped)", async () => {
|
||||
const res = await POST(
|
||||
postReq({
|
||||
entries: [
|
||||
@@ -348,7 +349,7 @@ describe("POST /api/nutrients/batch — write semantics", () => {
|
||||
const body = (await res.json()) as BatchResponse;
|
||||
expect(body.data.inserted).toBe(0);
|
||||
expect(body.data.updated).toBe(0);
|
||||
expect(invalidateUserDashboardSnapshot).not.toHaveBeenCalled();
|
||||
expect(invalidateUserHealthContext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports updated (never duplicate) when the composite key already exists", async () => {
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { invalidateUserDashboardSnapshot } from "@/lib/cache/invalidate";
|
||||
import { invalidateUserHealthContext } from "@/lib/cache/invalidate";
|
||||
import { NUTRIENT_CATALOG } from "@/lib/nutrients/catalog";
|
||||
import { maxAcceptableNutrientDay } from "@/lib/nutrients/day-bounds";
|
||||
import {
|
||||
@@ -347,12 +347,13 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
|
||||
if (inserted + updated > 0) {
|
||||
// v1.30.3 — a landed row changes the dashboard's water/nutrient
|
||||
// tile; without this the snapshot kept serving the pre-sync figure
|
||||
// until the analytics fresh TTL lapsed (~3 min). Mirrors the manual
|
||||
// water POST's own hard-evict (`nutrients/water/route.ts`) so an
|
||||
// Apple-Health-synced total reflects on the very next dashboard read,
|
||||
// same as a manually logged one.
|
||||
invalidateUserDashboardSnapshot(user.id);
|
||||
// tile and the correlation scan (water, caffeine and nutrient channels)
|
||||
// behind the metric pages and the Coach read; without this they kept
|
||||
// serving the pre-sync figure until the analytics fresh TTL lapsed.
|
||||
// Mirrors the manual water POST's own hard-evict
|
||||
// (`nutrients/water/route.ts`) so an Apple-Health-synced total
|
||||
// reflects on the very next read, same as a manually logged one.
|
||||
invalidateUserHealthContext(user.id);
|
||||
}
|
||||
|
||||
annotate({
|
||||
|
||||
@@ -26,7 +26,7 @@ vi.mock("@/lib/modules/gate", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/cache/invalidate", () => ({
|
||||
invalidateUserDashboardSnapshot: vi.fn(),
|
||||
invalidateUserHealthContext: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
|
||||
@@ -59,7 +59,7 @@ import { apiError } from "@/lib/api-response";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { invalidateUserDashboardSnapshot } from "@/lib/cache/invalidate";
|
||||
import { invalidateUserHealthContext } from "@/lib/cache/invalidate";
|
||||
import { NUTRIENT_CATALOG } from "@/lib/nutrients/catalog";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
|
||||
@@ -194,7 +194,7 @@ describe("POST /api/nutrients/water", () => {
|
||||
},
|
||||
update: { amount: { increment: 250 } },
|
||||
});
|
||||
expect(invalidateUserDashboardSnapshot).toHaveBeenCalledWith("user-1");
|
||||
expect(invalidateUserHealthContext).toHaveBeenCalledWith("user-1");
|
||||
});
|
||||
|
||||
it("set mode overwrites the MANUAL row's amount instead of incrementing", async () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ import { parseJsonBody } from "@/lib/api/body";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
import { invalidateUserDashboardSnapshot } from "@/lib/cache/invalidate";
|
||||
import { invalidateUserHealthContext } from "@/lib/cache/invalidate";
|
||||
import { NUTRIENT_CATALOG } from "@/lib/nutrients/catalog";
|
||||
import { maxAcceptableNutrientDay } from "@/lib/nutrients/day-bounds";
|
||||
import { nutrientWaterWriteSchema } from "@/lib/validations/nutrients";
|
||||
@@ -138,10 +138,11 @@ async function postWater(request: NextRequest): Promise<Response> {
|
||||
});
|
||||
}
|
||||
|
||||
// Interactive single-entry write — hard-evict (not mark-stale) so the
|
||||
// dashboard water tile reflects the new total on the very next read,
|
||||
// matching the mood / medication / measurement posture.
|
||||
invalidateUserDashboardSnapshot(user.id);
|
||||
// Interactive single-entry write: hard-evict (not mark-stale) the record's
|
||||
// analytics cells, so the dashboard water tile and the correlation scan
|
||||
// behind the metric pages and the Coach read reflect the new total on the
|
||||
// very next read, matching the mood / medication / measurement posture.
|
||||
invalidateUserHealthContext(user.id);
|
||||
|
||||
// A health-data write earns a durable row, like every other one. This was
|
||||
// the last manual write in the nutrients family that only reached the wide
|
||||
|
||||
Reference in new issue
Block a user