mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
Merge remote-tracking branch 'origin/release/v1.46.0' into fix/coach-reasoning-gateway
This commit is contained in:
commit
567cb19afc
4 files changed
+163
-32
No files matched your search
Generated
+27
-18
@@ -14,12 +14,12 @@ overrides:
|
||||
nanoid@<3.3.18: ^3.3.18
|
||||
'@hono/node-server@<1.19.15': ^1.19.15
|
||||
picomatch@<4.0.4: ^4.0.4
|
||||
body-parser@<2.3.0: ^2.3.0
|
||||
body-parser@<2.4.0: ^2.4.0
|
||||
source-map-js@<1.2.2: ^1.2.2
|
||||
postcss-selector-parser@<7.1.6: ^7.1.6
|
||||
smol-toml@<1.9.0: ^1.9.0
|
||||
qs@<6.16.0: ^6.16.0
|
||||
fast-uri@<3.1.8: ^3.1.8
|
||||
fast-uri@<3.1.9: ^3.1.9
|
||||
flatted@<3.4.2: ^3.4.2
|
||||
proxy-addr@<2.0.8: ^2.0.8
|
||||
mysql2@<3.22.0: ^3.22.0
|
||||
@@ -35,7 +35,7 @@ overrides:
|
||||
brace-expansion@<1.1.21: ^1.1.21
|
||||
brace-expansion@>=2.0.0 <2.1.7: ^2.1.7
|
||||
brace-expansion@>=4.0.0 <5.0.12: ^5.0.12
|
||||
undici@>=7.0.0 <7.29.1: ^7.29.1
|
||||
undici@>=7.0.0 <7.30.1: ^7.30.1
|
||||
vite@>=8.0.0 <8.1.6: ^8.1.6
|
||||
|
||||
patchedDependencies:
|
||||
@@ -3730,8 +3730,8 @@ packages:
|
||||
bn.js@4.12.3:
|
||||
resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==}
|
||||
|
||||
body-parser@2.3.0:
|
||||
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
|
||||
body-parser@2.4.0:
|
||||
resolution: {integrity: sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
bowser@2.14.1:
|
||||
@@ -4555,8 +4555,8 @@ packages:
|
||||
fast-string-width@3.0.2:
|
||||
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
|
||||
|
||||
fast-uri@3.1.8:
|
||||
resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
|
||||
fast-uri@3.1.9:
|
||||
resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==}
|
||||
|
||||
fast-wrap-ansi@0.2.2:
|
||||
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
|
||||
@@ -6199,6 +6199,10 @@ packages:
|
||||
resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
qs@6.16.1:
|
||||
resolution: {integrity: sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -6960,8 +6964,8 @@ packages:
|
||||
undici-types@8.9.0:
|
||||
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
|
||||
|
||||
undici@7.30.0:
|
||||
resolution: {integrity: sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==}
|
||||
undici@7.30.1:
|
||||
resolution: {integrity: sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
undici@8.12.3:
|
||||
@@ -7991,7 +7995,7 @@ snapshots:
|
||||
open: 8.4.2
|
||||
picomatch: 4.0.7
|
||||
systeminformation: 5.33.15
|
||||
undici: 7.30.0
|
||||
undici: 7.30.1
|
||||
which: 4.0.0
|
||||
yocto-spinner: 1.2.2
|
||||
|
||||
@@ -9986,7 +9990,7 @@ snapshots:
|
||||
stringify-object: 5.0.0
|
||||
tsconfig-paths: 4.2.0
|
||||
type-fest: 4.41.0
|
||||
undici: 7.30.0
|
||||
undici: 7.30.1
|
||||
zod: 4.6.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -10740,7 +10744,7 @@ snapshots:
|
||||
ajv@8.20.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.8
|
||||
fast-uri: 3.1.9
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
@@ -10966,7 +10970,7 @@ snapshots:
|
||||
|
||||
bn.js@4.12.3: {}
|
||||
|
||||
body-parser@2.3.0(supports-color@7.2.0):
|
||||
body-parser@2.4.0(supports-color@7.2.0):
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 2.1.0
|
||||
@@ -10974,7 +10978,7 @@ snapshots:
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.3
|
||||
on-finished: 2.4.1
|
||||
qs: 6.16.0
|
||||
qs: 6.16.1
|
||||
raw-body: 3.0.2
|
||||
type-is: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -11878,7 +11882,7 @@ snapshots:
|
||||
express@5.2.1(supports-color@7.2.0):
|
||||
dependencies:
|
||||
accepts: 2.0.0
|
||||
body-parser: 2.3.0(supports-color@7.2.0)
|
||||
body-parser: 2.4.0(supports-color@7.2.0)
|
||||
content-disposition: 1.1.0
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.2
|
||||
@@ -11962,7 +11966,7 @@ snapshots:
|
||||
dependencies:
|
||||
fast-string-truncated-width: 3.0.3
|
||||
|
||||
fast-uri@3.1.8: {}
|
||||
fast-uri@3.1.9: {}
|
||||
|
||||
fast-wrap-ansi@0.2.2:
|
||||
dependencies:
|
||||
@@ -13471,6 +13475,11 @@ snapshots:
|
||||
es-define-property: 1.0.1
|
||||
side-channel: 1.1.1
|
||||
|
||||
qs@6.16.1:
|
||||
dependencies:
|
||||
es-define-property: 1.0.1
|
||||
side-channel: 1.1.1
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
radix-ui@1.8.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1):
|
||||
@@ -13918,7 +13927,7 @@ snapshots:
|
||||
stringify-object: 5.0.0
|
||||
ts-morph: 26.0.0
|
||||
tsconfig-paths: 4.2.0
|
||||
undici: 7.30.0
|
||||
undici: 7.30.1
|
||||
validate-npm-package-name: 7.0.2
|
||||
zod: 4.6.7
|
||||
transitivePeerDependencies:
|
||||
@@ -14474,7 +14483,7 @@ snapshots:
|
||||
|
||||
undici-types@8.9.0: {}
|
||||
|
||||
undici@7.30.0: {}
|
||||
undici@7.30.1: {}
|
||||
|
||||
undici@8.12.3: {}
|
||||
|
||||
|
||||
+9
-5
@@ -30,7 +30,9 @@ overrides:
|
||||
# for.
|
||||
"@hono/node-server@<1.19.15": "^1.19.15"
|
||||
"picomatch@<4.0.4": "^4.0.4"
|
||||
"body-parser@<2.3.0": "^2.3.0"
|
||||
# CVE-2026-111181 (HIGH) raised the floor from 2.3.0. The copy arrives
|
||||
# under express in the MCP SDK, on a production path.
|
||||
"body-parser@<2.4.0": "^2.4.0"
|
||||
# CVE-2026-93749 (source-map-js < 1.2.2, HIGH, under postcss and the
|
||||
# Tailwind build), CVE-2026-104844 (postcss-selector-parser < 7.1.6, under
|
||||
# shadcn) and GHSA-r4xh-jqrq-34v2 (smol-toml < 1.9.0, under knip). All three
|
||||
@@ -46,8 +48,9 @@ overrides:
|
||||
# malformed authorities. It arrives under ajv, including the connector SDK's
|
||||
# copy, so it sits on a path this app exposes.
|
||||
# Authority injection through an unvalidated port in serialize, and a
|
||||
# follow-up fixed in 3.1.8, raised the floor from 3.1.6.
|
||||
"fast-uri@<3.1.8": "^3.1.8"
|
||||
# follow-up fixed in 3.1.8, raised the floor from 3.1.6. CVE-2026-110180 and
|
||||
# CVE-2026-110306 raised it again to 3.1.9.
|
||||
"fast-uri@<3.1.9": "^3.1.9"
|
||||
"flatted@<3.4.2": "^3.4.2"
|
||||
# GHSA-jqcg-44mw-7w3h: IP spoofing through IPv4-mapped addresses. It
|
||||
# arrives under express in the MCP SDK; the app resolves client IPs itself,
|
||||
@@ -100,12 +103,13 @@ overrides:
|
||||
|
||||
# Development-only copies (test containers, build tooling) that the
|
||||
# dependency alerts flag: gRPC under the test containers, brace-expansion on
|
||||
# three major lines, and undici 7 next to the app's own undici 8.
|
||||
# three major lines, and undici 7 next to the app's own undici 8
|
||||
# (CVE-2026-109869, CVE-2026-109841 and CVE-2026-109924 raised it to 7.30.1).
|
||||
"@grpc/grpc-js@>=1.14.0 <1.14.5": "^1.14.5"
|
||||
"brace-expansion@<1.1.21": "^1.1.21"
|
||||
"brace-expansion@>=2.0.0 <2.1.7": "^2.1.7"
|
||||
"brace-expansion@>=4.0.0 <5.0.12": "^5.0.12"
|
||||
"undici@>=7.0.0 <7.29.1": "^7.29.1"
|
||||
"undici@>=7.0.0 <7.30.1": "^7.30.1"
|
||||
|
||||
# GHSA-587h-7wmj-h5pm (HIGH), GHSA-rq7h-c2jc-7f22, GHSA-r825-w8mm-5pxw,
|
||||
# GHSA-698v-4r3x-3vcv and GHSA-9jrq-w75r-8gcw: dev-server file serving,
|
||||
|
||||
+62
-9
@@ -225,11 +225,15 @@ function randomWalk(
|
||||
// fresh seed — iOS #33); clamping every step keeps the series inside the
|
||||
// same band the input validator enforces.
|
||||
clamp?: { min: number; max: number },
|
||||
// How hard each step pulls back towards the target. The default lets a
|
||||
// series wander; blood pressure needs a firmer pull, or a seed could end
|
||||
// several mmHg above the settled band its stored texts describe.
|
||||
reversion = 0.03,
|
||||
): number[] {
|
||||
const values: number[] = [start];
|
||||
for (let i = 1; i < days; i++) {
|
||||
const prev = values[i - 1];
|
||||
const drift = (target - prev) * 0.03; // mean reversion
|
||||
const drift = (target - prev) * reversion;
|
||||
const noise = (Math.random() - 0.5) * volatility;
|
||||
let next = Math.round((prev + drift + noise) * 10) / 10;
|
||||
if (clamp) next = Math.min(clamp.max, Math.max(clamp.min, next));
|
||||
@@ -397,6 +401,23 @@ async function bulkInsert(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A busy stretch inside the 90-day window, in days before "now": blood
|
||||
* pressure and pulse rise and settle again, and the mood dips with them.
|
||||
* Without it every short-window series was flat noise around its target and a
|
||||
* 7-day trend line over 30 days drew a straight line; with it the line has a
|
||||
* shape to follow. It ends well before today, so the latest readings, the
|
||||
* tiles and the stored texts still describe a settled, optimal band.
|
||||
*/
|
||||
const BUSY_STRETCH = { from: 27, to: 12 } as const;
|
||||
|
||||
/** 0 outside the busy stretch, rising smoothly to 1 at its middle. */
|
||||
function busyStretch(daysBefore: number): number {
|
||||
const { from, to } = BUSY_STRETCH;
|
||||
if (daysBefore > from || daysBefore < to) return 0;
|
||||
return Math.sin((Math.PI * (from - daysBefore)) / (from - to)) ** 2;
|
||||
}
|
||||
|
||||
/** Pollen season bump: 0 outside `[from, to]`, peaking at `peak` mid-season. */
|
||||
function pollenSeason(
|
||||
doy: number,
|
||||
@@ -563,6 +584,7 @@ export async function seed() {
|
||||
span,
|
||||
3,
|
||||
VALUE_RANGES.BLOOD_PRESSURE_SYS,
|
||||
0.1,
|
||||
);
|
||||
// Diastolic BP: 82 → settled ~76 (clamped)
|
||||
const diaBP = randomWalk(
|
||||
@@ -571,6 +593,7 @@ export async function seed() {
|
||||
span,
|
||||
2.5,
|
||||
VALUE_RANGES.BLOOD_PRESSURE_DIA,
|
||||
0.1,
|
||||
);
|
||||
// Spot pulse (daytime heart rate): ~72, gently lower
|
||||
const pulse = randomWalk(74, 70, span, 3, VALUE_RANGES.PULSE);
|
||||
@@ -597,9 +620,35 @@ export async function seed() {
|
||||
);
|
||||
// Steps: 6500 → a solid ~9000
|
||||
const steps = randomWalk(6500, 9000, span, 1500);
|
||||
for (const series of [weights, sysBP, diaBP, pulse, restingHr]) {
|
||||
// The busy stretch lifts blood pressure and pulse, and weekends read a
|
||||
// touch higher than weekdays, as home readings tend to.
|
||||
for (let i = 0; i < span; i++) {
|
||||
const before = days - i;
|
||||
const weekday = daysAgo(before).getDay();
|
||||
const weekend = weekday === 0 || weekday === 6 ? 1 : 0;
|
||||
const busy = busyStretch(before);
|
||||
const lift = (
|
||||
series: number[],
|
||||
by: number,
|
||||
range: { min: number; max: number },
|
||||
) => {
|
||||
series[i] = Math.min(
|
||||
range.max,
|
||||
Math.max(range.min, Math.round((series[i] + by) * 10) / 10),
|
||||
);
|
||||
};
|
||||
lift(sysBP, busy * 12 + weekend * 2, VALUE_RANGES.BLOOD_PRESSURE_SYS);
|
||||
lift(diaBP, busy * 7 + weekend * 1, VALUE_RANGES.BLOOD_PRESSURE_DIA);
|
||||
lift(pulse, busy * 6, VALUE_RANGES.PULSE);
|
||||
}
|
||||
for (const series of [weights, restingHr]) {
|
||||
settleLatest(series);
|
||||
}
|
||||
// Today's reading settles on the days after the busy stretch only, so it
|
||||
// describes the band the series came back to.
|
||||
for (const series of [sysBP, diaBP, pulse]) {
|
||||
settleLatest(series, BUSY_STRETCH.to - 1);
|
||||
}
|
||||
|
||||
// Each series carries the source that writes it on a real account, the
|
||||
// way the integration writes it: the Withings scale and cuff (weight,
|
||||
@@ -1580,7 +1629,9 @@ export async function seed() {
|
||||
if (Math.random() < 0.05) continue;
|
||||
|
||||
const date = daysAgo(days - i);
|
||||
const rawScore = Math.round(Math.min(5, Math.max(1, moodTrend[i])));
|
||||
// The busy stretch costs about a point of mood at its height.
|
||||
const moodScore = moodTrend[i] - 1.2 * busyStretch(days - i);
|
||||
const rawScore = Math.round(Math.min(5, Math.max(1, moodScore)));
|
||||
const mood = moodOptions[5 - rawScore]; // invert: score 5 = SUPER_GUT
|
||||
const tags = tagOptions[5 - rawScore];
|
||||
const loggedAt = new Date(date);
|
||||
@@ -3302,8 +3353,9 @@ export async function seed() {
|
||||
content: "How has my blood pressure been trending lately?",
|
||||
},
|
||||
answer(
|
||||
"Your blood pressure has been settling nicely. Over the last few " +
|
||||
"weeks the morning readings have drifted into the optimal band, and " +
|
||||
"Your blood pressure has been settling nicely. After a higher " +
|
||||
"stretch about three weeks ago, the morning readings have come back " +
|
||||
"down into the optimal band, and " +
|
||||
"your resting heart rate is sitting in a comfortable range too. " +
|
||||
"Keeping up the steady sleep and daily movement looks like it is " +
|
||||
"paying off — worth staying consistent with the morning measurements " +
|
||||
@@ -3675,10 +3727,11 @@ export async function seed() {
|
||||
{
|
||||
scope: "blood-pressure",
|
||||
text:
|
||||
"Your blood pressure is averaging about 118/76 over the last few " +
|
||||
"weeks — down roughly 10 mmHg systolic from where this quarter " +
|
||||
"started, and now comfortably inside the optimal band. That is a " +
|
||||
"real, earned shift, not day-to-day noise. Keeping the morning " +
|
||||
"Your blood pressure is averaging about 118/76 over the last two " +
|
||||
"weeks, back down after a busier stretch before that. It sits " +
|
||||
"roughly 10 mmHg systolic below where this quarter started, " +
|
||||
"comfortably inside the optimal band. That is a real, earned " +
|
||||
"shift, not day-to-day noise. Keeping the morning " +
|
||||
"readings on a steady schedule will make the trend even easier to " +
|
||||
"follow.",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* The demo's short-window charts need a shape to show. Every 90-day series in
|
||||
* the seed is a mean-reverting walk, and around a settled target that is flat
|
||||
* noise: a 7-day trend line over the last 30 days of blood pressure drew a
|
||||
* straight line, which is exactly the view the demo exists to show off. The
|
||||
* seed now carries a busy stretch a few weeks back that lifts blood pressure
|
||||
* and lets it settle again.
|
||||
*
|
||||
* This runs the real `scripts/seed-demo.ts` and holds the last 30 days of
|
||||
* systolic readings to two things at once: the 7-day mean moves visibly, and
|
||||
* today's reading has come back down from the stretch. Before the stretch the
|
||||
* swing measured 2 to 3 mmHg; with it, about 8 to 14. The seed is random, so the
|
||||
* bounds sit between the two rather than at either.
|
||||
*/
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
let systolic: number[];
|
||||
let means: number[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const prisma = getPrismaClient();
|
||||
await truncateAllTables(prisma);
|
||||
const { seed } = await import("../../scripts/seed-demo");
|
||||
await seed();
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: { username: "demo" },
|
||||
select: { id: true },
|
||||
});
|
||||
const since = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000);
|
||||
const rows = await prisma.measurement.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: "BLOOD_PRESSURE_SYS",
|
||||
source: "WITHINGS",
|
||||
deletedAt: null,
|
||||
measuredAt: { gte: since },
|
||||
},
|
||||
orderBy: { measuredAt: "asc" },
|
||||
select: { value: true },
|
||||
});
|
||||
systolic = rows.map((r) => Number(r.value));
|
||||
means = [];
|
||||
for (let i = 6; i < systolic.length; i++) {
|
||||
const week = systolic.slice(i - 6, i + 1);
|
||||
means.push(week.reduce((a, b) => a + b, 0) / week.length);
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
describe("demo seed, short-window shape", () => {
|
||||
it("gives the 7-day systolic mean over 30 days a visible rise and fall", () => {
|
||||
expect(systolic.length).toBeGreaterThanOrEqual(28);
|
||||
const swing = Math.max(...means) - Math.min(...means);
|
||||
expect(swing).toBeGreaterThanOrEqual(6);
|
||||
});
|
||||
|
||||
it("brings today's systolic reading back down from the stretch", () => {
|
||||
expect(systolic.at(-1)).toBeLessThanOrEqual(Math.max(...means) - 3);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user