Skip to content

Two-way sync with the self-hosted sync server - #7

Draft
Oliver-Johnson wants to merge 9 commits into
masterfrom
claude/two-way-sync
Draft

Oliver-Johnson wants to merge 9 commits into
masterfrom
claude/two-way-sync

Conversation

@Oliver-Johnson

Copy link
Copy Markdown
Owner

Checks

  • I have not edited index.html or sw.js without understanding the PWA cache implications
  • The version string in index.html has been bumped (format: Version YYYY-MM-DD.N). Left for promotion, see notes
  • manifest.json is still valid JSON (unchanged)
  • The app works offline after the change (no change to sw.js; sync just waits when offline)
  • CI passes (version consistency, .nojekyll, symlinks, conflict markers, plus a new sync server test job)

What this changes

Before: Sync only pushed a full snapshot from one device to the server. On launch the app pulled back only missing fibre, sugar and sodium, plus today's targets. A desktop never saw what a phone logged.

After: every device pointed at the same server shares one log. That covers entries, notes, weight, water, exercise, fasts, recipes, custom foods, targets and settings. A sync runs a few seconds after a change, on opening the app, on coming back to it or back online, and every 2 minutes while open. Settings shows when the last sync ran.

How it merges: each device keeps a fingerprint of every record from its last sync and merges record by record.

  • A record that is unchanged on one side takes the other side's edit or deletion.
  • If both sides changed a record, this device's edit wins, and an edit beats a delete.
  • Water added on two devices on the same day is summed.
  • A device's first sync is a union. Against an old one-way copy this device wins; on a server others already sync to, the server's targets and settings win over a new device's defaults.

Safety:

  • It never syncs while the tour's demo data is active.
  • A sync that would delete more than a fifth of either side (and more than 10 records) pauses, and Sync now asks first.
  • Clear All Data forgets the last sync, so the next sync restores from the server instead of deleting everywhere.
  • Unreadable data stops the sync instead of counting as empty.
  • It waits while a sheet is open or someone is typing, and tabs take turns with navigator.locks.

Server: new GET/PUT /sync/state. The revision is a hash of the stored file, so direct edits (agents filling in fibre) are picked up. A PUT carries the revision it started from and gets 409 if another device synced in between. Writes are atomic and locked, and unknown top-level keys are kept. The old POST /sync/food-log keeps working until the file has two-way data, then refuses, so an outdated app can't overwrite it. An app talking to a server without /sync/state falls back to the old push and fibre fill-in.

Docs: the developer guide, sync server README and README describe two-way sync, and the changelog has a beta entry.

Testing done

  • 12 server unit tests in sync-server/test_server.py, run by a new sync-server job in CI.
  • 24 two-device checks against a real server. These cover concurrent edits, deletes on each side, water from both devices, the destructive-sync pause, Clear All Data restoring, a 409 retry, an edit made mid-sync, and the tour never syncing.
  • 7 upgrade checks: an old one-way copy on the server, and a server not yet updated.
  • 40/40 smoke tests on this branch locally (Chromium).
  • Ran CI's checks locally. All pass.

Notes for reviewer

This is a draft. It goes to beta testers first via #9. Before it leaves draft it needs a version bump and a What's New entry on top of master.

Known trade-off: on the first sync after upgrading from one-way sync, an entry deleted since the last one-way upload comes back once, which is the safe side. The same amount of water added on two devices between syncs counts once.

This builds on #2, #1 and #3 (merged in), so its own changes are commits ba5aaeb and 53d73ed.

🤖 Generated with Claude Code

https://claude.ai/code/session_019C67htt6KV7fmxsGCf5FR7


Generated by Claude Code

claude added 6 commits October 6, 2026 10:04
…6-10-06.1)

seedCustomFoods() ran on every launch and added 11 foods to each user's
custom foods list, re-adding any the user had deleted. The list is now
only used as a fibre lookup. Existing users keep whatever is already in
their list.

Also removes plans/training_plan_2026.md, which held personal health
data, and corrects the v2026-09-02.2 changelog date.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C67htt6KV7fmxsGCf5FR7
- README: use the Macrolog name, list current features, correct the
  default targets, explain Developer Mode, link the beta, and state the
  GPL-3.0 licence and the correct Open Food Facts data licence (ODbL).
