Skip to content

Releases: the-momentum/open-wearables

0.10 - New dashboard & nutrition data

Choose a tag to compare

@bartmichalak bartmichalak released this 16 Oct 14:12
d18304f

We encourage you to use our official Docker images:

Highlights

This release brings a rebuilt dashboard and support for nutrition data, with 42 new data types. Outgoing webhooks now tell you which sync each event came from, and calories and data sources are more accurate across providers. Plus a lot of smaller fixes and improvements, 123 PRs in total.

0.11 is coming very soon and focuses on further data model improvements, as we head towards Open Wearables 1.0. If there's something you think we should include or consider for 1.0, let us know on Discord or in Discussions. Feedback from teams that have been running Open Wearables for a while is especially welcome.

Caution

Breaking changes in 0.10.0. Most of them probably don't affect you at all, as they only touch specific providers or edge cases, but read the list carefully before upgrading.

  • active_calories_kcal is now null instead of 0 when a day has no active energy, for every provider. total_calories_kcal in /summaries/activity is null when a day has neither total nor basal energy. (#1801)
  • Whoop cycle energy is stored as the new total_energy series instead of active_energy, because it includes BMR. Webhooks for Whoop cycles now send series.total_energy.created instead of series.active_energy.created. A startup migration relabels stored rows and drops 0 kJ no-wear cycles. (#1801)
  • Ultrahuman active minutes move from active_time to exercise_time, and outgoing webhooks send series.exercise_time.created instead of series.active_time.created. active_minutes in /summaries/activity falls back to the step-based estimate for Ultrahuman users. A startup migration relabels stored rows. (#1765)
  • Ultrahuman HRV moves from heart_rate_variability_sdnn to heart_rate_variability_rmssd (confirmed by Ultrahuman). A startup migration relabels stored rows. Users whose only HRV source is Ultrahuman now get a Resilience score. (#1686)
  • Workouts from the mobile SDK and Apple XML import that report no calories now have null calories instead of 0. A startup migration clears the stored zeros. (#1874)
  • SDK refresh tokens of deleted applications are removed. Their users get a 401 on the next refresh and the SDK reports an auth error (Handle expired sessions). (#1841)
    • Check for SDK users of deleted applications
      SELECT rt.app_id,
             count(*) AS active_tokens,
             count(DISTINCT rt.user_id) AS users,
             max(coalesce(rt.last_used_at, rt.created_at)) AS last_activity
      FROM refresh_token rt
      LEFT JOIN application a ON a.app_id = rt.app_id
      WHERE rt.token_type = 'sdk'
        AND rt.revoked_at IS NULL
        AND a.id IS NULL
        AND split_part(rt.app_id, ':', 1) NOT IN ('admin', 'invite')
      GROUP BY rt.app_id
      ORDER BY last_activity DESC;

      Apps that handle the SDK auth error by getting new tokens from their backend and passing them to updateTokens() recover on their own, as long as that backend uses the credentials of an application that still exists.

  • Outgoing webhook payloads are kept in Svix for 7 days instead of 90. Change it with OUTGOING_WEBHOOK_PAYLOAD_RETENTION_DAYS (5-90). Payloads already stored keep their 90-day expiry. (#1719)
  • 24/7 sync results have a new, typed shape. data_247 is now an object with status, item counters and one entry per provider task, replacing the flat map of task names to row counts and the success/saved flags. It changes in the sync endpoint response (details.data_247), in sync.completed / sync.failed webhooks (metadata.params.data_247) and in sync status events. A sync where every 24/7 task fails now returns success: false and emits sync.failed. Error fields carry a short code (http_401, IntegrityError) instead of the exception text, and error in params.workouts is renamed to error_code. (#1501)

Note

The first startup after upgrading can take longer than usual: data migrations merge duplicated data sources and relabel the rows listed above. Expensive migrations are now recorded in a new data_migration table and run once per instance (Data migrations).


New dashboard

The dashboard is rewritten in SvelteKit by @KaliszS 🪄. The layout is cleaner, there are light and dark themes (or it follows your system setting), and it works on phones too, with a bottom navigation bar.

Heavy pages load faster, and Data Summary opens on the last 30 days by default. Tabs on the user profile have period presets (1W / 1M / 3M / 6M / 1Y) and a provider filter, and the Sleep tab can also show only night sleep, only naps, or only the highest-priority source. Each record shows which provider and device it came from.

The user profile also got a Meals tab, a Women's Health tab that's always visible (empty when there's no data), and data sources listed in priority order.

Dashboard User profile - Data Summary

User profile - Sleep User profile - Meals

User profile - Sleep

Note

The image name, port 3000 and VITE_API_URL are unchanged, so no configuration change is needed. Developers sign in again once after the upgrade. VITE_API_URL is now read at runtime, so one image works against any backend without a rebuild. Optionally set API_URL when the frontend can reach the API on an internal address, e.g. http://app:8000 on the same Docker network.

More: Frontend configuration


Nutrition data

Open Wearables now stores nutrition data, thanks to @McArthur955 🎉. There are 42 new series types: calories, carbs, protein, fat (total, saturated, mono- and polyunsaturated, trans), fiber, sugar, cholesterol, caffeine, and a full set of vitamins and minerals. Apps that log food as meals also get a new meal event type with a title, meal type and nutrients. Not every app does that, so nutrition samples logged outside a meal are stored as regular time series.

This is most useful with Apple Health. Food trackers like MyFitnessPal, Lose It! and Cronometer, and dozens of other apps, write what users eat into it, so you get their data without integrating each app separately. Apple Health covers 38 of the nutrition types, Health Connect all 42 and Samsung Health 17. The Google Health API nutrition log is supported too.

Note

For the mobile SDK, nutrition is supported on the backend side only for now. SDK versions that send meals from Apple Health, Samsung Health and Health Connect haven't been released yet, but they're coming soon - follow the iOS SDK and Android SDK repos. Google Health API nutrition works with this release.

GET    /api/v1/users/{user_id}/events/meals?start_date=2026-10-05&end_date=2026-10-05
DELETE /api/v1/users/{user_id}/events/meals/{meal_id}

The seed generator can create meals too, with a nutrition_focused preset.

More: Nutrition data types

Outgoing webhooks know which sync they came from

Every data webhook now carries a sync block, so a consumer can tell a historical backfill from live sync:

"sync": { "run_id": "backfill_9f3c8ea4", "source": "backfill", "scope": "historical" }

source is pull, webhook, sdk, backfill, xml_import or linked_account, and run_id joins to GET /sync/history/{run_key}. Two new events, sync.data_type.completed and sync.data_type.failed, report the result per data type, with item counts and the written and covered time ranges. Existing payloads only gain the new key.

Calories: total, active and basal

Providers that report only a total with no active/basal split now write to a new total_energy series (today that's Whoop), with a series.total_energy.created webhook. Basal energy is now stored for Garmin, Polar, Oura and SensorBio, and total_calories_kcal in summaries comes from total_energy first, with active + basal as the fallback. Whoop cycle metrics are also anchored on the right local day, so users in negative UTC offsets no longer get two cycles summed into one day.

Stable data sources

One device or app no longer splits into several data sources. A source is now matched by device ID, then by app bundle ID and model, and an existing source gets upgraded when better data arrives. In practice: renaming an iPhone, upgrading the phone that relays third-party Apple Health data, or Samsung sending steps without IDs no longer creates a new source. A startup migration merges the duplicates already stored. New device types were added as well: chest straps, earbuds, tablets, ...

Read more

0.9.0 - Google Health pack & breaking changes!

Choose a tag to compare

@KaliszS KaliszS released this 17 Sep 14:59
ff8527a

We encourage you to use our official Docker images:

Highlights

This release splits the Google integration into two providers, renames the energy series to active_energy, and fixes a long list of API inconsistencies around date bounds, filters and pagination.

Caution

Breaking changes in 0.9.0. Read this list before upgrading.

  • Google is now two providers. google is replaced by google_health (cloud OAuth, Google Health API) and health_connect (Android SDK). Outgoing webhooks carry the new slugs in source.provider, connection.created / connection.revoked and sync events, so anything filtering on provider == "google" stops matching. A data migration runs automatically on startup.
  • energy is renamed to active_energy in every response body, and the granular webhook event series.energy.created becomes series.active_energy.created. Requests still accept types=energy until 1.0. Webhook subscribers need the migration script below.
  • Google Health API rollup is disabled. A provider with data_granularity set to anything other than raw now fails those data types with UnsupportedGranularityError instead of writing duplicated, inflated rows.
  • Existing Google Health API energy data is wrong and has to be purged with an optional script, then re-synced.
  • Date-only end_date / end_time now includes the whole end day. If you compensated by passing the next day, you will get one extra day back.
  • /events/sleep no longer returns sleep_stage_intervals by default. Pass ?include=stages. Unreported stages are now null instead of 0.
  • /timeseries returns total_count: null on cursor pages and on every aggregated (resolution != raw) read.
  • resolution on /timeseries is no longer ignored. Requests that passed it and silently got raw samples now get aggregated buckets.

Google Health and Health Connect are now two providers

google covered two unrelated integrations under one slug: data pushed from Android through the mobile SDK, and the Google Health API cloud OAuth flow. They now have their own provider identities, their own connection rows and their own line on the coverage matrix.

Old New What it is
google google_health Google Health API, server side OAuth, REST pulls and notify-only webhooks
google health_connect Health Connect data uploaded through the mobile SDK
GET /api/v1/users/{user_id}/connections
{ "data": [ { "provider": "google_health", "status": "active" }, { "provider": "health_connect", "status": "active" } ] }

Filters follow the same names: ?provider=google_health, ?provider=health_connect.

What keeps working

  • Webhooks already registered with Google against /api/v1/providers/google/webhooks still deliver. Both /google/ and /google_health/ paths are served; new registrations use /google_health/.
  • Existing user connections with periodic sync keep running with no action.
  • The mobile SDK still sends google as the provider name in the payload body for Health Connect. google is kept as an alias for health_connect.

What breaks

Warning

Outgoing webhooks now send google_health or health_connect in provider. There is no alias here because it would be ambiguous. Affected payloads: connection.created, connection.revoked, source.provider on data events, and sync events. Update any consumer that filters on provider == "google".

Raw payload storage in S3 also splits into two provider paths instead of one google prefix. This only matters if you read raw payloads for debugging.

New OAuth path and GOOGLE_LEGACY_OAUTH_PATH

The OAuth redirect URI must match what is registered in Google Cloud, so the path is behind a flag:

GOOGLE_LEGACY_OAUTH_PATH=true   # default, sends the old /api/v1/oauth/google/callback
GOOGLE_LEGACY_OAUTH_PATH=false  # sends /api/v1/oauth/google_health/callback

Existing deployments need no change: the default keeps your registered client working. Both inbound paths are active, the flag only controls the redirect_uri Open Wearables sends to Google.

New integrations, and anyone who wants to be on the supported path before 1.0, should register {API_BASE_URL}/api/v1/oauth/google_health/callback in Google Cloud (Google accepts several redirect URIs, so both can coexist during the migration) and set GOOGLE_LEGACY_OAUTH_PATH=false.

Note

The legacy path and the flag are removed in 1.0.


Google Health API data corrections

Rollup is disabled

The windowed rollUp operation anchors its buckets to the requested range start, so every lookback or manual sync rewrote the same hour at a different timestamp:

{"timestamp": "2026-09-08T08:01:30Z", "value": 70.728}
{"timestamp": "2026-09-08T08:01:33Z", "value": 70.728}

It is now off. With the default data_granularity = raw nothing changes. Set to hourly or daily, the affected data types are skipped, the run is marked partial, and you get this in the logs and in Sentry:

UnsupportedGranularityError
Google Health data_granularity 'hourly' needs the windowed rollUp operation, which is disabled (#1577). Its data types were skipped; set the provider's granularity to 'raw' to resume them. Sleep and derived daily totals are unaffected.

Caution

If google settings have data_granularity set to anything other than raw, change it back before upgrading. Sleep sessions and derived daily totals sync either way.

Active and basal energy

Total calories used to be written into the energy series, which means active energy, so values were inflated by BMR and multiplied by repeated syncs. Active energy now comes from the native active-energy-burned intervals (list / reconcile), tagged with an external_id, and basal energy is derived per civil day as total calories minus active energy from dailyRollUp, stored as a daily total.

Migration: purge the bad energy rows (optional, manual)

Run once after deploying and before any historical re-sync. It deletes untagged legacy rows in the live table, plus every archived Google Health API energy bucket (the archive carries no per-row marker, and total-calories was its only writer before this fix).

uv run python scripts/data_migrations/purge_google_total_calories_energy.py --dry-run
uv run python scripts/data_migrations/purge_google_total_calories_energy.py

--batch (default 50000) commits per batch so no single transaction holds locks; --skip-archive if archival already ran over re-synced data. Not wired into startup, because it removes data only a re-sync can bring back.

New data types and other fixes

  • New series ingested: heart_rate_variability_rmssd (from daily-heart-rate-variability), vo2_max, oxygen_saturation (from daily-oxygen-saturation). Webhook subscriptions should include the two daily- types.
  • Zero-valued samples are no longer stored, so HRV and SpO2 averages stop being dragged toward 0, and a day with no activity has no row instead of a 0 row.
  • Daily series (resting heart rate, respiratory rate, daily HRV, daily SpO2) are written with is_daily_total: null, in line with every other provider, so they now appear in bucketed /timeseries?resolution=... reads. Rows written before this release keep is_daily_total: true and stay excluded.
  • Live pulls import overnight sleep sessions (matched on session end, not start) and the previous day's daily totals, so the first pull after upgrading may bring in one extra night and one extra day.
  • Each data type commits on its own, so a failure in one no longer discards the others.
  • A malformed or error-shaped page from Google fails that metric and marks the run partial, instead of reading as an empty window and reporting success.

Sync reliability and new settings

Variable Default Purpose
PROVIDER_REQUEST_TIMEOUT_SECONDS 30 Per-request timeout for provider API calls, previously hard coded
LINKED_SYNC_PULL_LEASE_SECONDS 120 Lease for the linked-account pull lock, renewed while the sync runs

Both optional, defaults keep current behaviour.

linked_sync:<provider>:<account>:pull:primary now expires around two minutes after its holder dies instead of up to four hours, so a manual redis-cli DEL after a worker restart is no longer needed. The Garmin backfill lock keeps its 4 hour TTL.

A linked-account pull that finds another profile syncing the same account now records a run with status: "skipped", items_processed: 0 and metadata.skipped: true in GET /users/{user_id}/sync/runs, instead of recording nothing and reporting success. last_synced_at is not advanced on a skip, so the primary's fan-out covers the full gap.


energy is now active_energy

energy sat next to basal_energy and looked like the total. It was active energy only, so it is renamed. The stored series id is unchanged (81), no data moves, and series_type_definition.code is rewritten by the existing startup seed.

Requests still accept the old name

GET /timeseries?...&types=energy         -> 200, "type": "active_energy"
GET /timeseries?...&types=active_energy  -> 200, "type": "active_energy"
GET /timeseries?...&types=nonsense       -> 400

Responses always return the new name

Warning

Anything matching the literal "energy" in a response body breaks: type on /timeseries, the key in /summaries/data and in /summaries/data/timeline?group_by=series_type, and code in /meta/coverage. Request values keep working until 1.0.

GET /summa...
Read more

0.8.0 - Withings & Historical Syncs records

Choose a tag to compare

@KaliszS KaliszS released this 11 Sep 15:57
53de57c

Highlights

We encourage you to use our official Docker images:

Note

We have changed the way we tag images:

  • latest - this is how you can get the newest stable image
  • 0.8.0, 0.8 - this is how you can get the chosen stable release
  • nightly - unstable, but always up to date with main (we deploy this tag every night)

Caution

A warning for nightly users regarding instability (migration #9b079...): If you have already used the nightly tag and carried out an update between 2026-09-07T17:46:43Z and 2026-09-08T14:55:55Z, please check whether your database contains the columns entry_source, intensity and label in the workout_details table. If not, this means you downloaded a version containing a faulty migration, in which case we recommend adding these columns manually using SQL:

ALTER TABLE workout_details
  ADD COLUMN IF NOT EXISTS entry_source VARCHAR(32),
  ADD COLUMN IF NOT EXISTS intensity    VARCHAR(10),
  ADD COLUMN IF NOT EXISTS label        VARCHAR(255);

Withings integration

We now have integration with a new provider, Withings! This integration introduces new elements to our global architecture. We are making all data categories available straight away, along with the methods for integrating them. Thank you to our contributor for preparing the pull request!

image

Historical Sync metadata preserved

Every sync now leaves a trace. We introduced a sync_run table that tracks a complete synchronization run - provider, scope (historical or live), status, timing, item counts and errors - plus a per-data-type breakdown within another table sync_run_data_type, so you get one row per data type per run instead of an opaque blob. Runs are tracked across all ingestion paths: provider pulls & pushes, SDK uploads, SDK logs, Apple XML imports and Garmin backfills. A stale sweep closes runs that stopped reporting, checking Redis for liveness first so a slow backfill isn't cut off prematurely. Two new endpoints expose it: GET /users/{id}/sync/history and GET /sync/history/{run_key}. It will be also added to our frontend in its next iteration.

Live syncs (webhooks, scheduled pulls, incremental SDK batches) are tracked in Redis for status updates but not stored by default, since they would add hundreds of rows a day per active user - set PERSIST_LIVE_SYNC_RUNS=true if you want them in the table too.

image image

SDK uploads queue a reference instead of the payload

Passing the full SDK upload body as a Celery task argument serialized it into the Redis broker queue, so every queued message carried several megabytes and a backlog grew in proportion to payload size. When new .env SDK_PAYLOAD_S3_OFFLOAD is enabled, the sync endpoint now enqueues only an S3 reference and the worker loads the body when the task runs, keeping each broker message at roughly 1 KB. The payload was already being written to S3, so this removes a duplicate copy rather than adding a write.

Note

New SDK_PAYLOAD_S3_OFFLOAD variable is off by default, and nothing changes for deployments that leave it that way.

Workouts now tell you where they came from

Three providers were already sending per-workout provenance and we were dropping it on the floor. Workouts now carry entry_source, intensity and label, so you can finally tell a measured workout from a manually entered one, and see the name the user gave it. Oura, Strava, Garmin and Apple all populate these fields.

Oura MET - movement dynamics throughout the day

Oura sends an intraday MET (metabolic equivalent of task) series alongside daily activity. We now expand that series into individual timestamped samples at the provider's own interval and store it as physical effort, so you get a continuous picture of how intense someone's movement was across the day - not just a daily total. It's also surfaced on the workouts API. Oura is the only provider exposing MET at this granularity today, so this is currently Oura-only.

Multi-gigabyte Apple Health exports, on any storage backend

The Apple Health XML upload limit went from 1 GiB to 5 GiB, with a progress dialog on the frontend. Just as importantly, the flow now works identically against AWS S3 and a self-hosted MinIO - previously the XML path used a hard-coded AWS client and depended on SNS, which MinIO cannot emit. A fully local, AWS-free deployment can now ingest multi-gigabyte Apple exports through the portal.

Security hardening

Four ways to take over an account or a deployment are now closed, all backwards-compatible. API keys are stored hashed and the raw value is shown only once - on create or rotate; the migration hashes existing keys in place, so current integrations keep working. Changing a password now revokes every refresh token the developer holds - previously a leaked token worked forever, since refresh tokens have no expiry. PATCH /developers/{id} is restricted to the caller's own account; before, any team member could set another developer's password. And Garmin webhooks now compare the garmin-client-id header against the configured value instead of just checking that it's present.

Ingestion got substantially faster

We rewrote the DataPointSeries bulk upsert. Instead of chunked INSERT ... VALUES statements carrying an enormous number of bind parameters, a batch is now COPY-ed into a temporary staging table and merged into the target with a single statement. Locally that's a 3.7× speedup on a 50,000-row batch (6.10s → 1.67s). XML imports got their own dedicated Celery queue so they stop competing with everything else.

Multi-arch Docker images

Backend and frontend images now publish for both linux/amd64 and linux/arm64 under the same tags as before.

Garmin backfill controls are going away - ⚠️ deprecation

The Garmin backfill cancel and retry endpoints are removed, and GET status is now deprecated - please stop using it; it will be removed in a future release. This whole mechanism dates back to before we understood how Garmin's backfill API behaves, and it never fit the way other integrations handle historical sync: Garmin allows one request per timeframe per type and answers 409 afterwards, so retry could never do anything useful. The periodic GC task is gone too - it accounted for roughly 15% of worker log lines and in practice never cleared anything, since the per-type timeout and the lock TTL already cover those cases.

What's Changed

Platform

  • fix: resolve race condition in token refresh using distributed lock by @sehmbimanvir in #1440
  • feat(backend): multipart upload + MinIO parity for Apple's XML imports by @MichalZak17 in #1445
  • feat(backend): offload SDK upload payload to S3, enqueue only a reference by @bartmichalak & @KaliszS in #1460
  • perf(backend): rewrite DataPointSeries bulk upsert to use COPY + staging merge by @McArthur955 in #1492
  • ci: publish multi-arch images via native arm64 runners by @knowald in #1584

Data model

  • feat(backend): historical sync status storage by @KaliszS in #1439
  • feat: SyncRun tracking - follow-up by @czajkub in #1448
  • feat(backend): add name, intensity and recording source to the workouts API by @McArthur955 in #1510

Security

  • fix(auth): revoke developer refresh tokens on password change by @bartmichalak in #1590
  • fix(auth): restrict developer update by ID to the caller's own account by @bartmichalak in #1599
  • fix(auth): store API keys hashed and show the raw key only once by @bartmichalak in #1592

Scores

  • fix: prevent duplicate sleep score calculation by @czajkub in #1484

Withings

Suunto

  • feat(integrations): pause-aware stopTime + gear from SummaryExtension by @HugoHeneault in #1043
  • fix(integrations): recover missing Suunto sleep windows safely by @bwickner in #1187
  • fix(backend): fetch Suunto awake time by @KaliszS in #1525
  • feat(backend): Spo2Max & AvgHRV from Suunto sleep by @KaliszS in #1528

Apple

  • feat(backend): raise upload limit to 5 GiB and add upload progress dialog for uploading Apple's XML by @MichalZak17 in #1444

Whoop

  • fix: Update Whoop workout type mappings by @czajkub in #1451
  • fix: split HRV into RMSSD/SDNN and deprecate recovery_score in recovery summaries endpoint by @LouisFerger-Andrews in http...
Read more

0.7.0 - Google Health API & SensorBio

Choose a tag to compare

@KaliszS KaliszS released this 12 Aug 13:06
c79410a

Highlights

We encourage you to use our official Docker images:

Google Health API & SensorBio

In our latest release, we’re introducing two new integrations with providers. Both integrations will be further developed, but it’s important to note two things:

  • The Google Health API replaces the Fitbit API, which is soon to be deprecated by Google
  • SenorBio does not currently support webhooks
image

Admin UI Dashboard improvements

On less efficient infrastructure, the front-end dashboard could take up to several seconds to load data. We optimised this by making improvements to React and removing unnecessary information. In return, we added more details about the volume of stored data and its specific types (see screenshot below). We also improved the display of large numbers (previously, even millions were shown in thousands). The dashboard should now load instantly, even with very large volumes of data and limited RAM.

image

More information on data sources for the frontend

On the frontend, each piece of data (workouts, sleep sessions, menstrual cycles, activity) will display more information. Until now, we’ve only shown the provider from which the data was saved. Now, the original provider will also be displayed (if applicable – for example, Apple allows data to be synchronised from devices belonging to other providers) along with the device from which the data was retrieved (currently, in the example described earlier, for Apple this will simply be an iPhone, but we also intend to start recording information about the original device soon, provided the providers allow it).

You’ll probably often notice gaps in this information; we intend to rectify them in the next release, as this requires changes to the way data is fetched from the sources.

image image image image

The option to delete data associated with a selected provider

From now on, the user profile includes an option to delete data from a selected provider. This also automatically disconnects the connection.

image image

SDK applications management in Settings

From now on, you can manage SDK applications via the frontend settings (Admin UI).

image

Logging improvements

The first change is an improvement to the SDK’s data-saving process. Previously, a single incorrect row would result in the loss of the entire batch. Now, instead, we display the sync status for the problematic rows and log their exact errors to Sentry.

image image

In addition, we have added a lot of new information to the logs, which may be useful for debugging.
HTTP logs from app:

  • response_body (an error details)
  • request_bytes (how many bytes we are expecting from the payload)
  • error_cause_type (can be helpful to decide whether an error parsing the body response_body is caused by connection or decode error)
  • error_cause_msg
  • content_type (should be application/json)
  • content_encoding (for example gzip)
{"timestamp": "2026-07-30T17:41:05.671050+00:00", "level": "error", "message": "http_request", "provider": null, "method": "POST", "path": "/api/v1/sdk/users/00000000-0000-0000-0000-0000000000bb/sync", "status": 400, "duration_ms": 17.7, "request_bytes": 3, "response_body": "There was an error parsing the body", "content_type": "application/json", "error_cause_type": "UnicodeDecodeError", "error_cause_msg": "'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}
{"timestamp": "2026-07-30T17:41:05.681616+00:00", "level": "error", "message": "http_request", "provider": null, "method": "POST", "path": "/api/v1/sdk/users/00000000-0000-0000-0000-0000000000bb/sync", "status": 400, "duration_ms": 0.6, "request_bytes": 62, "response_body": "There was an error parsing the body", "content_type": "application/json", "content_encoding": "gzip", "error_cause_type": "UnicodeDecodeError", "error_cause_msg": "'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte"}
{"timestamp": "2026-07-30T17:41:05.732281+00:00", "level": "error", "message": "http_request", "provider": null, "method": "POST", "path": "/api/v1/sdk/users/00000000-0000-0000-0000-0000000000bb/sync", "status": 400, "duration_ms": 0.9, "request_bytes": 1000000, "response_body": "There was an error parsing the body", "content_type": "application/json", "error_cause_type": "ClientDisconnect", "error_cause_msg": ""}

Logs from celery worker:

  • first_error_loc (position of the first broken row in S3 payload)
  • data_type (SDK related, example HKQuantityTypeIdentifierWalkingStepLength)
  • success (SDK related, example true)
  • record_count (SDK related, int)
  • task_type (SDK related, example foreground)
  • low_power (SDK related, example false)
  • thermal_state (SDK related, example nominal)
  • types (sync status related, example ["basal_energy", "energy", "heart_rate", "steps"])
{"timestamp": "2026-07-30T15:14:21.143378+00:00", "level": "info", "message": "Apple data import completed", "provider": "apple", "action": "apple_sdk_import_complete", "batch_id": "89d47924-a674-48bd-9001-32c3ec3065e2", "user_id": "3c213ef4-38f3-4c0c-89d3-c782a741125d", "incoming_records": 1750, "incoming_workouts": 125, "incoming_sleep": 125, "records_saved": 2023, "workouts_saved": 124, "sleep_saved": 124, "validation_ms": 56.5}
{"timestamp": "2026-07-30T15:14:21.143502+00:00", "level": "warning", "message": "Apple SDK dropped invalid records", "provider": "apple", "action": "apple_sdk_records_dropped", "batch_id": "89d47924-a674-48bd-9001-32c3ec3065e2", "user_id": "3c213ef4-38f3-4c0c-89d3-c782a741125d", "dropped_count": 3, "first_error_loc": "records[0].value", "first_error_msg": "Input should be a valid decimal"}
{"timestamp": "2026-08-11T11:20:50.019232+00:00", "level": "info", "message": "SDK log events received",
 "provider": "apple", "action": "sdk_logs_received", "batch_id": "823f86a1-b76a-4f48-....",
 "user_id": "d771d1c3-3098-....", "event_count": 2,
 "event_types": ["historical_data_type_sync_end", "device_state"], "sdk_version": "0.13.0",
 "data_type": "HKQuantityTypeIdentifierWalkingStepLength", "success": true, "record_count": 966,
 "task_type": "foreground", "low_power": false, "thermal_state": "nominal"}
{"timestamp": "2026-08-11T11:21:33.975802+00:00", "level": "info", "message": "Sync status",
 "provider": "apple", "action": "sync_status", "status": "success", "source": "sdk",
 "stage": "completed", "run_id": "a899503a-e7bb-....", "user_id": "d771d1c3-...",
 "items_processed": 1439, "types": ["basal_energy", "energy", "heart_rate", "steps"],
 "detail": "Apple batch saved"}

What's Changed

Google Health API

SensorBio

Oura

  • chore(backend): Use Oura 30-sec sleep stage granularity over 5-min by @czajkub in #1327

Data model

Frontend improvements

  • fix(frontend): emit root stylesheet from client build by @aritsyg in #1315
  • fix: include icon_url on user connections by @Gasiek in #1347
  • feat(backend): add missing Apple and Samsung provider icons by @bartmichalak in #1369
  • fix(frontend): load theme colors under Tailwind v4 by @MichalZak17 in #1376
  • refactor: optimise dashboard by @KaliszS in #1388
  • refactor(frontend): optimise react by @KaliszS in #1389
  • refactor: data points count K->M->B->T by @KaliszS in #1390
  • feat(frontend): event records in dashboard by @KaliszS in #1391
  • refactor(frontend): replace hsl(var(--x)) workarounds with theme utilities by @MichalZak17 in #1397
  • refactor(frontend): migrate Tailwind theme to CSS-first @theme b...
Read more

0.6.3 - Nap detection & HTTP logs middleware

Choose a tag to compare

@KaliszS KaliszS released this 15 Jul 15:16
3f0a7f8

Highlights

We encourage you to use our official Docker images:

Changes to HTTP logs

As HTTP logs usually account for more than 80% of all logs, we have disabled them by default for production deployments and added a new variable to control their flow (ACCESS_LOG_LEVEL). How it works:

  • ACCESS_LOG_LEVEL=all -> All HTTP logs are available, as before
  • ACCESS_LOG_LEVEL=errors -> only 4xx & 5xx log entries are available
  • ACCESS_LOG_LEVEL=off -> no HTTP logs
  • no ACCESS_LOG_LEVEL (None value):
    • if ENVIRONMENT=production -> errors level is set automatically
    • else all level is set automatically

We have also changed the structure of the HTTP logs:

  • before:
[2026-07-15 11:41:55,669 - uvicorn.access] (INFO) HOSTNAME:PORT - "GET /api/v1/users?page=1&limit=9&sort_by=created_at&sort_order=desc HTTP/1.1" 200
  • after:
{"timestamp": "2026-07-15T13:31:03.742767+00:00", "level": "info", "message": "http_request", "provider": null, "method": "OPTIONS", "path": "/api/v1/users?limit=5&sort_by=created_at&sort_order=desc", "status": 200, "duration_ms": 3.5}

Warning

By default, ACCESS_LOG_LEVEL is set to None, so if you have the ENVIRONMENT variable set to production, you will stop logging most messages. If you wish to retain the current behaviour, you must add the ACCESS_LOG_LEVEL variable and set it to all.

Note

For the time being, this change only affects HTTP logs from uvicorn. The new variable does not alter the behaviour of logs from Open Wearables.

Nap detection for Garmin & Oura

We have added support for naps based on data provided by Garmin and Oura. For the time being, this feature works correctly only for these two providers, but we will endeavour to develop our own algorithms to detect naps for providers that do not themselves flag which sleep sessions are naps.

image

Flag to enable/disable outgoing webhooks

We have added a new variable, OUTGOING_WEBHOOKS_ENABLED, which allows you to enable or disable outgoing webhooks. By default, it is set to false, so you need to add it and set it to true for the webhooks to start working.

Note

Please note that this has nothing to do with incoming webhooks from providers. It only concerns events that Open Wearables sends to client backends.

image

Strava workout samples

Once the INGEST_WORKOUT_SAMPLES variable is set to true (it was added in previous versions whilst working on Garmin workout samples and is set to false by default), Strava will start logging workout data at a high frequency. You can find out more about this mechanism in the entry “Ingest workout samples” for Garmin in the 0.6.0 release note: https://github.com/the-momentum/open-wearables/releases#release-0.6.0

Redis switched from RDF to AOF mode

We have changed the way Redis works to avoid errors in certain edge cases. You can read the details here: #1188. We have, of course, ensured that the data stored in Redis is preserved during the update.

The Docker Dev image has been reduced in size by around 40%

We achieved this by sealing the .dockerignore file and removing old and unused layers from the Dockerfile. This, of course, only applies to the local development version; nothing changes in the production build.

What's Changed

Outgoing webhooks

Google Health Connect

  • fix(backend): convert Health Connect blood glucose from mmol/L to mg/dL by @knowald in #1123

Oura

Suunto

  • fix(integrations): accept float totalDistance in Suunto workout schema by @thecodearrow in #1253

Garmin

  • fix(backend): remove dead sleep parameters from Garmin by @KaliszS in #1275
  • feat: Add nap detection for Oura, Garmin by @czajkub in #1149

Strava

Fitbit

  • fix(backend): convert Fitbit workout distance from kilometers to meters by @knowald in #1139

API improvements & optimisation

Observability & deployments

  • fix(backend): enable Redis AOF persistence so the svix queue survives restarts by @knowald & KaliszS in #1188
  • fix(frontend): remove redundant layers & add .output to .dockerignore by @KaliszS in #1296
  • feat(backend): middleware for uvicorn logs by @KaliszS in #1297
  • feat(backend): timestamp for HTTP middleware logs by @KaliszS in #1301

MCP

  • refactor(mcp): typed exception hierarchy + first test harness by @kjsteuer in #894

Docs & chores

New Contributors

Full Changelog: 0.6.2...0.6.3

0.6.2 - Data integrity

Choose a tag to compare

@KaliszS KaliszS released this 26 Jun 13:12
a07818f

Highlights

Counting steps and energy

We have updated the summaries for step count and energy (kcal). The data in the data_point_series table now includes a new is_daily_total column. This is because data providers supply data at varying levels of granularity. Previously, an error occurred during aggregation when some providers sent a daily entry alongside granular entries (e.g. Garmin in 15-minute chunks). From now on, these will be correctly flagged via the new column. The current logic for displaying totals in summaries means that the daily record will be used first. Only if this does not exist will all other entries from that day be summed.

Warning

The next time Open Wearables is launched, an automated script will set the value to true in a new column for the relevant rows in the data points. Intraday aggregates are not flagged as false by the script, so as not to place too much load on the application at start-up (although all new records will be). Therefore, the value none in the is_daily_total column should be treated as equivalent to false.

Active minutes

From now on, active_minutes will be treated as a separate series type and retrieved directly from all providers that supply this data. Until now, we have defined active minutes as the number of minutes in which more than 30 steps were recorded. This definition will remain the fallback for providers who do not supply this data. Our science team is developing a new, improved method for calculating this parameter, which will be implemented in the near future.

Warning

This change will not affect any data that has already been saved to the database. To retrieve correct active minutes for this data, you will need to resynchronise it. With Garmin, you must first delete the data, whereas with other providers, it will be overwritten during historical synchronisation.

Tip

Providers that directly support active minutes include: Garmin, Oura, Polar and Ultrahuman.

Filter sleep sessions by priority

Important

One API endpoint (/users/{user_id}/events/sleep) has been updated. However, this is a backwards-compatible change as a new parameter has been added that is set by default to not alter the existing behaviour of the endpoint.

We have added an optional filter_by_priority flag to the /events/sleep endpoint. This flag is disabled by default and, when enabled, filters out duplicate sleep sessions. On our frontend, this option is available as a toggle, as shown in the screenshot below. The screenshot illustrates a situation in which Oura connects to both Apple Health and Open Wearables, meaning sleep sessions are sent to the database directly from Oura and via Apple Health.

image image

Garmin's respiratory rate

We have corrected the way this parameter is retrieved. Until now, we had been searching for it in raw payloads under the wrong name, meaning it was not being saved to the database.

Improved data summary per user

We have enhanced the visual design of the data summary section and added time filters, enabling you to filter your data summaries by day or by a specified time period.

image

Manual invitation links

Thanks to our contributor, we can now manually copy invitation links, which is useful if you don't have an email account configured.

image

connection.revoked outgoing webhook

A new event is now available in outgoing webhooks, which will notify you when connections with providers are revoked.

What's Changed

Data integrity

  • fix(backend): take daily data OR sum intraday data by @KaliszS in #1232
  • fix(backend): active minutes as a new series type by @KaliszS in #1242
  • fix: correctly filter sleep sessions by priority on frontend by @czajkub in #1249

Oura

Apple Health

Garmin

  • fix: garmin respiratory rate + new data points batch insert chunk size by @KaliszS in #1235

Other Improvements

  • fix: Mark connection as revoked on failed token refresh + connection.revoked outgoing webhook by @czajkub in #1203
  • fix(backend): split iOS & Android coverage by @KaliszS in #1233
  • feat: support manual invite links when email is not configured by @edsammy in #1230
  • fix(frontend): optimise activity tab by @KaliszS in #1245
  • refactor(frontend): improve data summary per user by @KaliszS in #1246

Docs & chores

New Contributors

Full Changelog: 0.6.1...0.6.2

0.6.1 - Data Coverage & Observability

Choose a tag to compare

@KaliszS KaliszS released this 19 Jun 14:00
bbc5686

Highlights

Data Coverage tab

We have added a new Data Coverage tab to the admin panel which shows the data that we support for each provider. This information is always up to date.

Note

Although it includes OpenWearables capabilities, it does not display which data has already been synchronised with the system.

image image

Observability, deployments & logs

The focus of this release was on making debugging and troubleshooting easier by improving the visibility of logs and sync statuses. These changes include the following:

  • adding a workflow to publish an image to Docker Hub
  • linked webhook logs to users and/or traces (including failed attempts to receive a webhook).
  • the application version has been added to Sentry logs
  • improved reliability when saving sync statuses from webhooks to Redis (new skippedstatus added).
  • new metadata has been added to sync statuses, dividing new items into inserted and updated.
image image

Lookback for pull syncs

Although we recommend using webhooks, we have added lookbacks as a safeguard against data gaps if you need to use periodic pull syncs for any reason. This could happen if a user has not opened their wearables app for a long time, causing the live sync window to expire. This issue can now be resolved by syncing data from a specified number of minutes, hours or days in the past.

In order to use this feature, you will need to set the PULL_SYNC_LOOKBACK environment variable, specifying a string containing a number and a time unit. For example, 2d would mean 2 days, 3h would mean 3 hours, and 15m would mean 15 minutes. In periodic pull mode, live sync by default retrieves data recorded from the start of the day on which it is called until the present moment. Setting the lookback extends the start of the sync window by the specified duration and unit of time.

Note

PULL_SYNC_LOOKBACK is disabled by default.

image

This session is in committed state error

Thanks to our new contributor, the bug that caused ORM objects to be downgraded has been fixed.

What's Changed

Observability, deployments & logs

  • ci: add manual Docker Hub image publish workflow by @kczpl in #1179
  • fix(backend): build data source display name from reloaded provider strings by @knowald in #1154
  • chore(backend): add user/trace correlation to webhook logs by @bartmichalak in #1183
  • feat(backend): set Sentry release from version + commit SHA by @bartmichalak in #1196
  • docs: remove publishing the images section by @bartmichalak in #1205
  • fix(backend): webhooks syncs being saved into sync logs by @KaliszS in #1209
  • refactor: improved sync metadata logging by @KaliszS in #1211
  • fix(integrations): drop non-retriable 4xx webhook deliveries instead of retrying forever by @slavarosin in #1053

Docs & chores

Suunto

  • fix(backend): report accurate Suunto sleep sync stats, skip unsaveable sessions by @bwickner in #1182
  • fix(integrations): handle Suunto workout webhook edge cases by @HugoHeneault in #1042
  • fix(backend): suunto tests by @KaliszS in #1189

Garmin

  • fix(backend): persist body composition muscle mass from Garmin webhooks by @bartmichalak in #1194
  • fix(backend): persist blood pressure from Garmin webhooks by @bartmichalak in #1193

Polar

Whoop

  • refactor(backend): construct default Whoop sync window as UTC-aware datetimes by @knowald in #1157

Fitbit

  • fix(integrations): coerce Fitbit string start/end dates to datetime before strftime by @slavarosin in #1054

Other improvements

New Contributors

Full Changelog: 0.6.0...0.6.1

0.6.0 - Women's Health & FIT files (Garmin)

Choose a tag to compare

@KaliszS KaliszS released this 13 Jun 22:53
b0ff3d8

Highlights

Women's Health

We now support menstrual cycle tracking for Garmin wearables. The new women's health tab is only available if the user has fetched any related data. Otherwise, it is invisible.

image

Ingest workout samples (high data granularity)

We have added an option to enable second-by-second granularity for certain data from Garmin, such as heart rate, speed, cadence, power, latitude, longitude, elevation and running dynamics (which require a FIT file), as well as air temperature (also requires FIT file to be available).

Caution

This option is disabled by default. To enable it, you must set the INGEST_WORKOUT_SAMPLES environment variable to true.

Warning

According to our tests, this will increase database memory usage by approximately 170 MB per user per month.

FIT file parser and saving to S3

A parser for FIT files has been implemented. Currently, there is no option to upload these files manually; they are processed automatically via Garmin webhooks. However, the same parser will eventually support other providers.

Garmin currently extracts the following data from FIT files and saves it to the database:

  • training segments (laps, swim lengths, etc.)
  • time spent in each heart rate zone during a workout
  • time spent in each power zone during a workout
  • air temperature (INGEST_WORKOUT_SAMPLES required)
  • running vertical oscillation (INGEST_WORKOUT_SAMPLES required)
  • running vertical ratio (INGEST_WORKOUT_SAMPLES required)
  • running stride length (INGEST_WORKOUT_SAMPLES required)
  • running ground contact time (INGEST_WORKOUT_SAMPLES required)
  • running stance time balance (INGEST_WORKOUT_SAMPLES required)

Note

We have added a new environment variable called STORE_FIT_FILES, which is set to false by default. When this is set to true, raw FIT files from Garmin will be saved to a previously configured S3 bucket. See the S3 configuration guide for more information: https://openwearables.io/docs/dev-guides/raw-payload-storage

Warning

Storing raw FIT files in S3 results in an additional data usage of between a few MB and a dozen or so MB per user per month.

Many Oura bugs fixed

We have fixed several bugs relating to the Oura integration. We have added time zone offset saving to the database, supported sleep summary metrics, improved data retrieval for current day summaries and migrated HRV from SDNN to RMSSD. Existing SDNN values in the database will be migrated by an automatic script that runs when the application starts.

Added redis TLS support

To enable TLS support (rediss://), set the new environment variable REDIS_SSL to true.

The day prefix for sleep start times that began before midnight

From now on, the start time of a sleep session in the sleep section will be prefixed with the previous day’s name if it was before midnight. If a sleep session ends after midnight, it will be displayed under the following day in the admin UI (as it always has been). Please note that this is dynamic and will adjust to the user’s time zone.

image

Support for synchronising the same data across multiple accounts

We have added dedicated support for users who need to synchronise their devices with several Open Wearables accounts for testing purposes. No configuration is required to use this feature. Simply pair the relevant accounts with your wearables account and synchronise any of them. A badge will appear in each account to indicate which accounts are linked, along with the relevant sync statuses. This shows that, for a given account, the source of the currently syncing data is a linked Open Wearables account. This means that, instead of repeatedly syncing the same data - which is virtually impossible with Garmin during backfilling, for example - it will be synced once and copied to the other accounts. More information: https://openwearables.io/docs/architecture/multi-account-sync

image image image

What's Changed

Garmin FIT files & high granularity data support

  • feat(backend): add migration with new jsonb fields for workout_details by @KaliszS in #1079
  • feat(backend): add average_cadence support for Garmin by @KaliszS in #1080
  • feat(backend): Garmin workout samples by @KaliszS in #1082
  • feat(backend): FIT files parser by @KaliszS in #1096
  • fix(backend): fit map stride_length -> step_length by @KaliszS in #1097
  • feat(backend): Garmin mappings for elevation, GPS & running dynamics + optional saving FIT files to s3 by @KaliszS in #1099
  • fix(backend): stop storing Garmin average HR as heart_rate_min by @knowald in #1121
  • feat(backend): Garmin support for workout segments (laps, swim lengths etc) by @KaliszS in #1118
  • fix(backend): null workout heart_rate_min rows copied from average HR by @knowald in #1133
  • feat(backend): HR & Power Zones [Garmin] by @KaliszS in #1175

Garmin Women's Health

Oura bugs

Apple Health bugs

Suunto bugs

  • feat(integrations): emit resting_heart_rate series from sleep HRMin by @HugoHeneault in #1055

Polar bugs

  • feat(backend): map INDOOR_CYCLING, JUMP_ROPE, KICKBOXING_MARTIAL_ARTS by @kozikow in #1110

Infrastructure improvements

  • fix: mount redis volume at /data so persistence works by @knowald in #1127
  • feat(backend): support TLS (rediss://) for Redis connections by @kczpl in #1134

Other improvements

  • fix(frontend): stop activity & sleep dates shifting a day for users west of UTC by @bartmichalak and @KaliszS in #1160
  • refactor: fixed scopes db saving & improved UI by @KaliszS in #1088
  • feat: Add webhook management endpoints by @czajkub in #1049
  • fix: update last_synced_at on webhook receive by @czajkub in #1145
  • feat: sharable syncs (the same data) between different OW users by @KaliszS in #1147
  • feat(frontend): OpenWearables version visible under logout button by @KaliszS in #1165

Docs & chores

  • fix(docs): update GitHub repository links in iOS SDK documentation by @bartmichalak in #1064
  • docs: update available providers in readme by @bartmichalak in #1069
  • chore: bump the npm-minor-patch group across 1 directory with 4 updates by @dependabot[bot] in #1057
  • chore: update sentry-sdk[fastapi] requirement from >=2.59.0 to >=2.60.0 in /backend by @dependabot[bot] in #1061
  • refactor(backend): update deps (redis v8) by @KaliszS in #1087
  • chore: bump the npm-minor-patch group in /frontend with 13 updates by @dependabot[bot] in #1102
  • docs(mcp): remove formatted duration fields absent from sleep tool output by @knowald in #1122
  • docs: clarify migrations auto-run on deploy temporarily by @bartmichalak in #1129
  • docs: note developer program is on hold by @bartmichalak in #1130
  • docs: drop "| Open Wearables" suffix from page titles by @bartmichalak in #1131
  • docs: add Fitbit and Google Health Connect to the coverage matrix by @knowald in #11...
Read more

0.5.2 - Polar refactor

Choose a tag to compare

@KaliszS KaliszS released this 20 May 08:52
a2060c7

Highlights

Polar got refactor & webhook support

Polar now synchronises sleep and timeseries data. By default, the Periodic Pullsync method is enabled, but webhooks can be activated in Settings > Providers.

image

Expanded Sport Type Support across Multi-Provider Mappings

Expanded the WorkoutType enum to explicitly support specific activities that were previously bundled into WorkoutType.OTHER. Over 70 provider-specific mappings across Apple, Garmin, Suunto, and WHOOP have been updated. New explicitly supported sports include: Cricket, lacrosse, racquetball, bowling, archery, fencing, fishing, wakeboarding, parkour, curling, and more.

Garmin fetches new fields

allDayRespiration and moveIQActivities have been added to the list of Garmin-specific attributes that we support.

Strict RMSSD enforcement for HRV-CV Resilience Score

Temporarily disabled the automatic fallback to SDNN when overnight RMSSD data is missing within the sleep window. Until SDNN-based calculations are fully validated, users without RMSSD data will now receive a null score instead of a potentially misleading fallback value.

What's Changed

  • refactor(backend): updated all python dependencies by @KaliszS in #1023
  • refactor(frontend): updated all TypeScript dependencies by @KaliszS in #1025
  • fix(backend): garmin allDayRespiration & moveIQActivities by @KaliszS in #1026
  • feat(backend): add recovery summary endpoint by @KaliszS in #1027
  • chore: disable SDNN fallback in HRV-CV score calculation pending validation by @FBegiello in #1040
  • fix: commit each data type individually and rollback session on exception for Oura and Whoop by @czajkub in #1022
  • fix: Cast to local date for summary endpoints by @czajkub in #1033
  • fix(backend): validation for date parameters in OpenAPI specs by @KaliszS in #1052
  • fix(users): skip invalid user rows during email validation by @zxjt2z42px-creator in #1032
  • fix: Handle duplicate EventRecordDetail inserts gracefully with savepoint by @czajkub in #1047
  • refactor(frontend): update dependencies by @KaliszS in #1056
  • fix: Add missing workout types, rename series type to SDK by @czajkub in #1048
  • feat: Polar refactor by @czajkub in #1041
  • fix(integrations): emit outgoing webhook for suunto live workout push by @HugoHeneault in #1045

New Contributors

Full Changelog: 0.5.1...0.5.2

0.5.1 - Oura & Strava Webhooks

Choose a tag to compare

@KaliszS KaliszS released this 08 May 15:07
08d8af3

Highlights

Oura & Strava got webhook support

By default Periodic pull only is enabled, but webhooks can be activated in Settings > Providers.

image

Live sync status is now available

It's already avilable in our Admin Panel (there is global Syncs tab, but all of statues are also visible in user profiles).

If you need to integrate this feature with your backend, checkout our integration guide: https://openwearables.io/docs/api-reference/guides/sync-status-stream

Sync statuses are also available as outgoing webhook events. This is the preferred solution if you need to permanently store this data, because we only store it in Redis for 24 hours.

image image

New Admin Panel Dashboard design

image

RMSSD HRV is now included in Body Summary

image

What's Changed

  • chore: bump typescript from 5.9.3 to 6.0.2 in /frontend by @dependabot[bot] in #993
  • fix: add hrv rmssd to body summary by @czajkub in #949
  • refactor: migrate all recovery scores from data_point_series to health_score by @czajkub in #950
  • refactor: polish Oura webhooks and adjust them to new webhook pattern by @czajkub in #990
  • refactor(frontend): Dashboard v2 by @KaliszS in #1003
  • feat: sync status (fastapi SSE) by @KaliszS in #986
  • feat: Sleep score recalculation after sleep session merge by @czajkub in #884
  • refactor(frontend): Dashboard redesigned by @KaliszS in #1015
  • fix(backend): scope x100 ratio->percent conversion to Apple provider only by @kjsteuer in #917
  • refactor: Strava webhooks by @czajkub in #1010

Full Changelog: 0.5.0...0.5.1