Skip to content

2.1.0

Choose a tag to compare

@github-actions github-actions released this 25 Sep 16:06
· 46 commits to main since this release

The iOS app is back: it syncs on iOS 27, sends only what changed since the last
sync, and starts a sync from a Home Screen widget or Siri. On the server, each
user decides which metrics the ingest accepts, the iOS app is ranked ahead of
Health Auto Export where both deliver, and two stores that kept the first
delivery of a still-growing value now keep the latest.

Breaking changes

  • FreeReps for iOS 2.1 needs server 2.1. It reads the per-user allowlist and
    identifies itself with a header that older servers ignore. The server stays
    compatible with older clients: a request without the header is treated as
    Health Auto Export.
  • The iOS app needs iOS 27 and no longer syncs in the background. A sync
    starts from the app, the widget or the Shortcuts action — see iOS app below.
  • GET /api/v1/allowlist answers per user. enabled is resolved for the
    calling identity: a metric counts as enabled when it is enabled server-wide
    and not switched off by that user.
  • metrics_inserted counts changed rows too. Ingest responses and
    import_logs report rows inserted or updated; an identical re-delivery
    still counts zero.

iOS app 2.1

  • Works on iOS 27 again. 1.0 started its first full sync at 2000-01-01 and
    did not finish, and HealthKit's statistics query fails for some ranges on
    iOS 27 (Unable to invalidate interval: no data source available), which
    failed whole categories. Both have a fallback now: workout heart rate is
    aggregated from samples, hourly sums are queried hour by hour.
  • Only what changed. After the first backfill, each sync sends what
    HealthKit added since the previous one, through one HKAnchoredObjectQuery
    per type, and re-sends the hours those samples fall into. Measured: a sync
    went from 7,217 metric values to a single ingest call with none. Once a day
    the last 24 hours are re-sent in full.
  • Widget and Shortcuts action. A Home Screen widget shows when the last
    sync ran and whether it succeeded; tapping it opens the app and syncs. The
    action "Sync Health Data" runs from Siri, the Action button or a personal
    automation without opening the app, and reports a locked iPhone instead of
    failing — HealthKit is unreadable while it is locked. Siri understands the
    phrases in English and German.
  • A fixed set of 38 HealthKit types instead of every type the SDK offers:
    activity, body, vitals, sleep, mindfulness, workouts with routes, blood
    pressure, state of mind, caffeine and water. Clinical records, medications,
    vision prescriptions, ECG, audiograms, symptoms and reproductive health are
    no longer requested; the health-records entitlement is gone.
  • Redesign. A status card with the last outcome and the running step, and
    categories as tiles in light and dark. Import File moves to Settings; the
    first backfill can be as short as one week.

Per-user ingest

Settings → Ingest → Accepted metrics switches metrics off for your own
ingest (PUT /api/v1/metrics/enabled, stored in user_metric_enabled). A
switched-off metric is rejected from every client, and the iOS app skips it
before reading HealthKit. Stored rows are kept.

Two Apple Health clients

The iOS app sends X-FreeReps-Client: freereps-ios. import_logs records its
calls as freereps_ios instead of hae_rest, and health_metrics stores the
client per row. Both clients write Apple Health data under an empty source, so
their rows were summed together: 2026-09-19 held 11,628 steps as the app's
hourly rows plus 12,007 as Health Auto Export's minute rows. Queries now pick,
per day or window, the app's rows first, then Health Auto Export's, then rows
stored before this release.

Correctness

  • Growing values keep their latest delivery. Metrics were stored with
    ON CONFLICT DO NOTHING, so the first sync inside an hour fixed that hour:
    the app's 12:00 step bucket stayed at 230.94 while Apple Health showed 413.
    The same kept Oura's daily rows at their first delivery — on one installation
    22 days in July held near-zero steps. health_metrics now upserts; an
    identical row writes nothing.
  • Activity summaries had the same defect: a day's rings kept the value of
    the day's first sync, 6 kcal on one day against 1,287 summed from the hourly
    rows. They upsert as well.
  • The MCP tool descriptions state that avg is a total for cumulative metrics
    and that end is exclusive.

Upgrading from 2.0.2

cd server
docker compose pull
docker compose up -d

Migrations run at startup. Two apply: 000035 adds user_metric_enabled,
000036 adds health_metrics.client and rebuilds the table's unique and
covering index. Writes to health_metrics wait while the indexes are rebuilt.
No row is deleted on the way up.

Rows stored before the upgrade keep their values. To correct the ones that
kept a partial value:

  • Apple Health via the iOS app: in the app, Settings → Advanced → Reset
    Sync State
    , choose the range, then Full Sync. The re-sent rows are marked
    as the app's and replace the partial ones.
  • Oura: delete the user's rows from oura_sync_state; the next sync
    re-fetches oura.backfill_days (90 by default) and overwrites what differs.

No new config.yaml keys.


Server Setup

Fresh install:

git clone https://github.com/meltforce/FreeReps.git
cd FreeReps
cp server/config.example.yaml server/config.yaml
# Edit config.yaml — set database password, enable Tailscale
cd server && docker compose up -d

Upgrade existing server:

cd server
docker compose pull
docker compose up -d

Upload Tool (macOS)

One-liner install:

curl -sSL https://raw.githubusercontent.com/meltforce/FreeReps/main/server/scripts/install-upload.sh | bash

Or download manually from the assets below and place in /usr/local/bin/.

Requires lzfse: brew install lzfse

Full Changelog: 2.0.2...2.1.0