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 problems: [BackupProblem]
|
||||||
let sessions: [BackupClimbSession]
|
let sessions: [BackupClimbSession]
|
||||||
let attempts: [BackupAttempt]
|
let attempts: [BackupAttempt]
|
||||||
let deletedItems: [DeletedItem]
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
exportedAt: String,
|
exportedAt: String,
|
||||||
@@ -29,8 +28,7 @@ struct ClimbDataBackup: Codable {
|
|||||||
gyms: [BackupGym],
|
gyms: [BackupGym],
|
||||||
problems: [BackupProblem],
|
problems: [BackupProblem],
|
||||||
sessions: [BackupClimbSession],
|
sessions: [BackupClimbSession],
|
||||||
attempts: [BackupAttempt],
|
attempts: [BackupAttempt]
|
||||||
deletedItems: [DeletedItem] = []
|
|
||||||
) {
|
) {
|
||||||
self.exportedAt = exportedAt
|
self.exportedAt = exportedAt
|
||||||
self.version = version
|
self.version = version
|
||||||
@@ -39,7 +37,6 @@ struct ClimbDataBackup: Codable {
|
|||||||
self.problems = problems
|
self.problems = problems
|
||||||
self.sessions = sessions
|
self.sessions = sessions
|
||||||
self.attempts = attempts
|
self.attempts = attempts
|
||||||
self.deletedItems = deletedItems
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +49,7 @@ struct BackupGym: Codable {
|
|||||||
let difficultySystems: [DifficultySystem]
|
let difficultySystems: [DifficultySystem]
|
||||||
let customDifficultyGrades: [String]
|
let customDifficultyGrades: [String]
|
||||||
let notes: String?
|
let notes: String?
|
||||||
|
let isDeleted: Bool?
|
||||||
let createdAt: String
|
let createdAt: String
|
||||||
let updatedAt: String
|
let updatedAt: String
|
||||||
|
|
||||||
@@ -64,6 +62,8 @@ struct BackupGym: Codable {
|
|||||||
self.customDifficultyGrades = gym.customDifficultyGrades
|
self.customDifficultyGrades = gym.customDifficultyGrades
|
||||||
self.notes = gym.notes
|
self.notes = gym.notes
|
||||||
|
|
||||||
|
self.isDeleted = false // Default to false until model is updated
|
||||||
|
|
||||||
let formatter = ISO8601DateFormatter()
|
let formatter = ISO8601DateFormatter()
|
||||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
self.createdAt = formatter.string(from: gym.createdAt)
|
self.createdAt = formatter.string(from: gym.createdAt)
|
||||||
@@ -78,6 +78,7 @@ struct BackupGym: Codable {
|
|||||||
difficultySystems: [DifficultySystem],
|
difficultySystems: [DifficultySystem],
|
||||||
customDifficultyGrades: [String] = [],
|
customDifficultyGrades: [String] = [],
|
||||||
notes: String?,
|
notes: String?,
|
||||||
|
isDeleted: Bool = false,
|
||||||
createdAt: String,
|
createdAt: String,
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
) {
|
) {
|
||||||
@@ -88,6 +89,7 @@ struct BackupGym: Codable {
|
|||||||
self.difficultySystems = difficultySystems
|
self.difficultySystems = difficultySystems
|
||||||
self.customDifficultyGrades = customDifficultyGrades
|
self.customDifficultyGrades = customDifficultyGrades
|
||||||
self.notes = notes
|
self.notes = notes
|
||||||
|
self.isDeleted = isDeleted
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.updatedAt = updatedAt
|
self.updatedAt = updatedAt
|
||||||
}
|
}
|
||||||
@@ -115,6 +117,25 @@ struct BackupGym: Codable {
|
|||||||
updatedAt: updatedDate
|
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
|
// Platform-neutral problem representation for backup/restore
|
||||||
@@ -131,6 +152,7 @@ struct BackupProblem: Codable {
|
|||||||
let isActive: Bool
|
let isActive: Bool
|
||||||
let dateSet: String? // ISO 8601 format
|
let dateSet: String? // ISO 8601 format
|
||||||
let notes: String?
|
let notes: String?
|
||||||
|
let isDeleted: Bool?
|
||||||
let createdAt: String
|
let createdAt: String
|
||||||
let updatedAt: String
|
let updatedAt: String
|
||||||
|
|
||||||
@@ -146,6 +168,7 @@ struct BackupProblem: Codable {
|
|||||||
self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths
|
self.imagePaths = problem.imagePaths.isEmpty ? nil : problem.imagePaths
|
||||||
self.isActive = problem.isActive
|
self.isActive = problem.isActive
|
||||||
self.notes = problem.notes
|
self.notes = problem.notes
|
||||||
|
self.isDeleted = false // Default to false until model is updated
|
||||||
|
|
||||||
let formatter = ISO8601DateFormatter()
|
let formatter = ISO8601DateFormatter()
|
||||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
@@ -167,6 +190,7 @@ struct BackupProblem: Codable {
|
|||||||
isActive: Bool,
|
isActive: Bool,
|
||||||
dateSet: String?,
|
dateSet: String?,
|
||||||
notes: String?,
|
notes: String?,
|
||||||
|
isDeleted: Bool = false,
|
||||||
createdAt: String,
|
createdAt: String,
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
) {
|
) {
|
||||||
@@ -182,6 +206,7 @@ struct BackupProblem: Codable {
|
|||||||
self.isActive = isActive
|
self.isActive = isActive
|
||||||
self.dateSet = dateSet
|
self.dateSet = dateSet
|
||||||
self.notes = notes
|
self.notes = notes
|
||||||
|
self.isDeleted = isDeleted
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.updatedAt = updatedAt
|
self.updatedAt = updatedAt
|
||||||
}
|
}
|
||||||
@@ -232,10 +257,35 @@ struct BackupProblem: Codable {
|
|||||||
isActive: self.isActive,
|
isActive: self.isActive,
|
||||||
dateSet: self.dateSet,
|
dateSet: self.dateSet,
|
||||||
notes: self.notes,
|
notes: self.notes,
|
||||||
|
isDeleted: self.isDeleted ?? false,
|
||||||
createdAt: self.createdAt,
|
createdAt: self.createdAt,
|
||||||
updatedAt: self.updatedAt
|
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
|
// Platform-neutral climb session representation for backup/restore
|
||||||
@@ -248,6 +298,7 @@ struct BackupClimbSession: Codable {
|
|||||||
let duration: Int64? // Duration in seconds
|
let duration: Int64? // Duration in seconds
|
||||||
let status: SessionStatus
|
let status: SessionStatus
|
||||||
let notes: String?
|
let notes: String?
|
||||||
|
let isDeleted: Bool?
|
||||||
let createdAt: String
|
let createdAt: String
|
||||||
let updatedAt: String
|
let updatedAt: String
|
||||||
|
|
||||||
@@ -256,6 +307,7 @@ struct BackupClimbSession: Codable {
|
|||||||
self.gymId = session.gymId.uuidString
|
self.gymId = session.gymId.uuidString
|
||||||
self.status = session.status
|
self.status = session.status
|
||||||
self.notes = session.notes
|
self.notes = session.notes
|
||||||
|
self.isDeleted = false // Default to false until model is updated
|
||||||
|
|
||||||
let formatter = ISO8601DateFormatter()
|
let formatter = ISO8601DateFormatter()
|
||||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
@@ -276,6 +328,7 @@ struct BackupClimbSession: Codable {
|
|||||||
duration: Int64?,
|
duration: Int64?,
|
||||||
status: SessionStatus,
|
status: SessionStatus,
|
||||||
notes: String?,
|
notes: String?,
|
||||||
|
isDeleted: Bool = false,
|
||||||
createdAt: String,
|
createdAt: String,
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
) {
|
) {
|
||||||
@@ -287,6 +340,7 @@ struct BackupClimbSession: Codable {
|
|||||||
self.duration = duration
|
self.duration = duration
|
||||||
self.status = status
|
self.status = status
|
||||||
self.notes = notes
|
self.notes = notes
|
||||||
|
self.isDeleted = isDeleted
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.updatedAt = updatedAt
|
self.updatedAt = updatedAt
|
||||||
}
|
}
|
||||||
@@ -321,6 +375,26 @@ struct BackupClimbSession: Codable {
|
|||||||
updatedAt: updatedDate
|
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
|
// Platform-neutral attempt representation for backup/restore
|
||||||
@@ -334,6 +408,7 @@ struct BackupAttempt: Codable {
|
|||||||
let duration: Int64? // Duration in seconds
|
let duration: Int64? // Duration in seconds
|
||||||
let restTime: Int64? // Rest time in seconds
|
let restTime: Int64? // Rest time in seconds
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
|
let isDeleted: Bool?
|
||||||
let createdAt: String
|
let createdAt: String
|
||||||
let updatedAt: String?
|
let updatedAt: String?
|
||||||
|
|
||||||
@@ -346,6 +421,7 @@ struct BackupAttempt: Codable {
|
|||||||
self.notes = attempt.notes
|
self.notes = attempt.notes
|
||||||
self.duration = attempt.duration.map { Int64($0) }
|
self.duration = attempt.duration.map { Int64($0) }
|
||||||
self.restTime = attempt.restTime.map { Int64($0) }
|
self.restTime = attempt.restTime.map { Int64($0) }
|
||||||
|
self.isDeleted = false // Default to false until model is updated
|
||||||
|
|
||||||
let formatter = ISO8601DateFormatter()
|
let formatter = ISO8601DateFormatter()
|
||||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
@@ -364,6 +440,7 @@ struct BackupAttempt: Codable {
|
|||||||
duration: Int64?,
|
duration: Int64?,
|
||||||
restTime: Int64?,
|
restTime: Int64?,
|
||||||
timestamp: String,
|
timestamp: String,
|
||||||
|
isDeleted: Bool = false,
|
||||||
createdAt: String,
|
createdAt: String,
|
||||||
updatedAt: String?
|
updatedAt: String?
|
||||||
) {
|
) {
|
||||||
@@ -376,6 +453,7 @@ struct BackupAttempt: Codable {
|
|||||||
self.duration = duration
|
self.duration = duration
|
||||||
self.restTime = restTime
|
self.restTime = restTime
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
|
self.isDeleted = isDeleted
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.updatedAt = updatedAt
|
self.updatedAt = updatedAt
|
||||||
}
|
}
|
||||||
@@ -412,6 +490,27 @@ struct BackupAttempt: Codable {
|
|||||||
updatedAt: updatedDate
|
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
|
// MARK: - Backup Format Errors
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ struct DeltaSyncRequest: Codable {
|
|||||||
let problems: [BackupProblem]
|
let problems: [BackupProblem]
|
||||||
let sessions: [BackupClimbSession]
|
let sessions: [BackupClimbSession]
|
||||||
let attempts: [BackupAttempt]
|
let attempts: [BackupAttempt]
|
||||||
let deletedItems: [DeletedItem]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeltaSyncResponse: Codable {
|
struct DeltaSyncResponse: Codable {
|
||||||
@@ -22,5 +21,4 @@ struct DeltaSyncResponse: Codable {
|
|||||||
let problems: [BackupProblem]
|
let problems: [BackupProblem]
|
||||||
let sessions: [BackupClimbSession]
|
let sessions: [BackupClimbSession]
|
||||||
let attempts: [BackupAttempt]
|
let attempts: [BackupAttempt]
|
||||||
let deletedItems: [DeletedItem]
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import UniformTypeIdentifiers
|
|||||||
enum SheetType {
|
enum SheetType {
|
||||||
case export(Data)
|
case export(Data)
|
||||||
case importData
|
case importData
|
||||||
|
case syncSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@@ -16,7 +17,7 @@ struct SettingsView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
SyncSection()
|
SyncSection(activeSheet: $activeSheet)
|
||||||
.environmentObject(dataManager.syncService)
|
.environmentObject(dataManager.syncService)
|
||||||
|
|
||||||
HealthKitSection()
|
HealthKitSection()
|
||||||
@@ -67,6 +68,9 @@ struct SettingsView: View {
|
|||||||
ExportDataView(data: data)
|
ExportDataView(data: data)
|
||||||
case .importData:
|
case .importData:
|
||||||
ImportDataView()
|
ImportDataView()
|
||||||
|
case .syncSettings:
|
||||||
|
SyncSettingsView()
|
||||||
|
.environmentObject(dataManager.syncService)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +82,7 @@ extension SheetType: Identifiable {
|
|||||||
switch self {
|
switch self {
|
||||||
case .export: return "export"
|
case .export: return "export"
|
||||||
case .importData: return "import"
|
case .importData: return "import"
|
||||||
|
case .syncSettings: return "sync_settings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,7 +531,7 @@ struct SyncSection: View {
|
|||||||
@EnvironmentObject var syncService: SyncService
|
@EnvironmentObject var syncService: SyncService
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
@State private var showingSyncSettings = false
|
@Binding var activeSheet: SheetType?
|
||||||
@State private var showingDisconnectAlert = false
|
@State private var showingDisconnectAlert = false
|
||||||
|
|
||||||
private static let logTag = "SyncSection"
|
private static let logTag = "SyncSection"
|
||||||
@@ -567,7 +572,7 @@ struct SyncSection: View {
|
|||||||
|
|
||||||
// Configure Server
|
// Configure Server
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showingSyncSettings = true
|
activeSheet = .syncSettings
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "gear")
|
Image(systemName: "gear")
|
||||||
@@ -657,10 +662,6 @@ struct SyncSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingSyncSettings) {
|
|
||||||
SyncSettingsView()
|
|
||||||
.environmentObject(syncService)
|
|
||||||
}
|
|
||||||
.alert("Disconnect from Server", isPresented: $showingDisconnectAlert) {
|
.alert("Disconnect from Server", isPresented: $showingDisconnectAlert) {
|
||||||
Button("Cancel", role: .cancel) {}
|
Button("Cancel", role: .cancel) {}
|
||||||
Button("Disconnect", role: .destructive) {
|
Button("Disconnect", role: .destructive) {
|
||||||
@@ -702,24 +703,14 @@ struct SyncSettingsView: View {
|
|||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
TextField("Server URL", text: $serverURL)
|
TextField("Server URL", text: $serverURL, prompt: Text("http://your-server:8080"))
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.placeholder(when: serverURL.isEmpty) {
|
|
||||||
Text("http://your-server:8080")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField("Auth Token", text: $authToken)
|
TextField("Auth Token", text: $authToken, prompt: Text("your-secret-token"))
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.placeholder(when: authToken.isEmpty) {
|
|
||||||
Text("your-secret-token")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("Server Configuration")
|
Text("Server Configuration")
|
||||||
} footer: {
|
} footer: {
|
||||||
@@ -845,40 +836,37 @@ struct SyncSettingsView: View {
|
|||||||
let originalURL = syncService.serverURL
|
let originalURL = syncService.serverURL
|
||||||
let originalToken = syncService.authToken
|
let originalToken = syncService.authToken
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
// Ensure we are using the server provider
|
// Ensure we are using the server provider
|
||||||
await MainActor.run {
|
|
||||||
if syncService.providerType != .server {
|
if syncService.providerType != .server {
|
||||||
syncService.providerType = .server
|
syncService.providerType = .server
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Temporarily set the values for testing
|
// Temporarily set the values for testing
|
||||||
syncService.serverURL = testURL
|
syncService.serverURL = testURL
|
||||||
syncService.authToken = testToken
|
syncService.authToken = testToken
|
||||||
|
|
||||||
|
// Explicitly sync UserDefaults to ensure immediate availability
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
|
||||||
try await syncService.testConnection()
|
try await syncService.testConnection()
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
isTesting = false
|
isTesting = false
|
||||||
testResultMessage =
|
testResultMessage =
|
||||||
"Connection successful! You can now save and sync your data."
|
"Connection successful! You can now save and sync your data."
|
||||||
showingTestResult = true
|
showingTestResult = true
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// Restore original values if test failed
|
// Restore original values if test failed
|
||||||
syncService.serverURL = originalURL
|
syncService.serverURL = originalURL
|
||||||
syncService.authToken = originalToken
|
syncService.authToken = originalToken
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
isTesting = false
|
isTesting = false
|
||||||
testResultMessage = "Connection failed: \(error.localizedDescription)"
|
testResultMessage = "Connection failed: \(error.localizedDescription)"
|
||||||
showingTestResult = true
|
showingTestResult = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed AutoSyncSettingsView - now using simple toggle in main settings
|
// Removed AutoSyncSettingsView - now using simple toggle in main settings
|
||||||
|
|||||||
182
sync/main.go
182
sync/main.go
@@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "2.3.0"
|
const VERSION = "2.4.0"
|
||||||
|
|
||||||
func min(a, b int) int {
|
func min(a, b int) int {
|
||||||
if a < b {
|
if a < b {
|
||||||
@@ -22,12 +22,6 @@ func min(a, b int) int {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeletedItem struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
DeletedAt string `json:"deletedAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClimbDataBackup struct {
|
type ClimbDataBackup struct {
|
||||||
ExportedAt string `json:"exportedAt"`
|
ExportedAt string `json:"exportedAt"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
@@ -36,7 +30,6 @@ type ClimbDataBackup struct {
|
|||||||
Problems []BackupProblem `json:"problems"`
|
Problems []BackupProblem `json:"problems"`
|
||||||
Sessions []BackupClimbSession `json:"sessions"`
|
Sessions []BackupClimbSession `json:"sessions"`
|
||||||
Attempts []BackupAttempt `json:"attempts"`
|
Attempts []BackupAttempt `json:"attempts"`
|
||||||
DeletedItems []DeletedItem `json:"deletedItems"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeltaSyncRequest struct {
|
type DeltaSyncRequest struct {
|
||||||
@@ -45,7 +38,6 @@ type DeltaSyncRequest struct {
|
|||||||
Problems []BackupProblem `json:"problems"`
|
Problems []BackupProblem `json:"problems"`
|
||||||
Sessions []BackupClimbSession `json:"sessions"`
|
Sessions []BackupClimbSession `json:"sessions"`
|
||||||
Attempts []BackupAttempt `json:"attempts"`
|
Attempts []BackupAttempt `json:"attempts"`
|
||||||
DeletedItems []DeletedItem `json:"deletedItems"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeltaSyncResponse struct {
|
type DeltaSyncResponse struct {
|
||||||
@@ -54,7 +46,6 @@ type DeltaSyncResponse struct {
|
|||||||
Problems []BackupProblem `json:"problems"`
|
Problems []BackupProblem `json:"problems"`
|
||||||
Sessions []BackupClimbSession `json:"sessions"`
|
Sessions []BackupClimbSession `json:"sessions"`
|
||||||
Attempts []BackupAttempt `json:"attempts"`
|
Attempts []BackupAttempt `json:"attempts"`
|
||||||
DeletedItems []DeletedItem `json:"deletedItems"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackupGym struct {
|
type BackupGym struct {
|
||||||
@@ -65,6 +56,7 @@ type BackupGym struct {
|
|||||||
DifficultySystems []string `json:"difficultySystems"`
|
DifficultySystems []string `json:"difficultySystems"`
|
||||||
CustomDifficultyGrades []string `json:"customDifficultyGrades"`
|
CustomDifficultyGrades []string `json:"customDifficultyGrades"`
|
||||||
Notes *string `json:"notes,omitempty"`
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
IsDeleted bool `json:"isDeleted"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
@@ -82,6 +74,7 @@ type BackupProblem struct {
|
|||||||
IsActive bool `json:"isActive"`
|
IsActive bool `json:"isActive"`
|
||||||
DateSet *string `json:"dateSet,omitempty"`
|
DateSet *string `json:"dateSet,omitempty"`
|
||||||
Notes *string `json:"notes,omitempty"`
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
IsDeleted bool `json:"isDeleted"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
@@ -101,6 +94,7 @@ type BackupClimbSession struct {
|
|||||||
Duration *int64 `json:"duration,omitempty"`
|
Duration *int64 `json:"duration,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Notes *string `json:"notes,omitempty"`
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
IsDeleted bool `json:"isDeleted"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
@@ -115,7 +109,9 @@ type BackupAttempt struct {
|
|||||||
Duration *int64 `json:"duration,omitempty"`
|
Duration *int64 `json:"duration,omitempty"`
|
||||||
RestTime *int64 `json:"restTime,omitempty"`
|
RestTime *int64 `json:"restTime,omitempty"`
|
||||||
Timestamp string `json:"timestamp"`
|
Timestamp string `json:"timestamp"`
|
||||||
|
IsDeleted bool `json:"isDeleted"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt *string `json:"updatedAt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncServer struct {
|
type SyncServer struct {
|
||||||
@@ -147,7 +143,6 @@ func (s *SyncServer) loadData() (*ClimbDataBackup, error) {
|
|||||||
Problems: []BackupProblem{},
|
Problems: []BackupProblem{},
|
||||||
Sessions: []BackupClimbSession{},
|
Sessions: []BackupClimbSession{},
|
||||||
Attempts: []BackupAttempt{},
|
Attempts: []BackupAttempt{},
|
||||||
DeletedItems: []DeletedItem{},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +153,18 @@ func (s *SyncServer) loadData() (*ClimbDataBackup, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Read %d bytes from data file", len(data))
|
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
|
var backup ClimbDataBackup
|
||||||
if err := json.Unmarshal(data, &backup); err != nil {
|
if err := json.Unmarshal(data, &backup); err != nil {
|
||||||
@@ -250,7 +256,18 @@ func (s *SyncServer) mergeAttempts(existing []BackupAttempt, updates []BackupAtt
|
|||||||
|
|
||||||
for _, attempt := range updates {
|
for _, attempt := range updates {
|
||||||
if existingAttempt, exists := attemptMap[attempt.ID]; exists {
|
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
|
attemptMap[attempt.ID] = attempt
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -265,89 +282,6 @@ func (s *SyncServer) mergeAttempts(existing []BackupAttempt, updates []BackupAtt
|
|||||||
return result
|
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 {
|
func (s *SyncServer) saveData(backup *ClimbDataBackup) error {
|
||||||
backup.ExportedAt = time.Now().UTC().Format(time.RFC3339)
|
backup.ExportedAt = time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
@@ -383,6 +317,8 @@ func (s *SyncServer) handleGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
log.Printf("Sending data to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d",
|
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))
|
r.RemoteAddr, len(backup.Gyms), len(backup.Problems), len(backup.Sessions), len(backup.Attempts))
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -527,11 +463,10 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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,
|
r.RemoteAddr, deltaRequest.LastSyncTime,
|
||||||
len(deltaRequest.Gyms), len(deltaRequest.Problems),
|
len(deltaRequest.Gyms), len(deltaRequest.Problems),
|
||||||
len(deltaRequest.Sessions), len(deltaRequest.Attempts),
|
len(deltaRequest.Sessions), len(deltaRequest.Attempts))
|
||||||
len(deltaRequest.DeletedItems))
|
|
||||||
|
|
||||||
// Load current server data
|
// Load current server data
|
||||||
serverBackup, err := s.loadData()
|
serverBackup, err := s.loadData()
|
||||||
@@ -541,12 +476,9 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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
|
// 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.Gyms = s.mergeGyms(serverBackup.Gyms, deltaRequest.Gyms)
|
||||||
serverBackup.Problems = s.mergeProblems(serverBackup.Problems, deltaRequest.Problems)
|
serverBackup.Problems = s.mergeProblems(serverBackup.Problems, deltaRequest.Problems)
|
||||||
serverBackup.Sessions = s.mergeSessions(serverBackup.Sessions, deltaRequest.Sessions)
|
serverBackup.Sessions = s.mergeSessions(serverBackup.Sessions, deltaRequest.Sessions)
|
||||||
@@ -566,13 +498,6 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("Warning: Could not parse lastSyncTime '%s', sending all data", deltaRequest.LastSyncTime)
|
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
|
// Prepare response with items modified since client's last sync
|
||||||
response := DeltaSyncResponse{
|
response := DeltaSyncResponse{
|
||||||
ServerTime: time.Now().UTC().Format(time.RFC3339),
|
ServerTime: time.Now().UTC().Format(time.RFC3339),
|
||||||
@@ -580,14 +505,10 @@ func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
Problems: []BackupProblem{},
|
Problems: []BackupProblem{},
|
||||||
Sessions: []BackupClimbSession{},
|
Sessions: []BackupClimbSession{},
|
||||||
Attempts: []BackupAttempt{},
|
Attempts: []BackupAttempt{},
|
||||||
DeletedItems: []DeletedItem{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter gyms modified after client's last sync
|
// Filter gyms modified after client's last sync
|
||||||
for _, gym := range serverBackup.Gyms {
|
for _, gym := range serverBackup.Gyms {
|
||||||
if deletedItemMap["gym:"+gym.ID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
gymTime, err := time.Parse(time.RFC3339, gym.UpdatedAt)
|
gymTime, err := time.Parse(time.RFC3339, gym.UpdatedAt)
|
||||||
if err == nil && gymTime.After(clientLastSync) {
|
if err == nil && gymTime.After(clientLastSync) {
|
||||||
response.Gyms = append(response.Gyms, gym)
|
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
|
// Filter problems modified after client's last sync
|
||||||
for _, problem := range serverBackup.Problems {
|
for _, problem := range serverBackup.Problems {
|
||||||
if deletedItemMap["problem:"+problem.ID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
problemTime, err := time.Parse(time.RFC3339, problem.UpdatedAt)
|
problemTime, err := time.Parse(time.RFC3339, problem.UpdatedAt)
|
||||||
if err == nil && problemTime.After(clientLastSync) {
|
if err == nil && problemTime.After(clientLastSync) {
|
||||||
response.Problems = append(response.Problems, problem)
|
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
|
// Filter sessions modified after client's last sync
|
||||||
for _, session := range serverBackup.Sessions {
|
for _, session := range serverBackup.Sessions {
|
||||||
if deletedItemMap["session:"+session.ID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sessionTime, err := time.Parse(time.RFC3339, session.UpdatedAt)
|
sessionTime, err := time.Parse(time.RFC3339, session.UpdatedAt)
|
||||||
if err == nil && sessionTime.After(clientLastSync) {
|
if err == nil && sessionTime.After(clientLastSync) {
|
||||||
response.Sessions = append(response.Sessions, session)
|
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 {
|
for _, attempt := range serverBackup.Attempts {
|
||||||
if deletedItemMap["attempt:"+attempt.ID] {
|
attemptTime := attempt.CreatedAt
|
||||||
continue
|
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)
|
response.Attempts = append(response.Attempts, attempt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter deletions after client's last sync
|
log.Printf("Delta sync response to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d",
|
||||||
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",
|
|
||||||
r.RemoteAddr,
|
r.RemoteAddr,
|
||||||
len(response.Gyms), len(response.Problems),
|
len(response.Gyms), len(response.Problems),
|
||||||
len(response.Sessions), len(response.Attempts),
|
len(response.Sessions), len(response.Attempts))
|
||||||
len(response.DeletedItems))
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
json.NewEncoder(w).Encode(response)
|
||||||
|
|||||||
Reference in New Issue
Block a user