Skip to content

Switch sleep dashboard reads to ClickHouse - #1151

Merged
Asherlc merged 16 commits into
mainfrom
Asherlc/dashboard-sleep-stale
May 21, 2026
Merged

Asherlc merged 16 commits into
mainfrom
Asherlc/dashboard-sleep-stale

Conversation

@Asherlc

@Asherlc Asherlc commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

Moves runtime sleep reads from stale Postgres fitness.v_sleep to ClickHouse analytics.v_sleep across sleep, dashboard, recovery, stress, prediction, healthspan, and personalization paths.
Adds a shared ClickHouse sleep helper and a migration dropping clickhouse.v_sleep / fitness.v_sleep, removes the canonical Postgres view artifact, and records the production incident baseline.
Validated with pnpm lint, pnpm tsc --noEmit, cd packages/server && pnpm tsc --noEmit, pnpm migrate, and targeted sleep tests.
Known: pnpm test:changed still fails because existing unit/integration tests need conversion from mocked/refreshed Postgres v_sleep setup to ClickHouse test-store sync.

Summary by CodeRabbit

  • Bug Fixes

    • Dashboards and sleep endpoints now show fresher, non-stale sleep data by switching runtime reads to the updated analytics source.
  • Chores / Migrations

    • Removed legacy canonical sleep view and added a safe migration to drop obsolete view artifacts.
  • Documentation

    • Added an incident baseline and implementation plan documenting symptoms, mitigations, and verification steps.
  • Tests

    • Updated and expanded tests and helpers to reflect the new sleep data flow and ensure regression coverage.

Review Change Stack

Copilot AI review requested due to automatic review settings May 20, 2026 21:47
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Asherlc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 39 minutes and 43 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 430726d8-cbe6-4f53-bd8e-01ad6da26955

📥 Commits

Reviewing files that changed from the base of the PR and between 46b0758 and e241de9.

📒 Files selected for processing (1)
  • packages/server/src/routers/router.integration.test.ts
📝 Walkthrough

Walkthrough

Migrates runtime sleep-night reads from Postgres fitness.v_sleep to ClickHouse analytics.v_sleep, adds ClickHouse repository helpers (fetchSleepNights, fetchLatestSleepNight), removes SQL dedup helpers/views, and updates routers, repositories, tests, docs, and test infra accordingly.

Changes

Sleep Data Migration to ClickHouse

