verstak/cmd/verstak-gui/bindings_sync.go

215 lines
5.6 KiB
Go

package main
import (
"fmt"
"log"
"os"
"time"
"verstak/internal/core/config"
syncsvc "verstak/internal/core/sync"
)
type SyncStatusDTO struct {
Configured bool `json:"configured"`
ServerURL string `json:"serverUrl"`
DeviceID string `json:"deviceId"`
DeviceName string `json:"deviceName"`
Connected bool `json:"connected"`
Revoked bool `json:"revoked"`
TokenStored bool `json:"tokenStored"`
UnpushedOps int `json:"unpushedOps"`
LastSyncAt string `json:"lastSyncAt"`
SyncInterval int `json:"syncInterval"`
}
func (a *App) SyncStatus() (*SyncStatusDTO, error) {
serverURL, apiKey, _, lastSyncAt, err := a.sync.GetState()
if err != nil {
return &SyncStatusDTO{}, nil
}
cfg, _ := config.Load(a.vault)
deviceToken := config.LoadDeviceToken(a.vault)
dto := &SyncStatusDTO{
Configured: serverURL != "" && (apiKey != "" || deviceToken != ""),
ServerURL: serverURL,
LastSyncAt: lastSyncAt,
UnpushedOps: 0,
TokenStored: deviceToken != "",
}
if cfg != nil {
dto.DeviceID = cfg.Sync.DeviceID
dto.SyncInterval = cfg.Sync.SyncInterval
}
unpushed, _ := a.sync.GetUnpushedOps()
dto.UnpushedOps = len(unpushed)
if deviceToken != "" {
client := syncsvc.NewClient(serverURL, "", "", a.vault)
client.DeviceToken = deviceToken
if cfg != nil {
client.DeviceID = cfg.Sync.DeviceID
}
if info, err := client.GetMe(); err == nil {
dto.DeviceName = info.DeviceName
dto.DeviceID = info.DeviceID
dto.Connected = true
if info.RevokedAt != "" {
dto.Revoked = true
dto.Connected = false
}
}
}
return dto, nil
}
func (a *App) SyncConfigure(serverURL, username, password string) error {
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "unknown"
}
client := syncsvc.NewClient(serverURL, "", "", a.vault)
deviceID, deviceToken, err := client.PairDevice(serverURL, username, password, hostname, "verstak-gui/v2")
if err != nil {
return fmt.Errorf("pair: %w", err)
}
if err := config.SaveDeviceToken(a.vault, deviceToken); err != nil {
return fmt.Errorf("save token: %w", err)
}
if err := a.sync.SetState(serverURL, ""); err != nil {
return err
}
cfg, err := config.Load(a.vault)
if err != nil {
cfg = &config.Config{}
}
cfg.Sync.ServerURL = serverURL
cfg.Sync.DeviceID = deviceID
cfg.Sync.APIKey = ""
return config.Save(a.vault, cfg)
}
func (a *App) SyncDisconnect() error {
deviceToken := config.LoadDeviceToken(a.vault)
cfg, err := config.Load(a.vault)
if err != nil {
cfg = &config.Config{}
}
if deviceToken != "" {
client := syncsvc.NewClient(cfg.Sync.ServerURL, "", "", a.vault)
client.DeviceToken = deviceToken
_ = client.RevokeCurrent()
}
config.RemoveDeviceToken(a.vault)
cfg.Sync.ServerURL = ""
cfg.Sync.DeviceID = ""
cfg.Sync.APIKey = ""
if err := config.Save(a.vault, cfg); err != nil {
return err
}
return a.sync.SetState("", "")
}
func (a *App) SyncTestConnection(serverURL, username, password string) error {
client := syncsvc.NewClient(serverURL, "", "", a.vault)
return client.TestAuth(serverURL, username, password)
}
func (a *App) SyncSetInterval(minutes int) error {
cfg, err := config.Load(a.vault)
if err != nil {
cfg = &config.Config{}
}
if cfg.Sync.ServerURL == "" {
sURL, _, _, _, _ := a.sync.GetState()
if sURL != "" {
cfg.Sync.ServerURL = sURL
}
}
if cfg.Sync.DeviceID == "" {
cfg.Sync.DeviceID = a.sync.GetDeviceID()
}
cfg.Sync.SyncInterval = minutes
return config.Save(a.vault, cfg)
}
func (a *App) SyncNow() (map[string]interface{}, error) {
serverURL, apiKey, lastPullSeq, _, err := a.sync.GetState()
deviceToken := config.LoadDeviceToken(a.vault)
if err != nil || serverURL == "" || (apiKey == "" && deviceToken == "") {
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)
client.DeviceToken = deviceToken
unpushed, err := a.sync.GetUnpushedOps()
if err != nil {
return nil, fmt.Errorf("get ops: %w", err)
}
for i := range unpushed {
unpushed[i].LastSeenServerSeq = lastPullSeq
}
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)
}
}
pullResult, err := client.Pull(lastPullSeq)
if err != nil {
return nil, fmt.Errorf("pull: %w", err)
}
var applyErrors []string
for _, op := range pullResult.Ops {
if err := a.applyRemoteOp(op); err != nil {
applyErrors = append(applyErrors, fmt.Sprintf("%s/%s: %v", op.EntityType, op.OpID, err))
}
_ = a.sync.RecordRemoteOp(op)
}
if len(pullResult.Ops) > 0 {
opIDs := make([]string, len(pullResult.Ops))
for i, op := range pullResult.Ops {
opIDs[i] = op.OpID
}
_ = a.sync.MarkApplied(opIDs)
}
if len(pushResult.Conflicts) > 0 {
log.Printf("[sync] %d conflict(s) detected on push", len(pushResult.Conflicts))
for _, c := range pushResult.Conflicts {
log.Printf("[sync] conflict: op=%v entity=%v/%v",
c["op_id"], c["entity_type"], c["entity_id"])
}
}
if pullResult.ServerSequence > lastPullSeq {
_ = a.sync.SetLastPullSeq(pullResult.ServerSequence)
}
_ = a.sync.SetLastSyncAt(time.Now().UTC().Format(time.RFC3339))
result := map[string]interface{}{
"pushed": len(pushResult.Accepted),
"pulled": len(pullResult.Ops),
"serverSequence": pullResult.ServerSequence,
}
if len(applyErrors) > 0 {
result["applyErrors"] = applyErrors
}
if len(pushResult.Conflicts) > 0 {
result["conflicts"] = pushResult.Conflicts
}
return result, nil
}