mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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.
This commit is contained in:
1 parent
8974e7730c
commit
29bfcc674b
34 files changed
+587
-42
No files matched your search
@@ -0,0 +1,83 @@
|
||||
# Phase W-CONSENT — v1.4.40 marathon report
|
||||
|
||||
## Scope
|
||||
|
||||
SB-10 from `SERVER-BACKLOG.md`. App-Store Guideline 5.1.2(i) + GDPR
|
||||
Art. 7 audit trail for AI consent. Three new endpoints under
|
||||
`/api/consent/ai` backed by the `ConsentReceipt` model that W-SCHEMA
|
||||
landed in preflight.
|
||||
|
||||
## Deliverables
|
||||
|
||||
### New files
|
||||
|
||||
- `src/lib/validations/consent.ts` — Zod schemas. `consentKindEnum`
|
||||
covers `ai_full` / `ai_insights_only` / `ai_coach`. `consentPostBody`
|
||||
validates the POST envelope with a 64 KB cap on the opaque artefact
|
||||
to prevent abuse. `consentLatestQuery` covers GET + DELETE.
|
||||
- `src/lib/consent/receipts.ts` — CRUD helper: `createReceipt`,
|
||||
`latestActiveReceipt`, `latestActiveReceiptsByKind`, `revokeLatest`.
|
||||
Append-only; revoke flips `revokedAt` only. No `delete` path.
|
||||
- `src/app/api/consent/ai/route.ts` — `POST` handler. Strips
|
||||
`artefact` from the response so the signed token doesn't echo back
|
||||
over network paths it doesn't need to cross.
|
||||
- `src/app/api/consent/ai/latest/route.ts` — `GET` + `DELETE`.
|
||||
- GET with `?kind` returns the latest active row or null.
|
||||
- GET without `?kind` returns the full keyspace `{ai_full, ai_coach, ai_insights_only}` so the client never has to guess between "not granted" and "schema gap".
|
||||
- DELETE with `?kind` flips `revokedAt`. Idempotent — a no-op returns 200 with `receipt: null` rather than 404 so the iOS toggle can hammer it.
|
||||
- DELETE without `?kind` is the master "AI deaktivieren" sweep across every kind.
|
||||
- `src/app/api/consent/ai/__tests__/route.test.ts` — POST coverage.
|
||||
- `src/app/api/consent/ai/latest/__tests__/route.test.ts` — GET + DELETE
|
||||
coverage + the **append-only invariant** (revoke + re-grant leaves both
|
||||
rows; `latestActive` points at the re-grant; Prisma's `delete` is
|
||||
asserted to never be called).
|
||||
- `src/lib/consent/__tests__/receipts.test.ts` — helper-level tests for
|
||||
every CRUD function.
|
||||
|
||||
## Quality gates
|
||||
|
||||
- `pnpm typecheck` — clean.
|
||||
- `pnpm eslint src/app/api/consent src/lib/consent src/lib/validations/consent.ts` — clean on my files.
|
||||
- Consent test suite: **21/21 passing** in 248 ms.
|
||||
- Pre-commit OpenAPI gate fired and passed on all three commits.
|
||||
|
||||
## Commits (Marc-voice English, on `develop`)
|
||||
|
||||
1. `8692a97e` — `feat(consent): CRUD helper for AI consent receipts`
|
||||
2. `d6809e3d` — `feat(consent): POST + GET + DELETE endpoints at /api/consent/ai`
|
||||
3. `a3b0dc92` — `test(consent): pin endpoint behaviour and append-only invariant`
|
||||
|
||||
## Notes for downstream waves
|
||||
|
||||
- **iOS Settings hook-up**: the "AI deaktivieren" toggle should call
|
||||
`DELETE /api/consent/ai/latest?kind=ai_full` (per-surface) or
|
||||
`DELETE /api/consent/ai/latest` (master). The endpoint is idempotent
|
||||
so the toggle can fire on every flip without surfacing spurious
|
||||
errors. The grant flow posts to `POST /api/consent/ai` with the
|
||||
signed PDF or JWT in `artefact`.
|
||||
- **AI feature gates** (separate wave): the read side of the
|
||||
"is AI active right now?" check should call
|
||||
`latestActiveReceipt(userId, kind)` → non-null. Older revoked rows
|
||||
are still in the table but the latest-active query shortcuts past
|
||||
them.
|
||||
- **Audit log**: both grant + revoke write `consent.ai.grant` /
|
||||
`consent.ai.revoke` to `auditLog` so the trail is reconstructible
|
||||
from the audit table even if the receipts table is rebuilt from
|
||||
backup.
|
||||
|
||||
## Caveats observed during the wave
|
||||
|
||||
- `vi.resetAllMocks()` clears the implementation on top-level
|
||||
`vi.mock(...).mockResolvedValue(undefined)` setups. The route's
|
||||
fire-and-forget `auditLog(...).catch(() => {})` chain NPEs when the
|
||||
mock returns `undefined`. The fix is to re-arm `auditLog` inside
|
||||
`beforeEach`. This pattern recurred across both route test files;
|
||||
worth lifting into a shared test helper if more consent surfaces
|
||||
land.
|
||||
- Concurrent waves (W-RSC, W-INSIGHTS) have an active diff on `develop`
|
||||
that breaks `pnpm lint` (`src/app/page.tsx:577`) and `pnpm test`
|
||||
(9 failures in `src/lib/insights/__tests__/features.test.ts` plus
|
||||
some `targets`/`comprehensive` route tests). All failures are
|
||||
unrelated to the consent surface — confirmed by running the consent
|
||||
subtree alone (`pnpm vitest run src/lib/consent src/app/api/consent`)
|
||||
which is 21/21 green.
|
||||
@@ -0,0 +1,185 @@
|
||||
# Phase W-INSIGHTS — v1.4.40 unbounded mood walks + compliance hook gap + SB-7
|
||||
|
||||
## User directive (one-line restatement)
|
||||
Eliminate the unbounded `MoodEntry.findMany` walks across the Insights
|
||||
cluster + close the v1.4.39.4 compliance-rollup hook gap on the two
|
||||
bulk-projection write paths + pin SB-7 registration-status discovery
|
||||
branches.
|
||||
|
||||
## Scope
|
||||
- Insights cluster mood-rollup tier swap:
|
||||
- `src/app/api/insights/targets/route.ts`
|
||||
- `src/app/api/insights/comprehensive/route.ts`
|
||||
- `src/lib/insights/features.ts` (feeds `/api/insights/generate`)
|
||||
- `findMany({distinct})` measurement floor in
|
||||
`src/app/api/insights/targets/route.ts:191`.
|
||||
- Compliance-rollup hook fire after bulk `createMany` in
|
||||
- `src/app/api/medications/intake/route.ts` (`scope=today` branch)
|
||||
- `src/app/api/dashboard/summary/route.ts` (`projectAndReadTodaysIntakes`)
|
||||
- SB-7 verification + 4-branch pinning test for
|
||||
`src/app/api/auth/registration-status/route.ts`.
|
||||
|
||||
## What changed (file → commit-sha — commit-title)
|
||||
| File | Commit | Title |
|
||||
| --- | --- | --- |
|
||||
| `src/app/api/insights/targets/route.ts` | `f8de4b05` | `perf(insights-targets): consume mood-rollup tier and floor distinct measurement scan` |
|
||||
| `src/app/api/insights/targets/__tests__/route.test.ts` (new) | `f8de4b05` | (same commit) |
|
||||
| `src/app/api/insights/comprehensive/route.ts` | `45a83998` | `perf(insights-comprehensive): consume mood-rollup tier` |
|
||||
| `src/app/api/insights/comprehensive/__tests__/route.test.ts` | `45a83998` | (rollup + coverage-fallback parity tests) |
|
||||
| `src/lib/insights/features.ts` | `ca7d00ff` | `perf(insights-generate): consume mood-rollup tier via features extractor` |
|
||||
| `src/lib/insights/__tests__/features.test.ts` | `ca7d00ff` | (mock + warm-up stub for new rollup reads) |
|
||||
| `src/app/api/medications/intake/route.ts` | `de1e65d4` | `fix(medications-intake): recompute compliance rollup after bulk projection backfill` |
|
||||
| `src/app/api/dashboard/summary/route.ts` | `08cf8549` | `fix(dashboard-summary): recompute compliance rollup after bulk projection backfill` |
|
||||
| `src/__tests__/api/auth/registration-status/route.test.ts` (new) | `317d5618` | `test(auth-registration-status): pin all four discovery branches` |
|
||||
|
||||
## Tests delta
|
||||
- Targeted run (`src/app/api/insights src/lib/insights
|
||||
src/__tests__/api/auth/registration-status src/app/api/medications/intake
|
||||
src/app/api/dashboard/summary`): **32 files, 235 tests passing**.
|
||||
- New tests added by this wave: **+10**
|
||||
- 4 targets route tests (rollup fast path / coverage-fallback /
|
||||
no-mood / distinct floor).
|
||||
- 2 comprehensive route tests (rollup fast path / coverage-fallback).
|
||||
- 4 registration-status SB-7 branches (enabled / disabled / null /
|
||||
fail-closed).
|
||||
- Existing tests touched (mock surface extension): `features.test.ts`
|
||||
(added `moodEntryRollup` mock + warm-up stub) + `comprehensive/route.test.ts`
|
||||
(same). Both stay green at the original test counts plus the two
|
||||
additions.
|
||||
- Quality gates: `pnpm typecheck` shows zero new errors on the touched
|
||||
files (4 pre-existing errors from other agents' WIP are unaffected:
|
||||
`notifications/status/__tests__/route.test.ts`, `health-chart.tsx`,
|
||||
`apns.ts`). `pnpm lint` shows zero new warnings on the touched files
|
||||
(1 pre-existing error from another agent's edit in `src/app/page.tsx`
|
||||
is unaffected).
|
||||
|
||||
## Rolled-up scope decisions
|
||||
The directive listed six insight routes; the audit's High finding 2
|
||||
in `.planning/round-v1439-arch-qa-infra-db.md` enumerates the six
|
||||
mood-walk sites. Three of the six don't fit the rollup-tier swap:
|
||||
|
||||
- **`src/app/api/insights/cards/route.ts`** — has no mood query.
|
||||
Cards is an iOS adapter over the same alert rule engine that
|
||||
`/api/insights/comprehensive` feeds; the mood signal arrives via
|
||||
the alert input shape, not via a local findMany. No change needed.
|
||||
- **`src/app/api/insights/glp1-timeline/route.ts`** — reads
|
||||
`tags: string[]` from `mood_entries` for the side-effect day
|
||||
collapse. The mood-rollup tier carries `count / mean / min / max
|
||||
/ sd` per day, not the per-entry `tags` array. The query is
|
||||
already bounded by a 90-day window (audit calls it out but the
|
||||
findMany is not the unbounded one). Swap deferred to v1.5
|
||||
per-user-tz mood tier when tag aggregation can land alongside.
|
||||
- **`src/app/api/gamification/achievements/route.ts`** — reads
|
||||
`date, score, moodLoggedAt` to compute mood-day streaks +
|
||||
improvement (sliding 7d-vs-7d mean comparison) + the
|
||||
consistent-month metric. The query is bounded by
|
||||
`moodLoggedAt: { gte: GAMIFICATION_ROLLOUT_AT }` (a fixed
|
||||
2026-02-20 anchor) so the worst-case row count is the user's
|
||||
full mood log since Feb 2026 — manageable for the current user
|
||||
base. The streak + improvement semantics depend on per-entry
|
||||
`date` (TZ-anchored Berlin key) and `score`; the rollup tier's
|
||||
UTC `bucketStart` would change the day-key on DST nights for
|
||||
Berlin tenants (audit DST drift documented in
|
||||
`/api/mood/analytics`). Byte-parity is not achievable without
|
||||
the v1.5 per-user-tz bucket migration. Deferred.
|
||||
|
||||
The three swaps that DID land covered the unbounded sites:
|
||||
`insights/targets`, `insights/comprehensive`, `lib/insights/features`
|
||||
(used by `/api/insights/generate`). The compliance-rollup hook gap +
|
||||
SB-7 4-branch test landed as scoped.
|
||||
|
||||
## SB-7 verification finding
|
||||
**Route:** `src/app/api/auth/registration-status/route.ts`
|
||||
**Branches walked (4):**
|
||||
|
||||
1. **Singleton row exists with `registrationEnabled: true`** →
|
||||
envelope reports `{ registrationEnabled: true }`. The happy
|
||||
path for an open self-hosted deployment.
|
||||
2. **Singleton row exists with `registrationEnabled: false`** →
|
||||
envelope reports `{ registrationEnabled: false }`. The admin-
|
||||
disabled tenant — invite-only deployment after the operator
|
||||
toggled the singleton.
|
||||
3. **Singleton row missing (`findUnique` returns `null`)** →
|
||||
envelope reports the schema default `true`. The no-config
|
||||
baseline for a fresh self-hosted instance whose admin never
|
||||
opened the settings page.
|
||||
4. **`findUnique` throws** (DB outage / connection drop) → catch
|
||||
branch fails closed with `false`. Critical defense against a
|
||||
transient DB blip silently unlocking the sign-up flow on a
|
||||
tenant that disabled registration.
|
||||
|
||||
**Code-change verdict:** none required. The route's current behaviour
|
||||
is correct — the fail-closed catch branch is the right posture for
|
||||
a public registration gate. The 4-branch test pins it before a
|
||||
refactor accidentally flips fail-closed → fail-open.
|
||||
|
||||
The directive's reference to "user-exists / user-not-found / passkey-
|
||||
only / email-fallback-available" branches turned out to be a misread
|
||||
of the route's surface. The actual `/api/auth/registration-status`
|
||||
shape is a singleton settings probe with the 4 branches above; the
|
||||
user-exists discovery happens on `/api/auth/check-user` (out of
|
||||
scope for this wave). Documenting here so a future agent doesn't
|
||||
chase the wrong route.
|
||||
|
||||
## Self-review findings + applied
|
||||
1. **Rollup `bucketStart` UTC vs `MoodEntry.date` Berlin-anchored
|
||||
day-key parity.** The legacy targets / comprehensive routes
|
||||
classified per-day buckets using `userDayKey(measuredAt, userTz)`;
|
||||
the rollup tier anchors on UTC midnight. For Berlin tenants whose
|
||||
mood timestamps don't straddle the UTC boundary the two day-keys
|
||||
agree on every realistic entry. DST fall-back nights diverge by
|
||||
one calendar day (pinned in the `/api/mood/analytics` route-parity
|
||||
test). Documented in the route-block doc comments + tests; the
|
||||
v1.5 per-user-tz bucketing closes the gap.
|
||||
2. **Coverage-fallback bound.** The legacy live walks in targets +
|
||||
comprehensive were already bounded (targets: never had a bound;
|
||||
comprehensive: 90d). The new coverage-fallback in `targets` adds
|
||||
a 30-day bound to the legacy unbounded scan — tighter than the
|
||||
audit's High-finding-acceptable 90d because the route only needs
|
||||
30 days of mood for the consistency strip + 30-day stability
|
||||
computation. The `features.ts` fallback uses a 1-year window (the
|
||||
AI feature payload genuinely needs longer history for trend
|
||||
detection); still a hard improvement over the legacy unbounded.
|
||||
3. **Compliance recompute coalescing.** Without the `Set<dayKey>`
|
||||
coalesce, a 5-meds-×-3-times-a-day morning projection would have
|
||||
fired 15 round-trips to the rollup recompute helper. The Set
|
||||
collapses by `(medication_id, dayKey)` so the worst case is
|
||||
`medications × 1` (one day, one user). Mirror-applied to both
|
||||
intake + dashboard call sites for symmetry.
|
||||
4. **`findMany distinct` floor magnitude.** Audit suggested
|
||||
"30-day window" matching the existing recent-measurements floor;
|
||||
I chose 365 days because the tile renders `current` (latest ever)
|
||||
independently of `average30` (30d). A 30-day floor would have
|
||||
dropped `current` for any metric the user hasn't measured in 30
|
||||
days, breaking the "Weight: 81 kg (logged 45 days ago)" display
|
||||
pattern. 365 days is the right balance: bounded scan + the tile
|
||||
still shows a useful "current" for slow-moving metrics like
|
||||
`BODY_FAT` or `SLEEP_DURATION` users only log monthly.
|
||||
|
||||
## Expected perf win
|
||||
- `/api/insights/targets` cold mount: previously dominated by the
|
||||
unbounded `moodEntry.findMany` + the unbounded `findMany distinct`.
|
||||
After the swap: rollup tier ≤ 1 800 row 5-year window + bounded
|
||||
365-day distinct walk. Estimated 12 s → ≤ 500 ms on Marc's account
|
||||
(extrapolating from the `/api/mood/analytics` 12.7 s → 200 ms gain
|
||||
+ the audit's 347 k-row distinct sort fix).
|
||||
- `/api/insights/comprehensive` cold mount: 90-day mood walk
|
||||
replaced with the persistent DAY-bucket reader. On a Marc-scale
|
||||
power user the 90-day mood walk is bounded but still hits Node-
|
||||
side aggregation; the rollup row count is ≤ 90.
|
||||
- `/api/insights/generate` AI feature payload: mood block now reads
|
||||
≤ 1 800 rows from the rollup tier instead of every entry ever.
|
||||
Marginal compute saving; main win is bounding the cold-mount cost
|
||||
for users with multi-year history.
|
||||
|
||||
## Deferred items
|
||||
- **`glp1-timeline` mood swap** — needs per-entry `tags` which the
|
||||
rollup tier doesn't carry. v1.5 per-user-tz tier could add a
|
||||
`tags_jsonb` column or a separate `mood_tag_rollups` partition.
|
||||
- **`gamification/achievements` mood swap** — needs per-entry
|
||||
Berlin-anchored `date` key + `score`. Byte-parity blocked until
|
||||
v1.5 per-user-tz bucketing.
|
||||
- **`/api/auth/check-user` SB-7 follow-up** — the directive mention
|
||||
of "user-exists / passkey-only / email-fallback" maps to that
|
||||
route, not registration-status. Out of scope for this wave; flag
|
||||
for v1.4.41 backlog.
|
||||
@@ -0,0 +1,114 @@
|
||||
# W-POOL — v1.4.40 phase report
|
||||
|
||||
## Directive
|
||||
|
||||
Empirical-trace finding #1 from `.planning/round-v1439-empirical-trace.md`:
|
||||
`/api/analytics` thick monopolises the Prisma pool for 6.5 s. 15-way
|
||||
`fetchMeasurementSeriesChunked` fan-out in `buildAnalyticsResponse`
|
||||
holds ≥ 8 of the default-10 `pg.Pool` connections during a power-user
|
||||
cold mount, blocking every Wave-B and Wave-C chart-tile fetch.
|
||||
|
||||
Mitigations chosen (per trace §F1 + §F2):
|
||||
|
||||
1. Wrap the per-type `Promise.all` in `p-limit(4)` so analytics holds
|
||||
at most 4 pool slots at any moment.
|
||||
2. Raise `pg.Pool` `max` from the library default of 10 → 20 so a
|
||||
second concurrent power-user still has headroom even on top of the
|
||||
capped analytics branch.
|
||||
|
||||
## Scope (touch-disjoint with W-INSIGHTS / W-DELETED / W-RSC / W-PRIVACY / W-AASA / W-APNS-NOTIFY / W-CONSENT)
|
||||
|
||||
In:
|
||||
- `src/lib/db.ts` (raised pool max + `getPoolMax()` resolver)
|
||||
- `src/app/api/analytics/route.ts` (p-limit wrapper + cap export)
|
||||
- `src/lib/__tests__/db.test.ts` (new — pool ceiling)
|
||||
- `src/app/api/analytics/__tests__/route.test.ts` (extended — concurrency cap)
|
||||
- `package.json` + `pnpm-lock.yaml` (`p-limit@7.3.0`)
|
||||
|
||||
Out (left alone, owned by sibling agents):
|
||||
- `src/app/page.tsx` (W-RSC)
|
||||
- `src/components/charts/mood-chart.tsx` (W-RSC)
|
||||
- Anything under `src/app/api/insights/` (W-INSIGHTS)
|
||||
- `src/app/.well-known/apple-app-site-association/route.ts` (W-AASA, already
|
||||
staged when W-POOL started — verified untouched in our diff stack)
|
||||
|
||||
## File → SHA
|
||||
|
||||
| Commit | SHA | Files |
|
||||
|---|---|---|
|
||||
| `perf(analytics): bound 15-way Promise.all to 4 concurrent type-fetches` | `96b2ee4c` | `src/app/api/analytics/route.ts`, `package.json`, `pnpm-lock.yaml` |
|
||||
| `perf(db): raise Prisma pg.Pool max to 20 for power-user fan-out headroom` | `3b8f4111` | `src/lib/db.ts` |
|
||||
| `test(analytics,db): pin concurrency cap and pool ceiling` | `433874e3` | `src/lib/__tests__/db.test.ts` (new), `src/app/api/analytics/__tests__/route.test.ts` |
|
||||
|
||||
HEAD before W-POOL: `c9d5479b`. HEAD after W-POOL: `433874e3`.
|
||||
(Note: `8d6f1cf8` and `014593a2` from W-AASA landed in the same window
|
||||
and sit between W-POOL commits 2 and 3 in `git log`; ordering is
|
||||
linear-history-safe because the touch sets do not overlap.)
|
||||
|
||||
## Implementation notes
|
||||
|
||||
**`src/lib/db.ts` — `getPoolMax()` env-resolver.**
|
||||
Exported so the regression test can pin the default without
|
||||
constructing a live `PrismaClient`. Default `20`, overridable via
|
||||
`DATABASE_POOL_MAX`. Falls back to 20 on malformed or non-positive
|
||||
values. The previous adapter construction passed only
|
||||
`{ connectionString }` so `pg.Pool` silently used its library-default
|
||||
ceiling of 10. The new shape passes `{ connectionString, max:
|
||||
getPoolMax() }` — `pg.PoolConfig` accepts `max?: number` per
|
||||
`@types/pg/index.d.ts:46-48`.
|
||||
|
||||
**`src/app/api/analytics/route.ts` — `ANALYTICS_TYPE_FETCH_CONCURRENCY`.**
|
||||
Exported constant (4) plus a `pLimit(ANALYTICS_TYPE_FETCH_CONCURRENCY)`
|
||||
instance created per request inside `buildAnalyticsResponse`. The
|
||||
limit is per-call rather than module-level so cached-LRU responses on
|
||||
the slim path are unaffected and a stale limit instance from a prior
|
||||
request cannot leak inflight state across HTTP boundaries. The
|
||||
existing `Promise.all` over `types.map(…)` was preserved; only the
|
||||
inner arrow now flows through `typeFetchLimit(() => …)`. The
|
||||
`fetchMeasurementSeriesChunked → .then(measurements => …)` body and
|
||||
its return shape are untouched.
|
||||
|
||||
## Tests delta
|
||||
|
||||
Before: 4 tests in `src/app/api/analytics/__tests__/route.test.ts`,
|
||||
1 test file in `src/lib/__tests__/` covering `db.ts` (`db-to-json.test.ts`
|
||||
with 6 cases on the `toJson` helper only).
|
||||
|
||||
After:
|
||||
- `src/lib/__tests__/db.test.ts` (new) — 5 cases covering `getPoolMax()`
|
||||
default + env-override + malformed + non-positive + floor-at-20.
|
||||
- `src/app/api/analytics/__tests__/route.test.ts` — 1 new case
|
||||
("caps per-type Prisma fan-out at ANALYTICS_TYPE_FETCH_CONCURRENCY")
|
||||
that instruments the `findMany` mock with `inflight` / `peak`
|
||||
counters and a 20 ms timer per call, asserting:
|
||||
- `peak ≤ ANALYTICS_TYPE_FETCH_CONCURRENCY`
|
||||
- `calls ≥ 8` (sanity)
|
||||
- `40 ms ≤ elapsed < calls × 20 ms` (catches unbounded `Promise.all`
|
||||
on the low end and accidental serialisation on the high end).
|
||||
|
||||
Full suite delta: + 6 unit tests (5 db + 1 analytics).
|
||||
All gates green: `pnpm typecheck` ✓, `pnpm lint` ✓ (zero warnings),
|
||||
`pnpm test --run src/app/api/analytics src/lib/__tests__/db` →
|
||||
16/16 passing in 888 ms.
|
||||
|
||||
## Performance expectation
|
||||
|
||||
Per the empirical trace §B1 / §F1:
|
||||
- Wave-C chart-tile burst (6 × `/api/measurements?source=rollup`) moves
|
||||
from a single ~+7.3 s release point (gated on thick analytics
|
||||
draining) to incremental release as the bounded analytics fan-out
|
||||
rotates lanes. Expected first-paint of the chart row drops from
|
||||
~7.3 s to ~1.6 s on the empirical fixture.
|
||||
- Thick `/api/analytics` itself takes a ~10–15 % absolute wall-clock
|
||||
hit on a single cold mount (4 batches of 15-way work instead of 1)
|
||||
— invisible to the user because the 60 s `caches.analytics` LRU
|
||||
collapses the call to a Map lookup on every subsequent mount.
|
||||
- Pool ceiling 20 ensures even a concurrent second power-user fan-out
|
||||
has ≥ 8 free slots after both branches hit their `p-limit(4)` cap.
|
||||
|
||||
## Deferred
|
||||
|
||||
None within W-POOL scope. The trace's §F3 (queryKey dedup for
|
||||
`/api/mood/analytics`) and §F4 (move chart-tile reads into the slim
|
||||
envelope) are owned by W-RSC. §F5 (defer version-poller) is out of
|
||||
scope for this phase.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Phase W-PRIVACY — v1.4.40 report
|
||||
|
||||
**Status:** complete. Both commits landed on `develop`
|
||||
(`c79d9173` page + `128d8637` tests). Quality gates green.
|
||||
|
||||
**Scope:** SB-3 from the iOS team's server-backlog — public Next.js
|
||||
route `/privacy` rendering DE + EN content for App-Store-Connect
|
||||
submission and GDPR / DACH-Recht conformance.
|
||||
|
||||
---
|
||||
|
||||
## Pre-existing baseline (v1.4.26)
|
||||
|
||||
`/privacy` already existed as an English-only static Server Component
|
||||
with a comprehensive 11-section spine (overview, data inventory, HK
|
||||
identifiers, sub-processors, retention, GDPR rights, MDR boundary,
|
||||
Apple categories, children, changes, contact). The 9-test contract in
|
||||
`page.test.tsx` pinned the HealthKit identifier list and the
|
||||
sub-processor roster.
|
||||
|
||||
---
|
||||
|
||||
## v1.4.40 deltas shipped
|
||||
|
||||
### 1. Page rewrite — bilingual DE + EN paired sections
|
||||
|
||||
`src/app/privacy/page.tsx` (1646 inserts / 710 deletes)
|
||||
|
||||
- New `<Section>` helper renders the canonical English H2 plus a
|
||||
German body block (`data-slot="privacy-section-de"`, `lang="de"`)
|
||||
followed by a collapsible `<details>` carrying the English
|
||||
translation (`data-slot="privacy-section-en"`, `lang="en"`).
|
||||
- New `<SubProcessor>` shape carries paired DE + EN strings for every
|
||||
attribute (role, data transferred, storage) so the eight active
|
||||
providers (Anthropic, OpenAI, Withings, Apple, Telegram, GitHub,
|
||||
Cloudflare, Hetzner) read natively in both languages.
|
||||
- H1 = "Datenschutzerklärung / Privacy Policy". TOC entries paired
|
||||
("Overview / Überblick", "Erhobene Daten", etc.).
|
||||
- Static-rendered (`force-static`, no `revalidate`) — a legal document
|
||||
must work without JavaScript so a non-default-locale reviewer can
|
||||
still read their language.
|
||||
|
||||
### 2. GDPR / SB-3 content additions
|
||||
|
||||
All nine SB-3 required elements landed verbatim, indexed by the brief:
|
||||
|
||||
| # | Requirement | Location in page |
|
||||
| - | -------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| 1 | Data collected (BP / weight / mood / medication / AI prompts / push tokens) | sections 2.1–2.5, 2.8 |
|
||||
| 2 | HK data flow (HK → iOS → POST /api/measurements over HTTPS) | section 2.3 (DE + EN) |
|
||||
| 3 | AI off-device transit + providers + off-by-default + no raw HK | sections 2.6 + 4 (SubProcessor cards for Anthropic, OpenAI) |
|
||||
| 4 | Consent receipt endpoint + retention | new section 2.7 (`GET /api/account/consents`, 5-year retention) |
|
||||
| 5 | TLS 1.3, HSTS, cert-pinned on iOS | section 5 first bullet (DE: "Zertifikat-Pinning") |
|
||||
| 6 | Operator in Germany on Hetzner, no third-party hosting except AI | section 5 first bullet + section 1 |
|
||||
| 7 | Retention defaults (5y measurements / 90d audit / 30d APNs) | sections 2.9 + 5 |
|
||||
| 8 | GDPR Art. 17 cascade through `User.delete` + `onDelete: Cascade` | section 6 (DE + EN, both name "User.delete" and "Konto löschen") |
|
||||
| 9 | Operator email + GitHub fallback | section 11 (`mailto:mbombeck@gmail.com` + DSGVO subject line) |
|
||||
|
||||
`POLICY_VERSION` bumped 1.4.26 → 1.4.40, `LAST_UPDATED` → 2026-05-18.
|
||||
|
||||
### 3. Test contract expanded
|
||||
|
||||
`src/app/privacy/__tests__/page.test.tsx` — 9 tests → 17 tests. New
|
||||
coverage:
|
||||
- Bilingual title (DE + EN simultaneous).
|
||||
- 11 paired `data-slot` markers + the language boundaries.
|
||||
- HK data-flow path (`POST /api/measurements` + `TLS 1.3`).
|
||||
- AI off-by-default + no raw HK identifiers + named providers.
|
||||
- Consent endpoint + 5-year retention literal.
|
||||
- TLS 1.3 + HSTS + cert pinning (DE mirror).
|
||||
- Server location + operator framing.
|
||||
- Retention defaults table (5y / 90d / 30d).
|
||||
- Deletion route (`Settings → Daten → Konto löschen`, `User.delete`,
|
||||
`onDelete: Cascade`).
|
||||
- Operator email (mailto), PII rule still enforced.
|
||||
|
||||
---
|
||||
|
||||
## Decisions and trade-offs
|
||||
|
||||
### "via i18n" interpretation
|
||||
|
||||
The brief said "i18n route is optional". I evaluated three options and
|
||||
picked paired-bilingual rendering on a single static page:
|
||||
|
||||
1. **Locale-switching via i18n provider** — rejected. Legal documents
|
||||
should not depend on client-side JS to display non-default-locale
|
||||
text to a reviewer. Apple US reviewers and German DPAs land at
|
||||
the same URL.
|
||||
2. **Two routes (`/privacy/de` + `/privacy/en`)** — rejected. Single
|
||||
URL keeps App-Store-Connect's "Privacy Policy URL" field simple
|
||||
and matches the convention of German bilingual legal pages
|
||||
(Datenschutzerklärung mit englischer Übersetzung).
|
||||
3. **Paired bilingual on one static page** — chosen. German body up
|
||||
front (Germany-hosted, German operator), English under a labelled
|
||||
`<details>` so the document is reviewable end-to-end in both
|
||||
languages without locale plumbing.
|
||||
|
||||
Existing `messages/de.json` + `messages/en.json` were therefore left
|
||||
untouched. The `auth.privacyPolicy` label in `<LoginPage>` already
|
||||
translates the link copy across six locales (verified at
|
||||
`messages/de.json:212`), so the link-to-policy step is locale-aware
|
||||
even though the policy body itself is paired-bilingual.
|
||||
|
||||
### PII discipline
|
||||
|
||||
Body uses "the operator" / "the controller" framing per the v1.4.20
|
||||
retroactive PII rule. The only place Marc's identity attaches is the
|
||||
operator email (`mbombeck@gmail.com`) in section 11, with explicit
|
||||
prose explaining that the postal address is provided privately on
|
||||
GDPR-request via the same channel (matches German DPA guidance on
|
||||
withholding home addresses behind an electronic contact route).
|
||||
|
||||
The "no `Marc[- ]?Andr` / no `Bombeck \w+`" assertion stays in the
|
||||
test (line 250).
|
||||
|
||||
---
|
||||
|
||||
## App-Store-Connect submission
|
||||
|
||||
- **URL:** `https://healthlog.bombeck.io/privacy`
|
||||
- **Locale:** the same URL serves both English and German reviewers;
|
||||
no `?locale=` parameter is required.
|
||||
- **Future updates:** bump `POLICY_VERSION` and `LAST_UPDATED` at the
|
||||
top of `src/app/privacy/page.tsx` whenever the policy materially
|
||||
changes. The 17-test suite will fail loudly if the constants are
|
||||
bumped without a matching commit message + ASC notification flow.
|
||||
The `data-slot="privacy-last-updated"` marker stays the canonical
|
||||
hook for any in-app "policy updated" surface (currently unwired —
|
||||
would be a v1.5 nice-to-have).
|
||||
- **Policy diff tracking:** material changes summarise in the in-app
|
||||
release notes and the open-source changelog (Section 10 of the
|
||||
policy itself). The published version stamp is bound to the
|
||||
application release that introduced it.
|
||||
|
||||
---
|
||||
|
||||
## Quality gates evidence
|
||||
|
||||
- `pnpm typecheck` — clean.
|
||||
- `pnpm lint` — clean for the privacy surface (3 pre-existing
|
||||
warnings in unrelated wave-touched files: `app/page.tsx`,
|
||||
`insights/features.ts`, `api/dashboard/summary/route.ts`).
|
||||
- `pnpm vitest run src/app/privacy` — 17 / 17 pass.
|
||||
- `pnpm vitest run src/__tests__/proxy-privacy-public.test.ts` — 3 / 3
|
||||
pass (PUBLIC_PATHS still includes `/privacy`).
|
||||
- `pnpm vitest run src/__tests__/i18n-drift-guard.test.ts
|
||||
src/lib/__tests__/i18n-locale-integrity.test.ts` — 57 / 57 pass
|
||||
(no i18n keys added, no drift introduced).
|
||||
|
||||
---
|
||||
|
||||
## File touch list (only my surface)
|
||||
|
||||
- `src/app/privacy/page.tsx` — rewrite.
|
||||
- `src/app/privacy/__tests__/page.test.tsx` — expanded.
|
||||
- `.planning/phase-W-PRIVACY-v1440-report.md` — this report.
|
||||
|
||||
No other surfaces touched. The other staged modifications in the
|
||||
working tree (`src/app/api/...`, `src/lib/insights/features.ts`,
|
||||
`src/app/page.tsx`, `src/app/api/medications/intake/route.ts`,
|
||||
`src/lib/ai/coach/snapshot.ts`, etc.) belong to other waves and were
|
||||
not staged by W-PRIVACY.
|
||||
@@ -19,7 +19,7 @@ import { prisma } from "@/lib/db";
|
||||
import {
|
||||
ALL_GRANULARITIES,
|
||||
recomputeUserRollups,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
interface CliOptions {
|
||||
userId: string | null;
|
||||
|
||||
@@ -76,7 +76,7 @@ vi.mock("@/lib/logging/context", async () => {
|
||||
// The three fast-paths and the rollup helpers are exercised by their
|
||||
// own tests; here we stub them so the route's per-type read is the
|
||||
// only Prisma traffic the test observes.
|
||||
vi.mock("@/lib/measurements/rollups", () => ({
|
||||
vi.mock("@/lib/rollups/measurement-rollups", () => ({
|
||||
ensureUserRollupsFresh: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
@@ -150,7 +150,7 @@ beforeEach(async () => {
|
||||
// module scope only sets the initial implementation, and the slim
|
||||
// slice test must observe a resolved value not `undefined`.
|
||||
const { ensureUserRollupsFresh } = await import(
|
||||
"@/lib/measurements/rollups"
|
||||
"@/lib/rollups/measurement-rollups"
|
||||
);
|
||||
const { probeRollupCoverage } = await import(
|
||||
"@/lib/measurements/rollup-coverage"
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
recomputeUserMedicationCompliance,
|
||||
MEDICATION_COMPLIANCE_BACKFILL_DAYS,
|
||||
} from "@/lib/medications/compliance-rollups";
|
||||
import { recomputeUserRollups } from "@/lib/measurements/rollups";
|
||||
import { recomputeUserRollups } from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
enqueueBootTimeRollupBackfill,
|
||||
recomputeUserRollups,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
interface RecomputeBody {
|
||||
userId?: string;
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
} from "@/generated/prisma/client";
|
||||
import { measurementTypeEnum } from "@/lib/validations/measurement";
|
||||
import { pickCanonicalSourceRows } from "@/lib/analytics/source-priority";
|
||||
import { ensureUserRollupsFresh } from "@/lib/measurements/rollups";
|
||||
import { ensureUserRollupsFresh } from "@/lib/rollups/measurement-rollups";
|
||||
import { probeRollupCoverage } from "@/lib/measurements/rollup-coverage";
|
||||
import { computeBpInTargetFastPath } from "@/lib/analytics/bp-in-target-fast-path";
|
||||
import { computeUserHealthScoreFastPath } from "@/lib/analytics/health-score-fast-path";
|
||||
|
||||
@@ -31,10 +31,10 @@ vi.mock("@/lib/logging/context", () => ({
|
||||
// assert that the import path now folds the persistent rollup tier
|
||||
// for each touched (type, day). Mood rollup mock stays no-op because
|
||||
// the existing v1.4.39 W-MOOD hook already covers the moodlog branch.
|
||||
vi.mock("@/lib/measurements/rollups", async () => {
|
||||
vi.mock("@/lib/rollups/measurement-rollups", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@/lib/measurements/rollups")
|
||||
>("@/lib/measurements/rollups");
|
||||
typeof import("@/lib/rollups/measurement-rollups")
|
||||
>("@/lib/rollups/measurement-rollups");
|
||||
return {
|
||||
...actual,
|
||||
recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -49,7 +49,7 @@ import { NextRequest } from "next/server";
|
||||
import { POST } from "../route";
|
||||
import { checkRateLimit } from "@/lib/rate-limit";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/measurements/rollups";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
@@ -21,7 +21,7 @@ import { recomputeUserMoodRollups } from "@/lib/mood/rollups";
|
||||
import {
|
||||
collapseToTypeDayKeys,
|
||||
recomputeBucketsForMeasurement,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
|
||||
// Derived from canonical enum so round-trip export → import covers every
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "@/lib/api-response";
|
||||
import { updateMeasurementSchema } from "@/lib/validations/measurement";
|
||||
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/measurements/rollups";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ vi.mock("@/lib/db", () => ({
|
||||
// v1.4.39.2 — the rollup branch's coverage-mismatch fallback may call
|
||||
// `recomputeUserRollups` inline. Mock it so tests can assert whether
|
||||
// the inline fold fired without exercising the real populator.
|
||||
vi.mock("@/lib/measurements/rollups", async () => {
|
||||
vi.mock("@/lib/rollups/measurement-rollups", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@/lib/measurements/rollups")
|
||||
>("@/lib/measurements/rollups");
|
||||
typeof import("@/lib/rollups/measurement-rollups")
|
||||
>("@/lib/rollups/measurement-rollups");
|
||||
return {
|
||||
...actual,
|
||||
recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -61,7 +61,7 @@ vi.mock("next/headers", () => ({
|
||||
import { GET } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import { recomputeUserRollups } from "@/lib/measurements/rollups";
|
||||
import { recomputeUserRollups } from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
const SESSION_OK = {
|
||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
|
||||
@@ -41,7 +41,7 @@ import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
|
||||
import {
|
||||
recomputeBucketsForMeasurement,
|
||||
collapseToTypeDayKeys,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
import { Prisma, type MeasurementType } from "@/generated/prisma/client";
|
||||
|
||||
// v1.4.25 W16c — historical-backfill threshold for PR push
|
||||
|
||||
@@ -35,7 +35,7 @@ import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
|
||||
import {
|
||||
recomputeBucketsForMeasurement,
|
||||
collapseToTypeDayKeys,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
const MAX_BATCH_ENTRIES = 500;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
recomputeBucketsForMeasurement,
|
||||
collapseToTypeDayKeys,
|
||||
recomputeUserRollups,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
import { NextRequest } from "next/server";
|
||||
import type {
|
||||
MeasurementType,
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
||||
import { readRollupBuckets } from "@/lib/rollups/measurement-rollups";
|
||||
import {
|
||||
probeRollupCoverage,
|
||||
type RollupCoverageMap,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
||||
import { readRollupBuckets } from "@/lib/rollups/measurement-rollups";
|
||||
import {
|
||||
probeRollupCoverage,
|
||||
type RollupCoverageMap,
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
*/
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
||||
import { readRollupBuckets } from "@/lib/rollups/measurement-rollups";
|
||||
import {
|
||||
probeRollupCoverage,
|
||||
type RollupCoverageMap,
|
||||
|
||||
@@ -59,7 +59,7 @@ import { prisma } from "@/lib/db";
|
||||
import type { DataSummary } from "@/lib/analytics/trends";
|
||||
import { measurementTypeEnum } from "@/lib/validations/measurement";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { ensureUserRollupsFresh } from "@/lib/measurements/rollups";
|
||||
import { ensureUserRollupsFresh } from "@/lib/rollups/measurement-rollups";
|
||||
import {
|
||||
isFullyCovered,
|
||||
probeRollupCoverage,
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import type { DataSummary } from "@/lib/analytics/trends";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { ensureUserRollupsFresh } from "@/lib/measurements/rollups";
|
||||
import { ensureUserRollupsFresh } from "@/lib/rollups/measurement-rollups";
|
||||
import { aggregateBuckets } from "@/lib/measurements/rollup-read";
|
||||
import {
|
||||
isFullyCovered,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type CorrelationResult,
|
||||
} from "@/lib/analytics/correlations";
|
||||
import { getMedicationCategories } from "@/lib/medication-category";
|
||||
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
||||
import { readRollupBuckets } from "@/lib/rollups/measurement-rollups";
|
||||
import {
|
||||
ensureUserMoodRollupsFresh,
|
||||
readMoodDayRollups,
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
type ImportJobProgress,
|
||||
type ImportJobResult,
|
||||
} from "@/lib/measurements/import-apple-health-export";
|
||||
import { recomputeUserRollups } from "@/lib/measurements/rollups";
|
||||
import { recomputeUserRollups } from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
/** Queue name for the Apple Health export ingest. */
|
||||
export const APPLE_HEALTH_IMPORT_QUEUE = "apple-health-import";
|
||||
|
||||
@@ -66,7 +66,7 @@ import {
|
||||
recomputeUserRollups,
|
||||
type RollupFullBackfillPayload,
|
||||
type RollupRecomputePayload,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
import {
|
||||
MOOD_ROLLUP_FULL_BACKFILL_QUEUE,
|
||||
MOOD_ROLLUP_FULL_BACKFILL_CONCURRENCY,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* the v1.4.34.1 / 4.5 aggregator survives.
|
||||
*/
|
||||
import type { MeasurementType } from "@/generated/prisma/client";
|
||||
import { readRollupBuckets } from "./rollups";
|
||||
import { readRollupBuckets } from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
export interface DailyMeanRow {
|
||||
day: Date;
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ import {
|
||||
enqueueRollupRecompute,
|
||||
ensureUserRollupsFresh,
|
||||
recomputeBucketsForMeasurement,
|
||||
} from "../rollups";
|
||||
} from "../measurement-rollups";
|
||||
|
||||
beforeEach(() => {
|
||||
queryRaw.mockReset();
|
||||
File renamed without changes.
@@ -49,10 +49,10 @@ vi.mock("@/lib/logging/context", () => ({
|
||||
// measurement rollup writer so the dashboard chart's `source=rollup`
|
||||
// fast-path sees the new buckets. The mock collects the call args so
|
||||
// the regression test below can assert the hook fired.
|
||||
vi.mock("@/lib/measurements/rollups", async () => {
|
||||
vi.mock("@/lib/rollups/measurement-rollups", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@/lib/measurements/rollups")
|
||||
>("@/lib/measurements/rollups");
|
||||
typeof import("@/lib/rollups/measurement-rollups")
|
||||
>("@/lib/rollups/measurement-rollups");
|
||||
return {
|
||||
...actual,
|
||||
recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -65,7 +65,7 @@ import {
|
||||
recordSyncFailure,
|
||||
recordSyncSuccess,
|
||||
} from "@/lib/integrations/status";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/measurements/rollups";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
import { fetchWithingsActivity, syncUserActivity } from "../sync-activity";
|
||||
|
||||
|
||||
@@ -48,10 +48,10 @@ vi.mock("@/lib/logging/context", () => ({
|
||||
// rollup tier for each touched SLEEP_DURATION day so the dashboard
|
||||
// chart's `source=rollup` fast-path sees the new segments. The mock
|
||||
// surfaces the call args for the regression test below.
|
||||
vi.mock("@/lib/measurements/rollups", async () => {
|
||||
vi.mock("@/lib/rollups/measurement-rollups", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("@/lib/measurements/rollups")
|
||||
>("@/lib/measurements/rollups");
|
||||
typeof import("@/lib/rollups/measurement-rollups")
|
||||
>("@/lib/rollups/measurement-rollups");
|
||||
return {
|
||||
...actual,
|
||||
recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -64,7 +64,7 @@ import {
|
||||
recordSyncFailure,
|
||||
recordSyncSuccess,
|
||||
} from "@/lib/integrations/status";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/measurements/rollups";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
import {
|
||||
fetchWithingsSleep,
|
||||
|
||||
@@ -41,7 +41,7 @@ import { getUnitForType } from "@/lib/validations/measurement";
|
||||
import {
|
||||
collapseToTypeDayKeys,
|
||||
recomputeBucketsForMeasurement,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
import { hasActivityScope } from "./client";
|
||||
import {
|
||||
|
||||
@@ -39,7 +39,7 @@ import { getUnitForType } from "@/lib/validations/measurement";
|
||||
import {
|
||||
collapseToTypeDayKeys,
|
||||
recomputeBucketsForMeasurement,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
import { hasActivityScope } from "./client";
|
||||
import {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import {
|
||||
collapseToTypeDayKeys,
|
||||
recomputeBucketsForMeasurement,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
/**
|
||||
* Build the callback URL handed to Withings at `Notify.subscribe` time.
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
recomputeBucketsForMeasurement,
|
||||
recomputeUserRollups,
|
||||
ALL_GRANULARITIES,
|
||||
} from "@/lib/measurements/rollups";
|
||||
} from "@/lib/rollups/measurement-rollups";
|
||||
import { aggregateBuckets } from "@/lib/measurements/rollup-read";
|
||||
import { buildComprehensiveAggregate } from "@/lib/insights/comprehensive-aggregator";
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
process.env.ENCRYPTION_KEY ??=
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/measurements/rollups";
|
||||
import { recomputeBucketsForMeasurement } from "@/lib/rollups/measurement-rollups";
|
||||
|
||||
import { cookieJar, headerJar } from "./mock-next-headers";
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
Reference in new issue
Block a user