fix: break sync feedback loop, respect timestamps, surface errors

- Add migration v2: source column on change_log to distinguish local
  vs sync-originated entries, preventing the echo loop where synced
  tasks get re-pushed as local changes
- PushChanges handler now skips save when server version is newer
- Client PushChanges/pushQueuedChanges collect and report marshal errors
  instead of silently dropping them
- De-duplicate getLocalChanges/getLastSyncTime into exported sync
  package functions
- Fix logConflict winner detection via pointer identity instead of
  fragile UUID+timestamp comparison
- Fix sync down to actually parse, save, and tag-sync pulled changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 01:11:04 +01:00
parent 0e3750e755
commit a11f452d3b
5 changed files with 151 additions and 87 deletions
+7 -2
View File
@@ -104,6 +104,8 @@ func PushChanges(w http.ResponseWriter, r *http.Request) {
if err := task.Save(); err != nil {
continue
}
// Mark as sync-originated to prevent feedback loop
_ = engine.MarkChangeLogAsSync(task.UUID.String())
// Add tags
for _, tag := range task.Tags {
_ = task.AddTag(tag)
@@ -114,15 +116,18 @@ func PushChanges(w http.ResponseWriter, r *http.Request) {
// Task exists - check timestamps for conflicts
if existing.Modified.Unix() > task.Modified.Unix() {
// Server version is newer - conflict (but we'll apply last-write-wins)
// Server version is newer - skip this push
conflicts++
continue
}
// Apply changes (last-write-wins)
// Apply changes (client is newer or equal)
task.ID = existing.ID // Preserve database ID
if err := task.Save(); err != nil {
continue
}
// Mark as sync-originated to prevent feedback loop
_ = engine.MarkChangeLogAsSync(task.UUID.String())
// Sync tags
existingTags := make(map[string]bool)