Skip to content

Sync settings between computers through the account - #991

Merged
enaboapps merged 5 commits into
mainfrom
claude/986-sync-engine
Oct 5, 2026
Merged

enaboapps merged 5 commits into
mainfrom
claude/986-sync-engine

Conversation

@enaboapps

@enaboapps enaboapps commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #986
Part of #988.

What

Adds src-tauri/src/settings_sync.rs. When an account is signed in, the portable settings document (#985) is kept in the account's desktop_preferences row, using the client contract from switchifyapp/switchify-supabase#2:

  • Conditional writes only:

    • Create the row with a plain POST (409 means another computer created it first, so re-read).
    • Update with PATCH ?user_id=eq.…&revision=eq.N and Prefer: return=representation (an empty result means another computer wrote first, so re-read and merge).
    • Never upsert, and never delete.
    • Up to 3 attempts per sync.
  • Merge base: the last document synced with each account is stored in settings-sync.json, keyed by user_id, so a base from another account (e.g. one deleted and recreated) is never used.

    • A change on one side is applied to the other.
    • Changes on both sides are merged by section (app, profiles, switches, point scan, remote switches). The account's copy wins any section changed on both sides.
    • After an apply, the base is the resulting local document, not the incoming one. Apply can normalize values such as profile versions, and using the incoming one would make two computers echo uploads back and forth (tested).
  • First sync on a computer:

    • If its settings match the account's, they are recorded as the base.
    • An untouched install takes the account's settings.
    • Otherwise the user is asked: Use my account's settings or Keep this computer's settings. Nothing is replaced until they choose.
  • Newer and invalid copies: a copy written by a newer app (schema_version above ours) is never overwritten; this install shows an "update Switchify PC" message. An invalid copy is reported and left alone.

  • When it runs:

    • at start
    • after sign-in
    • when local settings have changed and stayed unchanged for one 3 s poll (no changes to the existing save commands)
    • every 5 minutes, to pick up other computers' changes
    • on Sync now

    Failures back off from 3 s up to 5 min.

  • Main thread: incoming changes are applied through portable_settings::apply on the main thread.

  • Screen refreshes: after applying, switch-profiles-changed and remote-switches-changed are emitted, and the Switch Forwarding and Remote switch screens refresh. A refresh is skipped while an edit is unsaved, so the edit wins and syncs afterwards.

  • Install id: updated_by is a random per-install id, never the BLE desktop id.

  • Size limit: documents over 200 KB are refused before upload (the server allows 256 KiB).

  • Account integration: sign-in wakes the engine, sign-out turns sync off, and deleting the account forgets the merge base. Account::endpoint() exposes the project URL and key, and Authorized.user_id is now used.

  • UI: when signed in, the Account tab shows the sync status (up to date with the last sync time, syncing, error or update-required message), a Sync now button, and the first-sync choice. Focus moves to Sync now after the choice.

Review fixes

  • Two merge bases: the base keeps the account's copy (cloud) and this computer's copy (local) separately. Local changes are measured against local and account changes against cloud, so a value normalized on apply (e.g. a profile version bump) never looks like an account change that overrides a later local edit. The review's repro is now a test.

  • Edits during a sync: Host::apply(expected, document) applies only if local settings still match what the merge was computed from. The check runs on the main thread, where the settings commands run, so no edit can slip in between. Otherwise the sync re-reads and merges again, and an edit made mid-sync is never reverted.

  • Apply, then upload: an interim base (the account's copy on both sides) is recorded before the upload. If the upload fails, sections taken from the account don't look like local changes, and this computer's own changes still upload next time.

  • Profiles merge by id: profiles added on two computers at the same time are both kept, and an edit survives a deletion on the other side. Names made equal by the merge get a (2) suffix.

  • Loop behaviour:

    • Sync state lives in memory, so polls don't re-read the file.
    • The loop stays idle while signed out or waiting for the user's choice.
    • Failed syncs retry on backoff even without a local change, for example after switch practice ends.
  • Deleted account: a 409 with code 23503 (account deleted elsewhere) is reported as "This account no longer exists" instead of being treated as a lost create race.

  • forget() waits for a running sync.

  • Opening the Account tab wakes sync if a locked keychain hid the session at startup.

  • UI: the sync panel moves focus to the first choice button when it replaces a focused Sync now, and the remote switches reload catches errors.

  • Re-review round 2:

    • Profile changes are judged on content (name, provider, bindings), not version, because versions drift between computers. A merged profile keeps the higher version. A version-only difference no longer beats a real edit or brings back a deletion; the review's repro is now a test.
    • A merge that would exceed 32 custom profiles stops with a clear message, and nothing is applied.
    • The applied result is read on the main thread right after applying.
  • Re-review round 3:

    • Same-content profiles keep the higher version when applied (portable_settings::merge_profiles), so computers converge on one version instead of uploading their own versions to each other forever after lost same-profile races.
    • The reserved-name rename bumps the version only at load, not during a merge.
    • The sync tests' fake host now stores profiles with the real merge rules, and a two-computer test replays the review sequence and checks it settles.

Validation

  • npm run lint, npm test (279 passed) and npm run build

  • cargo fmt --check and cargo clippy --all-targets -D warnings: clean

  • cargo test: 708 passed, 5 ignored, including 29 new settings_sync engine tests with a fake remote and host. They cover:

    • being signed out
    • the first computer creating the row
    • an untouched computer adopting the account's settings
    • the choice prompt for a customized computer, and both choices
    • identical settings needing no choice
    • a local change uploading on the last-seen revision
    • a remote change being applied without being echoed back
    • a merge by section, with the account winning a section changed on both sides
    • a lost write race being re-read and merged
    • a lost create race
    • a newer schema never being overwritten, even with an explicit choice
    • an invalid copy not being overwritten
    • another account's base being ignored
    • no ping-pong after a normalizing apply
    • network errors
    • forget
    • an oversized document being refused
  • Against a real local Supabase stack, the ignored test settings_sync::tests::local_stack_round_trips_the_document_and_enforces_revisions passed. It shows:

    • explicit nulls survive the jsonb column and parse strictly
    • create gives revision 1, and a second create gets 409
    • update gives revision 2, and a stale update is rejected
    • another account can't read the row, and a token for another account can't update it through a forged user_id

    The account local-stack test also still passes.

  • UI tests cover the sync panel (status, Sync now, both choices, backend messages, live updates, hidden while confirming deletion), the remote switch reload (and that it skips while edits are saving), and the Switch Forwarding refresh.

🤖 Generated with Claude Code

Add the settings sync engine. When signed in, the portable settings
document is kept in the account's desktop_preferences row:
- Every write is conditional on the revision last seen (POST to create,
  PATCH revision=eq.N to update, never upsert), so a lost race re-reads and
  merges instead of overwriting another computer's change.
- The last document synced with each account is kept locally as the merge
  base. Changes on one side apply to the other; sections changed on both
  merge by section, with the account's copy winning a section changed on
  both sides.
- The first sync on a computer whose settings differ from the account asks
  which to keep; an untouched install takes the account's settings.
- A copy from a newer app is never overwritten; this install asks to be
  updated. An invalid copy is reported, not replaced.
- Sync runs at start, after sign-in, when local settings have settled after
  a change, every five minutes for other computers' changes, and on demand,
  with backoff after failures.

The Account tab shows sync status, Sync now, and the first-sync choice.
Profiles and remote switches screens refresh when sync changes them.

Refs #986

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@enaboapps enaboapps added this to the v1.0.0-rc.22 milestone Oct 5, 2026
OwenMcGirr and others added 4 commits October 5, 2026 14:41
Address review on the settings sync engine:
- Keep the account's copy and this computer's copy as separate merge bases,
  so a value normalized on apply (e.g. a profile version) never looks like
  an account change that silently overrides a later local edit.
- Apply only if local settings still match what the merge was computed
  from, checked on the main thread; otherwise re-read and merge again, so
  an edit made during a sync is not reverted.
- When an apply is followed by an upload, record an interim base first so a
  failed upload neither re-uploads the account's sections nor forgets this
  computer's changes.
- Merge profiles by id: profiles added on two computers are both kept, an
  edit survives a deletion on the other side, and merged names stay unique.
- Keep sync state in memory, stay idle while signed out or waiting for the
  user, and retry failed syncs on backoff even without a local change.
- Report a deleted account (409 foreign key) instead of retrying a create.
- forget() waits for a running sync; opening the Account tab wakes sync if
  a locked keychain hid the session at startup.
- Sync panel moves focus to the choice when it replaces the focused button;
  the remote switches reload handles errors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address re-review on the settings sync engine:
- Profile changes are judged on name, provider and bindings, not version,
  since versions drift between computers; a merged profile keeps the higher
  version. A version-only difference no longer beats a real edit or brings
  back a deleted profile.
- A merge that would exceed 32 custom profiles stops with a clear message
  instead of failing every retry with a generic error.
- The applied result is read on the main thread right after applying, so a
  settings change made just afterwards is not mistaken for synced.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address re-review: applying a profile with unchanged content kept the lower
local version, so after lost same-profile races two computers could upload
their own versions to each other forever. Same-content profiles now keep
the higher version, so both converge. The reserved-name rename no longer
bumps the version during a merge (only once at load, for connected phones).

The sync tests' fake host now stores profiles with the real merge rules,
and a two-computer test replays the review sequence and checks it settles.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@enaboapps
enaboapps marked this pull request as ready for review October 5, 2026 14:13
@enaboapps
enaboapps merged commit 6cd5b32 into main Oct 5, 2026
6 checks passed
@enaboapps
enaboapps deleted the claude/986-sync-engine branch October 5, 2026 15:00
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.

Settings sync: sync engine with revision conflict handling

2 participants