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:
+52
-61
@@ -224,8 +224,8 @@ var syncUpCmd = &cobra.Command{
|
||||
client := sync.NewClient(cfg.SyncURL, cfg.SyncAPIKey, cfg.SyncClientID)
|
||||
|
||||
// Get local changes
|
||||
lastSync := getLastSyncTime(cfg.SyncClientID)
|
||||
localChanges, err := getLocalChanges(lastSync)
|
||||
lastSync := sync.GetLastSyncTime(cfg.SyncClientID)
|
||||
localChanges, err := sync.GetLocalChanges(lastSync)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting local changes: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -264,7 +264,7 @@ var syncDownCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
client := sync.NewClient(cfg.SyncURL, cfg.SyncAPIKey, cfg.SyncClientID)
|
||||
lastSync := getLastSyncTime(cfg.SyncClientID)
|
||||
lastSync := sync.GetLastSyncTime(cfg.SyncClientID)
|
||||
|
||||
changes, err := client.PullChanges(lastSync)
|
||||
if err != nil {
|
||||
@@ -277,7 +277,55 @@ var syncDownCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Pulled %d changes from server\n", len(changes))
|
||||
// Parse changes into tasks
|
||||
tasks, err := client.ParseChanges(changes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing changes: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Apply each task locally
|
||||
var applied int
|
||||
for _, task := range tasks {
|
||||
if err := task.Save(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to save task %s: %v\n", task.UUID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark as sync-originated to prevent feedback loop
|
||||
_ = engine.MarkChangeLogAsSync(task.UUID.String())
|
||||
|
||||
// Sync tags
|
||||
savedTask, err := engine.GetTask(task.UUID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to reload task %s: %v\n", task.UUID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
currentTags, _ := savedTask.GetTags()
|
||||
currentSet := make(map[string]bool)
|
||||
for _, tag := range currentTags {
|
||||
currentSet[tag] = true
|
||||
}
|
||||
desiredSet := make(map[string]bool)
|
||||
for _, tag := range task.Tags {
|
||||
desiredSet[tag] = true
|
||||
}
|
||||
for tag := range currentSet {
|
||||
if !desiredSet[tag] {
|
||||
savedTask.RemoveTag(tag)
|
||||
}
|
||||
}
|
||||
for tag := range desiredSet {
|
||||
if !currentSet[tag] {
|
||||
savedTask.AddTag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
applied++
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Pulled %d changes, applied %d tasks from server\n", len(changes), applied)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -414,63 +462,6 @@ func init() {
|
||||
syncCmd.PersistentFlags().BoolVarP(&quietFlag, "quiet", "q", false, "Suppress progress output")
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func getLastSyncTime(clientID string) int64 {
|
||||
db := engine.GetDB()
|
||||
if db == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var lastSync int64
|
||||
err := db.QueryRow("SELECT last_sync FROM sync_state WHERE client_id = ?", clientID).Scan(&lastSync)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return lastSync
|
||||
}
|
||||
|
||||
func getLocalChanges(since int64) ([]*engine.Task, error) {
|
||||
db := engine.GetDB()
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT DISTINCT task_uuid
|
||||
FROM change_log
|
||||
WHERE changed_at > ?
|
||||
ORDER BY changed_at ASC
|
||||
`, since)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tasks []*engine.Task
|
||||
for rows.Next() {
|
||||
var uuidStr string
|
||||
if err := rows.Scan(&uuidStr); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
taskUUID, err := uuid.Parse(uuidStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
task, err := engine.GetTask(taskUUID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func formatTimestamp(ts int64) string {
|
||||
t := time.Unix(ts, 0)
|
||||
now := time.Now()
|
||||
|
||||
Reference in New Issue
Block a user