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

+123 -26
View File
@@ -4,13 +4,15 @@ import { renderToStaticMarkup } from "react-dom/server";
import PrivacyPage from "../page";
/**
* v1.4.26 — Public privacy-policy page contract.
* v1.4.40 W-PRIVACY (SB-3) — Bilingual public privacy-policy contract.
*
* The page is the App-Store-submission blocker (Apple BLOCKER 2 of 5).
* These tests pin the structural commitments that any future redesign
* must preserve:
*
* 1. The H1 says "Privacy Policy".
* 1. The H1 carries both the German and English title so an Apple US
* reviewer AND a German DPA reviewer recognise the document
* without locale switching.
* 2. The page advertises a "Last updated" date alongside a policy
* version stamp — App-Store reviewers and German DPA inspectors
* use both to gauge currency.
@@ -18,31 +20,42 @@ import PrivacyPage from "../page";
* enumerated verbatim by identifier. Drift from the iOS source
* (`HealthKitWireConverter.swift` + `HealthKitService.swift`)
* would be a compliance hole.
* 4. The numbered section headings are all present, so the policy
* remains scannable without re-rendering with a specific locale.
* 4. The numbered section headings are all present (English form, so
* the scannable spine matches the in-app `Settings → Privacy`
* link target and the table of contents above each section).
* 5. v1.4.40 SB-3 additions — consent receipt endpoint, TLS 1.3,
* operator email + deletion-route disclosure, retention windows.
*/
function render() {
return renderToStaticMarkup(<PrivacyPage />);
}
const POLICY_VERSION = "1.4.40";
const LAST_UPDATED = "2026-05-18";
describe("<PrivacyPage>", () => {
it("renders without crashing and includes the H1", () => {
it("renders without crashing and includes the bilingual H1", () => {
const html = render();
// Single bilingual title — DE + EN side by side so a German DPA
// and an Apple US reviewer both see the document is for them.
expect(html).toContain("Datenschutzerklärung");
expect(html).toContain("Privacy Policy");
});
it("advertises the policy version + last-updated date", () => {
const html = render();
expect(html).toContain("Policy version 1.4.26");
expect(html).toContain("Last updated: 2026-05-15");
expect(html).toContain(`Policy version ${POLICY_VERSION}`);
expect(html).toContain(`Last updated: ${LAST_UPDATED}`);
expect(html).toContain(`Stand: ${LAST_UPDATED}`);
expect(html).toContain('data-slot="privacy-last-updated"');
});
it("renders the numbered section headings", () => {
const html = render();
// 11 numbered sections — H2 headings render the leading numeral so
// the reader can navigate without an explicit table of contents.
// 11 numbered sections — H2 headings carry the English form as the
// canonical scan spine, with the German title appearing inline in
// each section body. The TOC at the top mirrors both.
const expectedHeadings = [
"1. Overview",
"2. Data we collect",
@@ -61,17 +74,36 @@ describe("<PrivacyPage>", () => {
}
});
it("renders the bilingual section structure with DE + EN bodies", () => {
const html = render();
// Each section ships a `data-slot="privacy-section-de"` body and a
// collapsible `data-slot="privacy-section-en"` translation. 11
// sections total.
const deSlotCount = (
html.match(/data-slot="privacy-section-de"/g) ?? []
).length;
const enSlotCount = (
html.match(/data-slot="privacy-section-en"/g) ?? []
).length;
expect(deSlotCount).toBe(11);
expect(enSlotCount).toBe(11);
// The English translation `<details>` carries the "English
// translation" summary label so a reviewer can find it without
// guessing.
expect(html).toContain("English translation");
// The German bodies are `lang="de"` so screen readers switch
// pronunciation correctly.
expect(html).toContain('lang="de"');
expect(html).toContain('lang="en"');
});
it("enumerates every HealthKit quantity type read by the iOS app", () => {
const html = render();
// Source of truth: HealthKitService.defaultReadTypes +
// HealthKitWireConverter.preferredUnit in the iOS repo. Every
// identifier the iOS app reads must appear verbatim so an App
// Store reviewer can confirm coverage. The list must stay sorted
// by category in the page itself (vitals, cardio, activity,
// audio, daylight) — that's a UX commitment, not a test
// commitment; we just check membership here.
// Store reviewer can confirm coverage.
const requiredIdentifiers = [
// Vitals (point metrics)
"bodyMass",
"bodyFatPercentage",
"bodyTemperature",
@@ -79,20 +111,16 @@ describe("<PrivacyPage>", () => {
"bloodPressureDiastolic",
"bloodGlucose",
"oxygenSaturation",
// Cardio
"heartRate",
"restingHeartRate",
"heartRateVariabilitySDNN",
"vo2Max",
// Activity
"stepCount",
"activeEnergyBurned",
"flightsClimbed",
"distanceWalkingRunning",
// Audio exposure
"environmentalAudioExposure",
"headphoneAudioExposure",
// Time in Daylight (iOS 17+)
"timeInDaylight",
];
expect(
@@ -110,9 +138,6 @@ describe("<PrivacyPage>", () => {
it("lists sleepAnalysis as the sole HKCategoryTypeIdentifier read", () => {
const html = render();
expect(html).toContain("sleepAnalysis");
// Per-stage breakdown is the differentiator vs. a single sleep
// duration — name the four stage labels so the disclosure stays
// explicit.
expect(html).toContain("Awake");
expect(html).toContain("REM");
expect(html).toContain("Core");
@@ -142,24 +167,96 @@ describe("<PrivacyPage>", () => {
it("states the EU MDR medical-device boundary verbatim", () => {
const html = render();
expect(html).toContain("not a medical device");
expect(html).toContain("kein Medizinprodukt");
expect(html).toContain("2017/745");
expect(html).toContain("MDCG 2021-24");
});
it("links to the GDPR-request contact channel without exposing PII", () => {
it("discloses the HealthKit data-flow path (HK sample → HTTPS → server)", () => {
const html = render();
// SB-3 requirement 2: how HealthKit samples reach the server.
expect(html).toContain("POST /api/measurements");
expect(html).toContain("TLS 1.3");
});
it("discloses AI off-device transit with the named providers and consent gate", () => {
const html = render();
// SB-3 requirement 3: which AI providers receive prompts and the
// explicit guarantees (no raw HK identifiers, off by default,
// encrypted in transit).
expect(html).toContain("off by default");
expect(html).toContain("never includes raw HealthKit identifiers");
expect(html).toContain("Anthropic");
expect(html).toContain("OpenAI");
});
it("links the consent receipt endpoint with the retention window", () => {
const html = render();
// SB-3 requirement 4: consent receipt persistence — endpoint +
// retention period.
expect(html).toContain("GET /api/account/consents");
expect(html).toContain("consent receipt");
expect(html).toContain("five years");
});
it("declares encryption in transit (TLS 1.3 + HSTS + cert pinning)", () => {
const html = render();
// SB-3 requirement 5.
expect(html).toContain("TLS 1.3");
expect(html).toContain("HSTS");
// Cert pinning is described in two languages; both bodies mention
// the practice but with different surrounding whitespace, so check
// a tolerant regex.
expect(html).toMatch(/certificate\s+pinning/i);
expect(html).toMatch(/Zertifikat-Pinning/);
});
it("states the server location and operator framing", () => {
const html = render();
// SB-3 requirement 6: operated by an individual in Germany on
// Hetzner; no third-party hosting except AI providers.
expect(html).toContain("Germany");
expect(html).toContain("Hetzner");
expect(html).toContain("operator");
});
it("publishes the retention defaults (5y / 90d / 30d APNs)", () => {
const html = render();
// SB-3 requirement 7.
expect(html).toContain("5-year window");
expect(html).toContain("Audit logs: 90 days");
expect(html).toContain("Failed APNs deliveries: 30 days");
});
it("discloses the GDPR Art. 17 deletion route + cascade behaviour", () => {
const html = render();
// SB-3 requirement 8: Settings → Daten → Konto löschen cascades
// through User.delete + onDelete: Cascade.
expect(html).toContain("Konto löschen");
expect(html).toContain("Delete account");
expect(html).toContain("User.delete");
expect(html).toContain("onDelete: Cascade");
});
it("publishes the operator email + GitHub fallback channel", () => {
const html = render();
// SB-3 requirement 9: operator contact. The body uses the
// "operator" framing per the v1.4.20 PII guidance — only the
// email address attaches the actual contact.
expect(html).toContain("mailto:mbombeck@gmail.com");
expect(html).toContain("github.com/MBombeck/HealthLog/issues");
// Marc-Voice rule: no personal name in user-facing artefacts. The
// policy must speak in terms of "the operator" / "the maintainer"
// not Marc's full name.
// policy speaks in terms of "the operator" / "the controller"
// not Marc's full name. Only the bare email address attaches the
// contact route.
expect(html).not.toMatch(/Marc[- ]?Andr/i);
expect(html).not.toContain("Bombeck ");
expect(html).not.toMatch(/\bBombeck\s+[A-Z]/);
});
it("renders a discoverable footer with the version and license", () => {
const html = render();
expect(html).toContain('data-slot="privacy-footer"');
expect(html).toContain("AGPL-3.0");
expect(html).toContain("1.4.26");
expect(html).toContain(POLICY_VERSION);
});
});
+1646 -710
View File
File diff suppressed because it is too large. Load diff