Skip to content

feat: improve sync reliability and diagnostics#61

Merged
TheZupZup merged 1 commit into
mainfrom
claude/busy-darwin-nU3yL
May 22, 2026
Merged

feat: improve sync reliability and diagnostics#61
TheZupZup merged 1 commit into
mainfrom
claude/busy-darwin-nU3yL

Conversation

@TheZupZup
Copy link
Copy Markdown
Owner

Adds a reliability layer around NexaNote sync.

Includes:

  • sync planning
  • dry-run support
  • sanitized sync logs
  • better conflict visibility
  • tests for safe/idempotent sync behavior

This does not change the file-based storage layout or hardcode any deployment details.

What changed

Sync planningnexanote/sync/plan.py

  • A SyncPlan records the intent of a session: notes_to_push, notes_to_pull, notes_to_ignore, conflicts, warnings. It carries only metadata (note ids, titles, remote paths, reasons) — never note body — and is the single source the dry-run mode and the log read from.

Dry-run modenexanote/sync/client.py

  • NexaNoteSyncEngine(db, config, dry_run=True) (or POST /sync/trigger?dry_run=true) builds the plan but writes nothing: no note files, no .nexanote_sync_state.json, no remote PUTs, no log. Every state mutation is funneled through small _apply_* guards that no-op in dry-run, so the "writes nothing" guarantee is easy to audit.

Sanitized sync lognexanote/sync/sync_log.py

  • Each real sync writes <data_dir>/sync_logs/latest.json (resolves to /data/sync_logs/latest.json in the Docker image — derived from the configured data dir, not hardcoded), exposed via GET /sync/log.
  • Contains: timestamp, duration, counts, pushed/pulled note ids+titles, ignored legacy remote paths, conflicts, sanitized errors.
  • Excludes by construction: note body content, passwords, tokens. Error strings are additionally scrubbed of URLs (which embed the server host) and key=value credential pairs.

Conflict safetynexanote/sync/client.py

  • When a note changed both locally and remotely, the conflict is detected and surfaced in the plan/log instead of being resolved silently. If the chosen strategy would otherwise drop the local edits, a (conflit …) copy is preserved so both versions survive on disk. Existing conflict-resolution behavior (and conflicts_resolved) is unchanged for the KEEP_BOTH path.

Idempotent state

  • A failed sync still persists the sync-state registry atomically (tmp + os.replace), so a mid-session crash cannot corrupt .nexanote_sync_state.json.

Constraints honored

  • Did not rewrite the sync engine — changes thread a plan + dry-run guards through the existing _pull/_push flow.
  • File-based storage layout unchanged.
  • No domain, NAS IP, or Cloudflare details hardcoded.
  • No note body, passwords, tokens, or credentials in logs.

Test plan

  • tests/test_sync_reliability.py — 13 new tests covering: dry-run writes no files/state, sync log written after sync, log excludes secrets/body, ignored legacy files appear in diagnostics, conflict detected (not silently overwritten) + local preserved, failed sync keeps state valid, plus URL/secret sanitization units.
  • Full suite green: 292 passed (279 pre-existing + 13 new).

Generated by Claude Code

Adds a reliability layer around NexaNote sync without changing the
file-based storage layout or hardcoding any deployment details.

- SyncPlan records intent (push/pull/ignore/conflicts/warnings)
- dry-run mode builds the plan but writes nothing (no files, state,
  remote uploads, or log)
- sanitized <data_dir>/sync_logs/latest.json log via GET /sync/log;
  never includes note body, passwords, tokens, or server URLs
- conflict safety: both-changed notes are detected and surfaced
  instead of silently overwritten; local edits are preserved
- failed syncs persist sync state atomically (no corruption)
- tests for dry-run, log writing, sanitization, ignored-file
  diagnostics, conflict detection, and idempotent state

https://claude.ai/code/session_01G4o3UeXj3sJNZrW9PxGNG3
@TheZupZup TheZupZup marked this pull request as ready for review May 22, 2026 15:40
@TheZupZup TheZupZup merged commit 88545e6 into main May 22, 2026
1 check passed
@TheZupZup TheZupZup deleted the claude/busy-darwin-nU3yL branch May 22, 2026 15:40
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

Here are some automated review suggestions for this pull request.

Reviewed commit: 4cc5aade4b

ℹ️ 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 thread nexanote/sync/sync_log.py
# these, but a sync log is a durable artifact — belt and braces.
# FR: Filet de sécurité pour toute paire ``secret=valeur`` dans un message.
_KV_SECRET_RE = re.compile(
r"(?i)\b(password|passwd|pwd|token|secret|api[-_]?key|authorization|auth|credentials?)\s*[=:]\s*\S+"
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 Redact full Authorization header values

Update _KV_SECRET_RE so it removes the entire credential value, not just the first token. As written, sanitize_error("Authorization: Bearer abc.def.ghi") becomes Authorization=<redacted> abc.def.ghi, which still leaks the bearer token into sync_logs/latest.json and /sync/log. This affects any error message that uses multi-part auth formats (for example Bearer <token>), so sensitive credentials can still be exposed in persisted diagnostics.

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