release/v1.12.0#257
Merged
Merged
Conversation
…imestamps, tighten sleep cards
Move the time-range pills + period-over-period delta out of the slot between
the stat strip and the chart and render them as a clean row below the chart on
every metric sub-page (the HealthKit scaffold plus weight, BMI, pulse, blood
pressure, sleep). The control owns its own persisted range pref and delta read,
and the chart owns its window via its in-card range tabs, so the relocation is
a pure DOM reorder that leaves the chart range untouched.
Right-align the last-updated timestamp on the period narrative ("Zeitraum im
Überblick") and the daily-briefing footer so the "updated" line matches the
status cards, which already route through the right-aligned footer.
Tighten the sleep average and hypnogram cards — drop the default card gap and
the headline card's extra padding that left tall empty bands — and give the
hypnogram clock-aligned X-axis ticks (whole hours on a 1–3 h step picked from
the night's span) so the time axis reads 23:00 / 01:00 / 03:00 / 05:00 instead
of raw bedtime/wake stamps.
Add a compact density to the short-horizon projection card for the metric
sub-pages: it sheds the min-height floor, shortens the chart, and surfaces how
many more days of tracking are needed before a projection can appear, drawn
from the coverage envelope. The Insights overview keeps the roomier default
geometry for grid symmetry.
…ment source Lay the data-model foundation for the Fitbit/Pixel integration over the Google Health API, mirroring the WHOOP provider. No OAuth, sync, or UI yet. - Prisma: FitbitConnection (1:1 per user, encrypted access/refresh tokens) and FitbitOAuthState models, two encrypted BYO-credential columns on User, and FITBIT appended to the MeasurementSource enum. Migration 0117_v1120_fitbit_integration is additive only — one enum value plus two tables and two nullable columns, no new MeasurementType (every launch metric maps onto an existing type). - Wire the two new encrypted columns and the FitbitConnection token columns into scripts/rotate-encryption-key.ts. - FITBIT touchpoints: the read measurementSourceEnum (not the writable set), the DEFAULT_SOURCE_PRIORITY and workout-source ladders, the sources-section label map, the IntegrationKey union plus its display name, and a fitbit()/fitbitStatus() query-key pair. The source-rank SQL and read-tier collapse pick FITBIT up from the ladders with no code change. - Add the FITBIT source label to all six locale bundles and regenerate the OpenAPI contract so the source enum carries FITBIT for the iOS decoder.
…r legend Redraw the inline body-map silhouette to the proportions the iOS client calibrated against: an oval head on a short neck, sloped shoulders, a torso that tapers to the waist and flares to the hips, arms held slightly out to mid-thigh with simple hands, and separated legs ending in feet. The figure is composed as a torso-plus-legs outline with two separate arm paths so there is a clean underarm gap and the arms read as arms rather than a single blob. The eight site anchors keep their calibrated coordinates on the 100x200 viewBox, so every tappable region and its nearest-centre hit routing is preserved — the dots still land on real anatomy (upper arms, the four abdomen quadrants, the thighs) and stay selectable, theme-aware, and keyboard reachable. Add a compact marker legend to the injection-site dialog: a dashed primary ring marks the recommended next site and an amber ring marks the last-used site. The last-used marker on the picker switches to the same amber tone so the legend swatches mirror the body map exactly.
… illustration Swap the hand-authored body silhouette in the injection-site picker for a clean, gender-neutral medical line-art figure with correct proportions (arms to mid-thigh, head-to-body ≈1:7.5, separated legs, feet). The figure ships as a small transparent raster (~8 KB) and is painted theme-aware: its alpha drives an SVG mask over a currentColor rect, so the strokes take the picker's text colour and tint correctly in light and dark. Keeping the figure inside the SVG (rather than a CSS-masked sibling) means it shares the dots' 100×200 viewBox, so alignment stays responsive by construction. Recalibrate SITE_COORDS so the upper-arm, abdomen-quadrant and thigh anchors land on the new figure's anatomy; the pointer overlay, keyboard targets, recommendation ring, last-used marker and disabled-set logic are unchanged.
…thigh markers to the upper thigh
…or report, collapse the included-data list The Settings export page over-emphasised the doctor-report PDF: it owned the page hero while the broader health-record export (PDF + FHIR R4 + zip package) sat below it as a plain card. The relative prominence is now the other way round. - The health-record export panel takes the page hero, with the same hero-gradient + glow-purple treatment the Insights hero strip uses. - The doctor-report card drops the hero styling and moves to the bottom of the page as a small secondary card. It stays fully functional — the same dialog + /api/doctor-report flow runs from there. - The health-record "included data" checklist is now a disclosure, collapsed by default, so the panel opens compact instead of as a long always-expanded list. It uses the inline aria-expanded/aria-controls pattern already used elsewhere, with no new dependency. Pure presentation/IA change: export routes, payloads, and the exported data are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Land the OAuth + credentials half of the Fitbit / Google Health provider, cloning the WHOOP provider structure and adapting it to the Google identity flow. - Client OAuth half (src/lib/fitbit/client.ts): Google authorize-URL builder (access_type=offline + prompt=consent), authorization-code exchange, token refresh, and a profile fetch for the connection's external user id. Client credentials ride in an HTTP Basic-auth header on the token endpoint; the four launch Restricted read scopes are pinned in FITBIT_OAUTH_SCOPE. All egress goes through safeFetch. - Non-rotating refresh tokens: getValidToken refreshes at expiry-minus-five- minutes, persists the new access token + expiry, and overwrites the stored refresh token ONLY when the response carries a fresh one — Google keeps the refresh token stable, unlike WHOOP. - BYO credentials route (PUT/GET/DELETE /api/fitbit/credentials): per-user client id/secret stored encrypted on the User columns, built field-by-field under requireAuth. - OAuth state + connect + callback routes: a random base64url nonce backed by a 10-minute FitbitOAuthState ledger row carries the (nonce -> userId) mapping; the callback validates the cookie/URL state with timingSafeEqual, consumes the row atomically (replay/expired/cross-user reason tags), exchanges the code, upserts the encrypted FitbitConnection, clears prior reauth state, and enqueues the self-converging history backfill. - Response classifier mirrors WHOOP's HTTP-status buckets; Google signals a revoked grant with a 401, so there is no invalid_grant body special-case. Tests cover the authorize URL, Basic-auth token exchange, the no-rotation refresh branch (token preserved when omitted, overwritten when present), the classifier status table, and the OAuth-state nonce mint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ovenance, single disclaimer Token-by-token streaming was buffering on the server. The chat route enqueued every tokenised SSE frame synchronously inside the stream's start() callback, so the runtime coalesced them into a single network read and the client painted the full answer at once despite the per-token render path already wired in use-coach. Make the emit callback async and yield to the event loop between token frames so each one flushes as its own chunk; the visible cadence now reads ChatGPT/Claude- style. Refusal and error paths stay single-frame. The conversation history was a permanent left column on lg+, eating chat width on laptops and reading as a takeover behind the narrowed drawer cap. It now lives behind an always-visible "Conversations" toggle on every viewport and opens as the left-edge tray on demand; the thread keeps the full width by default and never loses it on mobile. The provenance block (source chips plus raw key-values) rendered fully expanded under every answer and was often taller than the reply. Fold the chips into the existing collapsed disclosure alongside the key- values so the whole grounding is one click-to-expand surface, collapsed by default, with the disclosure's aria-expanded/aria-controls intact. The clinical-decisions disclaimer rendered both at the bottom of the thread and in the sources-rail footer. Consolidate to a single line directly above the composer, always visible on every viewport. The auto-growing composer (single line growing to a capped multi-line with internal scroll, Enter-to-send, Shift+Enter-newline) and the aria-live streaming region are preserved. Plain-text React children throughout; no new dependencies.
Extend the Fitbit/Google Health provider with the read path: the dataPoints.list client half, the metrics-bundle sync, the poll + backfill jobs, and the connection-management routes. Builds on the existing OAuth + credentials + token-refresh layer. - client: paginated `fetchDataPoints` walker over `safeFetch` keyed on `nextPageToken`, encoding each data type kebab-case in the path and snake_case in the filter predicate; defensive per-type mappers (finite + positive guards) for weight, body fat, daily SpO2, daily HRV, daily resting heart rate, daily respiratory rate, intraday heart rate, sleep-temperature derivations, and a height profile-seed extractor. `FITBIT_FIELD_MAP` + `mapping.md` are the source of truth. - mapping (no new MeasurementType): weight→WEIGHT, body-fat→BODY_FAT, SpO2→OXYGEN_SATURATION, HRV→HEART_RATE_VARIABILITY (SDNN slot, not HRV_RMSSD), resting-HR→RESTING_HEART_RATE, respiratory-rate→RESPIRATORY_RATE, heart-rate→PULSE, sleep-temp→WRIST_TEMPERATURE, height→User.heightCm when null. Steps/distance/energy/floors/VO2 + sleep + workouts stay for a later wave. - sync: `syncUserFitbit` orchestrator with the reauth short-circuit, the per-data-class 403 soft-skip (the six Restricted bundles grant independently), the all-soft-skipped success guard, and incremental-vs-full windows; `upsertFitbitMeasurements` keyed (userId,type,FITBIT,externalId) with a 24 h overlap for after-the-fact daily re-rolls, recomputing rollups and invalidating status caches. Google refresh tokens do not rotate, so the stored refresh token is kept unless a new one is returned. - jobs: hourly `fitbit-sync` poll (poll-only; Pub/Sub deferred), the self-converging boot backfill, and the daily OAuth-state sweep — every queue registered in both `allQueues` and a `boss.work` binding, staggered off the WHOOP/Withings crons. - routes: status / sync / disconnect mirroring the WHOOP shape. Tests cover the mappers, the casing invariant, `fetchDataPoints`, the orchestrator soft-skip guard, the idempotent upsert, the backfill discovery, and the queue registration; an integration test pins the upsert idempotency, the DAY rollup fold, and the Fitbit-vs-Withings per-source no-collapse against the Postgres testcontainer.
…gKeys Deepen the structured mood-tag taxonomy and close a bulk-ingest data-loss gap the iOS client has been blocked on. Taxonomy (migration 0118, additive + idempotent, mirrors the 0101 seed): - New `hobbies` category: movies, reading, gaming, music, outdoors. - Nutrition-detail tags appended to the existing `health` category: fast_food, no_sweets, big_meal (alongside the original ate_well). - Lucide icon names seeded per row and registered in the client icon map; labelKeys added for every new category and tag across all six locales. Bulk tagKeys (data-loss fix): - `POST /api/mood-entries/bulk` previously Zod-stripped `tagKeys`, so structured taxonomy links were silently dropped on the adopt-on-pair backfill while the single-entry route persisted them correctly. Add `tagKeys` to the bulk entry schema and write the `MoodEntryTagLink` rows per entry via the shared `createTagLinks` helper — unknown keys are dropped against the catalog, the join insert skips duplicates so a re-posted batch stays idempotent. Tests cover the bulk tagKeys round-trip (links created, unknown keys ignored, over-long arrays rejected at the schema boundary) at the unit level and against real Postgres.
…t and resume
Give the Google Health (Fitbit & Pixel) integration the same settings-card
parity WHOOP and Withings already have. It is a BYO-key provider — the user
pastes their own Google OAuth client id/secret, then connects — so the card
mirrors the WHOOP flow rather than the server-brokered Withings one.
- New POST /api/integrations/fitbit/test does a live, rate-limited
(5/60s per user) probe against the Google Health profile endpoint via
the refreshed token from getValidToken, returning { ok, latencyMs,
lastSyncedAt } and the same error-code union WHOOP uses. The Bearer
token and upstream host are scrubbed before any annotation.
- New POST /api/integrations/fitbit/resume clears a parked integration
via resumeIntegrationFromPark, rate-limited to match the test route.
- FitbitCard renders the credentials form, OAuth connect, sync now /
sync all data / test connection / disconnect action row, the parked
resume banner, and a backfill-in-progress line. Status reads from the
existing /api/fitbit/status; the pill/error/parked state comes off the
cross-integration envelope, which now carries a fitbit entry.
- Card strings added under settings.fitbit* across all six locales.
…ealth Extend the Fitbit / Google Health provider with the remaining sync resources, driven by the existing syncUserFitbit orchestrator. Activity (daily cumulative): steps -> ACTIVITY_STEPS, distance -> WALKING_RUNNING_DISTANCE, the active-energy portion -> ACTIVE_ENERGY_BURNED, floors -> FLIGHTS_CLIMBED, and VO2 max -> VO2_MAX. Each daily total mints a stats:<tag>:<YYYY-MM-DD> externalId so a re-fetched day overwrites in place rather than duplicating, matching the Apple-Health daily-total contract. The running totals preserve a legitimate zero (a rest day is real data); VO2 max stays strictly positive (daily latest-wins). Sleep: map a session's per-stage segments to per-stage SLEEP_DURATION rows (minutes; measuredAt = the stage end), harmonised onto the shared SleepStage enum (light -> CORE, deep -> DEEP, rem -> REM, awake/restless -> AWAKE, in-bed -> IN_BED, classic asleep -> ASLEEP) so the night-total and hypnogram readers consume the same shape they already read for WHOOP and Apple. Workouts: map exercise sessions to Workout rows keyed (userId, FITBIT, externalId), with sport-type, start/end, duration, energy, distance, and HR. Cross-source twins stay distinct at ingest; the read-time canonical picker collapses them via the source ladder (Fitbit already ranked below WHOOP). All three join the orchestrator's resource list and ride the per-resource 403 soft-skip so an independently-granted Restricted bundle that the user withheld skips only its own resource. Rollups recompute and status-insight caches invalidate through the shared upsert. No new MeasurementType, no migration, no new dependency.
…rminology Bring the web per-metric Insights detail pages to structural parity with the canonical metric template: a single top-to-bottom spine, every block self-suppressing when its data is absent. New spine (above the chart): header (title + target gear + Coach circle) → intro → primary tile → Letzte Messung → stat strip; then the chart, the range controls + period delta, the per-metric correlation card, and the Einschätzung assessment last. - Add the primary tile (headline value + 30-day average + an In-range bar shown only when the metric carries a per-metric in-target pct). Blood pressure keeps its richer target panel as its primary tile. - Add the Letzte Messung card (labeled heading + last-reading date), reading the last-seen timestamp the slim analytics slice already carries — no extra fetch. - Relocate the owning correlation onto its metric page: weight × weekday on Weight, mood × pulse on Pulse. The card reads the correlations already on the analytics payload and self-gates below the surfacing bar; it is now the canonical home for those relationships. - Move the headline value, 30-day average and in-target share out of the target reference panel so each figure appears in exactly one place; the panel stays the band reference (range, status, positional bar, consistency strip, source). Blood pressure keeps its stitched S/D 30-day average inline. - Order the Einschätzung last on every page. Terminology: adopt the canonical labels — rename the mood patterns surface to "Was auffällt", pluralise the deviation-count card to "Signale des Tages", and align the settings section labels to "Über diese App" and "Quellen-Priorität". New In-range / 30-day-average / Letzte Messung strings land in all six locales. Web charts are unchanged — Recharts and the existing chart tokens stay; the iOS monochrome chart doctrine is not applied.
…multi-dose orals don't collapse A daily oral with multiple timesOfDay and a wide schedule window had its dose-slot capture zones overlap, so a second same-day dose could snap onto an already-actioned slot and be dropped by the no-downgrade guard — the intake never landed as its own row. A twice-daily 08:00 / 20:00 schedule with an 08:00-22:00 window yielded a +/-7h half-span tolerance that exceeded half the 12h inter-slot gap, so the evening write collapsed onto the morning slot. Weekly and single-dose schedules (one slot/day) were immune. snapToleranceMs now caps the tolerance at half the minimum gap between adjacent sorted timesOfDay entries (the midnight wrap counted as one of those gaps) whenever a schedule has more than one slot, keeping the two nearest slots' capture zones disjoint. Single-slot schedules keep the full half-window-span behaviour, and the >=1-minute drift floor is preserved, so weekly injectables and once-daily orals are unchanged. Adds unit coverage for the wide-window two-dose case (distinct slots, no collapse) plus an explicit weekly single-dose regression, and an integration test driving both same-day writes through the DB-backed slot resolver and canonical-slot upsert to assert two distinct live rows.
Extend the structured mood taxonomy so a tag can carry a magnitude. A
rated factor is a MoodTag of kind RATED that the user scores per entry;
the score persists on MoodEntryTagLink.rating. Binary tags keep
kind BINARY and a null rating, so every existing row and read path stays
byte-identical.
Migration 0119 is additive: four marker columns on mood_tags (kind,
scale_min, scale_max, inverse), a nullable rating on mood_entry_tag_links,
and a seeded factors category with five MVP factors (work, social, sleep
quality, stress, conflict). Stress and conflict are inverse-scaled
(higher means worse); conflict is a 1..2 Yes/No factor.
Write contract: POST /api/mood-entries and the bulk route accept an
optional ratedFactors: [{ key, rating }] array parallel to the binary
tagKeys. The resolver builds the join rows field-by-field, drops unknown
and non-RATED keys silently, and rejects a rating outside the factor's
own scaleMin..scaleMax (422 on the single-entry path, per-entry skipped
on bulk). Create and list responses surface the persisted factors split
from binary keys so a client hydrates without a refetch. GET
/api/mood/tags now emits kind, scaleMin, scaleMax, and inverse so a
client can render factors versus tags.
This is the ingestion half; analytics over rated factors follow later.
Factor labels ship in all six locales.
…e per-metric pages The per-metric pages are the canonical home for each metric's primary tile, target band, and correlation card. The overview was still rendering its own correlation row (weight × weekday, mood × pulse, BP × compliance) — the exact cross-view duplication to kill now that the Weight and Pulse pages own those cards via MetricCorrelationCard. Remove the on-overview CorrelationRow (component + test) and reorder the overview toward the iOS Insights structure: hero (coach + briefing + prompt chips) → detailed daily briefing → dynamics/alerts (today's signal + rhythm events) → vitals dashboard → trends → period retrospective → warm-assessments footer control. The overview carried no target-tile grid, so nothing to remove there. Drop the now-dead insights.correlationRow.subtitle key across all six locales; title and disclaimer stay (the per-metric correlation card still uses them).
…or ratings
Replace the score-number radio + free-text form with an icon-first
"How are you?" hero that matches the native client: a row of five mood
faces in best-on-the-left order is the primary input, and picking one
reveals a brief annotate panel (time, the category-grouped tag picker,
factor ratings, and the note field).
The tag picker now reads the catalog's `kind` / `scaleMin` / `scaleMax`
metadata. BINARY tags stay icon-above-label toggle tiles sent as
`tagKeys`; RATED factors (work / social / sleep quality / stress, and
the 1..2 Yes/No conflict factor) render a compact 1..scaleMax segmented
control and are sent as a parallel `ratedFactors: [{ key, rating }]`
array. The entries list / history stays reachable below the hero.
Add the mood-face icon map (Laugh / Smile / Meh / Frown / Angry) and the
`heroQuestion`, `factorYes`, `factorNo`, `factorRatingOption` strings
across all six locales.
…ite recorded intakes A weekly injectable (e.g. a GLP-1 dose) with real recorded intakes reported 0% compliance on the medication card across every window (7 / 30 / 90 days). The web reads the server-computed rate verbatim, so the server value itself was wrong. The intake-to-slot matcher used a fixed +/-12h pairing radius. That is correct for a daily cadence (24h inter-slot gap) but far too tight for a once-weekly dose: a real intake is rarely logged within 12h of the schedule's configured time-of-day, because the user takes the shot on whichever day and time of the dosing week suits them. Those intakes fell outside the radius, every weekly slot read "missed", and the rate collapsed to 0% while the matching intakes were orphaned. Scale the pairing radius with the cadence gap. `pairDoses` now derives a per-slot radius from half the distance to its nearer neighbouring slot (so two adjacent slots never double-claim one intake), floored at the 12h base so daily and multi-dose-daily cadences keep their exact prior behaviour. `buildCadenceTimeline` additionally probes the schedule's intrinsic cadence over a padded window and passes half the minimum inter-slot gap as a radius floor, so a window holding a single expected slot (a weekly med over a 7-day window) widens correctly too. A weekly cadence now pairs an intake logged anywhere in the dosing week to that week's slot, reporting the true rate. Daily-cadence matching is unchanged. Adds unit coverage for the widened radius and a real-Postgres integration test mirroring the weekly-injectable row + off-time intakes.
Extract the Coach chat surface (header actions, message thread, composer, provenance rails, settings, mobile trays) into a shared <CoachConversation> component so the drawer and a new full-page route render the exact same implementation — no forked chat logic. The drawer keeps only its <Sheet> chrome (close button + maximize control); the page supplies a minimize-back-to-drawer control. The maximize control in the drawer header navigates to /insights/coach and closes the drawer (aborting any in-flight stream); the page's minimize control routes back to /insights and reopens the drawer through the shared launch context. Both surfaces preserve incremental streaming, collapsible history + provenance, the single composer disclaimer, the auto-grow composer, refusal / budget handling, and the aiMode gating — the full page redirects to /insights when the operator flag is off or the user has opted out. Lift useResettableValue / nextResettableValue into their own module so the shared surface seeds its composer from the same controlled-prefill contract without importing the drawer shell. Register the new route in the coach feature-flag and per-user-disable gate-site invariants, and add the maximize / minimize labels across all six locales.
…erence into the profile Reshape the mobile bottom bar to the five-slot shape the native client uses: Home · Meds · Log · Insights · More. The center "Log" slot is a capture action rather than a destination — it opens a picker that routes to the existing measurement, medication-intake and mood quick-entry sheets (those forms already share a mount contract, so the picker reuses them verbatim). Expand the former overflow into a real "More" hub that holds the rest of the top-level destinations: Measurements, Mood, Workouts, Achievements, Notifications and Settings. The change is additive — Measurements and Mood leave the always-visible strip but stay reachable through the More hub and the capture picker, so nothing loses its route. On desktop the sidebar still lists every destination. Move the metric/imperial unit system from its standalone card into a dropdown in the Profile form beside the timezone picker — it is a display preference like language and timezone. Persistence is unchanged: the dropdown PATCHes /api/auth/me/unit-preference on change and invalidates the same query keys, so chart display transforms re-render without a reload. Retire the now-unused card.
…de the add button Place a monochrome ghost wrench icon-button immediately to the left of the dashboard add button, linking to the layout/tile editor at /settings/dashboard. The button carries an aria-label and matches the add button's 44 px mobile touch-target floor, shrinking to the icon footprint on sm+. New customizeDashboard string lands in all six locales.
…webhook Extend the mood-relations surface from mood to a health metric. For each structured mood tag present on enough days, compare a metric's value on tag-present vs tag-absent days: active energy on workout days (same day), sleep length on nights tagged sleep (same day), and next-day recovery after a tag like alcohol or food (D to D+1 lag). The comparison reuses the existing statistics engine wholesale — the Welch two-sample t-test, the per-group day floors, the confidence band, and the Benjamini-Hochberg FDR control already used by the influence and discovery boards — so no new statistics code lands. Only pairs that clear p < 0.05 and the FDR control surface, ranked by adjusted q then absolute delta and capped at eight rows. The result rides on GET /api/mood/insights as a new additive `tagMetricCrosstab` field and renders as a confidence-gated, observational card on the mood insights page, framed as an association in your own data rather than a cause. The endpoint is server-rendered and not in the OpenAPI contract, so no contract change is required. Deprecate the standalone moodLog integration. Mood is now tracked fully inside HealthLog through entries, structured tags, and rated factors, so the external webhook and reverse-sync bridge no longer add anything. The webhook route, the push and pull helpers, and the validation schemas carry an @deprecated note pointing at the native path; the surface stays functional this release and is slated for removal in a future major. The settings card shows a deprecation hint, localised across all six locales.
…with IF NOT EXISTS Thread the acting user id through createTagLinks, replaceTagLinks, and replaceRatedFactorLinks, and assert the target MoodEntry belongs to that user before writing any link. The check runs against the same client (tx or singleton) as the write, so it shares one snapshot. Behaviour is unchanged for the existing POST and bulk paths, where the entry is freshly created and owned; the guard closes a latent cross-user link hazard for the planned PATCH mood-edit route. A mismatched or missing entry throws MoodEntryOwnershipError before any catalog or join touch. Add IF NOT EXISTS to every ADD COLUMN in migration 0119 so a clean replay is idempotent, matching the 0117 convention. (cherry picked from commit 1bf983f6c2c5e44796c99983d57c1b1d90cd7099)
…ct interval time-anchor, mark provider experimental Snapshot the incremental watermark once per sync cycle and thread the same lower bound to every resource (metrics, activity, sleep, workout), stamping lastSyncedAt once at the end. Previously each resource read lastSyncedAt then stamped now() independently, so the first resource advanced the watermark and later resources only fetched the last overlap window — silently dropping the gap after an outage longer than the overlap. The full/backfill path is unaffected (it passes no lower bound). Reject an OAuth callback whose token response carries no refresh_token instead of persisting an empty string. An empty refresh token bricks every future token refresh; the callback now errors out cleanly so the user re-consents rather than landing a dead connection. Model the INTERVAL data types (steps/distance/calories/floors daily totals, sleep and exercise sessions) on Google Health's interval.start_time / interval.civil_start_time anchor instead of a sample_time / bare date. The incremental filter for sleep/exercise previously targeted sample_time.physical_time, which 400s/empties for interval types and stalls incremental sync. Daily-summary metrics and spot readings keep their existing date / sample anchors. Shape mismatches still fail safe (return []/null). Mark the Google Health (Fitbit & Pixel) settings card as experimental with a badge and a short note that data coverage is still being verified, in all six locales. (cherry picked from commit 3f116802149eebe9144729e2779f7f4405f248f2)
…ort-card styling Lift the bottom-bar center capture action into a proper floating-action button: a 56px target raised out of the bar with a card collar ring and a stronger shadow, so it reads as the primary CTA rather than a fifth flush tab. Desktop sidebar nav is unaffected. Unify the selectable controls across the mood logging surface onto one design language: the 5 mood faces, the tag tiles, the rated-factor steps and the GLP-1 chips now share a single selected-state treatment (border-primary, bg-primary/15, primary text, single border) and a consistent rounding family. Match the More-hub row height to the capture tiles so equivalent tappable rows share one min-height at the same tier. Give the demoted doctor-report export card a primary-tinted icon to match its peer cards — the demotion comes from position and size, not a muted icon. Drop the doubled heading atop the mood sheet by demoting the hero question to a field label so the sheet title is the single primary heading. Reword the capture mood option description to describe the action instead of echoing the hero question. (cherry picked from commit 1296f151f1c9a7c95ff917e3d69a5c0d369ba210)
…d source weighting Refresh the README for the v1.12 line: add the experimental Google Health/Fitbit connection (Fitbit/Pixel over the Google Health API, BYO OAuth client, server-side activity/health-metric/sleep/workout sync) and flesh out the existing WHOOP entry with its connect/test/sync/resume controls. Rebuild the Mood section around the five-face capture, the structured tag catalog (feelings/sleep/health/social/work/hobbies plus nutrition tags), and the rated daily-life factors, and note the moodLog webhook is deprecated. Add a Source priority section explaining why a per-metric source ladder picks one canonical reading when providers overlap. Bring the status line, integrations table, env-var table, API reference, and roadmap current. (cherry picked from commit 66d0987895e9a9d560afc456d91f38d54f7423ce)
The family factor existed only as a binary tag; add a rated factor_family (scale 1..5, not inverse) under the factors category so it can be scored per entry. Additive seed, read dynamically by the tag catalog.
Drop the no-op single-argument cn() wrapper on the chooser button class (and its now-unused import), and replace the three-way nested ternary that resolves the form-sheet title with an explicit per-kind lookup map. The null-kind fallback keeps the prior ternary's mood label, and the sheet stays closed in that state so the title is never read. (cherry picked from commit 856e620754753570e0d4ce8d6ce982778e929b75)
…ch and insights overhaul
Use FITBIT_ACTIVITY_PAGE_SIZE for the sleep and exercise reads instead of a magic 25, remove the unused FitbitDataTypeKey type, and declare the crosstab day floors independently so they aren't a duplicate re-export of the influence floors.
…tegration-count assertion The Fitbit & Pixel tag chip paired muted text on a muted background (below WCAG AA); use foreground text. Update the mobile integrations e2e to expect the fourth integration card (Google Health).
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.
v1.12.0 — Google Health sync, rated mood factors, and a Coach and insights overhaul.
Headline
Fixes
Migrations
0117 (Fitbit), 0118 (mood hobbies/nutrition), 0119 (rated factors), 0120 (rated family factor).
Gate
typecheck clean · lint clean (one documented allowed warning) · 7233 unit / 1 skipped · 342 integration / 3 skipped · build exit 0 · openapi:check in sync.