release/v1.11.4 — sleep totals, trend captions, and a round of fixes#255
Merged
Conversation
…bels The data-reset and account-delete cards under Settings → Advanced stacked their destructive button below the description. Move the button to a right-aligned slot on the same row as the title and description, matching the stack-on-mobile / right-align-on-desktop contract the onboarding-tour and about cards already use (flex-col gap-3 sm:flex-row sm:justify-between, button w-full sm:w-auto). The AlertDialog confirmation, i18n keys, and destructive styling are unchanged.
The wellness-score ring tiles passed the metric title into the ScoreRing as an in-SVG centred <tspan>. A long localised title (Schlaf-Score, Bereitschaft) was wider than the small ring's clear centre, and because the text is SVG-centred inside the recharts overflow:hidden svg the leading glyph was clipped (the missing leading "S" on Schlaf-Score). Render the title as an HTML caption below the ring instead, where it wraps within the tile and never truncates; the ring keeps just the number centred. The in-SVG label slot stays for the anatomy detail view, which only ever passes the short "/100" string.
The "mood by time of day" card spans the full overview width, unlike its distribution/weekday siblings which sit in a two-column grid. Sizing the chart wrapper with aspect-[3/2] derived the height off the full card width, so on a wide viewport the chart ballooned to roughly 800px tall and overflowed its card. Swap the aspect ratio for a fixed bounded height so the chart fits the card at the same size as the width-constrained sibling charts regardless of viewport. Chart internals and tokens are unchanged.
…ading A measurement logged by hand on the iOS app while offline is mirrored into HealthKit. On pairing a server, adopt-on-pair uploads it as a MANUAL row while HealthKit background sync independently re-ingests the same sample as an APPLE_HEALTH row. The two carry different external ids and sources, so the composite unique index keeps both — doubling the whole hand-logged history on first pair. Collapse the pair at ingest: an incoming MANUAL or APPLE_HEALTH row is dropped as a duplicate when a same-reading row of the opposite source already exists (same type and value, measuredAt within two seconds). The match is scoped to that one cross-source pair, so same-source rows and rows from server-owned sources keep their existing contracts, and two genuinely distinct readings at the same minute never collapse. stats:* cumulative rows stay on their per-day overwrite lane. The collapsed copy returns status "duplicate" with reason "cross_source_merge" so the client cursor advances unchanged; no schema, migration, or OpenAPI change.
…er blocks on the LLM The hero strip, daily-briefing card and trends row all read the advisor payload through `useInsightsAdvisorQuery`, which POSTed `/api/insights/generate` without `force`. That POST generates inline on a cache miss, pinning the page-load path on the full provider chain (up to the client's 8 s abort) before the briefing paints — even though a read-only lift of `User.insightsCachedText` costs nothing. Add a read-only GET to the route that serves the cached payload immediately and, when the cache is stale or missing and a provider is configured, enqueues an out-of-band warm on the existing `insight-pregenerate` queue. The read mount now uses the GET; user-initiated regeneration keeps the POST force path. Mirrors the stale-while-revalidate posture of the per-metric status and period-narrative reads — no provider call ever sits on a navigation request.
…tracting On a cold dashboard load the recent-achievements card briefly showed content (or its empty state) and then vanished. Two causes: - The layout-toggle gate read `DEFAULT_DASHBOARD_LAYOUT` while the real layout was still loading; achievements (and recent workouts) default to visible, so the card painted during the load window and was retracted once a layout that hid it arrived. Gate both layout-toggle-only cards on the resolved layout. - The card itself rendered its empty / content branch before its query settled, so the "no achievements yet" state flashed ahead of the real unlocks. Render a skeleton while the query is pending and commit to a content shape only once it resolves.
…p tile and series SLEEP_DURATION is stored in minutes, one row per sleep stage per night (IN_BED / AWAKE / ASLEEP / CORE / DEEP / REM) since the HealthKit category-sample ingest landed. The value is not cumulative, so reading the single most-recent row surfaced one stage's minutes — never the night. The dashboard sleep tile and the sleep series therefore showed a fragment of the night rather than the night. Add a shared `summarizeSleepNights` helper that groups the per-stage rows by the wake-day calendar date in the user's timezone and sums the asleep stages (CORE + DEEP + REM, plus a legacy bare-duration row), excluding IN_BED and AWAKE per the AASM convention already used by the sleep score. Wire it into: - the dashboard summary tile, which now emits last night's time asleep in hours with an explicit `unit: "h"` and a per-stage breakdown for a future detail view; - the sleep series, which now returns one point per night (time asleep in hours) plus a top-level `unit` for every kind so clients never infer the unit; - the slim summaries slice, so the web dashboard tile and the insights overview read per-night totals (kept in canonical minutes there; the web tile converts to hours) — strictly more correct than averaging across stage rows, and parity with the iOS value.
The metric chart withheld every reading behind a "more days needed" card until three distinct daily points existed, so a user with one or two days of real data (e.g. respiratory rate) saw a hint instead of their measurements. Render the available points for any non-empty window — a single marker for one day, a line for two — and add a subtle inline caption that more days fill out the trend. Only a genuinely empty window keeps the no-data card. The trend, moving-average, and comparison overlays already self-gate at two points, so relaxing the render gate cannot paint a misleading line from too little data. Drop the now-unused raw-count plumbing the old three-point copy split relied on.
…erflowing
The Trends row renders three mini-chart cards (blood pressure, weight,
mood). The mood card alone overflowed: its categorical y-axis ("Super
gut/Gut/Okay/...") and date x-axis paint inside a Card shell that runs
taller than the flat HealthChart minis beside it, so the x-axis date
labels bled past the fixed chart slot and overlapped the caption below.
Bound the overflow on the slot itself, without touching the chart
components:
- `overflow-hidden` clips anything the inner chart paints past the
180px envelope, so a stray axis label can never escape into the
caption row.
- `[--chart-height:120px]` drives both mini charts' internal band down
to a shared 120px (both honour `h-[var(--chart-height,140px)]`). The
mood card's full envelope — band + header chrome + axis labels — now
fits inside 180px, so all three tiles share one chart-band baseline.
Recharts and the chart tokens are untouched; the fix lives entirely on
the row's slot container.
…otation is absent
Each Trends-row card carries a one-line caption below its mini-chart.
The first choice is the AI advisor's per-metric sentence, but on a cold
briefing — a web-only account, a pre-briefing cached payload, or the
advisor still generating — that sentence is null and every card fell
back to a static "Awaiting more data" hint, even with a full 30-day
series painted right above it.
Add a deterministic, rule-based descriptor derived from the same series
the mini-chart plots, and apply a three-tier caption precedence:
1. advisor annotation present → the AI sentence (unchanged).
2. else, a computable series → the deterministic descriptor:
direction (rising / falling / stable) plus the first-vs-last
magnitude over the window, e.g. "Rising over 30 days (+8 mmHg)",
"Falling over 30 days (−1.4 kg)", "Stable over the last 30 days".
Mood reads on a categorical 1–5 scale, so it is phrased in plain
terms ("Mood trended slightly higher over 30 days") with no raw
point delta.
3. else (genuinely < 2 points) → the real "Awaiting more data" hint.
The descriptor is observational and neutral by construction: direction
and magnitude only, with no causal, diagnostic, or medical claim and no
value judgement — a rising weight and a rising step count read the same
way here. A per-metric stability floor (the larger of an absolute and a
relative bound) keeps day-to-day noise from reading as a trend.
The descriptor logic lives in a pure, unit-tested helper. The caption
reads the same series the chart shows: mood reuses the existing
mood-analytics cache slot, and numeric metrics take a small dedicated
30-day daily-aggregate read keyed under a new query-key factory entry
that joins the measurement-write invalidation set. New descriptor copy
ships in all six locales. The pending shimmer while the advisor is in
flight is unchanged.
… day Per-stage SLEEP_DURATION rows carry the stage segment's END instant, so a night where the user falls asleep before local midnight spread its stages across two calendar days. Keying each stage by its own day split one overnight sleep into two partial nights, and the headline last-night tile showed only the post-midnight slice. Reconstruct nights by clustering stage segments into sessions: a new session starts only when the next segment's start (end minus its minute duration) is more than three hours after the running session end. A single long stage block no longer splits a night, a daytime nap stays separable from the following overnight block, and the gap is measured on the absolute instant so the clustering is DST-immune. Each session is keyed by the local wake day of its latest segment end, matching the convention that a sleep session belongs to the morning you wake up. Collapse a multi-source night to one canonical source before summing, using the user's sleep source-priority ladder (default WHOOP > Apple Health > Withings), so a dual-source account no longer double-counts the total. Sources are never blended within a night. Bound the series sleep read to one year even on the 'Alle' range, matching the slim slice's sleep window, so a multi-year dual-source account does not walk a six-figure stage-row set into JS. Add fixtures straddling local midnight, crossing a DST spring-forward, keeping a nap separable, and collapsing a dual-source night.
…ation integration test The SLEEP_DURATION summary now reports one per-night asleep total (CORE + DEEP + REM) instead of a raw per-stage row count, so the stage-aggregation test expects one datapoint of 400 minutes rather than four rows. The per-stage breakdown assertions are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bugfix + iOS-parity patch over v1.11.3. Addresses a batch of reported issues plus two iOS coordination requests.
Highlights
Verification
pnpm typecheckclean,pnpm lintclean (one pre-existing allowed warning),pnpm test670 files / 6990 passed / 1 skipped.pnpm openapi:checkin sync (1.11.4).No schema migration. Additive over v1.11.3.