158 lines
4.2 KiB
Swift
158 lines
4.2 KiB
Swift
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<Void, Never>?
|
|
|
|
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
|
|
}
|