Skip to content

Commit eb7fbea

Browse files
committed
feat(device-sync): same data on several Macs via a shared folder (v0.139.0)
Opt-in, default OFF. Each device publishes ONE encrypted file into a shared folder (iCloud Drive by default) and reads the others' on each cycle — no server, no ports, no account. Worker thread + interval + debounced wake, the same shape as the cue snippet sync. The rule that outranks the others — an empty state must never overwrite a populated one — rests on FOUR independent mechanisms, deliberately not on one: 1. Structurally additive. Applying goes through backup::apply, which only upserts and appends; this module has no delete path at all, so merging an empty document is a no-op BY CONSTRUCTION rather than by a check someone could forget. That is why deletions deliberately do not propagate. 2. Publish gate. The previous file is read back and counted before being replaced; empty-over-populated is refused, and an UNREADABLE previous file refuses too — we cannot prove it is worthless, so it fails closed. 3. Atomic write: tmp + fsync + rename, keeping the previous version as .bak. 4. All-or-nothing read: a file that fails to decrypt or parse is skipped whole, never applied in part. Two defects found by the tests, both invisible from reading: - backup::apply appends notes with NO dedup — right for a one-shot restore, but a repeating sync duplicated every note per cycle. - snippets::merge_version has no "local is newer" branch: on differing content the incoming side always wins with a local+1 bump. Correct for cue, which is the declared master; between equal peers it silently REVERTED a local edit and bumped the version so the revert looked authoritative. filter_snippets now decides itself, with a deterministic tie-break so both devices reach the same answer instead of ping-ponging. Settings and timesheet data are never synced — a peer could otherwise overwrite hotkeys, brightness, or this feature's own configuration. 24 tests; mechanisms 1, 2 and 4 plus both defects above are mutation-verified. Mechanism 3 is NOT: replacing the rename with a plain write leaves even the concurrent-reader test green on APFS, so atomicity rests on the POSIX rename contract and the test says so in its name.
1 parent 0fbcba7 commit eb7fbea

22 files changed

Lines changed: 1433 additions & 30 deletions

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to Inspector Rust are documented here.
44

55
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.139.0] — 2026-08-27
8+
9+
### Added
10+
11+
- **Geräte-Sync (opt-in, Standard AUS).** Mehrere Macs halten denselben Datenstand über einen **gemeinsamen Ordner** (Standard: iCloud Drive) — kein Server, keine Ports, kein Konto. Jedes Gerät legt EINE verschlüsselte Datei ab und liest die der anderen. Einstellungen → **Geräte-Sync**: Schalter, Ordner, Passwort (liegt im Schlüsselbund, wird nie mitsynchronisiert), 2FA-Opt-in, „Jetzt abgleichen".
12+
- Synchronisiert werden Verlauf, Snippets samt Gruppen, Notizen und — nur auf Wunsch — 2FA-Konten. **Einstellungen und Zeiterfassung bewusst nicht**: Hotkeys, Monitor-Helligkeit und die Sync-Konfiguration selbst sind gerätegebunden.
13+
14+
### Sicherheit
15+
16+
- **Ein leerer Stand kann einen gefüllten nie überschreiben.** Vier voneinander unabhängige Mechanismen: die Anwendung ist strukturell nur additiv (es gibt keinen Lösch-Pfad), das Veröffentlichen prüft den vorherigen Stand und verweigert leer-über-gefüllt, geschrieben wird atomar (tmp + rename, plus `.bak`), und eine unlesbare Datei wird ganz übersprungen statt halb angewandt.
17+
- **Löschungen wandern deshalb nicht mit** — die Geräte vereinigen ihre Daten. Bewusster Preis für die obige Garantie.
18+
719
## [0.138.2] — 2026-08-27
820

921
### Fixed

‎CLAUDE.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,22 @@ Auftrag (2026-08-26): dieselben Daten auf zwei oder mehr MacBooks. **Noch keine
15151515

