This commit is contained in:
spaced4ndy
2024-06-25 17:32:11 +04:00
parent c2eb510b4a
commit c9c47bd1a6
5 changed files with 418 additions and 63 deletions
+7
View File
@@ -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
@@ -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)
}
}
@@ -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()
}
@@ -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 = "<group>"; };
641753552C2AC158005415B4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = "<group>"; };
641753562C2AC158005415B4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = "<group>"; };
6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
@@ -802,6 +804,7 @@
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
18415835CBD939A9ABDC108A /* UserPicker.swift */,
6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */,
);
path = ChatList;
sourceTree = "<group>";
@@ -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 */,
+89 -61
View File
@@ -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<Int64, ServersSummary>
// 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<String, WorkersDetails>
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
}