From c9c47bd1a60cc95f4623d33111da65ba3a2ca3e4 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:32:11 +0400 Subject: [PATCH] wip --- apps/ios/Shared/Model/SimpleXAPI.swift | 7 + .../Shared/Views/ChatList/ChatListView.swift | 4 +- .../Views/ChatList/ServersSummaryView.swift | 316 ++++++++++++++++++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + apps/ios/SimpleXChat/APITypes.swift | 150 +++++---- 5 files changed, 418 insertions(+), 63 deletions(-) create mode 100644 apps/ios/Shared/Views/ChatList/ServersSummaryView.swift diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 49152283ee..bc1f2b5859 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1334,6 +1334,13 @@ func apiGetVersion() throws -> CoreVersionInfo { throw r } +func getAgentServersSummary() throws -> PresentedServersSummary { + let userId = try currentUserId("getAgentServersSummary") + let r = chatSendCmdSync(.getAgentServersSummary(userId: userId)) + if case let .agentServersSummary(_, serversSummary) = r { return serversSummary } + throw r +} + private func currentUserId(_ funcName: String) throws -> Int64 { if let userId = ChatModel.shared.currentUser?.userId { return userId diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 54d689096e..07d20ba70e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -64,7 +64,7 @@ struct ChatListView: View { ConnectDesktopView() } .sheet(isPresented: $showServersSummary) { - Text("Servers summary view") + ServersSummaryView() } } @@ -138,7 +138,7 @@ struct ChatListView: View { showServersSummary = true } label: { Image(systemName: "wifi") - .foregroundColor(.accentColor) + .foregroundColor(.secondary) } } diff --git a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift new file mode 100644 index 0000000000..c3c364a7ad --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -0,0 +1,316 @@ +// +// ServersSummaryView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 25.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ServersSummaryView: View { + @State private var serversSummary: PresentedServersSummary? = nil + @State private var selectedUserCategory: PresentedUserCategory = .allUsers + @State private var selectedServerType: PresentedServerType = .smp + @State private var selectedSMPServer: String? = nil + + enum PresentedUserCategory { + case currentUser + case allUsers + } + + enum PresentedServerType { + case smp + case xftp + } + + var body: some View { + NavigationView { + viewBody() + .navigationTitle("Servers info") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + reloadButton() + } + ToolbarItem(placement: .navigationBarTrailing) { + shareButton() + } + } + } + .onAppear { + getServersSummary() + } + } + + private func shareButton() -> some View { + Button { + if let serversSummary = serversSummary { + showShareSheet(items: [encodeJSON(serversSummary)]) // TODO prettyJSON + } + } label: { + Image(systemName: "square.and.arrow.up") + } + .disabled(serversSummary == nil) + } + + private func reloadButton() -> some View { + Button { + getServersSummary() + } label: { + Image(systemName: "arrow.counterclockwise") + } + } + + @ViewBuilder private func viewBody() -> some View { + if let summ = serversSummary { + List { + Group { + Picker("User selection", selection: $selectedUserCategory) { + Text("All users").tag(PresentedUserCategory.allUsers) + Text("Current user").tag(PresentedUserCategory.currentUser) + } + .pickerStyle(.segmented) + + Picker("Server type", selection: $selectedServerType) { + Text("SMP").tag(PresentedServerType.smp) + Text("XFTP").tag(PresentedServerType.xftp) + } + .pickerStyle(.segmented) + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + switch (selectedUserCategory, selectedServerType) { + case (.allUsers, .smp): + if summ.allUsedSMP.count > 0 || summ.allPrevSMP.count > 0 || summ.allProxSMP.count > 0 { + if summ.allUsedSMP.count > 0 { + smpServersListView(summ.allUsedSMP, showReconnectButton: true, summ.statsStartedAt, "Currently used") + } + if summ.allPrevSMP.count > 0 { + smpServersListView(summ.allPrevSMP, showReconnectButton: false, summ.statsStartedAt, "Previously used") + } + if summ.allProxSMP.count > 0 { + smpServersListView(summ.allProxSMP, showReconnectButton: false, summ.statsStartedAt, "Proxied", "You are not connected to these servers directly.") + } + } else { + noCategoryInfoText() + } + case (.currentUser, .smp): + if summ.userUsedSMP.count > 0 || summ.userPrevSMP.count > 0 || summ.userProxSMP.count > 0 { + if summ.userUsedSMP.count > 0 { + smpServersListView(summ.userUsedSMP, showReconnectButton: true, summ.statsStartedAt, "Currently used") + } + if summ.userPrevSMP.count > 0 { + smpServersListView(summ.userPrevSMP, showReconnectButton: false, summ.statsStartedAt, "Previously used") + } + if summ.userProxSMP.count > 0 { + smpServersListView(summ.userProxSMP, showReconnectButton: false, summ.statsStartedAt, "Proxied", "You are not connected to these servers directly.") + } + } else { + noCategoryInfoText() + } + case (.allUsers, .xftp): + if summ.allUsedXFTP.count > 0 || summ.allPrevXFTP.count > 0 { + if summ.allUsedXFTP.count > 0 { + xftpServersListView(summ.allUsedXFTP, "Currently used") + } + if summ.allPrevXFTP.count > 0 { + xftpServersListView(summ.allPrevXFTP, "Previously used") + } + } else { + noCategoryInfoText() + } + case (.currentUser, .xftp): + if summ.userUsedXFTP.count > 0 || summ.userPrevXFTP.count > 0 { + if summ.userUsedXFTP.count > 0 { + xftpServersListView(summ.userUsedXFTP, "Currently used") + } + if summ.userPrevXFTP.count > 0 { + xftpServersListView(summ.userPrevXFTP, "Previously used") + } + } else { + noCategoryInfoText() + } + } + } + } else { + Text("No info, try to reload") + } + } + + @ViewBuilder private func smpServersListView( + _ servers: [SMPServerSummary], + showReconnectButton: Bool, + _ statsStartedAt: Date, + _ header: LocalizedStringKey? = nil, + _ footer: LocalizedStringKey? = nil + ) -> some View { + let sortedServers = servers.sorted { serverAddress($0.smpServer).compare(serverAddress($1.smpServer)) == .orderedAscending } + Section { + ForEach(sortedServers) { server in + smpServerView(server, showReconnectButton, statsStartedAt) + } + } header: { + if let header = header { + Text(header) + } + } footer: { + if let footer = footer { + Text(footer) + } + } + } + + private func smpServerView(_ server: SMPServerSummary, _ showReconnectButton: Bool, _ statsStartedAt: Date) -> some View { + NavigationLink(tag: server.id, selection: $selectedSMPServer) { + SMPServerSummaryView( + summary: server, + showReconnectButton: showReconnectButton, + statsStartedAt: statsStartedAt + ) + .navigationBarTitle("SMP server") + .navigationBarTitleDisplayMode(.large) + } label: { + Text(serverAddress(server.smpServer)) + .lineLimit(1) + } + } + + private func serverAddress(_ server: String) -> String { + parseServerAddress(server)?.hostnames.first ?? server + } + + @ViewBuilder private func xftpServersListView( + _ servers: [XFTPServerSummary], + _ header: LocalizedStringKey? = nil, + _ footer: LocalizedStringKey? = nil + ) -> some View { + let sortedServers = servers.sorted { serverAddress($0.xftpServer).compare(serverAddress($1.xftpServer)) == .orderedAscending } + Section { + ForEach(sortedServers) { server in + xftpServer(server) + } + } header: { + if let header = header { + Text(header) + } + } footer: { + if let footer = footer { + Text(footer) + } + } + } + + private func xftpServer(_ server: XFTPServerSummary) -> some View { + Text(serverAddress(server.xftpServer)) + .lineLimit(1) + } + + private func noCategoryInfoText() -> some View { + ZStack { + Rectangle() + .aspectRatio(contentMode: .fill) + .foregroundColor(Color.clear) + Text("No info") + .foregroundColor(.secondary) + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + } + + private func getServersSummary() { + do { + serversSummary = try getAgentServersSummary() + } catch let error { + logger.error("getAgentServersSummary error: \(responseError(error))") + } + } +} + +struct SMPServerSummaryView: View { + var summary: SMPServerSummary + var showReconnectButton: Bool + var statsStartedAt: Date + + var body: some View { + List { + Section { + Text(summary.smpServer) + .textSelection(.enabled) + if let known = summary.known, !known { + Button { + // TODO + } label: { + Text("TODO Add as known") + } + } + } header: { + Text("Server address") + } footer: { + if let known = summary.known, known { + // TODO open settings? + Text("Server can be configured in **Settings** -> **Network & servers** -> **SMP servers**") + } + } + + if showReconnectButton { + Section { + Button { + // TODO + } label: { + Text("TODO Reconnect") + } + } + } + + if let sess = summary.sessions { + Section("Sessions") { + infoRow("Connected", "\(sess.ssConnected)") + infoRow("Errors", "\(sess.ssErrors)") + infoRow("Connecting", "\(sess.ssConnecting)") + } + } + + if let subs = summary.subs { + Section("Subscriptions") { + infoRow("Active", "\(subs.ssActive)") + infoRow("Pending", "\(subs.ssPending)") + } + } + + if let stats = summary.stats { + Section("Statistics") { + infoRow("Messages sent directly", "\(stats._sentDirect)") + infoRow(" attempts", "\(stats._sentDirectAttempts)") + infoRow("Messages sent via proxy", "\(stats._sentViaProxy)") + infoRow(" attempts", "\(stats._sentViaProxyAttempts)") + infoRow("Messages sent to proxy", "\(stats._sentProxied)") + infoRow(" attempts", "\(stats._sentProxiedAttempts)") + infoRow("Send AUTH errors", "\(stats._sentAuthErrs)") + infoRow(" QUOTA errors", "\(stats._sentQuotaErrs)") + infoRow(" expired", "\(stats._sentExpiredErrs)") + infoRow(" other errors", "\(stats._sentOtherErrs)") + infoRow("Messages received", "\(stats._recvMsgs)") + infoRow(" duplicates", "\(stats._recvDuplicates)") + infoRow(" decryption", "\(stats._recvCryptoErrs)") + infoRow(" other errors", "\(stats._recvErrs)") + infoRow("Connections created", "\(stats._connCreated)") + infoRow(" secured", "\(stats._connSecured)") + infoRow(" completed", "\(stats._connCompleted)") + infoRow("Connections deleted", "\(stats._connDeleted)") + infoRow("Connections subscribed", "\(stats._connSubscribed)") + infoRow(" attempts", "\(stats._connSubAttempts)") + infoRow(" errors", "\(stats._connSubErrs)") + infoRow("From", localTimestamp(statsStartedAt)) + } + } + } + } +} + +#Preview { + ServersSummaryView() +} diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 7caf878110..3ef7516b6c 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -153,6 +153,7 @@ 641753592C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753542C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */; }; 6417535A2C2AC158005415B4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753552C2AC158005415B4 /* libgmpxx.a */; }; 6417535B2C2AC158005415B4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753562C2AC158005415B4 /* libffi.a */; }; + 6417535D2C2ACD77005415B4 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */; }; 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; @@ -448,6 +449,7 @@ 641753542C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; path = "Libraries/libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; sourceTree = ""; }; 641753552C2AC158005415B4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = ""; }; 641753562C2AC158005415B4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = ""; }; + 6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = ""; }; 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; @@ -802,6 +804,7 @@ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */, 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */, 18415835CBD939A9ABDC108A /* UserPicker.swift */, + 6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */, ); path = ChatList; sourceTree = ""; @@ -1256,6 +1259,7 @@ 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, 5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */, 8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */, + 6417535D2C2ACD77005415B4 /* ServersSummaryView.swift in Sources */, 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */, 5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */, 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 96a20b69fb..ec7ec60801 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -122,6 +122,7 @@ public enum ChatCommand { case apiEndCall(contact: Contact) case apiGetCallInvitations case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) + // WebRTC calls / case apiGetNetworkStatuses case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) @@ -142,6 +143,7 @@ public enum ChatCommand { case apiStandaloneFileInfo(url: String) // misc case showVersion + case getAgentServersSummary(userId: Int64) case string(String) public var cmdString: String { @@ -301,6 +303,7 @@ public enum ChatCommand { case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)" case let .apiStandaloneFileInfo(link): return "/_download info \(link)" case .showVersion: return "/version" + case let .getAgentServersSummary(userId): return "/get servers summary \(userId)" case let .string(str): return str } } @@ -435,6 +438,7 @@ public enum ChatCommand { case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" case .showVersion: return "showVersion" + case .getAgentServersSummary: return "getAgentServersSummary" case .string: return "console command" } } @@ -663,6 +667,7 @@ public enum ChatResponse: Decodable, Error { // misc case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) case cmdOk(user: UserRef?) + case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary) case chatCmdError(user_: UserRef?, chatError: ChatError) case chatError(user_: UserRef?, chatError: ChatError) case archiveImported(archiveErrors: [ArchiveError]) @@ -821,6 +826,7 @@ public enum ChatResponse: Decodable, Error { case .contactPQEnabled: return "contactPQEnabled" case .versionInfo: return "versionInfo" case .cmdOk: return "cmdOk" + case .agentServersSummary: return "agentServersSummary" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" case .archiveImported: return "archiveImported" @@ -984,6 +990,7 @@ public enum ChatResponse: Decodable, Error { case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)") case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" case .cmdOk: return noDetails + case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary)) case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) case let .chatError(u, chatError): return withUser(u, String(describing: chatError)) case let .archiveImported(archiveErrors): return String(describing: archiveErrors) @@ -2231,79 +2238,100 @@ public enum MsgType: String, Codable { case quota } -public struct AgentServersSummary: Decodable { - var usersServersSummary: Dictionary - // var totalServersSummary: ServersSummary +public struct PresentedServersSummary: Codable { + public var statsStartedAt: Date + public var currentUserServers: ServersSummary + public var allUsersServers: ServersSummary + + public var allUsedSMP: [SMPServerSummary] { self.allUsersServers.currentlyUsedSMPServers } + public var allPrevSMP: [SMPServerSummary] { self.allUsersServers.previouslyUsedSMPServers } + public var allProxSMP: [SMPServerSummary] { self.allUsersServers.onlyProxiedSMPServers } + + public var userUsedSMP: [SMPServerSummary] { self.currentUserServers.currentlyUsedSMPServers } + public var userPrevSMP: [SMPServerSummary] { self.currentUserServers.previouslyUsedSMPServers } + public var userProxSMP: [SMPServerSummary] { self.currentUserServers.onlyProxiedSMPServers } + + public var allUsedXFTP: [XFTPServerSummary] { self.allUsersServers.currentlyUsedXFTPServers } + public var allPrevXFTP: [XFTPServerSummary] { self.allUsersServers.previouslyUsedXFTPServers } + + public var userUsedXFTP: [XFTPServerSummary] { self.currentUserServers.currentlyUsedXFTPServers } + public var userPrevXFTP: [XFTPServerSummary] { self.currentUserServers.previouslyUsedXFTPServers } } -public struct ServersSummary: Decodable { - var smpServersSummary: SMPServerSummary - var xftpServersSummary: XFTPServerSummary +public struct ServersSummary: Codable { + public var currentlyUsedSMPServers: [SMPServerSummary] + public var previouslyUsedSMPServers: [SMPServerSummary] + public var onlyProxiedSMPServers: [SMPServerSummary] + public var currentlyUsedXFTPServers: [XFTPServerSummary] + public var previouslyUsedXFTPServers: [XFTPServerSummary] } -public struct SMPServerSummary: Decodable { - var smpServer: String - var usedForNewConnections: Bool - var subscriptionsSummary: SMPServerSubsSummary? - var workersSummary: SMPServerWorkersSummary - var commandsStats: [CommandStat] - var rcvMsgCounts: [SMPServerRcvMsgCounts] - var deliveryInfo: SMPServerDeliveryInfo? +public struct SMPServerSummary: Codable, Identifiable { + public var smpServer: String + public var known: Bool? + public var sessions: ServerSessions? + public var subs: SMPServerSubs? + public var stats: AgentSMPServerStatsData? + + public var id: String { smpServer } } -public struct XFTPServerSummary: Decodable { - var xftpServer: String - var usedForNewFiles: Bool - var workersSummary: SMPServerWorkersSummary - var commandsStats: [CommandStat] +public struct ServerSessions: Codable { + public var ssConnected: Int + public var ssErrors: Int + public var ssConnecting: Int } -public struct SMPServerSubsSummary: Decodable { - var activeSubscriptions: [SMPServerSubInfo] - var pendingSubscriptions: [SMPServerSubInfo] - var removedSubscriptions: [SMPServerSubInfo] +public struct SMPServerSubs: Codable { + public var ssActive: Int + public var ssPending: Int } -public struct SMPServerSubInfo: Decodable { - var rcvId: String - var subError: String? +public struct AgentSMPServerStatsData: Codable { + public var _sentDirect: Int + public var _sentViaProxy: Int + public var _sentProxied: Int + public var _sentDirectAttempts: Int + public var _sentViaProxyAttempts: Int + public var _sentProxiedAttempts: Int + public var _sentAuthErrs: Int + public var _sentQuotaErrs: Int + public var _sentExpiredErrs: Int + public var _sentOtherErrs: Int + public var _recvMsgs: Int + public var _recvDuplicates: Int + public var _recvCryptoErrs: Int + public var _recvErrs: Int + public var _connCreated: Int + public var _connSecured: Int + public var _connCompleted: Int + public var _connDeleted: Int + public var _connSubscribed: Int + public var _connSubAttempts: Int + public var _connSubErrs: Int } -public struct SMPServerWorkersSummary: Decodable { - var smpDeliveryWorkers_: Dictionary - var asyncCmdWorker_: WorkersDetails - var smpSubWorkers_: [String] +public struct XFTPServerSummary: Codable, Identifiable { + public var xftpServer: String + public var known: Bool? + public var sessions: ServerSessions? + public var stats: AgentXFTPServerStatsData? + public var rcvInProgress: Bool + public var sndInProgress: Bool + public var delInProgress: Bool + + public var id: String { xftpServer } } -public struct SMPServerRcvMsgCounts: Decodable { - var connId: String - var total: Int - var duplicate: Int -} - -public struct SMPServerDeliveryInfo: Decodable { - var host: String - var viaOnionHost: Bool - var viaSocksProxy: Bool - var smpProxy: String? -} - -public struct XFTPServerWorkersSummary: Decodable { - var rcvWorker: WorkersDetails? - var sndWorker: WorkersDetails? - var delWorker: WorkersDetails? -} - -public struct WorkersDetails: Decodable { - var restarts: Int - var hasWork: Bool - var hasAction: Bool -} - -public struct CommandStat: Decodable { - var host: String - var clientTs: Date - var cmd: String - var res: String - var count: Int +public struct AgentXFTPServerStatsData: Codable { + public var _uploads: Int + public var _uploadAttempts: Int + public var _uploadErrs: Int + public var _downloads: Int + public var _downloadAttempts: Int + public var _downloadAuthErrs: Int + public var _downloadErrs: Int + public var _deletions: Int + public var _deleteAttempts: Int + public var _deleteErrs: Int }