15161516
**Pflicht-Tests laut Auftrag:** leere Gegenseite überschreibt nichts · Abbruch mitten in der Übertragung hinterlässt keinen beschädigten Stand · Konfliktfall wird wie entworfen aufgelöst · deaktivierter Schalter löst keinerlei Sync aus.
15171517

1518+
### Geräte-Sync (`device_sync.rs`, v0.139.0)
1519+
1520+
Mehrere Macs, ein Datenstand — über einen **gemeinsamen Ordner** (Default iCloud Drive), nicht über einen Server. Jedes Gerät veröffentlicht EINE Datei `ir-<device_id>.irsync` (sein eigener Voll-Export, passwortverschlüsselt über `backup::encrypt_backup`) und liest je Zyklus die der anderen. Aufbau 1:1 wie `sync.rs`: Worker-Thread, Intervall (120 s) + entprellter Weckruf, Config/Status in der `settings`-Tabelle. Passwort im **Schlüsselbund** (`device-sync-passphrase-v1`, gleicher Service wie `crypto.rs`) — pro Gerät eingegeben, nie mitsynchronisiert.
1521+
1522+
⚠️ **Die Regel „leer überschreibt nie gefüllt" ruht auf VIER unabhängigen Mechanismen**, absichtlich nicht auf einem: (1) **strukturell additiv** — angewandt wird über `backup::apply`, das nur upsertet und anhängt; in diesem Modul existiert kein Lösch-Pfad, ein leeres Dokument ist damit *per Konstruktion* ein No-op statt per Prüfung, die man vergessen kann. Genau deshalb wandern **Löschungen bewusst nicht mit** (die Geräte vereinigen ihre Daten); (2) **Publish-Gate** `publish_verdict` — vor dem Ersetzen der eigenen Datei wird die alte zurückgelesen und gezählt; leer-über-gefüllt wird verweigert, und eine **unlesbare** Vorgängerdatei verweigert ebenfalls (fail closed, weil sich ihre Wertlosigkeit nicht beweisen lässt); (3) **atomares Schreiben** tmp + fsync + `rename`, plus `.bak` der Vorversion; (4) **Ganz-oder-gar-nicht-Lesen** — eine Peer-Datei, die nicht entschlüsselt oder nicht parst, wird komplett übersprungen.
1523+
1524+
⚠️ **Zwei Fallen aus dem Bau, beide erst durch Tests sichtbar geworden:**
1525+
* **`backup::apply` hängt Notizen OHNE Dedup an** — für eine einmalige Wiederherstellung dokumentiert und richtig, in einem *wiederholten* Sync verdoppelt es jede Notiz pro Zyklus. `dedup_notes`/`note_key` filtern vorher.
1526+
* **`snippets::merge_version` hat keinen „lokal ist neuer"-Zweig** — bei abweichendem Inhalt gewinnt IMMER das Eingehende (mit `local+1`-Bump). Für cue korrekt (cue ist erklärter Master), zwischen gleichberechtigten Geräten ist es Datenverlust: eine veraltete Kopie hat eine lokale Bearbeitung still zurückgesetzt und die Version so hochgezählt, dass der Rücksetzer autoritativ aussah (live beobachtet). `filter_snippets` entscheidet deshalb selbst: höhere Version gewinnt, niedrigere wird verworfen, bei Gleichstand gewinnt der **lexikografisch größere Inhalt** — deterministisch, damit BEIDE Geräte dieselbe Antwort finden und es kein Ping-Pong gibt.
1527+
1528+
⚠️ **Nie synchronisiert: `settings` und Zeiterfassung.** Der Export läuft mit `include_settings: false`, und `apply_incoming` leert die Felder zusätzlich, was auch immer die Datei behauptet — ein Peer könnte sonst Hotkeys, Monitor-Helligkeit oder die Sync-Konfiguration selbst überschreiben.
1529+
1530+
⚠️ **Test-Ehrlichkeit:** die Atomarität (Mechanismus 3) ist auf APFS **nicht empirisch nachweisbar** — `rename` durch ein direktes `fs::write` zu ersetzen lässt auch den nebenläufigen Leser-Test grün (gemessen: 2-MB-Nutzlasten, 12 Wechsel). Der Test pinnt nur die beobachtbare Invariante; die Garantie ruht auf dem POSIX-`rename`-Vertrag. Die Mechanismen 1, 2 und 4 sowie beide Fallen oben sind mutationsgeprüft.
1531+
1532+
Dateien: `core/rust-lib/src/device_sync.rs` (neu, 24 Tests) · IPC in `commands.rs` (`get_/set_device_sync_config`, `get_device_sync_status`, `set_device_sync_passphrase`, `device_sync_now` — die drei mit Datei-/Argon2-Arbeit `async` + `spawn_blocking`) · Start in `lib.rs` neben `sync::start` · `backup::apply` ist dafür `pub(crate)` · UI `DeviceSyncSection` in `SettingsPanel.tsx` + Registry-Eintrag `device-sync`.
1533+
15181534
**`settings`-Command (Alias `config`, v0.87.1).** Öffnet den Settings-Tab per Texteingabe; ein Argument deep-linkt fuzzy (DE+EN Synonyme) zu einer Sektion — Registry = pure, unit-getestete `lib/settings-sections.ts` (`SETTINGS_SECTIONS` mit 21 Einträgen, `matchSettingsSection` via `fuzzyScore`). `dispatchCommand` setzt `settingsJump {id, nonce}` + `setActiveTab("settings")`; `SettingsPanel` bekommt `jumpTo`, scrollt (`scrollIntoView`, reduced-motion-aware) zum Anker `settings-<id>` (die `Section`-Komponente hat dafür eine optionale `id`-Prop) und flasht `.settings-jump-highlight` (styles.css, paint-only). Beim Hinzufügen einer neuen Settings-Sektion: `id` an der `<Section>` vergeben + Eintrag in `SETTINGS_SECTIONS`.
15191535

