import SwiftUI @MainActor class AppState: ObservableObject { // API endpoints let memoryAPI = "http://localhost:3202/api/memories" let portAPI = "http://localhost:3201/api/ports" let portStatsAPI = "http://localhost:3201/api/ports/stats" let asrStatusAPI = "http://localhost:4125/api/asr-status" // Data @Published var projects: [Project] = [] @Published var portStats: PortStats = PortStats() @Published var asrStatus: ASRStatus = ASRStatus() @Published var isLoading = false @Published var lastRefresh: Date? @Published var searchText = "" // Services status @Published var memoryOnline = false @Published var portsOnline = false @Published var asrOnline = false private var refreshTask: Task? init() { startAutoRefresh() } deinit { refreshTask?.cancel() } private func startAutoRefresh() { refreshTask = Task { @MainActor [weak self] in guard let self = self else { return } // Initial refresh await self.refresh() // Auto-refresh every 30 seconds while !Task.isCancelled { try? await Task.sleep(nanoseconds: 30_000_000_000) if !Task.isCancelled { await self.refresh() } } } } func refresh() async { isLoading = true async let p: Void = fetchProjects() async let s: Void = fetchPortStats() async let a: Void = fetchASRStatus() _ = await (p, s, a) isLoading = false lastRefresh = Date() } var filteredProjects: [Project] { if searchText.isEmpty { return projects } let q = searchText.lowercased() return projects.filter { $0.name.lowercased().contains(q) || $0.slug.lowercased().contains(q) || ($0.what ?? "").lowercased().contains(q) } } var activeProjects: [Project] { projects.filter { $0.status == "active" || $0.status == "进行中" } } // MARK: - Fetch private func fetchProjects() async { guard let url = URL(string: memoryAPI) else { return } do { let (data, _) = try await URLSession.shared.data(from: url) let decoded = try JSONDecoder().decode([Project].self, from: data) projects = decoded memoryOnline = true } catch { memoryOnline = false } } private func fetchPortStats() async { guard let url = URL(string: portStatsAPI) else { return } do { let (data, _) = try await URLSession.shared.data(from: url) let decoded = try JSONDecoder().decode(PortStats.self, from: data) portStats = decoded portsOnline = true } catch { portsOnline = false } } private func fetchASRStatus() async { guard let url = URL(string: asrStatusAPI) else { return } do { let (data, _) = try await URLSession.shared.data(from: url) let decoded = try JSONDecoder().decode(ASRStatus.self, from: data) asrStatus = decoded asrOnline = true } catch { asrOnline = false } } } // MARK: - Models struct Project: Codable, Identifiable { var id: String { slug } let slug: String let name: String let path: String? let status: String? let category: String? let what: String? let updatedAt: String? let eventCount: Int? enum CodingKeys: String, CodingKey { case slug, name, path, status, category, what case updatedAt = "updated_at" case eventCount = "event_count" } } struct PortStats: Codable { var total: Int = 0 var project: Int = 0 var adhoc: Int = 0 var legacy: Int = 0 var running: Int = 0 var nextBlock: Int? enum CodingKeys: String, CodingKey { case total, project, adhoc, legacy, running case nextBlock = "next_block" } } struct ASRStatus: Codable { var total: Int = 0 var done: Int = 0 var failed: Int = 0 var chars: Int = 0 var running: Bool = false }