feat: Add visual progress indicators to sync operations
- Implement ProgressReporter interface with InteractiveProgress and NoOpProgress - Add real-time progress bars using go-pretty/progress library - Track 6 sync phases: connection test, queue push, pull, parse, apply, and server push - Add --quiet flag to suppress progress output - Auto-detect TTY to disable progress when piped/redirected - Show task-level progress during apply phase with descriptions - Display percentage complete and elapsed time for each phase
This commit is contained in:
@@ -152,17 +152,24 @@ type ChangeLogEntry struct {
|
||||
}
|
||||
|
||||
// Sync performs a full bidirectional sync
|
||||
func (c *Client) Sync(strategy ConflictResolution) (*SyncResult, error) {
|
||||
func (c *Client) Sync(strategy ConflictResolution, reporter ProgressReporter) (*SyncResult, error) {
|
||||
result := &SyncResult{}
|
||||
|
||||
// Use NoOp progress if none provided
|
||||
if reporter == nil {
|
||||
reporter = &NoOpProgress{}
|
||||
}
|
||||
|
||||
// Check if we have a queue with pending changes
|
||||
queue, err := NewQueue()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load queue: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
// Phase 1: Test connection
|
||||
reporter.StartPhase("Testing connection", 1)
|
||||
if !c.testConnection() {
|
||||
reporter.CompletePhase()
|
||||
// Server offline - queue changes if any
|
||||
cfg, _ := engine.GetConfig()
|
||||
if cfg != nil && cfg.SyncQueueOffline {
|
||||
@@ -177,28 +184,46 @@ func (c *Client) Sync(strategy ConflictResolution) (*SyncResult, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("server unreachable")
|
||||
}
|
||||
reporter.UpdatePhase(1, "Connected")
|
||||
reporter.CompletePhase()
|
||||
|
||||
// Server is online - process queue first if it has items
|
||||
// Phase 2: Process queued changes if any
|
||||
if queue.Size() > 0 {
|
||||
reporter.StartPhase("Pushing queued changes", int64(queue.Size()))
|
||||
if err := c.pushQueuedChanges(queue.GetPending()); err != nil {
|
||||
reporter.CompletePhase()
|
||||
return nil, fmt.Errorf("failed to push queued changes: %w", err)
|
||||
}
|
||||
result.Pushed = queue.Size()
|
||||
reporter.UpdatePhase(int64(queue.Size()), fmt.Sprintf("%d changes pushed", queue.Size()))
|
||||
reporter.CompletePhase()
|
||||
queue.Clear()
|
||||
}
|
||||
|
||||
// Get last sync time
|
||||
lastSync := c.getLastSyncTime()
|
||||
|
||||
// Pull changes from server
|
||||
// Phase 3: Pull changes from server
|
||||
reporter.StartPhase("Pulling from server", 1)
|
||||
changes, err := c.PullChanges(lastSync)
|
||||
if err != nil {
|
||||
reporter.CompletePhase()
|
||||
return nil, fmt.Errorf("failed to pull changes: %w", err)
|
||||
}
|
||||
reporter.UpdatePhase(1, fmt.Sprintf("Received %d changes", len(changes)))
|
||||
reporter.CompletePhase()
|
||||
|
||||
// Phase 4: Parse and sort changes
|
||||
if len(changes) > 0 {
|
||||
reporter.StartPhase("Processing changes", int64(len(changes)))
|
||||
}
|
||||
|
||||
// Convert changes to tasks
|
||||
remoteTasks, err := c.parseChanges(changes)
|
||||
if err != nil {
|
||||
if len(changes) > 0 {
|
||||
reporter.CompletePhase()
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse changes: %w", err)
|
||||
}
|
||||
|
||||
@@ -217,6 +242,11 @@ func (c *Client) Sync(strategy ConflictResolution) (*SyncResult, error) {
|
||||
return false // maintain original order
|
||||
})
|
||||
|
||||
if len(changes) > 0 {
|
||||
reporter.UpdatePhase(int64(len(changes)), fmt.Sprintf("Parsed %d tasks", len(remoteTasks)))
|
||||
reporter.CompletePhase()
|
||||
}
|
||||
|
||||
result.Pulled = len(remoteTasks)
|
||||
|
||||
// Get local changes since last sync
|
||||
@@ -233,8 +263,21 @@ func (c *Client) Sync(strategy ConflictResolution) (*SyncResult, error) {
|
||||
|
||||
result.ConflictsResolved = conflicts
|
||||
|
||||
// Apply merged tasks locally
|
||||
for _, task := range merged {
|
||||
// Phase 5: Apply merged tasks locally
|
||||
if len(merged) > 0 {
|
||||
reporter.StartPhase("Applying changes locally", int64(len(merged)))
|
||||
}
|
||||
|
||||
for i, task := range merged {
|
||||
// Update progress every task or every 10% for large syncs
|
||||
if len(merged) < 20 || i%(len(merged)/10+1) == 0 {
|
||||
desc := task.Description
|
||||
if len(desc) > 40 {
|
||||
desc = desc[:37] + "..."
|
||||
}
|
||||
reporter.UpdatePhase(int64(i+1), desc)
|
||||
}
|
||||
|
||||
if err := task.Save(); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("failed to save task %s: %v", task.UUID, err))
|
||||
continue
|
||||
@@ -276,18 +319,29 @@ func (c *Client) Sync(strategy ConflictResolution) (*SyncResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Push local changes to server
|
||||
if len(merged) > 0 {
|
||||
reporter.CompletePhase()
|
||||
}
|
||||
|
||||
// Phase 6: Push local changes to server
|
||||
if len(localChanges) > 0 {
|
||||
reporter.StartPhase("Pushing to server", int64(len(localChanges)))
|
||||
if err := c.PushChanges(localChanges); err != nil {
|
||||
reporter.CompletePhase()
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("failed to push changes: %v", err))
|
||||
} else {
|
||||
result.Pushed += len(localChanges)
|
||||
reporter.UpdatePhase(int64(len(localChanges)), fmt.Sprintf("%d changes pushed", len(localChanges)))
|
||||
reporter.CompletePhase()
|
||||
}
|
||||
}
|
||||
|
||||
// Update last sync time
|
||||
c.updateLastSyncTime(time.Now().Unix())
|
||||
|
||||
// Complete progress
|
||||
reporter.Done()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user