mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
chore(release): v1.4.25
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites). See CHANGELOG.md for the full per-section delta.
This commit is contained in:
1 parent
de0689e419
commit
17b8d8d2c2
528 files changed
+104033
-6524
No files matched your search
@@ -23,6 +23,7 @@ import { auditLog } from "@/lib/auth/audit";
|
||||
import { apiError, getClientIp } from "@/lib/api-response";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { toCSV, formatMeasurementsForExport } from "@/lib/export";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const GET = apiHandler(async (request: NextRequest) => {
|
||||
@@ -37,12 +38,15 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
const { since, until } = parseRange(request.url);
|
||||
const where = buildWhere(user.id, { since, until });
|
||||
|
||||
const measurements = await prisma.measurement.findMany({
|
||||
where,
|
||||
orderBy: { measuredAt: "desc" },
|
||||
});
|
||||
const [measurements, userTz] = await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where,
|
||||
orderBy: { measuredAt: "desc" },
|
||||
}),
|
||||
resolveUserTimezone(user.id),
|
||||
]);
|
||||
|
||||
const csv = toCSV(formatMeasurementsForExport(measurements));
|
||||
const csv = toCSV(formatMeasurementsForExport(measurements, userTz));
|
||||
|
||||
await auditLog("user.export.measurements", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
formatMedicationsForExport,
|
||||
formatIntakeEventsForExport,
|
||||
} from "@/lib/export";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const GET = apiHandler(async (request: NextRequest) => {
|
||||
@@ -48,23 +49,26 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
// without a matching migration); a wildcard `include: { schedules: true }`
|
||||
// would 500 on the missing column. Pin the columns instead — the export
|
||||
// shape doesn't need every future field anyway.
|
||||
const medications = await prisma.medication.findMany({
|
||||
where: { userId: user.id },
|
||||
select: {
|
||||
name: true,
|
||||
dose: true,
|
||||
active: true,
|
||||
schedules: {
|
||||
select: {
|
||||
windowStart: true,
|
||||
windowEnd: true,
|
||||
label: true,
|
||||
dose: true,
|
||||
const [medications, userTz] = await Promise.all([
|
||||
prisma.medication.findMany({
|
||||
where: { userId: user.id },
|
||||
select: {
|
||||
name: true,
|
||||
dose: true,
|
||||
active: true,
|
||||
schedules: {
|
||||
select: {
|
||||
windowStart: true,
|
||||
windowEnd: true,
|
||||
label: true,
|
||||
dose: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
resolveUserTimezone(user.id),
|
||||
]);
|
||||
|
||||
const sections: string[] = [
|
||||
"# Medications",
|
||||
@@ -91,7 +95,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
intakeCount = events.length;
|
||||
sections.push("# Intake history");
|
||||
sections.push(toCSV(formatIntakeEventsForExport(events)));
|
||||
sections.push(toCSV(formatIntakeEventsForExport(events, userTz)));
|
||||
}
|
||||
|
||||
const body = sections.join("\n\n");
|
||||
|
||||
@@ -19,6 +19,7 @@ import { auditLog } from "@/lib/auth/audit";
|
||||
import { apiError, getClientIp } from "@/lib/api-response";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { toCSV, formatMoodEntriesForExport } from "@/lib/export";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const GET = apiHandler(async (request: NextRequest) => {
|
||||
@@ -50,12 +51,15 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
if (until) where.moodLoggedAt.lte = until;
|
||||
}
|
||||
|
||||
const entries = await prisma.moodEntry.findMany({
|
||||
where,
|
||||
orderBy: { moodLoggedAt: "desc" },
|
||||
});
|
||||
const [entries, userTz] = await Promise.all([
|
||||
prisma.moodEntry.findMany({
|
||||
where,
|
||||
orderBy: { moodLoggedAt: "desc" },
|
||||
}),
|
||||
resolveUserTimezone(user.id),
|
||||
]);
|
||||
|
||||
const csv = toCSV(formatMoodEntriesForExport(entries));
|
||||
const csv = toCSV(formatMoodEntriesForExport(entries, userTz));
|
||||
|
||||
await auditLog("user.export.mood", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
formatIntakeEventsForExport,
|
||||
formatMoodEntriesForExport,
|
||||
} from "@/lib/export";
|
||||
import { resolveUserTimezone } from "@/lib/tz/resolver";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
}
|
||||
|
||||
const userId = user.id;
|
||||
const userTz = await resolveUserTimezone(userId);
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
@@ -58,8 +60,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
data.measurements =
|
||||
format === "csv"
|
||||
? toCSV(formatMeasurementsForExport(measurements))
|
||||
: formatMeasurementsForExport(measurements);
|
||||
? toCSV(formatMeasurementsForExport(measurements, userTz))
|
||||
: formatMeasurementsForExport(measurements, userTz);
|
||||
}
|
||||
|
||||
if (type === "medications" || type === "all") {
|
||||
@@ -82,8 +84,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
data.intakeEvents =
|
||||
format === "csv"
|
||||
? toCSV(formatIntakeEventsForExport(events))
|
||||
: formatIntakeEventsForExport(events);
|
||||
? toCSV(formatIntakeEventsForExport(events, userTz))
|
||||
: formatIntakeEventsForExport(events, userTz);
|
||||
}
|
||||
|
||||
if (type === "mood" || type === "all") {
|
||||
@@ -93,8 +95,8 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
data.moodEntries =
|
||||
format === "csv"
|
||||
? toCSV(formatMoodEntriesForExport(moodEntries))
|
||||
: formatMoodEntriesForExport(moodEntries);
|
||||
? toCSV(formatMoodEntriesForExport(moodEntries, userTz))
|
||||
: formatMoodEntriesForExport(moodEntries, userTz);
|
||||
}
|
||||
|
||||
await auditLog("export.download", {
|
||||
|
||||
Reference in new issue
Block a user