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
+6 -7
View File
@@ -57,7 +57,11 @@ func MergeTasks(local, remote []*engine.Task, strategy ConflictResolution) ([]*e
if DetectConflict(task, remoteTask) {
conflicts++
winner := resolveConflict(task, remoteTask, strategy)
logConflict(task, remoteTask, winner)
winnerLabel := "local"
if winner == remoteTask {
winnerLabel = "remote"
}
logConflict(task, remoteTask, winnerLabel)
result = append(result, winner)
} else {
// No conflict - use either (same content)
@@ -110,17 +114,12 @@ func resolveConflict(local, remote *engine.Task, strategy ConflictResolution) *e
}
// logConflict writes conflict information to log file
func logConflict(local, remote *engine.Task, winner *engine.Task) {
func logConflict(local, remote *engine.Task, winnerLabel string) {
logPath, err := engine.GetSyncConflictLogPath()
if err != nil {
return
}
winnerLabel := "local"
if winner.UUID == remote.UUID && winner.Modified.Equal(remote.Modified) {
winnerLabel = "remote"
}
entry := fmt.Sprintf(
"[%s] Conflict on task %s\n"+
" Local: modified %s - %s\n"+