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.
This commit is contained in:
chick
2026-09-12 17:10:02 +08:00
parent 20632be86a
commit e01a105645
25 changed files with 318 additions and 1393 deletions
@@ -5,7 +5,7 @@ public enum RuntimeLogSource: String, Codable, Equatable, Sendable, CaseIterable
public var label: String {
switch self {
case .delivery: return "通知投递"
case .delivery: return "节点运行"
}
}
}
@@ -102,17 +102,7 @@ public struct LogCenterService: Sendable {
))
}
let deliveryEntries = probe.outcome == .failed ? [] : await deliveryLogs(instance)
result.runtime.append(contentsOf: deliveryEntries)
if probe.outcome == .failed {
modules["delivery"] = .failure(UpstreamError.notSupported)
} else if deliveryEntries.contains(where: { $0.code == "UNAUTHORIZED" }) {
modules["delivery"] = .failure(UpstreamError.unauthorized)
} else if deliveryEntries.contains(where: { $0.code == "UPSTREAM" }) {
modules["delivery"] = .failure(UpstreamError.invalidResponse)
} else {
modules["delivery"] = .success(true)
}
modules["status"] = .success(true)
result.diagnostic = diagnostic(
instance,
probe: probe,
@@ -190,41 +180,6 @@ public struct LogCenterService: Sendable {
}
}
private func deliveryLogs(_ instance: Instance) async -> [RuntimeLogEntry] {
do {
let response = try await request(.notificationLogs(limit: 50, offset: 0), instance)
let entries = Self.deliveryEntries(response, instance)
if entries.isEmpty {
throw UpstreamError.invalidResponse
}
return entries
} catch UpstreamError.unauthorized, UpstreamError.forbidden {
return [RuntimeLogEntry(
id: "delivery-\(instance.id.uuidString)-auth",
source: .delivery,
level: .warning,
message: "通知日志未同步:需要登录",
instanceID: instance.id,
instanceName: instance.name,
occurredAt: Date(),
code: "UNAUTHORIZED",
durationMs: nil
)]
} catch {
return [RuntimeLogEntry(
id: "delivery-\(instance.id.uuidString)-failed",
source: .delivery,
level: .warning,
message: "通知日志未同步:接口异常",
instanceID: instance.id,
instanceName: instance.name,
occurredAt: Date(),
code: "UPSTREAM",
durationMs: nil
)]
}
}
private func diagnostic(
_ instance: Instance,
probe: ProbeOutcome,
@@ -234,7 +189,7 @@ public struct LogCenterService: Sendable {
let featureStatus = modules
.sorted { $0.key < $1.key }
.map { key, result -> FeatureStatus in
let name = key == "connection" ? "连接" : key == "device" ? "设备" : "通知"
let name = key == "connection" ? "连接" : key == "status" ? "状态" : "设备"
switch result {
case .success:
return FeatureStatus(name: name, state: .ready)
@@ -270,39 +225,6 @@ public struct LogCenterService: Sendable {
)
}
static func deliveryEntries(
_ response: UpstreamResponse,
_ instance: Instance
) -> [RuntimeLogEntry] {
guard let root = try? JSONSerialization.jsonObject(with: response.data) as? [String: Any],
let data = root["data"] as? [String: Any],
let raw = data["logs"] as? [[String: Any]]
else {
return []
}
return raw.compactMap { item in
guard let id = Self.text(item["id"]),
let status = Self.text(item["status"]),
let eventType = Self.text(item["event_type"])
else {
return nil
}
let level: RuntimeLogLevel = status == "failed" ? .error : .info
return RuntimeLogEntry(
id: "delivery-\(instance.id.uuidString)-\(id)",
source: .delivery,
level: level,
message: Self.text(item["message"]) ?? "\(eventType) · \(status)",
eventType: eventType,
instanceID: instance.id,
instanceName: instance.name,
occurredAt: Self.date(item["created_at"]),
code: status,
durationMs: nil
)
}
}
private func request(_ route: UpstreamRoute, _ instance: Instance) async throws -> UpstreamResponse {
var cookie = await sessions.cookie(for: instance.id, origin: instance.origin)
do {
@@ -8,11 +8,14 @@ struct LogsView: View {
}
@StateObject private var model: LogsViewModel
private let autoRefreshOnAppear: Bool
@State private var tab: Tab = .runtime
@State private var showsPruneDialog = false
init(model: LogsViewModel? = nil) {
_model = StateObject(wrappedValue: model ?? DefaultContainer.makeLogsModel())
let resolvedModel = model ?? DefaultContainer.makeLogsModel(autoRefreshOnAppear: false)
_model = StateObject(wrappedValue: resolvedModel)
self.autoRefreshOnAppear = resolvedModel.autoRefreshOnAppear
}
var body: some View {
@@ -81,7 +84,9 @@ struct LogsView: View {
await model.refresh()
}
.task {
await model.refresh()
if model.snapshot == nil && autoRefreshOnAppear {
await model.refresh()
}
}
}
@@ -16,15 +16,18 @@ final class LogsViewModel: ObservableObject {
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
store: any LogStore,
autoRefreshOnAppear: Bool = true
) {
self.repository = repository
self.service = service
self.store = store
self.autoRefreshOnAppear = autoRefreshOnAppear
}
var filteredRuntime: [RuntimeLogEntry] {