15201536
**Settings → Backup & restore (reworked v0.84.237).** Export always covers the whole app (the old per-section tickboxes are gone) + one opt-in **"Timesheet data"** checkbox; an **"Encrypt with password"** checkbox reveals password + repeat fields (show-toggle, match validation, no-recovery warning). Import picks a file, probes `is_backup_encrypted`, and for an encrypted file shows an **inline unlock row** (password field + "Unlock & import"; a wrong password keeps the row so the user corrects it without re-picking the file). The result banner shows all counts (history/snippets/notes/2FA/settings + timesheet events).

‎Cargo.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ resolver = "2"
33
members = ["core/rust-lib", "win/src-tauri", "macos/src-tauri", "linux/src-tauri"]
44

55
[workspace.package]
6-
version = "0.138.2"
6+
version = "0.139.0"
77
edition = "2021"
88
authors = ["Martin Pfeffer <martinpaush@gmail.com>"]
99
license = "MIT"

‎README.de.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,16 @@
6464

6565
### 🧰 Tech-Stack
6666

67-
Tauri 2 (WebView2 / WKWebView) · Rust-Workspace (`core/rust-lib` geteilt, 2-Zeilen-Per-OS-Bundle-Shells) · React 19 + TypeScript 5 + Tailwind v4 + Vite 7 · Helligkeit via CoreGraphics/GDI-Gamma + DDC/CI (`ddc-hi`). **3797 Unit-Tests (1406 Rust + 2391 Frontend).** MIT-lizenziert.
67+
Tauri 2 (WebView2 / WKWebView) · Rust-Workspace (`core/rust-lib` geteilt, 2-Zeilen-Per-OS-Bundle-Shells) · React 19 + TypeScript 5 + Tailwind v4 + Vite 7 · Helligkeit via CoreGraphics/GDI-Gamma + DDC/CI (`ddc-hi`). **3821 Unit-Tests (1430 Rust + 2391 Frontend).** MIT-lizenziert.
6868

