Skip to content

Implement adopt-not-replace sync with remote .md file tracking#59

Merged
TheZupZup merged 1 commit into
mainfrom
claude/fix-webdav-sync-sJ19T
May 3, 2026
Merged

Implement adopt-not-replace sync with remote .md file tracking#59
TheZupZup merged 1 commit into
mainfrom
claude/fix-webdav-sync-sJ19T

Conversation

@TheZupZup
Copy link
Copy Markdown
Owner

Summary

Overhaul the sync engine to adopt remote notes into the local database instead of replacing it wholesale. This enables idempotent pulls, preserves local-only work, and tracks remote .md files by their canonical server id to avoid duplicates on re-sync.

Key Changes

  • Adopt-not-replace pull strategy: Remote notebooks and notes are now upserted in place rather than wiping the local DB. Running pullRemote() multiple times yields the same row set (idempotent).

  • Remote id tracking: Added remote_id and remote_path columns to the notes table (schema v1 → v2) so adopted notes can be matched on subsequent pulls without creating duplicates. Plain Markdown files with synthetic md.<base64> ids are decoded to recover their canonical path.

  • Push guard for adopted notes: pushLocal() now skips any note that already carries a remoteId, preventing duplicate creation via the non-idempotent POST /notes endpoint.

  • Conflict detection: When a remote note arrives with the same cleaned title as an unrelated local row (different id), both are kept and the local row is marked sync_status='conflict'.

  • Timestamp-based merge: When a remote note matches a local one by id, the side with the newer updatedAt wins—except local modified rows beat older remotes to preserve in-flight edits.

  • Title cleanup: Extracted cleanRemoteTitle() into a dedicated module to strip .md extensions and WebDAV slug suffixes (__<id-prefix>) that leak into note titles from the filesystem.

  • Selective deletion: Synced local notes that disappear from the remote are removed (server-side delete propagates), but local-only and modified notes survive a pull.

  • Database migration: Added Schema.onUpgrade() to safely add the new columns when upgrading from v1 to v2.

Implementation Details

  • SyncResult now includes notesAdopted count to distinguish newly-adopted remote notes from those already in the local DB.
  • _PullCounts and _PushCounts separate the return types for clarity.
  • LocalNoteService exposes new upsert and hard-delete methods (upsertNote, upsertNotebook, hardDeleteNote, hardDeleteNotebook) used by the sync engine.
  • NoteRepository.getNoteByRemoteId() enables lookup of locally-adopted notes by their remote id.
  • Comprehensive test coverage added for adoption, conflict detection, title cleanup, and idempotent re-sync scenarios.

https://claude.ai/code/session_01T83ptxrcqCCk9Yx7WerGUd

Treat the WebDAV/NAS Markdown files as the canonical note representation
and merge them into the local DB instead of wiping local rows on every
pull. Each adopted note now carries the server-side identity (`remote_id`
plus `remote_path` derived from synthetic `md.<base64>` ids) so repeated
syncs upsert in place rather than minting duplicates. Push side respects
the adopted identity and never POSTs a note that already has a remote_id.
Conflict policy keeps both rows when titles collide on different ids and
prefers the newer `updated_at` on identity matches. Display titles are
stripped of `.md` and `__<id-prefix>` slug artifacts at the API ingestion
boundary so the corrupted `__Md.Q2Hhd` form never reaches the UI.

https://claude.ai/code/session_01T83ptxrcqCCk9Yx7WerGUd
@TheZupZup TheZupZup merged commit 7beac5c into main May 3, 2026
1 check passed
@TheZupZup TheZupZup deleted the claude/fix-webdav-sync-sJ19T branch May 3, 2026 17:19
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

if (_isAlreadySynced(nb.syncStatus)) continue;
await _api.createNotebook(name: nb.name, color: nb.color);
notebooks++;

P1 Badge Record remote notebook IDs after creating local notebooks

After createNotebook succeeds, the local notebook is left unchanged (sync_status stays non-synced and no server ID mapping is stored), but pullRemote now uses adopt-not-replace semantics. In a normal sync loop, that means the same local notebook is posted again on every subsequent sync(), and each pull adds another remote copy instead of reconciling the original. This regression is introduced by the new merge strategy because the old replace-all pull used to wipe the unsynced local row.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +92 to 97
if (note.remoteId != null && note.remoteId!.isNotEmpty) continue;
await _api.createNote(
title: note.title,
title: cleanRemoteTitle(note.title),
noteType: note.noteType,
notebookId: note.notebookId,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist pushed-note linkage to avoid re-posting duplicates

pushLocal posts unsynced notes but ignores the created remote note metadata, so the local row keeps no remoteId and remains non-synced. With adopt-not-replace pull, the returned server note is treated as a different record (often causing a conflict-marked local copy plus adopted remote copy), and the original local note is posted again on the next sync. This creates an unbounded duplicate cycle for newly created local notes.

Useful? React with 👍 / 👎.

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