Fixed a number of sync issues I noticed
All checks were successful
Ascently - Sync Deploy / build-and-push (push) Successful in 2m30s
All checks were successful
Ascently - Sync Deploy / build-and-push (push) Successful in 2m30s
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
202
sync/main.go
202
sync/main.go
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user