Skip to content

feat(sync): per-domain settings sync control - #160

Merged
kipavy merged 12 commits into
devfrom
feat/settings-sync-domains
Aug 22, 2026
Merged

feat(sync): per-domain settings sync control#160
kipavy merged 12 commits into
devfrom
feat/settings-sync-domains

Conversation

@kipavy

@kipavy kipavy commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Part 1 of #42. Adds per-domain cloud-sync control: a user can switch sync off for Themes, Interface, Shortcuts, App settings or Recent people, and the held-back domain never enters the uploaded blob.

Per-setting (leaf-level) control is PR 2 and is not in this change.

What changed

The guarantee is enforced at write time. settings.json is both the local merge base and part of the encrypted blob (backup_export slurps every *.json in the config dir), so a switched-off domain has to be absent from that file, not merely ignored on arrival. push() now writes filterOutgoing(buildUserDataBundle()) before the exporter reads it, and filterIncoming drops switched-off sections from a remote bundle before the merge.

The duplicate theme wire is retired. Themes previously travelled twice: the themes bundle section and the whole theme.json file, which the exporter uploaded wholesale and applyRemoteTheme applied on pull. The second wire would have defeated the toggle entirely. It is gone, and backup_export gained a generic skip_files parameter (mirroring the existing excluded_ids) to withhold named config files.

That deletion is only safe because themesHandler was first widened to carry the six fields only theme.json held — mode, lightThemeId, darkThemeId, both schedule times, and location. Verified on the live app: without that, auto light/dark and the scheduled switch would have silently stopped syncing.

Re-enabling publishes this device. Each handler gained touch(). Switching a domain back on stamps it as changed now, so a value curated locally while sync was off is not lost to the other device's newer timestamp. Switching a domain off also schedules a push, so the copy already on the server is withdrawn rather than sitting there indefinitely.

Settings → Sync gains a second toggle group. vaults is deliberately absent — it is tombstone-merged data, not a preference, and switching it off would strand deletes.

Verification

Live, on a real decrypted blob (headless build, WebDriver), run twice — once mid-branch and again at final HEAD:

themes ON themes OFF
theme.json in blob false false
themes section in blob true false
activeThemeId "voltius-light" null
mode (automation) "manual" null

With themes off, only that section disappears — the other five survive. Plugin destinations skip [] when themes are on and ["theme.json"] when off.

Suites: 490 files / 3793 tests green; tsc --noEmit clean; cargo test commands::sync 7/7.

Behaviour changes worth knowing

  • Importing a settings bundle with Themes selected now replaces this device's custom themes rather than merging them, and publishes that deletion to your other devices. This is what makes a theme deletion propagate at all. The import UI now warns when the bundle contains a themes section.
  • mergeUserDataBundle reads live stores when a section is missing from the local merge base. Without it, re-enabling a domain let a stale remote win unconditionally — lastWriteWins never compared timestamps because the local side was absent. Handlers' export() must stay side-effect free; that is now stated on the interface.
  • Server and plugin destinations differ deliberately. The server always withholds theme.json (themes travel in the bundle); plugin/gist destinations withhold it only when themes are off, because that path applies theme.json directly and never applies the settings bundle. Deleting its wire would have silently ended theme sync for gist users.

Needs a human

Russian strings are machine-written and want a native check — раскладки for "layouts" reads as keyboard layout in common usage, which collides with the separate Shortcuts domain (макет/компоновка suggested), and Недавние люди is a stiff calque.

Cross-version

An older client pulling a new blob finds no theme.json, keeps its local theme, and still receives themes through the bundle section — present since v0.1.40. A newer client pulling an older blob ignores the theme.json still in it.

kipavy added 12 commits August 20, 2026 21:35
Collapses the three copied json-collection loops into one helper and gives
it a skip set, so the TypeScript side can withhold a config file from the
uploaded blob. Refs #42
…omment

collect_json_dir_skip_matches_the_prefixed_key_not_the_bare_filename proves
skip matching is on prefix+filename, not the bare name, in both directions.
Also restores the client-side/hash-verified note dropped when the three
collection loops were consolidated.
mode, the light/dark pair, both schedule times and location reached other
devices only via theme.json. Refs #42
Re-enabling a domain must publish this device's values instead of losing the
next merge to an old local timestamp. Refs #42
…wire

Themes now travel only in the settings bundle: applyRemoteTheme is gone and
theme.json is withheld from the blob, so the themes toggle actually holds.
settings.json is written already-filtered, since that file is what
backup_export uploads. Refs #42
- themes.ts: replace customThemes on import instead of upserting, so a
  remote deletion actually propagates instead of resurrecting on the
  next export.
- sync.ts: split theme.json withholding into a server variant (always
  withheld — themes travel in the settings bundle) and a plugin variant
  (withheld only when the themes domain is off — it's the plugin path's
  only theme route via runtime.ts importStates).
- sync.ts/runtime.ts: extract writeFilteredSettings() and call it from
  both push() and the plugin exportState path, so exportState no longer
  uploads a stale, unfiltered settings.json (same class as issue #47).
- registry.ts: mergeUserDataBundle falls back to a handler's live
  export()/getTimestamp() when its section is absent from the local
  bundle, so re-enabling a domain runs a real LWW comparison instead of
  letting a stale remote win unconditionally.

Refs #42
Collapses the section's four copied group blocks onto the shared
SettingsGroup and extracts the repeated toggle row. Also corrects the sync
preferences footer, which still described pre-#43 behaviour. Refs #42
Final review fix wave for per-setting cloud sync toggles:
- schedule a push when a domain is switched off, so the server copy is
  withdrawn instead of held forever (the "never uploaded" guarantee was
  false for data already synced before the toggle)
- gate the plugin import path's theme apply on the themes domain toggle,
  matching the outbound filter
- stop swallowing settings_save failures in writeFilteredSettings, since a
  hidden failure meant backup_export uploaded the unfiltered pre-toggle file
- warn in the manual-import UI that including Themes replaces this device's
  custom themes (import now REPLACES, not merges)
- add a drift test tying SYNC_SETTING_DOMAINS to USER_DATA_HANDLERS so a new
  handler can't silently end up permanently synced
- doc/comment cleanup: stale getSkippedSyncFiles docstring, the
  filterOutgoing/filterIncoming duplication, buildUserDataBundle's
  unfiltered-by-design contract, and export() side-effect-free requirement
It is a consequence notice, not a failed import; error-red read as
"something is broken". Refs #42
@kipavy
kipavy merged commit 20581c2 into dev Aug 22, 2026
4 checks passed
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.

1 participant