Fixed a number of sync issues I noticed
All checks were successful
Ascently - Sync Deploy / build-and-push (push) Successful in 2m30s

This commit is contained in:
2026-01-09 14:39:28 -07:00
parent afb0456692
commit d002c703d5
6 changed files with 478 additions and 954 deletions

View File

@@ -20,7 +20,6 @@ struct ClimbDataBackup: Codable {
let problems: [BackupProblem]
let sessions: [BackupClimbSession]
let attempts: [BackupAttempt]
let deletedItems: [DeletedItem]
init(
exportedAt: String,
@@ -29,8 +28,7 @@ struct ClimbDataBackup: Codable {
gyms: [BackupGym],
problems: [BackupProblem],
sessions: [BackupClimbSession],
attempts: [BackupAttempt],
deletedItems: [DeletedItem] = []
attempts: [BackupAttempt]
) {
self.exportedAt = exportedAt
self.version = version
@@ -39,7 +37,6 @@ struct ClimbDataBackup: Codable {
self.problems = problems
self.sessions = sessions
self.attempts = attempts
self.deletedItems = deletedItems
}
}
@@ -52,6 +49,7 @@ struct BackupGym: Codable {
let difficultySystems: [DifficultySystem]
let customDifficultyGrades: [String]
let notes: String?
let isDeleted: Bool?
let createdAt: String
let updatedAt: String
@@ -64,6 +62,8 @@ struct BackupGym: Codable {
self.customDifficultyGrades = gym.customDifficultyGrades
self.notes = gym.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
self.createdAt = formatter.string(from: gym.createdAt)
@@ -78,6 +78,7 @@ struct BackupGym: Codable {
difficultySystems: [DifficultySystem],
customDifficultyGrades: [String] = [],
notes: String?,
isDeleted: Bool = false,
createdAt: String,
updatedAt: String
) {
@@ -88,6 +89,7 @@ struct BackupGym: Codable {
self.difficultySystems = difficultySystems
self.customDifficultyGrades = customDifficultyGrades
self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -115,6 +117,25 @@ struct BackupGym: Codable {
updatedAt: updatedDate
)
}
static func createTombstone(id: String, deletedAt: Date) -> BackupGym {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let dateString = formatter.string(from: deletedAt)
return BackupGym(
id: id,
name: "DELETED",
location: nil,
supportedClimbTypes: [],
difficultySystems: [],
customDifficultyGrades: [],
notes: nil,
isDeleted: true,
createdAt: dateString,
updatedAt: dateString
)
}
}
// Platform-neutral problem representation for backup/restore
@@ -131,6 +152,7 @@ struct BackupProblem: Codable {
let isActive: Bool
let dateSet: String? // ISO 8601 format
let notes: String?
let isDeleted: Bool?
let createdAt: String
let updatedAt: String
@@ -146,6 +168,7 @@ struct BackupProblem: Codable {
self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths
self.isActive = problem.isActive
self.notes = problem.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -167,6 +190,7 @@ struct BackupProblem: Codable {
isActive: Bool,
dateSet: String?,
notes: String?,
isDeleted: Bool = false,
createdAt: String,
updatedAt: String
) {
@@ -182,6 +206,7 @@ struct BackupProblem: Codable {
self.isActive = isActive
self.dateSet = dateSet
self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -232,10 +257,35 @@ struct BackupProblem: Codable {
isActive: self.isActive,
dateSet: self.dateSet,
notes: self.notes,
isDeleted: self.isDeleted ?? false,
createdAt: self.createdAt,
updatedAt: self.updatedAt
)
}
static func createTombstone(id: String, gymId: String = UUID().uuidString, deletedAt: Date) -> BackupProblem {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let dateString = formatter.string(from: deletedAt)
return BackupProblem(
id: id,
gymId: gymId,
name: "DELETED",
description: nil,
climbType: ClimbType.allCases.first!,
difficulty: DifficultyGrade(system: DifficultySystem.allCases.first!, grade: "0"),
tags: [],
location: nil,
imagePaths: nil,
isActive: false,
dateSet: nil,
notes: nil,
isDeleted: true,
createdAt: dateString,
updatedAt: dateString
)
}
}
// Platform-neutral climb session representation for backup/restore
@@ -248,6 +298,7 @@ struct BackupClimbSession: Codable {
let duration: Int64? // Duration in seconds
let status: SessionStatus
let notes: String?
let isDeleted: Bool?
let createdAt: String
let updatedAt: String
@@ -256,6 +307,7 @@ struct BackupClimbSession: Codable {
self.gymId = session.gymId.uuidString
self.status = session.status
self.notes = session.notes
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -276,6 +328,7 @@ struct BackupClimbSession: Codable {
duration: Int64?,
status: SessionStatus,
notes: String?,
isDeleted: Bool = false,
createdAt: String,
updatedAt: String
) {
@@ -287,6 +340,7 @@ struct BackupClimbSession: Codable {
self.duration = duration
self.status = status
self.notes = notes
self.isDeleted = isDeleted
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -321,6 +375,26 @@ struct BackupClimbSession: Codable {
updatedAt: updatedDate
)
}
static func createTombstone(id: String, gymId: String = UUID().uuidString, deletedAt: Date) -> BackupClimbSession {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let dateString = formatter.string(from: deletedAt)
return BackupClimbSession(
id: id,
gymId: gymId,
date: dateString,
startTime: nil,
endTime: nil,
duration: nil,
status: .finished,
notes: nil,
isDeleted: true,
createdAt: dateString,
updatedAt: dateString
)
}
}
// Platform-neutral attempt representation for backup/restore
@@ -334,6 +408,7 @@ struct BackupAttempt: Codable {
let duration: Int64? // Duration in seconds
let restTime: Int64? // Rest time in seconds
let timestamp: String
let isDeleted: Bool?
let createdAt: String
let updatedAt: String?
@@ -346,6 +421,7 @@ struct BackupAttempt: Codable {
self.notes = attempt.notes
self.duration = attempt.duration.map { Int64($0) }
self.restTime = attempt.restTime.map { Int64($0) }
self.isDeleted = false // Default to false until model is updated
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -364,6 +440,7 @@ struct BackupAttempt: Codable {
duration: Int64?,
restTime: Int64?,
timestamp: String,
isDeleted: Bool = false,
createdAt: String,
updatedAt: String?
) {
@@ -376,6 +453,7 @@ struct BackupAttempt: Codable {
self.duration = duration
self.restTime = restTime
self.timestamp = timestamp
self.isDeleted = isDeleted
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -412,6 +490,27 @@ struct BackupAttempt: Codable {
updatedAt: updatedDate
)
}
static func createTombstone(id: String, sessionId: String = UUID().uuidString, problemId: String = UUID().uuidString, deletedAt: Date) -> BackupAttempt {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let dateString = formatter.string(from: deletedAt)
return BackupAttempt(
id: id,
sessionId: sessionId,
problemId: problemId,
result: AttemptResult.allCases.first!,
highestHold: nil,
notes: nil,
duration: nil,
restTime: nil,
timestamp: dateString,
isDeleted: true,
createdAt: dateString,
updatedAt: dateString
)
}
}
// MARK: - Backup Format Errors

View File

@@ -13,7 +13,6 @@ struct DeltaSyncRequest: Codable {
let problems: [BackupProblem]
let sessions: [BackupClimbSession]
let attempts: [BackupAttempt]
let deletedItems: [DeletedItem]
}
struct DeltaSyncResponse: Codable {
@@ -22,5 +21,4 @@ struct DeltaSyncResponse: Codable {
let problems: [BackupProblem]
let sessions: [BackupClimbSession]
let attempts: [BackupAttempt]
let deletedItems: [DeletedItem]
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import UniformTypeIdentifiers
enum SheetType {
case export(Data)
case importData
case syncSettings
}
struct SettingsView: View {
@@ -16,7 +17,7 @@ struct SettingsView: View {
var body: some View {
NavigationStack {
List {
SyncSection()
SyncSection(activeSheet: $activeSheet)
.environmentObject(dataManager.syncService)
HealthKitSection()
@@ -67,6 +68,9 @@ struct SettingsView: View {
ExportDataView(data: data)
case .importData:
ImportDataView()
case .syncSettings:
SyncSettingsView()
.environmentObject(dataManager.syncService)
}
}
}
@@ -78,6 +82,7 @@ extension SheetType: Identifiable {
switch self {
case .export: return "export"
case .importData: return "import"
case .syncSettings: return "sync_settings"
}
}
}
@@ -526,7 +531,7 @@ struct SyncSection: View {
@EnvironmentObject var syncService: SyncService
@EnvironmentObject var dataManager: ClimbingDataManager
@EnvironmentObject var themeManager: ThemeManager
@State private var showingSyncSettings = false
@Binding var activeSheet: SheetType?
@State private var showingDisconnectAlert = false
private static let logTag = "SyncSection"
@@ -567,7 +572,7 @@ struct SyncSection: View {
// Configure Server
Button(action: {
showingSyncSettings = true
activeSheet = .syncSettings
}) {
HStack {
Image(systemName: "gear")
@@ -657,10 +662,6 @@ struct SyncSection: View {
}
}
}
.sheet(isPresented: $showingSyncSettings) {
SyncSettingsView()
.environmentObject(syncService)
}
.alert("Disconnect from Server", isPresented: $showingDisconnectAlert) {
Button("Cancel", role: .cancel) {}
Button("Disconnect", role: .destructive) {
@@ -702,24 +703,14 @@ struct SyncSettingsView: View {
NavigationStack {
Form {
Section {
TextField("Server URL", text: $serverURL)
.textFieldStyle(.roundedBorder)
TextField("Server URL", text: $serverURL, prompt: Text("http://your-server:8080"))
.keyboardType(.URL)
.autocapitalization(.none)
.disableAutocorrection(true)
.placeholder(when: serverURL.isEmpty) {
Text("http://your-server:8080")
.foregroundColor(.secondary)
}
TextField("Auth Token", text: $authToken)
.textFieldStyle(.roundedBorder)
TextField("Auth Token", text: $authToken, prompt: Text("your-secret-token"))
.autocapitalization(.none)
.disableAutocorrection(true)
.placeholder(when: authToken.isEmpty) {
Text("your-secret-token")
.foregroundColor(.secondary)
}
} header: {
Text("Server Configuration")
} footer: {
@@ -845,37 +836,34 @@ struct SyncSettingsView: View {
let originalURL = syncService.serverURL
let originalToken = syncService.authToken
Task {
Task { @MainActor in
do {
// Ensure we are using the server provider
await MainActor.run {
if syncService.providerType != .server {
syncService.providerType = .server
}
if syncService.providerType != .server {
syncService.providerType = .server
}
// Temporarily set the values for testing
syncService.serverURL = testURL
syncService.authToken = testToken
// Explicitly sync UserDefaults to ensure immediate availability
UserDefaults.standard.synchronize()
try await syncService.testConnection()
await MainActor.run {
isTesting = false
testResultMessage =
"Connection successful! You can now save and sync your data."
showingTestResult = true
}
isTesting = false
testResultMessage =
"Connection successful! You can now save and sync your data."
showingTestResult = true
} catch {
// Restore original values if test failed
syncService.serverURL = originalURL
syncService.authToken = originalToken
await MainActor.run {
isTesting = false
testResultMessage = "Connection failed: \(error.localizedDescription)"
showingTestResult = true
}
isTesting = false
testResultMessage = "Connection failed: \(error.localizedDescription)"
showingTestResult = true
}
}
}

View File

@@ -13,7 +13,7 @@ import (
"time"
)
const VERSION = "2.3.0"
const VERSION = "2.4.0"
func min(a, b int) int {
if a < b {
@@ -22,12 +22,6 @@ func min(a, b int) int {
return b
}
type DeletedItem struct {
ID string `json:"id"`
Type string `json:"type"`
DeletedAt string `json:"deletedAt"`
}
type ClimbDataBackup struct {
ExportedAt string `json:"exportedAt"`
Version string `json:"version"`
@@ -36,7 +30,6 @@ type ClimbDataBackup struct {
Problems []BackupProblem `json:"problems"`
Sessions []BackupClimbSession `json:"sessions"`
Attempts []BackupAttempt `json:"attempts"`
DeletedItems []DeletedItem `json:"deletedItems"`
}
type DeltaSyncRequest struct {
@@ -45,16 +38,14 @@ type DeltaSyncRequest struct {
Problems []BackupProblem `json:"problems"`
Sessions []BackupClimbSession `json:"sessions"`
Attempts []BackupAttempt `json:"attempts"`
DeletedItems []DeletedItem `json:"deletedItems"`
}
type DeltaSyncResponse struct {
ServerTime string `json:"serverTime"`
Gyms []BackupGym `json:"gyms"`
Problems []BackupProblem `json:"problems"`
Sessions []BackupClimbSession `json:"sessions"`
Attempts []BackupAttempt `json:"attempts"`
DeletedItems []DeletedItem `json:"deletedItems"`
ServerTime string `json:"serverTime"`
Gyms []BackupGym `json:"gyms"`
Problems []BackupProblem `json:"problems"`
Sessions []BackupClimbSession `json:"sessions"`
Attempts []BackupAttempt `json:"attempts"`
}
type BackupGym struct {
@@ -65,6 +56,7 @@ type BackupGym struct {
DifficultySystems []string `json:"difficultySystems"`
CustomDifficultyGrades []string `json:"customDifficultyGrades"`
Notes *string `json:"notes,omitempty"`
IsDeleted bool `json:"isDeleted"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
@@ -82,6 +74,7 @@ type BackupProblem struct {
IsActive bool `json:"isActive"`
DateSet *string `json:"dateSet,omitempty"`
Notes *string `json:"notes,omitempty"`
IsDeleted bool `json:"isDeleted"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
@@ -101,6 +94,7 @@ type BackupClimbSession struct {
Duration *int64 `json:"duration,omitempty"`
Status string `json:"status"`
Notes *string `json:"notes,omitempty"`
IsDeleted bool `json:"isDeleted"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
@@ -115,7 +109,9 @@ type BackupAttempt struct {
Duration *int64 `json:"duration,omitempty"`
RestTime *int64 `json:"restTime,omitempty"`
Timestamp string `json:"timestamp"`
IsDeleted bool `json:"isDeleted"`
CreatedAt string `json:"createdAt"`
UpdatedAt *string `json:"updatedAt,omitempty"`
}
type SyncServer struct {
@@ -147,7 +143,6 @@ func (s *SyncServer) loadData() (*ClimbDataBackup, error) {
Problems: []BackupProblem{},
Sessions: []BackupClimbSession{},
Attempts: []BackupAttempt{},
DeletedItems: []DeletedItem{},
}, nil
}
@@ -158,7 +153,18 @@ func (s *SyncServer) loadData() (*ClimbDataBackup, error) {
}
log.Printf("Read %d bytes from data file", len(data))
log.Printf("File content preview: %s", string(data[:min(200, len(data))]))
// Basic check to see if we have JSON content
if len(data) == 0 {
return &ClimbDataBackup{
ExportedAt: time.Now().UTC().Format(time.RFC3339),
Version: "2.0",
FormatVersion: "2.0",
Gyms: []BackupGym{},
Problems: []BackupProblem{},
Sessions: []BackupClimbSession{},
Attempts: []BackupAttempt{},
}, nil
}
var backup ClimbDataBackup
if err := json.Unmarshal(data, &backup); err != nil {
@@ -250,7 +256,18 @@ func (s *SyncServer) mergeAttempts(existing []BackupAttempt, updates []BackupAtt
for _, attempt := range updates {
if existingAttempt, exists := attemptMap[attempt.ID]; exists {
if attempt.CreatedAt >= existingAttempt.CreatedAt {
// Resolve update time for comparison
updateTime := attempt.CreatedAt
if attempt.UpdatedAt != nil {
updateTime = *attempt.UpdatedAt
}
existingUpdateTime := existingAttempt.CreatedAt
if existingAttempt.UpdatedAt != nil {
existingUpdateTime = *existingAttempt.UpdatedAt
}
if updateTime >= existingUpdateTime {
attemptMap[attempt.ID] = attempt
}
} else {
@@ -265,89 +282,6 @@ func (s *SyncServer) mergeAttempts(existing []BackupAttempt, updates []BackupAtt
return result
}
func (s *SyncServer) mergeDeletedItems(existing []DeletedItem, updates []DeletedItem) []DeletedItem {
deletedMap := make(map[string]DeletedItem)
for _, item := range existing {
key := item.Type + ":" + item.ID
deletedMap[key] = item
}
for _, item := range updates {
key := item.Type + ":" + item.ID
if existingItem, exists := deletedMap[key]; exists {
if item.DeletedAt >= existingItem.DeletedAt {
deletedMap[key] = item
}
} else {
deletedMap[key] = item
}
}
// Clean up tombstones older than 30 days to prevent unbounded growth
cutoffTime := time.Now().UTC().Add(-30 * 24 * time.Hour)
result := make([]DeletedItem, 0, len(deletedMap))
for _, item := range deletedMap {
deletedTime, err := time.Parse(time.RFC3339, item.DeletedAt)
if err == nil && deletedTime.Before(cutoffTime) {
log.Printf("Cleaning up old deletion record: type=%s, id=%s, deletedAt=%s",
item.Type, item.ID, item.DeletedAt)
continue
}
result = append(result, item)
}
return result
}
func (s *SyncServer) applyDeletions(backup *ClimbDataBackup, deletedItems []DeletedItem) {
deletedMap := make(map[string]map[string]bool)
for _, item := range deletedItems {
if deletedMap[item.Type] == nil {
deletedMap[item.Type] = make(map[string]bool)
}
deletedMap[item.Type][item.ID] = true
}
if deletedMap["gym"] != nil {
filtered := []BackupGym{}
for _, gym := range backup.Gyms {
if !deletedMap["gym"][gym.ID] {
filtered = append(filtered, gym)
}
}
backup.Gyms = filtered
}
if deletedMap["problem"] != nil {
filtered := []BackupProblem{}
for _, problem := range backup.Problems {
if !deletedMap["problem"][problem.ID] {
filtered = append(filtered, problem)
}
}
backup.Problems = filtered
}
if deletedMap["session"] != nil {
filtered := []BackupClimbSession{}
for _, session := range backup.Sessions {
if !deletedMap["session"][session.ID] {
filtered = append(filtered, session)
}
}
backup.Sessions = filtered
}
if deletedMap["attempt"] != nil {
filtered := []BackupAttempt{}
for _, attempt := range backup.Attempts {
if !deletedMap["attempt"][attempt.ID] {
filtered = append(filtered, attempt)
}
}
backup.Attempts = filtered
}
}
func (s *SyncServer) saveData(backup *ClimbDataBackup) error {
backup.ExportedAt = time.Now().UTC().Format(time.RFC3339)
@@ -383,6 +317,8 @@ func (s *SyncServer) handleGet(w http.ResponseWriter, r *http.Request) {
return
}
log.Printf("Sending data to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d",
r.RemoteAddr, len(backup.Gyms), len(backup.Problems), len(backup.Sessions), len(backup.Attempts))
w.Header().Set("Content-Type", "application/json")
@@ -527,11 +463,10 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
return
}
log.Printf("Delta sync from %s: lastSyncTime=%s, gyms=%d, problems=%d, sessions=%d, attempts=%d, deletedItems=%d",
log.Printf("Delta sync from %s: lastSyncTime=%s, gyms=%d, problems=%d, sessions=%d, attempts=%d",
r.RemoteAddr, deltaRequest.LastSyncTime,
len(deltaRequest.Gyms), len(deltaRequest.Problems),
len(deltaRequest.Sessions), len(deltaRequest.Attempts),
len(deltaRequest.DeletedItems))
len(deltaRequest.Sessions), len(deltaRequest.Attempts))
// Load current server data
serverBackup, err := s.loadData()
@@ -541,12 +476,9 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
return
}
// Merge and apply deletions first to prevent resurrection
serverBackup.DeletedItems = s.mergeDeletedItems(serverBackup.DeletedItems, deltaRequest.DeletedItems)
s.applyDeletions(serverBackup, serverBackup.DeletedItems)
log.Printf("Applied deletions: total=%d deletion records", len(serverBackup.DeletedItems))
// Merge client changes into server data
// Note: We no longer need separate deletion handling as IsDeleted is part of the struct
// and handled by standard merge logic (latest timestamp wins)
serverBackup.Gyms = s.mergeGyms(serverBackup.Gyms, deltaRequest.Gyms)
serverBackup.Problems = s.mergeProblems(serverBackup.Problems, deltaRequest.Problems)
serverBackup.Sessions = s.mergeSessions(serverBackup.Sessions, deltaRequest.Sessions)
@@ -566,28 +498,17 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
log.Printf("Warning: Could not parse lastSyncTime '%s', sending all data", deltaRequest.LastSyncTime)
}
// Build deleted item lookup map
deletedItemMap := make(map[string]bool)
for _, item := range serverBackup.DeletedItems {
key := item.Type + ":" + item.ID
deletedItemMap[key] = true
}
// Prepare response with items modified since client's last sync
response := DeltaSyncResponse{
ServerTime: time.Now().UTC().Format(time.RFC3339),
Gyms: []BackupGym{},
Problems: []BackupProblem{},
Sessions: []BackupClimbSession{},
Attempts: []BackupAttempt{},
DeletedItems: []DeletedItem{},
ServerTime: time.Now().UTC().Format(time.RFC3339),
Gyms: []BackupGym{},
Problems: []BackupProblem{},
Sessions: []BackupClimbSession{},
Attempts: []BackupAttempt{},
}
// Filter gyms modified after client's last sync
for _, gym := range serverBackup.Gyms {
if deletedItemMap["gym:"+gym.ID] {
continue
}
gymTime, err := time.Parse(time.RFC3339, gym.UpdatedAt)
if err == nil && gymTime.After(clientLastSync) {
response.Gyms = append(response.Gyms, gym)
@@ -596,9 +517,6 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
// Filter problems modified after client's last sync
for _, problem := range serverBackup.Problems {
if deletedItemMap["problem:"+problem.ID] {
continue
}
problemTime, err := time.Parse(time.RFC3339, problem.UpdatedAt)
if err == nil && problemTime.After(clientLastSync) {
response.Problems = append(response.Problems, problem)
@@ -607,39 +525,29 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
// Filter sessions modified after client's last sync
for _, session := range serverBackup.Sessions {
if deletedItemMap["session:"+session.ID] {
continue
}
sessionTime, err := time.Parse(time.RFC3339, session.UpdatedAt)
if err == nil && sessionTime.After(clientLastSync) {
response.Sessions = append(response.Sessions, session)
}
}
// Filter attempts created after client's last sync
// Filter attempts modified after client's last sync
for _, attempt := range serverBackup.Attempts {
if deletedItemMap["attempt:"+attempt.ID] {
continue
attemptTime := attempt.CreatedAt
if attempt.UpdatedAt != nil {
attemptTime = *attempt.UpdatedAt
}
attemptTime, err := time.Parse(time.RFC3339, attempt.CreatedAt)
if err == nil && attemptTime.After(clientLastSync) {
parsedTime, err := time.Parse(time.RFC3339, attemptTime)
if err == nil && parsedTime.After(clientLastSync) {
response.Attempts = append(response.Attempts, attempt)
}
}
// Filter deletions after client's last sync
for _, deletedItem := range serverBackup.DeletedItems {
deletedTime, err := time.Parse(time.RFC3339, deletedItem.DeletedAt)
if err == nil && deletedTime.After(clientLastSync) {
response.DeletedItems = append(response.DeletedItems, deletedItem)
}
}
log.Printf("Delta sync response to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d, deletedItems=%d",
log.Printf("Delta sync response to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d",
r.RemoteAddr,
len(response.Gyms), len(response.Problems),
len(response.Sessions), len(response.Attempts),
len(response.DeletedItems))
len(response.Sessions), len(response.Attempts))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)