feat(federation): enhance bd federation status command (bd-wkumz.5)

Improve the federation status command to show more comprehensive
information similar to `git remote -v` with health info:

- Show peer URLs alongside peer names
- Display pending local changes count (uncommitted)
- Test connectivity to each peer (via fetch)
- Track and display last sync time in metadata table
- Show reachability status with error messages on failure

Changes:
- cmd/bd/federation.go: Enhanced status output with URLs, connectivity
  checks, pending changes, and last sync time
- internal/storage/dolt/federation.go: Added getLastSyncTime/setLastSyncTime
  methods using metadata table, record sync time on successful sync

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/collins
2026-01-20 21:26:30 -08:00
committed by Steve Yegge
parent b7242a67d1
commit c3f68caf7a
2 changed files with 107 additions and 15 deletions

View File

@@ -241,52 +241,117 @@ func runFederationStatus(cmd *cobra.Command, args []string) {
FatalErrorRespectJSON("%v", err)
}
// Get all remotes for URL lookup
allRemotes, err := ds.ListRemotes(ctx)
if err != nil {
FatalErrorRespectJSON("failed to list remotes: %v", err)
}
remoteURLs := make(map[string]string)
for _, r := range allRemotes {
remoteURLs[r.Name] = r.URL
}
// Get peers to check
var peers []string
if federationPeer != "" {
peers = []string{federationPeer}
} else {
remotes, err := ds.ListRemotes(ctx)
if err != nil {
FatalErrorRespectJSON("failed to list peers: %v", err)
}
for _, r := range remotes {
for _, r := range allRemotes {
peers = append(peers, r.Name)
}
}
if len(peers) == 0 {
if jsonOutput {
outputJSON(map[string]interface{}{"peers": []string{}})
outputJSON(map[string]interface{}{
"peers": []string{},
"pendingChanges": 0,
})
} else {
fmt.Println("No federation peers configured.")
}
return
}
var statuses []*storage.SyncStatus
// Get pending local changes
doltStatus, _ := ds.Status(ctx)
pendingChanges := 0
if doltStatus != nil {
pendingChanges = len(doltStatus.Staged) + len(doltStatus.Unstaged)
}
// Collect status for each peer
type peerStatus struct {
Status *storage.SyncStatus
URL string
Reachable bool
ReachError string
}
var peerStatuses []peerStatus
for _, peer := range peers {
ps := peerStatus{
URL: remoteURLs[peer],
}
// Get sync status
status, _ := ds.SyncStatus(ctx, peer)
statuses = append(statuses, status)
ps.Status = status
// Test connectivity by attempting a fetch
fetchErr := ds.Fetch(ctx, peer)
if fetchErr == nil {
ps.Reachable = true
// Re-get status after successful fetch for accurate ahead/behind
status, _ = ds.SyncStatus(ctx, peer)
ps.Status = status
} else {
ps.ReachError = fetchErr.Error()
}
peerStatuses = append(peerStatuses, ps)
}
if jsonOutput {
outputJSON(map[string]interface{}{
"peers": peers,
"statuses": statuses,
"peers": peerStatuses,
"pendingChanges": pendingChanges,
})
return
}
fmt.Printf("\n%s Federation Status:\n\n", ui.RenderAccent("🌐"))
for _, status := range statuses {
fmt.Printf(" %s\n", ui.RenderAccent(status.Peer))
// Show local pending changes
if pendingChanges > 0 {
fmt.Printf(" %s %d pending local changes\n\n", ui.RenderWarn("⚠"), pendingChanges)
}
for _, ps := range peerStatuses {
status := ps.Status
fmt.Printf(" %s %s\n", ui.RenderAccent(status.Peer), ui.RenderMuted(ps.URL))
// Connectivity status
if ps.Reachable {
fmt.Printf(" %s Reachable\n", ui.RenderPass("✓"))
} else {
fmt.Printf(" %s Unreachable: %s\n", ui.RenderFail("✗"), ps.ReachError)
}
// Sync status
if status.LocalAhead >= 0 {
fmt.Printf(" Ahead: %d commits\n", status.LocalAhead)
fmt.Printf(" Behind: %d commits\n", status.LocalBehind)
} else {
fmt.Printf(" Status: not fetched yet\n")
fmt.Printf(" Sync: %s\n", ui.RenderMuted("not fetched yet"))
}
// Last sync time
if !status.LastSync.IsZero() {
fmt.Printf(" Last sync: %s\n", status.LastSync.Format("2006-01-02 15:04:05"))
}
// Conflicts
if status.HasConflicts {
fmt.Printf(" %s Unresolved conflicts\n", ui.RenderWarn("⚠"))
}

View File

@@ -118,12 +118,36 @@ func (s *DoltStore) SyncStatus(ctx context.Context, peer string) (*storage.SyncS
status.HasConflicts = true
}
// TODO: Track last sync time in metadata
status.LastSync = time.Time{} // Zero time indicates never synced
// Get last sync time from metadata
status.LastSync = s.getLastSyncTime(ctx, peer)
return status, nil
}
// getLastSyncTime retrieves the last sync time for a peer from metadata.
func (s *DoltStore) getLastSyncTime(ctx context.Context, peer string) time.Time {
key := "last_sync_" + peer
var value string
err := s.db.QueryRowContext(ctx, "SELECT value FROM metadata WHERE `key` = ?", key).Scan(&value)
if err != nil {
return time.Time{}
}
t, err := time.Parse(time.RFC3339, value)
if err != nil {
return time.Time{}
}
return t
}
// setLastSyncTime records the last sync time for a peer in metadata.
func (s *DoltStore) setLastSyncTime(ctx context.Context, peer string) error {
key := "last_sync_" + peer
value := time.Now().Format(time.RFC3339)
_, err := s.db.ExecContext(ctx,
"REPLACE INTO metadata (`key`, value) VALUES (?, ?)", key, value)
return err
}
// Sync performs a full bidirectional sync with a peer:
// 1. Fetch from peer
// 2. Merge peer's changes (handling conflicts per strategy)
@@ -195,6 +219,9 @@ func (s *DoltStore) Sync(ctx context.Context, peer string, strategy string) (*Sy
result.Pushed = true
}
// Record last sync time
_ = s.setLastSyncTime(ctx, peer)
result.EndTime = time.Now()
return result, nil
}