Layer / File(s) Summary
DB migration and view removal
drizzle/0025_drop_v_sleep.sql, drizzle/_views/02_v_sleep.sql
Adds migration to drop clickhouse.v_sleep and fitness.v_sleep, and deletes the Postgres materialized view definition.
ClickHouse sleep repository
packages/server/src/repositories/clickhouse-sleep-repository.ts
New ClickHouse-backed fetch helpers and Zod schemas: fetchSleepNights, fetchLatestSleepNight, and exported ClickHouseSleepNight/FetchSleepNightsInput.
Remove SQL fragments / tests
packages/server/src/lib/sql-fragments.ts, packages/server/src/lib/sql-fragments.test.ts
Remove SQL CTE helpers (sleepNightDate, sleepDedupCte) and their unit tests.
Repository integrations
packages/server/src/repositories/*
Replace fitness.v_sleep SQL reads with ClickHouse fetches and/or JS-side baseline/stat computation across anomaly, predictions, insights, correlation, life-events, stress, and sleep repos.
SleepRepository and tests
packages/server/src/repositories/sleep-repository.ts, packages/server/src/repositories/sleep-repository.test.ts
Inject ActivitySensorStore, use fetchSleepNights/fetchLatestSleepNight for list/latest, refactor stages/latestStages to join sleep_session, and update tests to assert ClickHouse queries and numeric coercion.
Router changes
packages/server/src/routers/*
Split SQL+sleep flows: metrics SQLs null efficiency_pct, fetch sleep nights from ClickHouse concurrently, and merge results in JS for readiness/stress/healthspan/mobile-dashboard/sleep endpoints; routers now pass ctx.sensorStore into repos.
Anomaly / Predictions / LifeEvents
packages/server/src/repositories/* and tests
Move sleep-baseline/stat computation into JS using fetchSleepNights; update tests to supply sensorStore-driven sleep-row fixtures and adjust expected DB/CH call counts.
Recovery & Sleep-Need
packages/server/src/routers/recovery*.ts, packages/server/src/routers/sleep-need*.ts and tests
Fetch ordered sleep nights from ClickHouse, compute medians/stddevs/bedtime hours in JS, and update performance/readiness/strain flows plus tests and fixtures.
Personalization / Refit
src/personalization/refit.ts and tests
Pass sensorStore into sleep-fit steps; overload fitSleepFromDb to accept sensorStore; split readiness fit to fetch and merge sleep efficiency from CH.
Test infra & integration tests
src/db/test-helpers.ts, many *.integration.test.ts
Stop refreshing Postgres fitness.v_sleep in test setups; add ClickHouse test helpers and sync test sensor store where needed; update integration tests to mirror rows into ClickHouse instead of refreshing materialized views.
Docs & incident plan
docs/production-incident-baseline.md, docs/superpowers/plans/2026-05-20-clickhouse-sleep-dashboard.md
Document incident, remediation plan to switch runtime to ClickHouse, migration to drop v_sleep, and a verification/test checklist.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Possibly related PRs

  • Suggested labels
    area/server, area/db, type/feature, breaking-change

…ep-stale

# Conflicts:
#	docs/production-incident-baseline.md
@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Storybook previews for ccf67827 are ready:

This comment updates automatically on each PR push.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@github-actions

Copy link
Copy Markdown
Contributor

Review app is ready:

This environment runs on a dedicated Hetzner server for PR #1151 and updates on each push.

Asherlc added 3 commits May 20, 2026 15:38
The setupTestDatabase loop that re-runs migrations 0008/0017/0019/0025
was missing the filter that the primary migration loop uses. Migration
0019 creates clickhouse.v_sleep referencing fitness.v_sleep, but 0025
drops fitness.v_sleep, so the proxy creation fails on the secondary
replay. Apply the same filter so the CREATE statement is skipped
consistently in both loops.
The previous query aliased formatDateTime(started_at) AS started_at in
the same projection that the row_number() window function ran over.
ClickHouse resolved the inner toTimeZone(started_at, ...) call to the
aliased String column, causing 'Illegal type String of argument of
function toTimezone' on initial Stryker test runs.

Push the formatting up into the outer SELECT so the inner subquery
operates on the raw DateTime64 columns. The outer shape (and Zod
schema) is unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/server/src/repositories/anomaly-detection-repository.ts (1)

35-49: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Schema retains unused sleep columns.

anomalyCheckRowSchema still defines duration_minutes, sleep_mean, sleep_sd, sleep_count (lines 45-48), but these are now always NULL/0 from the SQL. Consider removing them to keep the schema honest about what the query actually returns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/repositories/anomaly-detection-repository.ts` around
lines 35 - 49, The schema anomalyCheckRowSchema includes unused sleep-related
fields (duration_minutes, sleep_mean, sleep_sd, sleep_count) that the SQL now
always returns as NULL/0; remove these four keys from the z.object so the schema
matches the actual query result and avoid misleading nullable fields—update
anomalyCheckRowSchema definition to only include date, resting_hr, rhr_mean,
rhr_sd, rhr_count, hrv, hrv_mean, hrv_sd, and hrv_count.
src/personalization/refit.ts (1)

371-454: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing Zod validation for Postgres sleep rows path.

Lines 443-450 access row.duration_minutes and row.date directly on the result of db.execute (Postgres path, lines 373-385) without schema validation. The ClickHouse path at lines 386-410 correctly uses a Zod schema via sensorStore.query, but the Postgres path bypasses this. Per AGENTS.md: "Use Zod to parse data crossing runtime boundaries (DB results)."

The hrvRows processing at lines 435-442 validates with Zod inline. Apply the same pattern to sleepRows:

🛡️ Proposed fix to add Zod validation for Postgres sleepRows
+const sleepRowDbSchema = z.object({
+  date: z.string(),
+  duration_minutes: z.coerce.number().nullable(),
+});
+
 const [sleepRows, hrvRows] = await Promise.all([
   typeof sensorStoreOrUserId === "string"
-    ? db.execute(
+    ? db.execute(
         sql`WITH nightly AS (
           ...
         )
         SELECT date, duration_minutes FROM nightly ORDER BY date ASC`,
-      )
+      ).then((rows) => rows.map((row) => sleepRowDbSchema.parse(row)))
     : sensorStoreOrUserId.query(
         z.object({
           date: z.string(),
           duration_minutes: z.coerce.number().nullable(),
         }),
         ...
       ),

Alternatively, use executeWithSchema from typed-sql.ts for the Postgres query.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/personalization/refit.ts` around lines 371 - 454, The Postgres branch
that populates sleepRows (db.execute call when sensorStoreOrUserId is a string)
returns raw rows but is used later assuming shape (row.date and
row.duration_minutes); add Zod validation like the ClickHouse path to parse each
Postgres row before using it (or replace the db.execute with executeWithSchema
from typed-sql.ts). Specifically, validate the result of the db.execute used to
produce sleepRows with a schema z.object({ date: z.string(), duration_minutes:
z.coerce.number().nullable() }) and parse/transform to the same shape the
sensorStore.query branch produces so the later rows.flatMap logic (and
parseSleepRows) can safely access row.date and row.duration_minutes.
packages/server/src/routers/mobile-dashboard.test.ts (1)

63-91: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Mock sequence is brittle and undocumented.

The query mock relies on a specific call order that must match production code exactly. If the production code reorders queries or adds new ones, this test will silently return wrong data instead of failing clearly.

Consider adding inline comments documenting which call each mockResolvedValueOnce corresponds to, or use a more explicit matching approach.

Suggested documentation
 function makeSensorStore(
   dailyLoads: Array<{ metric_date: string; daily_load: number }> = [],
   yesterdayLoad = 0,
   currentPhysiologyRows: Array<{ physiological_load: number | null }> = [],
   baselineSleepRows: SleepTestRow[] = [],
   lastNightSleepRows: SleepTestRow[] = baselineSleepRows,
 ): SensorStore {
   const query = vi.fn();
-  query.mockResolvedValueOnce(dailyLoads);
-  query.mockResolvedValueOnce([{ load: yesterdayLoad }]);
-  query.mockResolvedValueOnce([]);
-  query.mockResolvedValueOnce(sleepRowsForClickHouse(baselineSleepRows));
-  query.mockResolvedValueOnce(sleepRowsForClickHouse(lastNightSleepRows));
-  query.mockResolvedValueOnce(sleepRowsForClickHouse(baselineSleepRows));
-  query.mockResolvedValueOnce(currentPhysiologyRows);
-  query.mockResolvedValue([]);
+  query.mockResolvedValueOnce(dailyLoads); // 1: daily load (60d)
+  query.mockResolvedValueOnce([{ load: yesterdayLoad }]); // 2: yesterday load
+  query.mockResolvedValueOnce([]); // 3: resting heart rate
+  query.mockResolvedValueOnce(sleepRowsForClickHouse(baselineSleepRows)); // 4: readiness sleep (60d)
+  query.mockResolvedValueOnce(sleepRowsForClickHouse(lastNightSleepRows)); // 5: sleep analytics (14d)
+  query.mockResolvedValueOnce(sleepRowsForClickHouse(baselineSleepRows)); // 6: sleep baseline (90d)
+  query.mockResolvedValueOnce(currentPhysiologyRows); // 7: current physiology
+  query.mockResolvedValue([]); // fallback for any additional calls
   return {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/routers/mobile-dashboard.test.ts` around lines 63 - 91,
The test helper makeSensorStore builds a brittle query mock by using a fixed
sequence of query.mockResolvedValueOnce calls; replace this with an explicit
mapping or document each call: either (A) change query to
query.mockImplementation((sql, params) => { if (matches expected SQL/params for
dailyLoads) return Promise.resolve(dailyLoads); if (matches yesterday load
query) return Promise.resolve([{ load: yesterdayLoad }]); if (matches baseline
sleep) return Promise.resolve(sleepRowsForClickHouse(baselineSleepRows)); ...
else return Promise.resolve([]); }) using the actual query text/params used in
production, or (B) at minimum add inline comments next to each
query.mockResolvedValueOnce that state which production query it corresponds to
(e.g., "dailyLoads query", "yesterday load query", "baseline sleep rows", "last
night sleep rows", "current physiology rows"), referencing makeSensorStore,
query, and sleepRowsForClickHouse to locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-05-20-clickhouse-sleep-dashboard.md`:
- Line 13: The heading "Task 1: Add Red Tests For ClickHouse Sleep Reads"
currently uses an H3 and causes a heading-level jump; change it to H2 (use "##
Task 1: Add Red Tests For ClickHouse Sleep Reads") or insert an intermediate H2
above it so the document flows H1 → H2 → H3 and resolves the markdown lint
failure.
- Line 3: Replace the agent-only directive "**For agentic workers: REQUIRED
SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or
superpowers:executing-plans to implement this plan task-by-task. Steps use
checkbox (`- [ ]`) syntax for tracking." with human-actionable instructions that
do not reference agent sub-skills (e.g., "Follow the step-by-step checklist
below and mark tasks complete using `- [ ]` syntax"). Update any phrasing that
assumes an agent (search for the exact agent-only phrase) so the plan reads as
standalone human-executable guidance and maintain the checklist syntax and task
structure.

In `@packages/server/src/repositories/anomaly-detection-repository.ts`:
- Around line 298-306: The fallback using row.duration_minutes / row.sleep_mean
/ row.sleep_sd / row.sleep_count is dead because the SQL now returns NULL/0 for
those columns; update the logic in anomaly-detection-repository.ts so sleepStats
is only derived from fetchSleepNights results: replace the current ternary so
sleepStats = sleepRows.length > 0 ? sleepStatsForDate(sleepRows, date) : null
(or a well-defined empty default), remove references to
row.duration_minutes/row.sleep_* there, and update any callers of sleepStats to
handle the null/empty-default case accordingly; key symbols: sleepStats,
sleepRows, sleepStatsForDate, and the surrounding block where row is used.

In `@packages/server/src/repositories/clickhouse-sleep-repository.ts`:
- Around line 41-46: The accessWindowClause uses raw started_at bounds which
mismatch the query's canonical sleep-day expression; update accessWindowClause
to apply the same timezone-shifted canonical-sleep-day transformation used in
selection (the toTimeZone(... ) - INTERVAL 6 HOUR expression) and compare that
transformed value against parseDateTimeBestEffort({accessStartDate:String}) and
parseDateTimeBestEffort({accessEndDateExclusive:String}) (or their toDate
equivalents) instead of started_at, and make the identical replacement at the
other occurrences referenced (the similar started_at comparisons around the
blocks at the other locations).

In `@packages/server/src/repositories/insights-repository.ts`:
- Around line 68-88: The mapping of fetchSleepNights results hardcodes is_nap:
false inside the sleepRowSchema.parse call, which will mislabel nap sessions;
change the mapping in the promise handler so it reads the nap flag from the
fetched row (e.g., use row.is_nap or row.isNap with a safe fallback like false)
instead of the literal false, i.e. pass the actual nap property through to
sleepRowSchema.parse for each row returned by fetchSleepNights.

In `@packages/server/src/repositories/predictions-repository.ts`:
- Around line 387-395: The endDate is being computed in UTC with new
Date().toISOString().slice(0,10) which misaligns timezone-local daily metrics;
change the endDate calculation used before calling fetchSleepNights to produce
the current date in this.#timezone (timezone-local) instead of UTC so
fetchSleepNights({ ..., endDate, ... }) receives the local date string; update
the code that sets endDate (the variable referenced and the call site to
fetchSleepNights) to derive the YYYY-MM-DD for this.#timezone (e.g., using a
timezone-aware formatter or library) so sleep windows align with the user's
timezone.

In `@packages/server/src/repositories/sleep-repository.test.ts`:
- Around line 136-162: The test for getLatestStages is missing an assertion that
the Postgres path is invoked after ClickHouse returns data; update the test in
sleep-repository.test.ts to assert that the repo's this.query/execute mock was
called (e.g., expect(execute).toHaveBeenCalled() or toHaveBeenCalledTimes(1))
after calling repo.getLatestStages(), so that when clickHouseRows is non-empty
the code still calls the Postgres query path; locate the test block that
constructs makeRepository and repo.getLatestStages() and add the execute/mock
verification immediately after the existing result assertions.

In `@packages/server/src/routers/healthspan-query.ts`:
- Around line 203-220: fetchSleepNights is called without the accessWindow
filter so sleepRows may include data outside the user's billing entitlements;
update the fetchSleepNights invocation in healthspan-query.ts to pass
ctx.accessWindow (e.g., add accessWindow: ctx.accessWindow to the argument
object) so sleepRows, and derived values like sleepDurations, bedtimes,
avgSleepMin and bedtimeStddevMin, are computed only for data within the user's
allowed window.

In
`@packages/server/src/routers/recovery-settings-sleep-need-sport-settings.test.ts`:
- Around line 82-85: The test is using a hardcoded today for every synthesized
sleep schedule row so all rows share the same date; change the generator to use
each fixture row's date (e.g., row.date or fixture.date) when filling the date
field and when calling hourTimestamp for started_at and ended_at (instead of
today) so each schedule entry reflects its corresponding fixture row's date;
update the created object where date, started_at, ended_at, and any
duration/minute computations derive from that per-row date.

In `@packages/server/src/routers/recovery.ts`:
- Around line 709-714: The call in strainTarget to fetchLatestSleepNight
currently retrieves the globally latest sleep night and can return data after
the requested endDate; modify strainTarget to constrain that lookup by passing
input.endDate to fetchLatestSleepNight (or extend fetchLatestSleepNight to
accept an endDate/ cutoff parameter), update the fetchLatestSleepNight signature
and all its call sites accordingly to enforce historical determinism, and ensure
strainTarget uses the returned sleep night filtered by that endDate when
computing readiness/target.

In `@packages/server/src/routers/router-logic.integration.test.ts`:
- Line 84: Delete the dead no-op function refreshViews() from the test file and
remove all calls to refreshViews() (it is invoked in five places) since
fitness.v_sleep no longer exists; ensure any necessary behavior is handled by
the existing syncClickHouseTestActivitySensorStore() calls already present (do
not replace those calls with refreshViews(), just delete the refreshViews()
calls and the function definition).

In `@packages/server/src/routers/sleep-need.ts`:
- Around line 247-252: The route is calling fetchLatestSleepNight without
scoping to the requested analysis date, so lastSleep can be newer than
input.endDate; update the call in the performance route to pass the requested
end date (input.endDate) and, if fetchLatestSleepNight lacks that parameter, add
an endDate (or asOf) argument to fetchLatestSleepNight and make it filter/search
for the latest sleep whose end/time <= endDate. Ensure the symbol names
fetchLatestSleepNight and lastSleep are updated accordingly and that all
historical endpoints that rely on fetchLatestSleepNight are adjusted to use the
new endDate/asOf parameter.
- Around line 129-141: The HRV query omits an upper bound and proper
access-window gating, allowing out-of-range dates to affect median/baseline;
update the SQL in the executeWithSchema call that queries
fitness.v_daily_metrics to constrain date to the requested window by adding both
a lower and upper bound (e.g., change the WHERE clause to use date >=
dateWindowStart(input.endDate, 90) AND date <= ${input.endDate} or a BETWEEN
equivalent) and ensure you keep the user filter (ctx.userId) — this will prevent
future or out-of-range HRV rows from influencing medianHrv/baseline
calculations.

In `@src/db/test-helpers.ts`:
- Around line 83-86: Extract the duplicated string predicate into a single
constant and reuse it in both filter calls: define a constant (e.g.,
SKIP_SLEEP_VIEW = "CREATE OR REPLACE VIEW clickhouse.v_sleep AS") near the top
of the module and replace the inline .filter(...) checks that call
.includes("CREATE OR REPLACE VIEW clickhouse.v_sleep AS") with
.includes(SKIP_SLEEP_VIEW) so both occurrences (the filter at the earlier
.filter(...) and the later similar .filter(...)) reference the same symbol.

---

Outside diff comments:
In `@packages/server/src/repositories/anomaly-detection-repository.ts`:
- Around line 35-49: The schema anomalyCheckRowSchema includes unused
sleep-related fields (duration_minutes, sleep_mean, sleep_sd, sleep_count) that
the SQL now always returns as NULL/0; remove these four keys from the z.object
so the schema matches the actual query result and avoid misleading nullable
fields—update anomalyCheckRowSchema definition to only include date, resting_hr,
rhr_mean, rhr_sd, rhr_count, hrv, hrv_mean, hrv_sd, and hrv_count.

In `@packages/server/src/routers/mobile-dashboard.test.ts`:
- Around line 63-91: The test helper makeSensorStore builds a brittle query mock
by using a fixed sequence of query.mockResolvedValueOnce calls; replace this
with an explicit mapping or document each call: either (A) change query to
query.mockImplementation((sql, params) => { if (matches expected SQL/params for
dailyLoads) return Promise.resolve(dailyLoads); if (matches yesterday load
query) return Promise.resolve([{ load: yesterdayLoad }]); if (matches baseline
sleep) return Promise.resolve(sleepRowsForClickHouse(baselineSleepRows)); ...
else return Promise.resolve([]); }) using the actual query text/params used in
production, or (B) at minimum add inline comments next to each
query.mockResolvedValueOnce that state which production query it corresponds to
(e.g., "dailyLoads query", "yesterday load query", "baseline sleep rows", "last
night sleep rows", "current physiology rows"), referencing makeSensorStore,
query, and sleepRowsForClickHouse to locate the code.

In `@src/personalization/refit.ts`:
- Around line 371-454: The Postgres branch that populates sleepRows (db.execute
call when sensorStoreOrUserId is a string) returns raw rows but is used later
assuming shape (row.date and row.duration_minutes); add Zod validation like the
ClickHouse path to parse each Postgres row before using it (or replace the
db.execute with executeWithSchema from typed-sql.ts). Specifically, validate the
result of the db.execute used to produce sleepRows with a schema z.object({
date: z.string(), duration_minutes: z.coerce.number().nullable() }) and
parse/transform to the same shape the sensorStore.query branch produces so the
later rows.flatMap logic (and parseSleepRows) can safely access row.date and
row.duration_minutes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3ef61f49-6da2-4c58-8249-d730e1fc56eb

📥 Commits

Reviewing files that changed from the base of the PR and between 27c4893 and d2101e9.

📒 Files selected for processing (54)
  • docs/production-incident-baseline.md
  • docs/superpowers/plans/2026-05-20-clickhouse-sleep-dashboard.md
  • drizzle/0025_drop_v_sleep.sql
  • drizzle/_views/02_v_sleep.sql
  • packages/server/src/billing/entitlement.ts
  • packages/server/src/insights/schemas.ts
  • packages/server/src/lib/sql-fragments.test.ts
  • packages/server/src/lib/sql-fragments.ts
  • packages/server/src/repositories/anomaly-detection-repository.test.ts
  • packages/server/src/repositories/anomaly-detection-repository.ts
  • packages/server/src/repositories/clickhouse-sleep-repository.ts
  • packages/server/src/repositories/correlation-repository.test.ts
  • packages/server/src/repositories/correlation-repository.ts
  • packages/server/src/repositories/derived-cardio-repository.integration.test.ts
  • packages/server/src/repositories/insights-repository.test.ts
  • packages/server/src/repositories/insights-repository.ts
  • packages/server/src/repositories/life-events-repository.test.ts
  • packages/server/src/repositories/life-events-repository.ts
  • packages/server/src/repositories/predictions-repository.test.ts
  • packages/server/src/repositories/predictions-repository.ts
  • packages/server/src/repositories/sleep-repository.test.ts
  • packages/server/src/repositories/sleep-repository.ts
  • packages/server/src/repositories/stress-repository.ts
  • packages/server/src/repositories/training-repository.test.ts
  • packages/server/src/repositories/training-repository.ts
  • packages/server/src/routers/anomaly-detection.test.ts
  • packages/server/src/routers/body-calendar-sleep-nutrition.test.ts
  • packages/server/src/routers/healthspan-query.ts
  • packages/server/src/routers/healthspan.integration.test.ts
  • packages/server/src/routers/hiking-insights-life-events.test.ts
  • packages/server/src/routers/life-events.test.ts
  • packages/server/src/routers/mobile-dashboard.integration.test.ts
  • packages/server/src/routers/mobile-dashboard.test.ts
  • packages/server/src/routers/mobile-dashboard.ts
  • packages/server/src/routers/predictions.integration.test.ts
  • packages/server/src/routers/predictions.test.ts
  • packages/server/src/routers/recovery-settings-sleep-need-sport-settings.test.ts
  • packages/server/src/routers/recovery.test.ts
  • packages/server/src/routers/recovery.ts
  • packages/server/src/routers/router-data.integration.test.ts
  • packages/server/src/routers/router-logic.integration.test.ts
  • packages/server/src/routers/router.integration.test.ts
  • packages/server/src/routers/sleep-consistency-across-endpoints.integration.test.ts
  • packages/server/src/routers/sleep-need.integration.test.ts
  • packages/server/src/routers/sleep-need.test.ts
  • packages/server/src/routers/sleep-need.ts
  • packages/server/src/routers/sleep.integration.test.ts
  • packages/server/src/routers/sleep.test.ts
  • packages/server/src/routers/sleep.ts
  • packages/server/src/routers/stress.ts
  • src/db/test-helpers.ts
  • src/personalization/refit.integration.test.ts
  • src/personalization/refit.test.ts
  • src/personalization/refit.ts
💤 Files with no reviewable changes (13)
  • packages/server/src/lib/sql-fragments.test.ts
  • packages/server/src/routers/sleep-consistency-across-endpoints.integration.test.ts
  • packages/server/src/lib/sql-fragments.ts
  • packages/server/src/routers/mobile-dashboard.integration.test.ts
  • drizzle/_views/02_v_sleep.sql
  • packages/server/src/billing/entitlement.ts
  • packages/server/src/routers/sleep-need.integration.test.ts
  • packages/server/src/routers/predictions.integration.test.ts
  • packages/server/src/repositories/derived-cardio-repository.integration.test.ts
  • packages/server/src/routers/router-data.integration.test.ts
  • packages/server/src/routers/sleep.integration.test.ts
  • packages/server/src/routers/router.integration.test.ts
  • packages/server/src/routers/healthspan.integration.test.ts

Comment thread docs/superpowers/plans/2026-05-20-clickhouse-sleep-dashboard.md Outdated
Comment thread docs/superpowers/plans/2026-05-20-clickhouse-sleep-dashboard.md Outdated
Comment thread packages/server/src/repositories/anomaly-detection-repository.ts
Comment thread packages/server/src/repositories/clickhouse-sleep-repository.ts
Comment thread packages/server/src/repositories/insights-repository.ts
Comment thread packages/server/src/routers/recovery.ts
Comment thread packages/server/src/routers/router-logic.integration.test.ts Outdated
Comment thread packages/server/src/routers/sleep-need.ts
Comment thread packages/server/src/routers/sleep-need.ts
Comment thread src/db/test-helpers.ts Outdated
Asherlc added 6 commits May 20, 2026 16:53
The previous fix moved formatting to the outer SELECT, but the outer
SELECT still aliased formatDateTime(started_at, ...) AS started_at
alongside toString(toDate(toTimeZone(started_at, ...))) AS date.
ClickHouse resolves identifiers in the SELECT list to other aliases
in the same clause, so toTimeZone(started_at) hit the String alias
again.

Project the raw DateTime64 columns out of the subquery under distinct
names (started_at_dt, ended_at_dt) and compute the partition-key date
inside the subquery. The outer SELECT then only formats the
distinctly-named columns. Same wire shape and Zod schema.
…ep-stale

# Conflicts:
#	packages/server/src/repositories/training-repository.test.ts
#	packages/server/src/repositories/training-repository.ts
- test-helpers.ts: extract shared filter predicate for v_sleep skip
- router-logic.integration.test.ts: delete dead refreshViews() no-op and
  its 5 call sites
- sleep-repository.test.ts: assert execute is called when ClickHouse
  returns sleep windows in getLatestStages
- recovery-settings-sleep-need-sport-settings.test.ts: use each fixture
  row's date when synthesizing schedule rows so cutoff assertions remain
  meaningful
- clickhouse-sleep-repository.ts: align accessWindowClause with the
  timezone-shifted sleep-day predicate so entitlement windows agree with
  the rest of the query
- predictions-repository.ts: compute endDate in user timezone so sleep
  windows align with daily metrics
- healthspan-query.ts: pass ctx.accessWindow into fetchSleepNights
- sleep-need.ts: bound the HRV query to <= endDate and apply
  dateAccessPredicate so out-of-window HRV cannot influence baselines
- docs/superpowers/plans/2026-05-20-clickhouse-sleep-dashboard.md:
  replace agent-only directive with human-executable note and bump task
  headings from H3 to H2 to satisfy markdownlint
Add an optional endDate parameter that caps the search to the
timezone-shifted sleep-day <= endDate. Routes parameterized by an end
date (recovery.strainTarget, sleepNeed.performance) now pass
input.endDate so historical readiness/performance calls cannot pull
sleep data newer than the requested date. Sleep-repository call sites
that genuinely want the global latest are left unchanged (the new
parameter is optional).
Three issues surfaced in CI:

1) sleepMinutes regression in recovery.sleepAnalytics — the migration
   started computing sleepMinutes from stage sums for every provider,
   but on main only Apple Health gets that treatment because other
   providers already exclude awake time from duration_minutes. Restore
   provider-aware logic by surfacing provider_id through
   fetchSleepNights / ClickHouseSleepNight and only deriving from
   stages when provider_id === 'apple_health'. Also use the same
   logic for the 7-day rolling average.

2) days=N window off-by-one — fetchSleepNights used a strict '>'
   lower bound on the timezone-shifted sleep day, dropping the
   night exactly N days before endDate. Tests that seed N nights
   expect all N to be returned. Switch the lower bound to '>='.

3) sleep.integration.test.ts and the AH branch of
   router.integration.test.ts insert sleep_session rows into Postgres
   and then query the ClickHouse-backed analytics view. The old test
   refreshed fitness.v_sleep, but that matview no longer exists; mirror
   the rows into ClickHouse via createClickHouseTestActivitySensorStore
   (initial sync) and syncClickHouseTestActivitySensorStore (post-
   insert sync) so the analytics view sees them.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/server/src/repositories/clickhouse-sleep-repository.ts (1)

110-111: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Remove redundant Zod parsing after sensorStore.query.

ActivitySensorStore.query is documented to “parse rows with the supplied Zod schema”, and ClickHouseActivitySensorStore.query already does return rows.map((row) => schema.parse(row)). clickhouse-sleep-repository.ts then parses again (clickHouseSleepNightSchema.parse(row)), making the extra .parse() redundant.

Suggested fix
-  return rows.map((row) => clickHouseSleepNightSchema.parse(row));
+  return rows;

Apply the same adjustment to the fetchLatestSleepNight path (the parsedRows = rows.map(...parse...) section).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/repositories/clickhouse-sleep-repository.ts` around lines
110 - 111, The rows returned by ActivitySensorStore.query are already parsed by
ClickHouseActivitySensorStore.query using the provided Zod schema, so remove the
redundant clickHouseSleepNightSchema.parse calls: in the function that currently
does return rows.map((row) => clickHouseSleepNightSchema.parse(row)) simply
return rows (or rows as the expected type), and in fetchLatestSleepNight remove
the parsedRows = rows.map(...parse...) step and use the already-parsed rows
directly; keep references to clickHouseSleepNightSchema only where the schema is
passed into ActivitySensorStore.query (not for re-parsing).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/server/src/routers/activity-dedup.integration.test.ts`:
- Around line 24-28: The dateDaysAgo function recomputes new Date() on each call
which can cross UTC midnight and cause flakiness; fix by capturing a single base
timestamp once (e.g., const baseDate = new Date() or baseIso = new
Date().toISOString()) at the top of the test or before generating fixtures and
then change dateDaysAgo to derive dates from that base (either by accepting
baseDate/baseIso as a parameter or closing over the captured baseDate) so all
calls use the same reference moment when computing date strings.

In `@packages/server/src/routers/sleep.integration.test.ts`:
- Around line 91-99: The UTC ISO regex used in the "sleep.list returns
started_at in UTC ISO 8601 format" test (UTC_ISO_REGEX) is too strict and
rejects valid fractional-second timestamps like "...:ss.SSSZ"; update
UTC_ISO_REGEX to allow an optional fractional-second component (e.g. \.\d+ )
before the trailing Z so strings like "2023-01-01T12:00:00.123Z" match, keeping
the existing Date parsing check (new Date(row.started_at).getTime()) in the test
unchanged.

---

Outside diff comments:
In `@packages/server/src/repositories/clickhouse-sleep-repository.ts`:
- Around line 110-111: The rows returned by ActivitySensorStore.query are
already parsed by ClickHouseActivitySensorStore.query using the provided Zod
schema, so remove the redundant clickHouseSleepNightSchema.parse calls: in the
function that currently does return rows.map((row) =>
clickHouseSleepNightSchema.parse(row)) simply return rows (or rows as the
expected type), and in fetchLatestSleepNight remove the parsedRows =
rows.map(...parse...) step and use the already-parsed rows directly; keep
references to clickHouseSleepNightSchema only where the schema is passed into
ActivitySensorStore.query (not for re-parsing).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0f90b04d-496e-477a-a0a3-e9cd3d5e8120

📥 Commits

Reviewing files that changed from the base of the PR and between d2101e9 and 6cab36a.

📒 Files selected for processing (22)
  • docs/production-incident-baseline.md
  • docs/superpowers/plans/2026-05-20-clickhouse-sleep-dashboard.md
  • packages/server/src/repositories/anomaly-detection-repository.test.ts
  • packages/server/src/repositories/anomaly-detection-repository.ts
  • packages/server/src/repositories/clickhouse-sleep-repository.ts
  • packages/server/src/repositories/predictions-repository.ts
  • packages/server/src/repositories/sleep-repository.test.ts
  • packages/server/src/routers/activity-dedup.integration.test.ts
  • packages/server/src/routers/anomaly-detection.test.ts
  • packages/server/src/routers/body-calendar-sleep-nutrition.test.ts
  • packages/server/src/routers/healthspan-query.ts
  • packages/server/src/routers/mobile-dashboard.integration.test.ts
  • packages/server/src/routers/mobile-dashboard.test.ts
  • packages/server/src/routers/mobile-dashboard.ts
  • packages/server/src/routers/recovery-settings-sleep-need-sport-settings.test.ts
  • packages/server/src/routers/recovery.test.ts
  • packages/server/src/routers/recovery.ts
  • packages/server/src/routers/router-logic.integration.test.ts
  • packages/server/src/routers/router.integration.test.ts
  • packages/server/src/routers/sleep-need.ts
  • packages/server/src/routers/sleep.integration.test.ts
  • src/db/test-helpers.ts
💤 Files with no reviewable changes (3)
  • packages/server/src/routers/mobile-dashboard.test.ts
  • packages/server/src/routers/mobile-dashboard.integration.test.ts
  • packages/server/src/routers/router-logic.integration.test.ts

Comment thread packages/server/src/routers/activity-dedup.integration.test.ts
Comment thread packages/server/src/routers/sleep.integration.test.ts Outdated
@Asherlc
Asherlc merged commit db76f0b into main May 21, 2026
77 checks passed
@Asherlc
Asherlc deleted the Asherlc/dashboard-sleep-stale branch May 21, 2026 14:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants