mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
feat(nutrients): source-aware day totals; manual water write path
NutrientIntakeDay's composite primary key gains `source` (migration 0249_nutrient_intake_day_source): a manual water entry and the Apple-synced day total now live as two rows instead of the manual write being silently clobbered by the next Apple last-writer-wins sync. Every existing row already defaults to source=APPLE_HEALTH, so the migration is a pure PK-constraint change with no data rewrite. - POST /api/nutrients/batch keeps writing the APPLE_HEALTH row, byte-identical upsert semantics against the wider key. - GET /api/nutrients sums amounts within (nutrient, day) across sources before folding into the window summary. - POST /api/nutrients/water (new): manual water quick-add, add/set modes against the MANUAL row only, module-gated, rate-limited, idempotency-key aware, hard-evicts the dashboard snapshot cache. - GET /api/nutrients/daily (new): dense per-day series for one nutrient (summed across sources) plus the EFSA reference resolved against the caller's profile sex — omitted when sex is unknown. - catalog.ts gains resolveNutrientReference(), the sex-resolution helper both new routes and the insights cards share. docs/api/openapi.yaml regenerated for the two new routes.
This commit is contained in:
1 parent
bef3b9dd3f
commit
da2de015fe
15 files changed
+1075
-11
No files matched your search
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { NUTRIENT_CODES } from "@/lib/nutrients/catalog";
|
||||
import { NUTRIENT_CATALOG, NUTRIENT_CODES } from "@/lib/nutrients/catalog";
|
||||
|
||||
export const MAX_NUTRIENT_ENTRIES_PER_BATCH = 500;
|
||||
|
||||
@@ -106,3 +106,72 @@ export const nutrientOverviewSchema = z
|
||||
description:
|
||||
"Per-nutrient window summary in catalog order: latest synced day + total, and the count of days carrying data inside the window. Nutrients without data in the window are omitted.",
|
||||
});
|
||||
|
||||
/**
|
||||
* `POST /api/nutrients/water` body (v1.29) — manual water quick-add.
|
||||
*
|
||||
* Water only in slice 1 (manual vitamin entry is out of scope — see
|
||||
* the design memo). `amountMl` is bounded to the catalog's own
|
||||
* per-day plausibility cap for water; `mode: "add"` increments the
|
||||
* MANUAL row for `day`, `mode: "set"` overwrites it (the "edit
|
||||
* today's total" undo path). `day` defaults server-side to the
|
||||
* caller's current local day when omitted.
|
||||
*/
|
||||
export const nutrientWaterWriteSchema = z
|
||||
.object({
|
||||
amountMl: z
|
||||
.number()
|
||||
.finite()
|
||||
.min(1)
|
||||
.max(NUTRIENT_CATALOG.water.plausibleDailyMax),
|
||||
mode: z.enum(["add", "set"]),
|
||||
day: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||
.optional(),
|
||||
})
|
||||
.meta({
|
||||
id: "NutrientWaterWriteRequest",
|
||||
description:
|
||||
"Manual water quick-add against the MANUAL source row. add increments today's manual total; set overwrites it. Never touches the APPLE_HEALTH row.",
|
||||
});
|
||||
|
||||
export const nutrientWaterWriteResponseSchema = z
|
||||
.object({
|
||||
day: z.string(),
|
||||
nutrient: z.literal("water"),
|
||||
source: z.literal("MANUAL"),
|
||||
amount: z.number(),
|
||||
unit: z.string(),
|
||||
})
|
||||
.meta({
|
||||
id: "NutrientWaterWriteResponse",
|
||||
description: "The MANUAL water row after the add/set write.",
|
||||
});
|
||||
|
||||
/** `GET /api/nutrients/daily` query — one nutrient's day-bucketed series. */
|
||||
export const nutrientDailyQuerySchema = z.object({
|
||||
nutrient: nutrientCodeEnum,
|
||||
days: z.coerce.number().int().min(1).max(90).default(30),
|
||||
});
|
||||
|
||||
const resolvedNutrientReferenceSchema = z.object({
|
||||
kind: z.enum(["PRI", "AI", "safeLevel"]),
|
||||
direction: z.enum(["target", "upperGuidance"]),
|
||||
value: z.number(),
|
||||
source: z.string(),
|
||||
});
|
||||
|
||||
export const nutrientDailySeriesSchema = z
|
||||
.object({
|
||||
nutrient: nutrientCodeEnum,
|
||||
unit: z.string(),
|
||||
windowDays: z.number().int().min(1),
|
||||
days: z.array(z.object({ day: z.string(), amount: z.number() })),
|
||||
reference: resolvedNutrientReferenceSchema.nullable(),
|
||||
})
|
||||
.meta({
|
||||
id: "NutrientDailySeries",
|
||||
description:
|
||||
"Dense day-bucketed series (one entry per day in the window, 0 for a day with no data) summed across sources, plus the EFSA reference resolved against the caller's profile sex (null when sex is unknown on the profile).",
|
||||
});
|
||||
Reference in new issue
Block a user