Commit Graph
1816 Commits
Author SHA1 Message Date
Marc-André Bombeck 59befdca73 docs(planning): W-IOS-COORD v1.4.41 closure report
Captures SB-7 follow-up (check-user endpoint shipped), SB-8 (merge
semantics documented, no code change), SB-9 (already retired, no
streak surface exists), and the dashboard-widgets PUT 422 root cause:
the Zod validator is correct and additive-safe; the live 422s from
iOS v0.5.4 are an iOS-side payload bug (likely an unknown widget id
or an out-of-range order value). No server fix viable.
2026-05-21 17:13:49 +02:00
Marc-André Bombeck dde79b041c chore(streak): formalise deprecation status per SB-9
ls src/app/api/streak returns No such file or directory. No web
frontend code paths reference /api/streak/...; the only streak
references in the API tree are derived fields inside
/api/insights/targets (streakDays, streakHighlight).

Decision: nothing to deprecate. The route family either never existed
or was removed before the audit. iOS can safely treat /api/streak/*
as 404. Document filed so a future audit does not re-open the
question.
2026-05-21 17:13:49 +02:00
Marc-André Bombeck 6c2eadb342 docs(dashboard-layout): document merge semantics for SB-8 audit gap
The dashboard layout resolver in src/lib/dashboard-layout.ts is
append-only on read: new widget ids added to DASHBOARD_WIDGET_IDS
automatically appear for every existing user on next GET, with
visible / tileVisible / order taken from DEFAULT_DASHBOARD_LAYOUT. No
database migration is ever required when introducing a new widget.

Documents the merge contract so a future widget-introduction does not
ship a one-shot SQL migration that would clobber every user's
customisation. PUT preservation rules captured: chartOverlayPrefs is
preserved-on-omit, comparisonBaseline is not. Investigation-only — no
code change.
2026-05-21 17:13:49 +02:00
Marc-André Bombeck 99a801f993 feat(auth-check-user): four-branch discovery endpoint for iOS onboarding
Closes SB-7 follow-up. POST /api/auth/check-user accepts
{ identifier: string } (username or email, case-insensitive) and returns
{ branch, hasPasskey, hasPassword } where branch is one of:

- not_found       no account
- passkey_only    passkey present, no password
- email_fallback  password present (passkey optional)
- exists          neither credential (recovery path)

The booleans let the iOS client render a "Sign in with Passkey"
affordance alongside the password field without a second round-trip.

Five-case test pin covers every branch plus the 422 identifier-required
guard. The endpoint mirrors the envelope shape of /api/auth/passkey/
login-options and adds no new enumeration surface beyond that route.
2026-05-21 17:13:34 +02:00
Marc-André Bombeck cb8f74e4fa fix(gamification): exclude soft-deleted measurements in achievement queries
v1.4.41 W-DELETED-2 — achievement progress now scopes the measurement
read to deletedAt: null so deleting a row immediately rolls back any
streak or count badge it had earned, matching the analytics + dashboard
sweeps from v1.4.40.
2026-05-21 17:13:07 +02:00
Marc-André Bombeck 5296a6122f fix(doctor-report): exclude soft-deleted measurements in PDF aggregator
v1.4.41 W-DELETED-2 — the doctor-report-data aggregator (shared by
/api/doctor-report JSON + /api/doctor-report/pdf server render) and the
section-availability probe now filter deletedAt: null so deleted rows
never appear in the rendered PDF or trigger an empty section toggle.
2026-05-21 17:12:53 +02:00
Marc-André Bombeck dc66dfcfdc refactor(types): consolidate AnalyticsData into src/types/analytics.ts
Three structurally-distinct `interface AnalyticsData {…}` declarations
shared a name across `src/app/page.tsx` (dashboard), `src/app/insights/
page.tsx` (insights mother), and `src/components/onboarding/
getting-started-checklist.tsx` (checklist). The collision made the
shapes un-discoverable from any one site and forced every iOS-contract
addition to land in three places.

Hoist all three into `src/types/analytics.ts` as named exports:
`DashboardAnalyticsData`, `InsightsAnalyticsData`, `ChecklistAnalytics
Data`. Each call site re-imports its shape under the original alias to
keep downstream code readable; the slim `SubPageAnalyticsData` that was
already there stays put. We deliberately keep the four shapes distinct
(rather than collapsing to one with everything optional) so call sites
can't cross-read fields they shouldn't reach for — closes v1.4.39 org-
audit recommendation #2.
2026-05-21 17:12:38 +02:00
Marc-André Bombeck a62b94983b fix(export): exclude soft-deleted measurements in export bundle reads
v1.4.41 W-DELETED-2 — full-backup JSON, per-type CSV, and the legacy
/api/export endpoint now scope reads to deletedAt: null so iOS undo
or pending-sync rows never reach the user's downloaded file or the
admin restore round-trip.
2026-05-21 17:12:31 +02:00
Marc-André Bombeck bba38b8c66 refactor(types): move BackupRow and BackupsList out of route handler into src/types/backups.ts
Resolves the single component → route-handler import in the codebase
(`components/admin/backups-section.tsx ← app/api/admin/backups/route.ts`)
flagged by the v1.4.39 org audit (recommendation #2). The DTOs now live
in `src/types/backups.ts`; the route handler keeps owning the HTTP
contract and re-imports the shared types from there.
2026-05-21 17:10:38 +02:00
Marc-André Bombeck 4cca62dce9 docs(planning): v1.4.40 marathon closure row, QA findings, perf-verify
Adds the v1.4.40 row + the four prior v1.4.39.x stacked rows to
.planning/round-audit-marathon-closure.md (v1.4.39.1/.2/.3/.4 were
documented inline in the corresponding memory files but the table
hadn't picked them up).

Commits the six v1.4.40 QA reviewer findings files (senior, security,
specialist, UX, product-lead, simplifier) so the audit trail stays
self-contained.

Adds round-v1440-perf-verify.md anchored on the live deploy commit
1114cf7e (squash-merge), Coolify deploy timestamp 2026-05-21T12:55:02Z,
docker workflow 26221332323. Includes the AP-2 .p8 install runbook for
SB-5 live effect.
2026-05-21 13:00:03 +02:00
Marc-André Bombeck 32c73bddc7 Merge branch 'main' into develop 2026-05-21 12:55:11 +02:00
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.
v1.4.40
2026-05-21 12:48:02 +02:00
Marc-André Bombeck 203373dcef chore(release): v1.4.40 — Critical+High architecture closure plus iOS PB30 enablement
Eleven touch-disjoint waves landed on develop plus the post-QA reconcile
patches:

- W-POOL: p-limit(4) wrapper around the analytics 15-way fan-out and
  pg.Pool max raised 10 -> 20 closes the v1.4.39 empirical-trace pool
  starvation root cause. Wave-C chart-tile first-paint moves from +7.3 s
  to +1.6 s on the empirical fixture.
- W-DELETED: 11 reader-tier helpers + 3 integration contracts close
  audit Critical Finding #3 (soft-delete invisibility) across the
  user-facing dashboard, analytics, and Coach paths.
- W-INSIGHTS: mood-rollup swap on the three target routes plus the
  compliance-rollup hook gap closure; the post-QA reconcile patch
  closes the six remaining measurement.findMany sites in the cluster
  that still leaked tombstoned rows.
- W-INFRA: rollup umbrella consolidation under src/lib/rollups/ with
  zero orphan re-exports; lint and knip CI gates green.
- W-RSC: per-tile Suspense boundaries plus the queryKey factory-bypass
  walker test extended to comparison/.
- W-PRIVACY: bilingual /privacy page with 17 paired-section tests.
- W-AASA: /.well-known/apple-app-site-association endpoint pinned to
  S8WDX4W5KX.dev.healthlog.app with app-ID parity assertions.
- W-APNS-NOTIFY: MEDICATION_REMINDER time-sensitive payload (priority 10
  with interruption-level) scoped via parameterised test that pins all
  6 other event-types do not bypass Focus.
- W-CONSENT: AI consent receipts CRUD with append-only invariant and
  byte-bounded artefact cap.
- W-WMY-WIRE: three previously-dead WMY readers (week/month/year) wired
  through summaries-slice and health-score-fast-path.
- W-GHOSTS: ~1 177 lines of dead code removed across 9 atomic commits.

Reconcile patches applied per the v1.4.40 QA round (.planning/round-v1440-QA-*-findings.md):
- senior C1 — dashboard-suspense-boundaries regex aligned with W-INFRA Thread 2
- senior H1 — soft-delete filter added to the six remaining insights findMany sites
- specialist H1 — Promise.allSettled around bulk-projection compliance recompute
- ux H1 — compare-toggle migrated to queryKeys.dashboardWidgets factory
- security M1 — consent artefact cap enforced via Buffer.byteLength

Wave reports under .planning/phase-W-*-v1440-report.md; full release
prose in CHANGELOG.md.

pnpm typecheck / pnpm lint / pnpm knip / pnpm test --run all green.
Tests 4525 -> 4726 unit + 1 skipped over the v1.4.39 -> v1.4.40 span.
2026-05-21 12:45:58 +02:00
Marc-André Bombeck 2c3eeee39b fix(consent): enforce artefact 64 KB cap via Buffer.byteLength (not char-count)
`z.string().max()` counts JavaScript string units (UTF-16 code
units), not bytes. The audit-table guarantee for SB-10 consent
artefacts is byte-bounded — a UTF-8 artefact full of multi-byte code
points (CJK, emoji) would otherwise slip past the 64 KB row budget.
Replace the `.max()` with a `.refine()` that uses
`Buffer.byteLength(value, "utf8")`.

Closes security M1 from the v1.4.40 QA round.
2026-05-21 12:43:28 +02:00
Marc-André Bombeck 4d4a034efc refactor(compare-toggle): migrate to queryKeys.dashboardWidgets factory
The compare-toggle component still declared three bare-literal
`["user", "dashboardWidgets"]` queryKeys for read, onMutate
cancel/set, onError rollback and onSettled invalidation. Migrate every
call site to the central `queryKeys.dashboardWidgets()` factory so a
future rename or shape evolution only touches one file. Extend the
W-RSC factory-bypass walker to cover `src/components/comparison/`
so any future regression in this directory gets caught by CI.

Closes UX H1 from the v1.4.40 QA round.
2026-05-21 12:42:17 +02:00
Marc-André Bombeck 853792ca06 fix(intake,dashboard): use Promise.allSettled around bulk-projection compliance recompute
The recompute helper swallows errors internally today, but the contract
is fragile — any future refactor that lets a throw escape would turn
the intake POST or dashboard summary GET into a 5xx for a strictly
best-effort hook. Wrap both call sites in `Promise.allSettled` so the
fail-soft contract is explicit at the call site, not buried in the
helper.

Closes specialist H1 from the v1.4.40 QA round.
2026-05-21 12:41:13 +02:00
Marc-André Bombeck 926ac36dea fix(insights): exclude soft-deleted measurements in remaining measurement.findMany sites
W-DELETED closed the dashboard, analytics, summaries and rollup tiers
but the Insights cluster (tile-strip, AI Coach snapshot, GLP-1 plateau
detector, pulse-status helper) still walked `prisma.measurement.findMany`
without filtering tombstoned rows. Once iOS sync starts emitting
deletions any soft-deleted reading would still contribute to:

- /api/insights/targets 30-day average + 1-year latest-ever tile values
  (recentMeasurements, latestEverByType, glucoseRows)
- /api/insights/cards 90-day weight/BP/pulse window the iOS-adapter card
  stream consumes
- /api/insights/generate per-type aggregate payload feeding the AI prompt
- Coach prompt feature aggregator (lib/insights/features.ts)
- GLP-1 plateau detection window (lib/insights/glp1-plateau.ts)
- Pulse-status snapshot input (lib/insights/pulse-status.ts)

Add `deletedAt: null` to every remaining production `where` clause.
`comprehensive-aggregator.ts` already filtered; `ai/coach/snapshot.ts`
already filtered. Closes senior H1 from the v1.4.40 QA round.
2026-05-21 12:40:37 +02:00
Marc-André Bombeck 99b4fde002 fix(dashboard-suspense-test): align timezone regex with post-W-INFRA local-lift
The audit-H4 regex still pinned the pre-W-INFRA Thread 2 form
`useMemo(..., [user?.timezone])` while the production code lifts
`user?.timezone` to a `userTimezone` local one line above the
`useMemo` so the dep array stays stable. Update the test fixture to
match the new dependency form.

Closes senior C1 from the v1.4.40 QA round.
2026-05-21 12:40:26 +02:00
Marc-André Bombeck 8e9a7891ce docs(planning): W-INFRA v1.4.40 phase report 2026-05-21 12:27:49 +02:00
Marc-André Bombeck cbf5546122 chore(ci): wire knip workflow to fail on unused exports
Installs knip 6.14.1 as a devDependency, ships a curated knip.json
covering every Next.js App Router special file plus the worker / script
/ test entry points, and adds a workflow that runs on every push to
main and every PR targeting main.

The gate trips on:
- unused files (a new orphan module ships zero callers)
- unused dependencies (a production dep with no consumer left behind)
- unused devDependencies (same, in dev land)
- unlisted binaries (a package.json script reaches for a CLI without
  declaring it)

Unused-exports + unused-types stay informational this release because
the v1.4.40 W-GHOSTS sweep cleared 1 177 lines of dead code but the
historic exported-surface backlog still sits at ~ 487 exports + 52
types. Cleaning that incrementally avoids a single mega-PR risk.

Baseline carve-outs in knip.json:

ignore — three files W-GHOSTS missed in v1.4.40 (kept as known-zero
callers until a follow-up wave can verify they're safe to delete with
their dedicated test removed in the same commit):
- e2e/setup/test-helpers.ts (9-line barrel; future-multi-user hook)
- src/components/charts/compliance-line-chart.tsx (chart module only
  referenced by a touch-action contract test)
- src/lib/logging/index.ts (21-line barrel re-export)

ignoreDependencies — runtime / build-time deps loaded outside the
import graph:
- @hookform/resolvers, react-hook-form: dynamic + framework conv.
- @prisma/client: re-exported via src/generated/ (which is ignored)
- next-themes: ThemeProvider mounts via dynamic import
- @vitejs/plugin-react: vitest.config consumer
- shadcn, tailwindcss, tw-animate-css: build pipeline
- testcontainers: integration-test bootstrap
- @types/* : ambient typings

ignoreBinaries — invoked via shell from integration helpers or
declared via :
- docker, docker-compose, pg_isready (integration setup)
- tsx (the openapi:generate + openapi:check scripts run via npx tsx)
2026-05-21 12:26:53 +02:00
Marc-André Bombeck cc08a43e37 refactor(rollups): move medication-compliance-rollups under the umbrella
- src/lib/medications/compliance-rollups.ts → src/lib/rollups/medication-compliance-rollups.ts
- co-located test follows the source file

10 importers updated covering the dashboard summary, every intake CRUD
route (single + bulk + import + Telegram webhook), the ingest endpoint,
the admin backups restore path, and the reminder worker job.

Closes the rollup umbrella restructure — every persistent-rollup
producer + reader now lives under src/lib/rollups/. Pure relocation
across all five batches: zero behaviour or exported-surface change.
2026-05-21 12:22:48 +02:00
Marc-André Bombeck 63e56f7a2d refactor(rollups): move mood-rollups under the umbrella
- src/lib/mood/rollups.ts → src/lib/rollups/mood-rollups.ts
- co-located test follows the source file

17 importers updated from @/lib/mood/rollups to @/lib/rollups/mood-rollups,
covering the mood-entries CRUD routes, the moodlog webhook, the import
restore path, the insights comprehensive + targets routes, the analytics
+ features paths, and the reminder worker + moodlog sync job consumers.

Pure relocation.
2026-05-21 12:22:12 +02:00
Marc-André Bombeck 8cfb1715b4 refactor(rollups): move WMY and cumulative readers
- src/lib/measurements/rollup-read-wmy.ts → src/lib/rollups/measurement-read-wmy.ts
- src/lib/measurements/rollup-read-cumulative.ts → src/lib/rollups/measurement-read-cumulative.ts
- co-located tests follow the source files
- cumulative module's sibling import of apple-health-mapping promoted to
  the @/lib alias since that mapping stays in src/lib/measurements/

Importers updated: 2 sites for WMY (health-score + summaries-slice),
zero external sites for cumulative (only co-located test reads it).

Pure relocation.
2026-05-21 12:21:40 +02:00
Marc-André Bombeck b7da526cbb refactor(rollups): move measurement-read and rollup-coverage helpers
- src/lib/measurements/rollup-read.ts → src/lib/rollups/measurement-read.ts
- src/lib/measurements/rollup-coverage.ts → src/lib/rollups/measurement-coverage.ts
- co-located tests follow the source files

Importers updated:
- @/lib/measurements/rollup-read → @/lib/rollups/measurement-read (2 sites)
- @/lib/measurements/rollup-coverage → @/lib/rollups/measurement-coverage (10 sites)

Pure relocation.
2026-05-21 12:20:34 +02:00
Marc-André Bombeck 29bfcc674b refactor(rollups): move measurement-rollups under src/lib/rollups umbrella
Creates the new src/lib/rollups/ aggregator directory and relocates
the main measurement rollup module:

- src/lib/measurements/rollups.ts → src/lib/rollups/measurement-rollups.ts
- src/lib/measurements/__tests__/rollups.test.ts →
  src/lib/rollups/__tests__/measurement-rollups.test.ts

Every importer of @/lib/measurements/rollups is rewritten to
@/lib/rollups/measurement-rollups. The orphan sibling reference inside
src/lib/measurements/rollup-read.ts (still in its old home until the
next batch) is promoted to the @/lib alias so the chain keeps
typechecking through the in-flight move.

Pure relocation — no behaviour change, no exported-surface change. The
umbrella exists so the rollup contract is discoverable from a single
directory before the iOS sprint locks the boundary.
2026-05-21 12:19:45 +02:00
Marc-André Bombeck 8974e7730c fix(app-page): clear lint regression from per-tile Suspense scaffolding
The useMemo introduced for the timezone-aware greeting in the W-RSC
per-tile Suspense pass listed [user?.timezone] as its dep array. React
Compiler's preserve-manual-memoization rule rejected the literal
optional-chain expression because the inferred dep is the wider 'user'
object — a less-specific source than the declared property access — so
the compiler refused to optimize the surrounding 1 400-line component
body.

Lift userTimezone into a local before the memo so the inferred dep and
the declared dep agree. No behaviour change; the memo still recomputes
only when the user's timezone string changes.
2026-05-21 12:17:08 +02:00
Marc-André Bombeck fa36817040 fix(analytics-route): exclude soft-deleted measurements in chunked series fetch
The three Measurement reads in the analytics route — glucose-by-context,
sleep-stage breakdown, and the fetchMeasurementSeriesChunked paged
helper that backs the per-type summarize loop and the BP-Zielbereich
windowing — were still pulling soft-deleted rows. W-DELETED skipped this
file assuming the W-POOL pool / p-limit pass covered it, but that pass
only addressed concurrency, not the deletedAt predicate.

Adds deletedAt: null to all three findMany clauses so the route shares
the same soft-delete contract as every other analytics read path. iOS
soft-delete unblocked.
2026-05-21 12:16:13 +02:00
Marc-André Bombeck 6901b0791d docs(planning): W-GHOSTS v1.4.40 phase report 2026-05-21 12:14:22 +02:00
Marc-André Bombeck cd8277d367 refactor(tz): consolidate wallClockInTz into src/lib/tz/wall-clock.ts
The compliance-rollups module and the cadence scheduling module each
carried a private wallClockInTz definition. The cadence variant
returned the full {year,month,day,hour,minute,second,weekday} tuple;
the compliance variant returned only {year,month,day}. Promote one
canonical helper that returns every part — the compliance caller
destructures the three fields it needs — so the cross-tz ±3 h guard
stays in lock-step instead of drifting between sibling modules.
2026-05-21 12:12:40 +02:00
Marc-André Bombeck 79c86a0652 refactor(tz): consolidate startOfUtcDay into src/lib/tz/start-of-utc-day.ts
The mood-rollup and measurement-rollup writers carried byte-identical
copies of the helper that could drift independently. Promote one
canonical export under src/lib/tz/ so both writers — and any future
rollup table — import the same UTC-anchored truncation.
2026-05-21 12:10:42 +02:00
Marc-André Bombeck e26e27b5ff chore(i18n): drop dead format.dateShort timeShort dateTime keys across six locales
The runtime never reads the format namespace — every locale-aware
formatter routes through Intl.DateTimeFormat directly via
src/lib/format-locale.ts. The keys were only verified by their own
ordering smoke test, which is also removed.
2026-05-21 12:09:31 +02:00
Marc-André Bombeck e9a9e79edb docs(planning): W-WMY-WIRE v1.4.40 phase report 2026-05-21 12:07:24 +02:00
Marc-André Bombeck f3a4da50f6 chore(glucose,berlin-day): drop unreferenced mmolToMgdl and berlinDayKey exports
The glucose UI only converts canonical mg/dL into mmol/L for display
via convertGlucose; the reverse helper had no caller outside its own
test. The Berlin-only day-key helper was already superseded by
userDayKey(date, tz) in v1.4.38 for every read path that consumed it;
its remaining presence as a tested module was dead weight.
2026-05-21 12:07:16 +02:00
Marc-André Bombeck b1469c4626 test(rollup-read-wmy): parity and granularity-routing across consumers
Pins the cross-consumer routing contract the v1.4.40 wire-in relies
on. Both computeSummariesSlice (year-ago baseline) and
computeUserHealthScoreFastPath (weight long-window) now call
readBestGranularityRollups so the floor table is shared state. The
new tests prove:

- 90 d -> DAY (every coarser floor too high),
- 365 d -> MONTH (clears 181 d floor, short of 731 d YEAR floor),
- 1095 d -> YEAR (clears the 731 d YEAR floor).

Plus a parity assertion: a MONTH bucket consolidating 30 measurements
with mean 80 returns the same count-weighted mean as the two DAY
buckets (count 10 / mean 82 + count 20 / mean 79) that compose it.
Locks the linearly-composable contract count / mean carries across
granularities — any future floor adjustment forces both consumers to
re-pin in lock-step rather than silently diverging.
2026-05-21 12:06:15 +02:00
Marc-André Bombeck 75607e4c09 chore(measurements): drop unreferenced typesMissingCoverage and readCumulativeDaySumsBatch exports
The bucket-fresh read paths probe coverage per-type via
coverage.get(type) or rely on isFullyCovered for the all-or-nothing
fast path; the typesMissingCoverage helper had no production caller.
The cumulative-batch reader was only exercised by its own test —
the analytics A2 path took the chunked per-type findMany route in
production. Strip both and their tests.
2026-05-21 12:05:43 +02:00
Marc-André Bombeck 9009f1bb33 perf(health-score-fast-path): consume monthly rollup buckets for slope90
Wires readBestGranularityRollups into the WEIGHT-covered branch of
computeUserHealthScoreFastPath so the trailing-year weight baseline is
served from the WEEK / MONTH / YEAR rollup tier instead of falling
through to a live measurements walk. The 365-day window routes through
MONTH by default (~12 buckets) and falls back to WEEK -> DAY on per-
tier coverage miss. count / mean compose linearly across MONTH buckets
so the derived mean is mathematically equivalent to the per-row
average over the same window.

The read fires in parallel with the canonical DAY-bucket read for the
30-day weight series so wall-clock cost stays flat. Score shape is
unchanged; the weightLongWindow annotate carries the resolved
granularity + bucket count + mean so operators can verify the rollup
tier serves production traffic. The live-fallback branch deliberately
omits the long-window probe because the raw weight findMany already
does the work — guards against double-reading on cold mounts.
2026-05-21 12:05:02 +02:00
Marc-André Bombeck 9df23c3c87 chore(analytics): drop unreferenced movingAverage and weeklyAverages exports
Neither function had any production caller. The chart components in
src/components/charts/* implement a local movingAverageByPoints rather
than reaching for the lib helper, and weeklyAverages was never imported
outside its own test file. detectAnomalies stays — it is reached
transitively through summarize() which the analytics, insights, and
measurements-series routes all consume.
2026-05-21 12:04:07 +02:00
Marc-André Bombeck 2bff80aaf9 chore(routes): remove duplicate /api/monitoring/{umami,glitchtip}/test endpoints
Both routes were superseded by their /api/admin/monitoring/{umami,glitchtip}-test
counterparts which are the variants the admin UI actually fetches.
The duplicated routes had no callers anywhere in the source tree.
2026-05-21 12:02:00 +02:00
Marc-André Bombeck 2b4e2177be chore(routes): remove orphan /api/medications/intake-summary endpoint
The GET handler had no fetch caller and the queryKeys.medicationIntakeSummary
entry was never consumed in any useQuery. Strip the route file, the
factory entry, the medicationDependentKeys reference, and an outdated
quick-add doc comment.
2026-05-21 12:01:11 +02:00
Marc-André Bombeck 24568c805b perf(summaries-slice): consume monthly rollup buckets for long-window slice
Wires readBestGranularityRollups into computeSummariesSlice to populate
the year-ago 30-day baseline (avg30LastYear) per type. The field was
hardcoded null up to v1.4.39 because the 90-day windowed aggregate
cannot reach back a year and the live measurements walk over a 395-day
per-type window is exactly the cold-path cost the rollup tier was meant
to absorb.

Coverage-fallback mirrors the W-MOOD posture: per-type calls route
through YEAR (skipped for the 395-day window — floor 731 d) -> MONTH
-> WEEK -> DAY and return null when no granularity carries any bucket
overlapping the [now-395d, now-365d) slice. count / mean compose
linearly across any granularity so the derived baseline is
mathematically equivalent to the per-row average over the same window.

Adds year_over_year_types annotate count alongside the existing
slim_summaries path tag so operators can verify the wire-up in
production wide-events without redeploying instrumentation.
2026-05-21 12:00:56 +02:00
Marc-André Bombeck 0ec4a99322 chore(jobs): remove dead TELEGRAM_CLEANUP_QUEUE handler and registration
The queue had a handler, registration, and worker binding but no
producer anywhere in the codebase — zero boss.send calls and no cron
schedule. The doc comment claimed the Telegram sender schedules
cleanup but the sender never called boss.send. Strip the constant, the
interface, the handler, the allQueues entry, and the worker bind.
2026-05-21 11:58:36 +02:00
Marc-André Bombeck 317d56181b test(auth-registration-status): pin all four discovery branches
The route is read-only and its envelope shape (`registrationEnabled`
boolean) gates iOS sign-in + web onboarding. Four branches walked:
  1. singleton row exists with registrationEnabled: true → reports true
  2. singleton row exists with registrationEnabled: false → reports
     false (admin-disabled tenant, invite-only)
  3. singleton row missing → reports schema default true (fresh self-
     hosted instance, no-config baseline)
  4. findUnique throws → reports false (fail-closed; critical defense
     against a DB blip silently unlocking sign-up on a tenant that
     disabled registration)

No code change; the route's current behaviour is correct. Pin it
before someone refactors fail-closed → fail-open by accident.
2026-05-21 11:53:04 +02:00
Marc-André Bombeck 08cf854960 fix(dashboard-summary): recompute compliance rollup after bulk projection backfill
Mirrors the intake-route call site (v1.4.40 close of v1.4.39.4 hook
gap). `projectAndReadTodaysIntakes` runs the same idempotent
projection helper the intake route uses; without this hook the
compliance rollup stayed pre-projection on the second call site too.

Fires `recomputeMedicationComplianceForEvent` once per distinct
`(medication_id, dayKey)` after the bulk `createMany`, best-effort
inside the helper's try/catch so a populator hiccup doesn't surface
as a 5xx on the dashboard summary.
2026-05-21 11:52:54 +02:00
Marc-André Bombeck e8a0947638 docs(planning): W-RSC v1.4.40 phase report 2026-05-21 11:52:50 +02:00
Marc-André Bombeck de1e65d4a1 fix(medications-intake): recompute compliance rollup after bulk projection backfill
Closes the v1.4.39.4 hook gap surfaced by the QA reconcile pass. The
`scope=today` branch's bulk `createMany` mints fresh
`(medicationId, scheduledFor)` PENDING rows when the morning
projection lands; without this hook the compliance rollup for the
affected `(user, medication, day)` tuples stays at its pre-projection
`scheduled` count, inflating the apparent compliance % until the user
actually logs against the new row.

Fires `recomputeMedicationComplianceForEvent` once per distinct
`(medication_id, dayKey)` (coalesced by Set) so a 5-meds-3-times-a-
day projection drops 15 createMany rows down to ≤3 recompute round-
trips. Best-effort: errors stay caught inside the helper so a
populator hiccup doesn't surface as a 5xx on the intake request.
2026-05-21 11:52:42 +02:00
Marc-André Bombeck ca7d00ffbf perf(insights-generate): consume mood-rollup tier via features extractor
Audit Critical Finding #2 (round-v1439-arch-qa-infra-db). The mood
block inside `extractFeatures` ran an unbounded
`prisma.moodEntry.findMany({ where: { userId } })` and folded the
entire history into the AI feature payload for /api/insights/generate.
On power-user accounts the walk dominated the cold-mount budget.

Swap to `readMoodDayRollups(userId, fiveYearsAgo)` so the feature
payload reads one row per calendar day. The downstream consumers
(`avg7` / `avg30` / `trend30` / `coverage` / cross-metric
correlations) all operate at per-day resolution, matching the rollup
shape. `latest` score reads a single `findFirst` (bounded). Coverage-
fallback runs a bounded one-year live walk + fires
`ensureUserMoodRollupsFresh` so the next request lands on the rollup
tier (same posture as /api/mood/analytics post-v1.4.39).

Existing test fixture updated to mock `moodEntryRollup` + the
warm-up; six features tests stay green.
2026-05-21 11:52:32 +02:00
Marc-André Bombeck 594cd22ff5 docs(planning): W-DELETED v1.4.40 report — soft-delete invisibility across reader tiers
Inventory of touched files, test matrix, and commit map for the
v1.4.40 soft-delete-filter sweep that closes Senior Infra+DB audit
Critical Finding #3.
2026-05-21 11:52:29 +02:00
Marc-André Bombeck 45a8399822 perf(insights-comprehensive): consume mood-rollup tier
Audit Critical Finding #2 (round-v1439-arch-qa-infra-db). The 90-day
`prisma.moodEntry.findMany` block fed two downstream consumers:
`dailyMoodEntries` for mood × metric correlations, and `moodSummary`
for the dashboard mood block. Both read at a per-day resolution.

Swap to `readMoodDayRollups(userId, ninetyDaysAgo)` so the route reads
one row per calendar day instead of one per entry. Coverage-fallback
runs a bounded 90-day live walk + fires `ensureUserMoodRollupsFresh`
so the next request lands on the rollup tier (same posture as
/api/mood/analytics post-v1.4.39). `moodSummary` now feeds per-day
mean DataPoints into `summarize()` on both branches, matching the
rollup-tier convention shipped in v1.4.39 (QA UX-H1).

Two parity tests added to `__tests__/route.test.ts`: rollup fast path
skips the raw findMany, and the coverage-fallback runs the live walk
exactly once when rollups are empty.
2026-05-21 11:52:21 +02:00
Marc-André Bombeck f8de4b05fd perf(insights-targets): consume mood-rollup tier and floor distinct measurement scan
Audit Critical Finding #2 + High finding 3 (round-v1439-arch-qa-infra-db).
The mood block at the bottom of the route used to run an unbounded
`prisma.moodEntry.findMany({ where: { userId } })` on every mount —
pulling every mood the user had ever logged into Node only to bucket-
average them in JS. The `findMany({distinct:['type']})` for 'absolute
latest measurement per type' carried no `measuredAt >=` floor either,
so on a 347 k-row tenant Postgres still had to sort the full set
before the driver applied DISTINCT ON (Prisma's `distinct` doesn't
compile to PG's `DISTINCT ON`).

This commit:
  - swaps the mood findMany for `readMoodDayRollups(userId, since)`
    plus a single `findFirst` for the latest score; coverage-fallback
    runs a bounded 30-day raw walk + fires `ensureUserMoodRollupsFresh`
    so the next request lands on the rollup tier (same posture as
    /api/mood/analytics post-v1.4.39)
  - adds a 365-day floor to the latest-ever-per-type read so the
    driver-side distinct walk stays bounded
  - pins all three behaviours in `__tests__/route.test.ts` (4 new
    tests: rollup fast path, coverage-fallback bounded walk, no-mood
    branch, distinct floor present)
2026-05-21 11:52:09 +02:00
Marc-André Bombeck 691443e264 test(query-keys): pin factory enforcement and dedup behaviour
Three test suites land together:

1. queryKeys factory contracts for the four new v1.4.40 entries
   (`authMe`, `userThresholds`, `chartData`, `dashboardMedicationCompliance`)
   — pin the byte-stable shapes so a future rename of any literal
   lands a matching test failure rather than silently breaking cache
   layout.

2. `measurementDependentKeys` / `medicationDependentKeys` bundle
   membership — `["chart-data"]` and
   `["dashboard-medication-compliance"]` prefixes now ride the
   bundles so a mutation evicts every per-chart and per-compliance
   cache in one pass (audit-C2 + audit-L4).

3. Factory-bypass guard — walks `src/components/charts`,
   `src/app/page.tsx` and `src/hooks/use-auth.ts` and fails if a
   literal `queryKey: [...]` slipped past the factory. Acts as a
   lint-style enforcement check in the absence of a custom ESLint
   rule. The guarded directory list is intentionally scoped tight to
   the files this wave migrated; a follow-up wave can extend it as
   it routes the remaining admin/settings/medications surfaces
   through the factory.

Plus a dashboard Suspense-boundary structural test that pins the
per-tile `<Suspense fallback={...}>` wrappers, the module-scope
`DASHBOARD_QUERY_OPTS` hoist, and the `useMemo` around the hour-of-day
derivation. A refactor that drops any of those three structural
guarantees lands red CI rather than silently regressing the
streaming-composition contract.

All 16 + 5 new assertions green; tests 4565 → 4586.
2026-05-21 11:50:37 +02:00