6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
107 lines
2.6 KiB
Swift
107 lines
2.6 KiB
Swift
import Foundation
|
|
import Combine
|
|
|
|
// MARK: - Models
|
|
|
|
struct DashboardMetrics: Codable {
|
|
let activeAgents: Int
|
|
let tasksCompleted: Int
|
|
let tasksFailed: Int
|
|
let avgResponseTimeMs: Double
|
|
let uptimePercent: Double
|
|
let alertCount: Int
|
|
|
|
static let empty = DashboardMetrics(
|
|
activeAgents: 0,
|
|
tasksCompleted: 0,
|
|
tasksFailed: 0,
|
|
avgResponseTimeMs: 0,
|
|
uptimePercent: 0,
|
|
alertCount: 0
|
|
)
|
|
}
|
|
|
|
struct AuditEntry: Identifiable, Codable {
|
|
let id: UUID
|
|
let timestamp: Date
|
|
let actor: String
|
|
let action: String
|
|
let resource: String
|
|
let outcome: AuditOutcome
|
|
let detail: String?
|
|
|
|
enum AuditOutcome: String, Codable {
|
|
case success, failure, warning
|
|
}
|
|
}
|
|
|
|
// MARK: - ViewModel
|
|
|
|
@MainActor
|
|
final class DashboardViewModel: ObservableObject {
|
|
|
|
@Published private(set) var metrics: DashboardMetrics = .empty
|
|
@Published private(set) var recentAudit: [AuditEntry] = []
|
|
@Published private(set) var isLoading: Bool = false
|
|
@Published private(set) var error: Error?
|
|
|
|
private var pollingTask: Task<Void, Never>?
|
|
private let pollingInterval: TimeInterval
|
|
private let apiService: APIServiceProtocol
|
|
|
|
init(apiService: APIServiceProtocol = APIService.shared, pollingInterval: TimeInterval = 30) {
|
|
self.apiService = apiService
|
|
self.pollingInterval = pollingInterval
|
|
}
|
|
|
|
deinit {
|
|
pollingTask?.cancel()
|
|
}
|
|
|
|
// MARK: - Public
|
|
|
|
func loadDashboard() async {
|
|
guard !isLoading else { return }
|
|
isLoading = true
|
|
error = nil
|
|
|
|
do {
|
|
async let fetchedMetrics = apiService.fetchMetrics()
|
|
async let fetchedAudit = apiService.fetchRecentAudit(limit: 20)
|
|
|
|
let (m, a) = try await (fetchedMetrics, fetchedAudit)
|
|
metrics = m
|
|
recentAudit = a
|
|
} catch {
|
|
self.error = error
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
|
|
func startPolling() {
|
|
pollingTask?.cancel()
|
|
pollingTask = Task { [weak self] in
|
|
guard let self else { return }
|
|
await self.loadDashboard()
|
|
while !Task.isCancelled {
|
|
try? await Task.sleep(for: .seconds(self.pollingInterval))
|
|
guard !Task.isCancelled else { break }
|
|
await self.loadDashboard()
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopPolling() {
|
|
pollingTask?.cancel()
|
|
pollingTask = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - API protocol
|
|
|
|
protocol APIServiceProtocol {
|
|
func fetchMetrics() async throws -> DashboardMetrics
|
|
func fetchRecentAudit(limit: Int) async throws -> [AuditEntry]
|
|
}
|