diff --git a/cmd/verstak-gui/app.go b/cmd/verstak-gui/app.go index 4c306d6..d384e53 100644 --- a/cmd/verstak-gui/app.go +++ b/cmd/verstak-gui/app.go @@ -14,6 +14,7 @@ import ( "verstak/internal/core/actions" "verstak/internal/core/activity" + "verstak/internal/core/config" "verstak/internal/core/files" "verstak/internal/core/notes" "verstak/internal/core/nodes" @@ -835,6 +836,93 @@ func (a *App) Search(query string) ([]SearchResultDTO, error) { return out, nil } +// ============================================================ +// Sync +// ============================================================ + +type SyncStatusDTO struct { + Configured bool `json:"configured"` + ServerURL string `json:"serverUrl"` + DeviceID string `json:"deviceId"` + UnpushedOps int `json:"unpushedOps"` + LastSyncAt string `json:"lastSyncAt"` +} + +func (a *App) SyncStatus() (*SyncStatusDTO, error) { + serverURL, apiKey, _, lastSyncAt, err := a.sync.GetState() + if err != nil { + return &SyncStatusDTO{}, nil + } + unpushed, _ := a.sync.GetUnpushedOps() + cfg, _ := config.Load(a.vault) + dto := &SyncStatusDTO{ + Configured: serverURL != "" && apiKey != "", + ServerURL: serverURL, + UnpushedOps: len(unpushed), + LastSyncAt: lastSyncAt, + } + if cfg != nil { + dto.DeviceID = cfg.Sync.DeviceID + } + return dto, nil +} + +func (a *App) SyncConfigure(serverURL, apiKey string) error { + if err := a.sync.SetState(serverURL, apiKey); err != nil { + return err + } + // Persist to vault config. + cfg, err := config.Load(a.vault) + if err != nil { + return err + } + cfg.Sync.ServerURL = serverURL + cfg.Sync.APIKey = apiKey + return config.Save(a.vault, cfg) +} + +func (a *App) SyncNow() (map[string]interface{}, error) { + serverURL, apiKey, lastRev, _, err := a.sync.GetState() + if err != nil || serverURL == "" || apiKey == "" { + return nil, fmt.Errorf("sync not configured") + } + + deviceID := "" + if cfg, err := config.Load(a.vault); err == nil { + deviceID = cfg.Sync.DeviceID + } + + client := syncsvc.NewClient(serverURL, apiKey, deviceID, a.vault) + + // Push unpushed ops. + unpushed, err := a.sync.GetUnpushedOps() + if err != nil { + return nil, fmt.Errorf("get ops: %w", err) + } + pushResult := &syncsvc.PushResponse{} + if len(unpushed) > 0 { + pushResult, err = client.Push(unpushed) + if err != nil { + return nil, fmt.Errorf("push: %w", err) + } + if err := a.sync.MarkPushed(pushResult.Accepted); err != nil { + return nil, fmt.Errorf("mark pushed: %w", err) + } + } + + // Pull remote ops. + pullResult, err := client.Pull(lastRev) + if err != nil { + return nil, fmt.Errorf("pull: %w", err) + } + + return map[string]interface{}{ + "pushed": len(pushResult.Accepted), + "pulled": len(pullResult.Ops), + "serverRevision": pullResult.ServerRevision, + }, nil +} + // ============================================================ // File Dialogs (Wails v2 Runtime) // ============================================================