6969
<!-- ── Headline-Kennzahlen — XXL Hero-Badges ─────────────────── -->
7070
<p>
7171
<a href="https://github.com/pepperonas/inspector-rust" title="Codezeilen (Rust + TypeScript Quellcode)">
72-
<img src="https://img.shields.io/badge/lines%20of%20code-~153k-2b3137?style=for-the-badge&logo=rust&logoColor=white" height="64" alt="Lines of code" />
72+
<img src="https://img.shields.io/badge/lines%20of%20code-~154k-2b3137?style=for-the-badge&logo=rust&logoColor=white" height="64" alt="Lines of code" />
7373
</a>
7474
&nbsp;
75-
<a href="https://github.com/pepperonas/inspector-rust/actions/workflows/ci.yml" title="Unit-Tests — 1406 Rust + 2391 Frontend, alle grün">
76-
<img src="https://img.shields.io/badge/unit%20tests-3797%20passing-2ea043?style=for-the-badge&logo=vitest&logoColor=white" height="64" alt="Unit tests" />
75+
<a href="https://github.com/pepperonas/inspector-rust/actions/workflows/ci.yml" title="Unit-Tests — 1430 Rust + 2391 Frontend, alle grün">
76+
<img src="https://img.shields.io/badge/unit%20tests-3821%20passing-2ea043?style=for-the-badge&logo=vitest&logoColor=white" height="64" alt="Unit tests" />
7777
</a>
7878
</p>
7979

