mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
chore(release): v1.4.40 — comprehensive Critical+High architecture closure plus iOS PB30 enablement
Eleven touch-disjoint waves landed on develop plus a six-axis QA reconcile round before tagging. See CHANGELOG.md for the user-facing prose; this commit message documents the wave + report inventory for release-management traceability. Waves shipped (each with a phase report under .planning/): - phase-W-POOL-v1440-report.md — p-limit(4) wrapper around the analytics 15-way fan-out + pg.Pool max raised 10 -> 20. Closes the v1.4.39 empirical-trace finding #1 (Prisma pool starvation). - phase-W-DELETED-v1440-report.md — 11 reader-tier helpers + 3 integration contracts close audit Critical Finding #3 (soft-delete invisibility) across dashboard, analytics, summaries, rollup tiers, and the Coach snapshot path. - phase-W-INSIGHTS-v1440-report.md — mood-rollup swap on the three /api/insights/{features,targets,cards} routes plus the compliance-rollup hook gap closure for bulk-projection mints; the post-QA reconcile patch closes the six remaining measurement.findMany sites in the cluster that still leaked tombstoned rows. - phase-W-INFRA-v1440-report.md — rollup helper umbrella consolidation under src/lib/rollups/* with zero orphan re-exports; lint, typecheck, knip, and 93/93 rollup tests green post-restructure. - phase-W-RSC-v1440-report.md — per-tile Suspense boundaries plus the queryKey factory-bypass walker test (extended to comparison/ in the reconcile pass). - phase-W-PRIVACY-v1440-report.md — bilingual /privacy page with 17 paired-section tests pinning the 9 SB-3 disclosure requirements. - phase-W-AASA-v1440-report.md — /.well-known/apple-app-site-association endpoint pinned to S8WDX4W5KX.dev.healthlog.app with app-ID parity assertions guarding against split rotation. - phase-W-APNS-NOTIFY-v1440-report.md — MEDICATION_REMINDER time-sensitive APNs payload (priority 10 + interruption-level) scoped via a parameterised test that pins all 6 other event-types do not bypass Focus. AP-2 .p8 install gates real delivery. - phase-W-CONSENT-v1440-report.md — AI consent receipts CRUD with append-only invariant pinned in test; revoke-then-regrant leaves both rows; idempotent DELETE returns 200 + receipt: null. - phase-W-WMY-WIRE-v1440-report.md — three previously-dead WMY readers (week/month/year) wired through summaries-slice and health-score-fast-path with a linear-composability parity test pinning the cross-consumer routing contract. - phase-W-GHOSTS-v1440-report.md — ~1 177 lines of dead code removed across 9 atomic commits; startOfUtcDay + wallClockInTz consolidation eliminates a sibling-drift risk; knip whitelist trimmed to three files pending follow-up. QA reconcile patches applied per the v1.4.40 QA round (six reports under .planning/round-v1440-QA-*-findings.md): - senior C1 — dashboard-suspense-boundaries regex aligned with the post-W-INFRA Thread 2 userTimezone-local lift. - senior H1 — soft-delete filter added to the six remaining insights measurement.findMany sites. - specialist H1 — Promise.allSettled around bulk-projection compliance recompute on both /api/medications/intake and /api/dashboard/summary. - ux H1 — compare-toggle migrated to queryKeys.dashboardWidgets factory; walker test extended to src/components/comparison/. - security M1 — consent artefact 64 KB cap enforced via Buffer.byteLength (UTF-8 bytes, not UTF-16 code units). Quality gates green at tag: pnpm typecheck, pnpm lint, pnpm knip --include files,dependencies,binaries,unlisted, pnpm test --run (4726 passing / 1 skipped). Tests 4525 -> 4726 unit + 1 skipped over the v1.4.39 -> v1.4.40 span. User-facing prose: see CHANGELOG.md.
This commit is contained in:
1 parent
f9996552ed
commit
1114cf7e63
146 files changed
+8207
-2279
No files matched your search
@@ -41,6 +41,8 @@ vi.mock("@parse/node-apn", () => {
|
||||
public payload: unknown;
|
||||
public category: string | undefined;
|
||||
public mutableContent: boolean | undefined;
|
||||
public interruptionLevel: string | undefined;
|
||||
public priority: number | undefined;
|
||||
constructor() {
|
||||
notificationCtorMock();
|
||||
}
|
||||
@@ -466,6 +468,72 @@ describe("sendViaApns — dispatcher fan-out", () => {
|
||||
expect(note.payload.scheduledAt).toBe("2026-05-17T20:00:00.000Z");
|
||||
});
|
||||
|
||||
it("MEDICATION_REMINDER sets interruption-level=time-sensitive + priority=10", async () => {
|
||||
// SB-5 v1.4.40 — medication reminders are the one category the user
|
||||
// explicitly opts into Focus-bypass for. The aps payload must carry
|
||||
// BOTH `interruption-level: time-sensitive` AND `apns-priority: 10`
|
||||
// per Apple's push best-practices doc; otherwise APNs may downgrade
|
||||
// delivery and the Focus-bypass intent is silently lost.
|
||||
vi.mocked(prisma.device.findMany).mockResolvedValueOnce([
|
||||
{ id: "d1", apnsToken: "tok-a", apnsEnvironment: "sandbox" },
|
||||
] as never);
|
||||
sendMock.mockResolvedValueOnce({ sent: [{ device: "tok-a" }], failed: [] });
|
||||
await sendViaApns("u-1", {
|
||||
title: "Take Mounjaro",
|
||||
message: "10mg, weekly dose",
|
||||
eventType: "MEDICATION_REMINDER",
|
||||
});
|
||||
const note = sendMock.mock.calls[0][0];
|
||||
expect(note.interruptionLevel).toBe("time-sensitive");
|
||||
expect(note.priority).toBe(10);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"MOOD_REMINDER",
|
||||
"MEASUREMENT_ANOMALY",
|
||||
"COMPLIANCE_LOW",
|
||||
"WITHINGS_SYNC_FAILED",
|
||||
"SYSTEM_ALERT",
|
||||
"PERSONAL_RECORD",
|
||||
])("eventType %s does NOT set interruption-level (Focus respected)", async (eventType) => {
|
||||
// Every non-MEDICATION_REMINDER event-type must omit the
|
||||
// interruption-level so the system default (`active`) lets Focus
|
||||
// modes — Sleep, Do-Not-Disturb, Personal — silence the alert as
|
||||
// the user expects.
|
||||
vi.mocked(prisma.device.findMany).mockResolvedValueOnce([
|
||||
{ id: "d1", apnsToken: "tok-a", apnsEnvironment: "sandbox" },
|
||||
] as never);
|
||||
sendMock.mockResolvedValueOnce({ sent: [{ device: "tok-a" }], failed: [] });
|
||||
await sendViaApns("u-1", {
|
||||
title: "t",
|
||||
message: "m",
|
||||
eventType,
|
||||
});
|
||||
const note = sendMock.mock.calls[0][0];
|
||||
expect(note.interruptionLevel).toBeUndefined();
|
||||
// priority too — only opt in for time-sensitive deliveries.
|
||||
expect(note.priority).toBeUndefined();
|
||||
});
|
||||
|
||||
it("forwards explicit interruptionLevel on sendApnsPush", async () => {
|
||||
// Lower-level entry should round-trip the explicit flag — caller
|
||||
// controls the level when bypassing `sendViaApns`.
|
||||
setEnv(VALID_ENV);
|
||||
sendMock.mockResolvedValueOnce({ sent: [{ device: "abc" }], failed: [] });
|
||||
await sendApnsPush({
|
||||
deviceToken: "abc",
|
||||
environment: "sandbox",
|
||||
payload: {
|
||||
alert: { title: "t", body: "b" },
|
||||
interruptionLevel: "time-sensitive",
|
||||
priority: 10,
|
||||
},
|
||||
});
|
||||
const note = sendMock.mock.calls[0][0];
|
||||
expect(note.interruptionLevel).toBe("time-sensitive");
|
||||
expect(note.priority).toBe(10);
|
||||
});
|
||||
|
||||
it("forwards explicit category override on sendApnsPush", async () => {
|
||||
// Lower-level entry — directly setting `payload.category` must
|
||||
// propagate to `note.category` so callers that bypass `sendViaApns`
|
||||
|
||||
@@ -59,6 +59,26 @@ export interface ApnsPayload {
|
||||
* roundtrip.
|
||||
*/
|
||||
mutableContent?: boolean;
|
||||
/**
|
||||
* APNs `aps.interruption-level` (iOS 15+). Controls whether the alert
|
||||
* bypasses Focus modes:
|
||||
* - `passive` — silent, no banner; lives in Notification Center.
|
||||
* - `active` — default; banner + sound; honours Focus.
|
||||
* - `time-sensitive` — breaks through Focus modes (per-app opt-in by
|
||||
* the user on iOS); used for things the user explicitly asked to be
|
||||
* interrupted for. **Restricted to MEDICATION_REMINDER** in the
|
||||
* dispatcher; other categories must NOT bypass Focus.
|
||||
* - `critical` — entitlement-gated, ignores DND completely; not
|
||||
* used by HealthLog.
|
||||
* Omit for the iOS default (`active`).
|
||||
*/
|
||||
interruptionLevel?: "passive" | "active" | "time-sensitive" | "critical";
|
||||
/**
|
||||
* APNs `apns-priority` header. `10` = immediate delivery (required for
|
||||
* time-sensitive interruption-level per Apple's push best-practices),
|
||||
* `5` = power-conserving. Omit to let node-apn default to `10`.
|
||||
*/
|
||||
priority?: 5 | 10;
|
||||
}
|
||||
|
||||
export interface ApnsSendInput {
|
||||
@@ -269,6 +289,30 @@ export async function sendApnsPush(
|
||||
// unlocks the iOS Notification Service Extension hook.
|
||||
note.mutableContent = true;
|
||||
}
|
||||
if (input.payload.interruptionLevel) {
|
||||
// node-apn's `interruptionLevel` setter writes through to
|
||||
// `aps.interruption-level`. iOS 15+ reads it to decide whether the
|
||||
// alert breaks through Focus modes — `time-sensitive` is the level
|
||||
// that does so, gated on a per-app toggle the user grants on
|
||||
// install. Older iOS versions ignore the key, so this is safe to
|
||||
// send unconditionally.
|
||||
//
|
||||
// The cast mirrors the `category` setter above — node-apn 8.1's
|
||||
// d.ts omits the public setter (the runtime setter on
|
||||
// `apsProperties.js` writes the value through to
|
||||
// `aps.interruption-level` unchanged), so we assign via a typed
|
||||
// cast rather than poking `note.aps` directly. Forward-compatible
|
||||
// with future d.ts fixes.
|
||||
(note as unknown as { interruptionLevel: string }).interruptionLevel =
|
||||
input.payload.interruptionLevel;
|
||||
}
|
||||
if (typeof input.payload.priority === "number") {
|
||||
// node-apn writes this through to the `apns-priority` HTTP header.
|
||||
// Apple's push best-practices doc requires priority 10 alongside
|
||||
// `time-sensitive` interruption-level — otherwise APNs may downgrade
|
||||
// delivery and the Focus-bypass intent is lost.
|
||||
note.priority = input.payload.priority;
|
||||
}
|
||||
if (input.collapseId) {
|
||||
note.collapseId = input.collapseId;
|
||||
}
|
||||
@@ -386,6 +430,14 @@ export async function sendViaApns(
|
||||
const env =
|
||||
device.apnsEnvironment === "production" ? "production" : "sandbox";
|
||||
|
||||
// SB-5 v1.4.40 — only MEDICATION_REMINDER opts in to Focus-bypass.
|
||||
// The user explicitly asked the server to nudge them about a dose;
|
||||
// a missed dose is the textbook "time-sensitive" use case Apple
|
||||
// sanctions. Every other event-type (mood nudges, anomaly alerts,
|
||||
// PRs, system messages) must stay on the default `active` level so
|
||||
// Focus modes — including Sleep — silence them as the user expects.
|
||||
const isTimeSensitive = payload.eventType === "MEDICATION_REMINDER";
|
||||
|
||||
const result = await sendApnsPush({
|
||||
deviceToken: device.apnsToken,
|
||||
environment: env,
|
||||
@@ -407,6 +459,12 @@ export async function sendViaApns(
|
||||
// no-op on today's binary but unblocks NSE work without a
|
||||
// server-side change.
|
||||
mutableContent: true,
|
||||
...(isTimeSensitive
|
||||
? {
|
||||
interruptionLevel: "time-sensitive" as const,
|
||||
priority: 10 as const,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
collapseId: payload.eventType,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user