Files
HealthLog/src/lib/db.ts
T
Marc-André Bombeck 1114cf7e63 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.
2026-05-21 12:48:02 +02:00

58 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Prisma, PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
/**
* v1.4.40 W-POOL — Prisma `pg.Pool` ceiling raised from the library
* default of 10 → 20.
*
* The v1.4.39 empirical cold-mount trace
* (`.planning/round-v1439-empirical-trace.md` § B2) showed thick
* `/api/analytics` holding ≥ 8 of the 10 default pool slots for
* 6.5 s on Marc's 347k-row tenant, starving every other Wave-B and
* Wave-C useQuery fan-out for the duration. Coolify's Postgres
* container runs the stock `max_connections` 100 ceiling, so a
* 20-slot Node-side pool sits well under the server's hard cap with
* plenty of headroom for concurrent power-users (≤ 5 simultaneous
* tenants × 20 = 100). Complements the W-POOL `p-limit(4)` cap on
* the analytics fan-out itself — the bounded concurrency keeps any
* single analytics call to ≤ 4 slots, so the remaining 16 stay
* available for every other dashboard query.
*
* Env-overridable via `DATABASE_POOL_MAX` so an operator running
* with a smaller Postgres `max_connections` can dial down without a
* code change.
*/
export function getPoolMax(): number {
const raw = process.env.DATABASE_POOL_MAX;
if (raw) {
const parsed = Number.parseInt(raw, 10);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
}
return 20;
}
function createPrismaClient() {
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
max: getPoolMax(),
});
return new PrismaClient({ adapter });
}
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
// Prisma's `InputJsonValue` requires an explicit index signature that
// typed application shapes (Zod-validated, hand-written interfaces)
// don't carry. Every JSON-column write would otherwise repeat the same
// `value as unknown as Prisma.InputJsonValue` escape hatch — this one
// helper centralises the cast so the WHY stays in a single place.
export const toJson = <T>(v: T) => v as unknown as Prisma.InputJsonValue;