@@ -95,7 +95,7 @@
9595
[![Issues](https://img.shields.io/github/issues/pepperonas/inspector-rust?style=flat-square)](https://github.com/pepperonas/inspector-rust/issues)
9696
[![Stars](https://img.shields.io/github/stars/pepperonas/inspector-rust?style=flat-square)](https://github.com/pepperonas/inspector-rust/stargazers)
9797
[![Maintenance](https://img.shields.io/badge/maintained-yes-brightgreen?style=flat-square)](https://github.com/pepperonas/inspector-rust/commits/main)
98-
[![Unit tests](https://img.shields.io/badge/unit%20tests-3797%20(1406%20Rust%20%2B%202391%20TS)-success?style=flat-square)](https://github.com/pepperonas/inspector-rust/actions/workflows/ci.yml)
98+
[![Unit tests](https://img.shields.io/badge/unit%20tests-3821%20(1430%20Rust%20%2B%202391%20TS)-success?style=flat-square)](https://github.com/pepperonas/inspector-rust/actions/workflows/ci.yml)
9999
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square)](./CONTRIBUTING.md)
100100
[![Code Style](https://img.shields.io/badge/code%20style-clippy%20%2B%20eslint-orange?style=flat-square)](./scripts/check.sh)
101101
[![Downloads](https://img.shields.io/github/downloads/pepperonas/inspector-rust/total?style=flat-square&label=downloads&color=8957e5)](https://github.com/pepperonas/inspector-rust/releases)
@@ -162,7 +162,7 @@
162162
<!-- ── Quality ─────────────────────────────────────────────── -->
163163
[![ESLint](https://img.shields.io/badge/ESLint-flat%20config-4B32C3?style=flat-square&logo=eslint&logoColor=white)](https://eslint.org)
164164
[![Vitest](https://img.shields.io/badge/Vitest-3-6E9F18?style=flat-square&logo=vitest&logoColor=white)](https://vitest.dev)
165-
[![cargo test](https://img.shields.io/badge/cargo%20test-1406%20passing-success?style=flat-square&logo=rust&logoColor=white)](#)
165+
[![cargo test](https://img.shields.io/badge/cargo%20test-1430%20passing-success?style=flat-square&logo=rust&logoColor=white)](#)
166166
[![vitest](https://img.shields.io/badge/vitest-2391%20passing-success?style=flat-square&logo=vitest&logoColor=white)](#)
167167
[![cargo clippy](https://img.shields.io/badge/cargo%20clippy-D%20warnings-success?style=flat-square&logo=rust&logoColor=white)](#)
168168
[![tsc strict](https://img.shields.io/badge/tsc-strict-3178C6?style=flat-square&logo=typescript&logoColor=white)](#)
@@ -196,7 +196,7 @@
196196
[![exe size](https://img.shields.io/badge/.exe-~14%20MB-blue?style=flat-square&logo=windows&logoColor=white)](#)
197197

198198
<!-- ── Features (numerical) ────────────────────────────────── -->
199-
[![Tests](https://img.shields.io/badge/tests-3797%20passing-success?style=flat-square)](#)
199+
[![Tests](https://img.shields.io/badge/tests-3821%20passing-success?style=flat-square)](#)
200200
[![IPC commands](https://img.shields.io/badge/IPC%20commands-284-blueviolet?style=flat-square)](./core/rust-lib/src/commands.rs)
201201
[![Search-bar commands](https://img.shields.io/badge/search--bar%20commands-74-blueviolet?style=flat-square)](./core/rust-lib/src/commands.rs)
202202
[![Tauri events](https://img.shields.io/badge/events-33-blueviolet?style=flat-square)](#)
@@ -931,7 +931,7 @@ Inspector Rust hält seine **pure Logik** — Parser, Mathematik, State-Machines
931931
932932
```bash
933933
pnpm test # Frontend-Unit-Tests (vitest + happy-dom) — 2391 Tests
934-
cargo test --workspace # Rust-Unit-Tests — 1406 Tests
934+
cargo test --workspace # Rust-Unit-Tests — 1430 Tests
935935
```
936936
937937
Ein einzelnes Modul während der Iteration:

‎README.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,16 @@
6464

6565
### 🧰 Tech stack
6666

67-
Tauri 2 (WebView2 / WKWebView) · Rust workspace (`core/rust-lib` shared, 2-line per-OS bundle shells) · React 19 + TypeScript 5 + Tailwind v4 + Vite 7 · brightness via CoreGraphics/GDI gamma + DDC/CI (`ddc-hi`). **3797 unit tests (1406 Rust + 2391 frontend).** MIT-licensed.
67+
Tauri 2 (WebView2 / WKWebView) · Rust workspace (`core/rust-lib` shared, 2-line per-OS bundle shells) · React 19 + TypeScript 5 + Tailwind v4 + Vite 7 · brightness via CoreGraphics/GDI gamma + DDC/CI (`ddc-hi`). **3821 unit tests (1430 Rust + 2391 frontend).** MIT-licensed.
6868

6969
<!-- ── Headline metrics — XXL hero badges ────────────────────── -->
7070
<p>
7171
<a href="https://github.com/pepperonas/inspector-rust" title="Lines of code (Rust + TypeScript source)">
72-
<img src="https://img.shields.io/badge/lines%20of%20code-~153k-2b3137?style=for-the-badge&logo=rust&logoColor=white" height="64" alt="Lines of code" />
72+
<img src="https://img.shields.io/badge/lines%20of%20code-~154k-2b3137?style=for-the-badge&logo=rust&logoColor=white" height="64" alt="Lines of code" />
7373
</a>
7474
&nbsp;
75-
<a href="https://github.com/pepperonas/inspector-rust/actions/workflows/ci.yml" title="Unit tests — 1406 Rust + 2391 frontend, all passing">
76-
<img src="https://img.shields.io/badge/unit%20tests-3797%20passing-2ea043?style=for-the-badge&logo=vitest&logoColor=white" height="64" alt="Unit tests" />
75+
<a href="https://github.com/pepperonas/inspector-rust/actions/workflows/ci.yml" title="Unit tests — 1430 Rust + 2391 frontend, all passing">
76+
<img src="https://img.shields.io/badge/unit%20tests-3821%20passing-2ea043?style=for-the-badge&logo=vitest&logoColor=white" height="64" alt="Unit tests" />
7777
</a>
7878
</p>
7979

@@ -95,7 +95,7 @@
9595
[![Issues](https://img.shields.io/github/issues/pepperonas/inspector-rust?style=flat-square)](https://github.com/pepperonas/inspector-rust/issues)
9696
[![Stars](https://img.shields.io/github/stars/pepperonas/inspector-rust?style=flat-square)](https://github.com/pepperonas/inspector-rust/stargazers)
9797
[![Maintenance](https://img.shields.io/badge/maintained-yes-brightgreen?style=flat-square)](https://github.com/pepperonas/inspector-rust/commits/main)
98-
[![Unit tests](https://img.shields.io/badge/unit%20tests-3797%20(1406%20Rust%20%2B%202391%20TS)-success?style=flat-square)](https://github.com/pepperonas/inspector-rust/actions/workflows/ci.yml)
98+
[![Unit tests](https://img.shields.io/badge/unit%20tests-3821%20(1430%20Rust%20%2B%202391%20TS)-success?style=flat-square)](https://github.com/pepperonas/inspector-rust/actions/workflows/ci.yml)
9999
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square)](./CONTRIBUTING.md)
100100
[![Code Style](https://img.shields.io/badge/code%20style-clippy%20%2B%20eslint-orange?style=flat-square)](./scripts/check.sh)
101101
[![Downloads](https://img.shields.io/github/downloads/pepperonas/inspector-rust/total?style=flat-square&label=downloads&color=8957e5)](https://github.com/pepperonas/inspector-rust/releases)
@@ -162,7 +162,7 @@
162162
<!-- ── Quality ─────────────────────────────────────────────── -->
163163
[![ESLint](https://img.shields.io/badge/ESLint-flat%20config-4B32C3?style=flat-square&logo=eslint&logoColor=white)](https://eslint.org)
164164
[![Vitest](https://img.shields.io/badge/Vitest-3-6E9F18?style=flat-square&logo=vitest&logoColor=white)](https://vitest.dev)
165-
[![cargo test](https://img.shields.io/badge/cargo%20test-1406%20passing-success?style=flat-square&logo=rust&logoColor=white)](#)
165+
[![cargo test](https://img.shields.io/badge/cargo%20test-1430%20passing-success?style=flat-square&logo=rust&logoColor=white)](#)
166166
[![vitest](https://img.shields.io/badge/vitest-2391%20passing-success?style=flat-square&logo=vitest&logoColor=white)](#)
167167
[![cargo clippy](https://img.shields.io/badge/cargo%20clippy-D%20warnings-success?style=flat-square&logo=rust&logoColor=white)](#)
168168
[![tsc strict](https://img.shields.io/badge/tsc-strict-3178C6?style=flat-square&logo=typescript&logoColor=white)](#)
@@ -269,7 +269,7 @@
269269
[![exe size](https://img.shields.io/badge/.exe-~14%20MB-blue?style=flat-square&logo=windows&logoColor=white)](#)
270270

271271
<!-- ── Features (numerical) ────────────────────────────────── -->
272-
[![Tests](https://img.shields.io/badge/tests-3797%20passing-success?style=flat-square)](#)
272+
[![Tests](https://img.shields.io/badge/tests-3821%20passing-success?style=flat-square)](#)
273273
[![IPC commands](https://img.shields.io/badge/IPC%20commands-284-blueviolet?style=flat-square)](./core/rust-lib/src/commands.rs)
274274
[![Search-bar commands](https://img.shields.io/badge/search--bar%20commands-74-blueviolet?style=flat-square)](./core/rust-lib/src/commands.rs)
275275
[![Tauri events](https://img.shields.io/badge/events-33-blueviolet?style=flat-square)](#)
@@ -933,7 +933,7 @@ Inspector Rust keeps its **pure logic** — parsers, math, state machines, arg-b
933933
934934
```bash
935935
pnpm test # frontend unit tests (vitest + happy-dom) — 2391 tests
936-
cargo test --workspace # Rust unit tests — 1406 tests
936+
cargo test --workspace # Rust unit tests — 1430 tests
937937
```
938938
939939
Iterate on one module:

‎core/frontend/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "inspector-rust-frontend",
33
"private": true,
4-
"version": "0.138.2",
4+
"version": "0.139.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

0 commit comments

Comments
 (0)