- CONTRIBUTING: list the checks CI actually runs, and describe the sync
  server and AI features as opt-in extras.
- Sync server README and developer guide: port 5001, the cd into
  sync-server, SYNC_API_KEY, where data is stored, what syncs today,
  Developer Mode, and that browsers block plain http:// servers from the
  HTTPS app, so a tunnel or HTTPS proxy is needed.
- Changelog: add the 3 Sep beta release and 7 Sep docs guides, and fix
  the old /beta/ path.
- Docs home: drop the mention of Docker instructions, which don't exist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C67htt6KV7fmxsGCf5FR7
…(v2026-10-06.2)

- Trends and Macro History: dates under the charts were cut off; the
  Trends kg/kcal labels overlapped the top axis values and the end bars
  covered the axis numbers.
- History calendar: an inline padding override put "Jump to Today"
  behind the bottom nav, so tapping it opened Settings.
- Additional Nutrients in Manual and Quick Add never collapsed because
  no .hidden rule existed. It now collapses on tap and stays open by
  default, as users see it today.
- Macro breakdown popup: escape food names before inserting them.
- First-run tour: steps whose target is hidden show the tip centred.
- Ko-fi card on tablets sits above the bottom nav.
- Docs: What's New cards now reveal in Chrome; the observer watched a
  card clipped to nothing, which never intersects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C67htt6KV7fmxsGCf5FR7
Devices that point at the same sync server now share one food log:
entries, notes, weight, water, exercise, fasting, recipes, custom foods,
targets and settings. A desktop left open picks up what a phone logs.

How it merges:
- Each device keeps a fingerprint of every record from its last sync.
  A record that still matches on one side wasn't changed there, so the
  other side's version or deletion wins.
- When both sides changed a record, this device's edit wins and an edit
  beats a delete. Water added on two devices the same day is summed.
- A device's first sync is a union. Against an old one-way copy this
  device wins, as the one-way push did; on a server others already sync
  to, the server's targets win over a new device's defaults.
- Uploads carry the revision they started from; the server answers 409
  if another device synced in between and the app merges again. Edits
  made during a sync are kept on top of what was uploaded.

Safety:
- Never syncs while the tour's demo data is active.
- A sync that would delete more than a fifth of either side (and more
  than 10 records) pauses, and Sync now asks before going ahead.
- Clear All Data forgets the last sync, so the next sync restores from
  the server instead of deleting everywhere.
- Unreadable local or server data stops the sync instead of counting
  as empty.
- Changes wait while a sheet is open or someone is typing.

When: shortly after a change, on opening the app, when it comes back to
the foreground or online, and every 2 minutes while open.

Server: new GET/PUT /sync/state with a content-hash revision, so edits
made to the file directly (agents filling in fibre) are picked up.
Writes are atomic and locked; unknown top-level keys are kept. The old
POST /sync/food-log still works until a server has two-way data, then
refuses so an outdated app can't overwrite it. Servers without
/sync/state still get the old upload and fibre fill-in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C67htt6KV7fmxsGCf5FR7
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C67htt6KV7fmxsGCf5FR7
Developer guide, sync server README and README now describe two-way
sync, how merging works and the upgrade path from one-way sync. The
changelog gets a beta entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C67htt6KV7fmxsGCf5FR7
claude added 3 commits October 7, 2026 08:17
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRtr2mPSPusKdCyJWSbct5
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRtr2mPSPusKdCyJWSbct5
- Today's targets filled in from settings at launch are marked auto. When
  both sides changed a day's targets, an auto record gives way to one
  someone set, and if both are auto the server's stand. A desktop opening
  later in the day no longer reverts targets set on the phone.
- An automatic sync that pauses because it would delete too much now says
  so with a toast, once, instead of stopping silently.
- Unreadable local logs stop the sync with "can't be read" rather than
  "server offline?".
- Seed foods that older versions added on every device, each with its own
  id, are matched by content, so syncing two devices doesn't list them twice.
- Server: the legacy one-way push holds the state lock from its read to
  its write, so it can't overwrite a two-way sync that lands in between.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRtr2mPSPusKdCyJWSbct5

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants