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:
Marc-André Bombeck committed 2026-05-21 12:48:02 +02:00
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`
+58
View File
@@ -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,
});