chore(release): v1.5.6

Pure-history medication detail page, legacy step consolidation, and the
finish of the outbound-fetch migration.

- /medications/[id] becomes a read-only history surface; every setting
  moves into a new advanced-settings sheet, and the edit trigger becomes
  a two-option picker. The list page is unchanged.
- A pg-boss job folds pre-v1.5.0 granular step rows into one canonical
  daily total per user per day and soft-deletes the originals; migration
  0087 adds a partial index for the converging discovery scan.
- Every remaining server-side fetch routes through safeFetch (Withings,
  Codex OAuth + SSE, bug-report, Umami, GlitchTip, Loki); a new
  healthlog/safe-fetch-required ESLint rule guards the boundary.
- Fixes: intake-edit dialog seeded from real values, consolidation skips
  a unique-constraint day instead of stranding it, avatar upload reads
  the body once under a bounded cap, egress lint rejects absolute URLs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-29 22:07:10 +02:00
1 parent d54addd61a
commit 8528c3b4fe
54 files changed
+2343 -324

No files matched your search

+87
View File
@@ -44,6 +44,7 @@ import {
MEDICATION_TREATMENT_CLASS_VALUES,
} from "@/lib/validations/medication";
import { medicationExtractionSchema } from "@/lib/ai/coach/medication-extract-prompt";
import { INSIGHTS_TILE_IDS } from "@/lib/insights-layout";
/**
* Common envelopes — every HealthLog API response wraps payload in
@@ -786,6 +787,29 @@ medicationExtractionSchema.meta({
"Citation-guarded partial extraction of medication scheduling fields. Every field is optional; the wizard merges what is present onto the form state and leaves the rest blank. `name` and `dose` are post-validated against the original free-text and dropped when not substring-matched, so the wizard cannot silently land a hallucinated brand or dose. `cadenceKind` / `doseUnit` / `weekdays` are closed enums; numeric fields are clamped to the wizard's wire bounds.",
});
// Insights tile layout — mirrors the Zod schema in
// `src/app/api/insights/layout/route.ts`. The tile-id enum is derived
// from the same `INSIGHTS_TILE_IDS` source so the contract cannot drift.
const insightsLayoutSchema = z
.object({
version: z.literal(1),
tiles: z
.array(
z.object({
id: z.enum(INSIGHTS_TILE_IDS),
visible: z.boolean(),
order: z.number().int().min(0).max(99),
}),
)
.min(1)
.max(50),
})
.meta({
id: "InsightsLayoutBody",
description:
"Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout schema version; tile ids are a closed enum derived from the server's tile registry.",
});
// ── Standard 401 / 422 / 429 responses ───────────────────────────────
const stdResponses = {
@@ -1579,6 +1603,69 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
},
"/api/insights/layout": {
get: {
tags: ["Insights"],
summary: "Read the calling user's Insights tile layout",
description:
"Returns the per-user Insights tile layout (visibility + order). Falls back to the default layout when the user has not customised it. Mirrors the dashboard-widgets contract.",
responses: {
"200": {
description: "The resolved layout (custom or default).",
content: {
"application/json": {
schema: dataEnvelope(insightsLayoutSchema, "InsightsLayout"),
},
},
},
...stdResponses,
},
},
put: {
tags: ["Insights"],
summary: "Replace the calling user's Insights tile layout",
description:
"Persists the full tile layout. The normalised layout is returned. Invalid bodies return the multi-issue 422 envelope, matching the dashboard-widgets route.",
requestBody: {
required: true,
content: {
"application/json": { schema: insightsLayoutSchema },
},
},
responses: {
"200": {
description: "Layout saved; the normalised layout is echoed back.",
content: {
"application/json": {
schema: dataEnvelope(insightsLayoutSchema, "InsightsLayoutSaved"),
},
},
},
// 422 (multi-issue validation envelope) comes from stdResponses.
...stdResponses,
},
},
delete: {
tags: ["Insights"],
summary: "Reset the calling user's Insights tile layout",
description:
"Clears the persisted layout and returns the default layout. Idempotent.",
responses: {
"200": {
description: "Layout reset; the default layout is returned.",
content: {
"application/json": {
schema: dataEnvelope(
insightsLayoutSchema,
"InsightsLayoutReset",
),
},
},
},
...stdResponses,
},
},
},
"/api/insights/chat/messages/{id}/feedback": {
post: {
tags: ["Insights"],