Repository navigation
2.1.0
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/allowlistanswers per user.enabledis resolved for the
calling identity: a metric counts as enabled when it is enabled server-wide
and not switched off by that user.metrics_insertedcounts changed rows too. Ingest responses and
import_logsreport 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 oneHKAnchoredObjectQuery
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; thehealth-recordsentitlement 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_metricsnow 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
avgis a total for cumulative metrics
and thatendis exclusive.
Upgrading from 2.0.2
cd server
docker compose pull
docker compose up -dMigrations 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-fetchesoura.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 -dUpgrade existing server:
cd server
docker compose pull
docker compose up -dUpload Tool (macOS)
One-liner install:
curl -sSL https://raw.githubusercontent.com/meltforce/FreeReps/main/server/scripts/install-upload.sh | bashOr 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