Files
multi-simadmin/apps/ios/Sources/MultiSimAdmin/Features/Logs/LogsViewModel.swift
T
chick e01a105645 feat(ios): simplify navigation and ship 0.2.0
Move diagnostics into settings, remove the app-local notification module, unify the global theme switcher, refine privacy copy and the instance detail experience, and rebuild the app icon.
2026-09-12 17:10:02 +08:00

132 lines
5.0 KiB
Swift

import Foundation
@MainActor
final class LogsViewModel: ObservableObject {
@Published private(set) var snapshot: LogSnapshot?
@Published private(set) var instances: [Instance] = []
@Published private(set) var isLoading = false
@Published var errorMessage: String?
@Published var selectedSource: RuntimeLogSource?
@Published var selectedLevel: RuntimeLogLevel?
@Published var selectedOutcome: ConnectionOutcome?
@Published var selectedInstanceID: UUID?
@Published var searchText = ""
private let repository: any InstanceRepository
private let service: LogCenterService
private let store: any LogStore
let autoRefreshOnAppear: Bool
init(
repository: any InstanceRepository,
service: LogCenterService,
store: any LogStore,
autoRefreshOnAppear: Bool = true
) {
self.repository = repository
self.service = service
self.store = store
self.autoRefreshOnAppear = autoRefreshOnAppear
}
var filteredRuntime: [RuntimeLogEntry] {
guard let snapshot else { return [] }
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return snapshot.runtime.filter { entry in
(selectedSource == nil || entry.source == selectedSource)
&& (selectedLevel == nil || entry.level == selectedLevel)
&& (selectedInstanceID == nil || entry.instanceID == selectedInstanceID)
&& (query.isEmpty || matches(entry.message, query)
|| matches(entry.code, query)
|| matches(entry.eventType, query)
|| matches(entry.instanceName, query))
}
}
var filteredConnections: [ConnectionLogEntry] {
guard let snapshot else { return [] }
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return snapshot.connections.filter { entry in
(selectedOutcome == nil || entry.outcome == selectedOutcome)
&& (selectedInstanceID == nil || entry.instanceID == selectedInstanceID)
&& (query.isEmpty || matches(entry.errorCode, query)
|| matches(entry.instanceName, query))
}
}
var filteredDiagnostics: [InstanceDiagnostic] {
guard let snapshot else { return [] }
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !query.isEmpty else { return snapshot.diagnostics }
return snapshot.diagnostics.filter { diagnostic in
matches(diagnostic.name, query)
|| matches(diagnostic.origin, query)
|| diagnostic.tags.contains { matches($0, query) }
}
}
var offlineCount: Int {
snapshot?.diagnostics.filter { $0.state == .offline }.count ?? 0
}
func refresh() async {
isLoading = true
errorMessage = nil
do {
let loaded = try await repository.list(InstanceQuery())
instances = loaded
let fetched = try await service.refresh(loaded)
let storedRuntime = try await store.runtimeLogs()
let storedConnections = try await store.connectionLogs()
var runtime = fetched.runtime
for entry in storedRuntime where !runtime.contains(where: { $0.id == entry.id }) {
runtime.append(entry)
}
runtime.sort { $0.occurredAt > $1.occurredAt }
var connections = fetched.connections
for entry in storedConnections where !connections.contains(where: { $0.id == entry.id }) {
connections.append(entry)
}
connections.sort { $0.observedAt > $1.observedAt }
snapshot = LogSnapshot(
observedAt: fetched.observedAt,
runtime: Array(runtime.prefix(500)),
counts: Self.recount(runtime),
connections: Array(connections.prefix(200)),
diagnostics: fetched.diagnostics
)
} catch {
errorMessage = "无法加载日志中心。"
}
isLoading = false
}
func pruneConnections(olderThanDays days: Int) async -> Int {
let cutoff = Date().addingTimeInterval(TimeInterval(-days * 24 * 60 * 60))
do {
let removed = try await store.pruneConnections(before: cutoff)
if removed > 0 {
await refresh()
}
return removed
} catch {
errorMessage = "清理连接日志失败。"
return 0
}
}
private func matches(_ value: String?, _ query: String) -> Bool {
value?.localizedLowercase.contains(query) == true
}
private static func recount(_ runtime: [RuntimeLogEntry]) -> [RuntimeLogSource: Int] {
var counts: [RuntimeLogSource: Int] = [:]
for source in RuntimeLogSource.allCases {
counts[source] = runtime.filter { $0.source == source }.count
}
return counts
}
}