Build iOS App / Build .ipa and publish source (push) Failing after 5m44s
- LogStore: bounded in-memory + persisted log of API calls and errors, viewable and shareable from Settings → View logs. - GiteaClient: log every request/response status (esp. 404s). - Inbox: a single failing repo no longer wipes the whole inbox — per-repo errors are caught, logged, and the rest still load. - Bump to 1.2 so SideStore offers the update.
84 lines
2.6 KiB
Swift
84 lines
2.6 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
/// In-app diagnostic log. Keeps a bounded in-memory ring buffer and persists
|
|
/// the last N entries to UserDefaults so they survive relaunches and can be
|
|
/// shared from the Logs screen in Settings.
|
|
@MainActor
|
|
final class LogStore: ObservableObject {
|
|
static let shared = LogStore()
|
|
|
|
struct Entry: Identifiable, Equatable {
|
|
let id: UUID
|
|
let timestamp: Date
|
|
let level: Level
|
|
let message: String
|
|
|
|
enum Level: String {
|
|
case info = "INFO"
|
|
case warn = "WARN"
|
|
case error = "ERROR"
|
|
}
|
|
}
|
|
|
|
@Published private(set) var entries: [Entry] = []
|
|
|
|
private let maxEntries = 500
|
|
private let persistKey = "giteapr.log.entries"
|
|
|
|
private init() {
|
|
load()
|
|
}
|
|
|
|
func log(_ level: Entry.Level, _ message: String) {
|
|
let entry = Entry(id: UUID(), timestamp: Date(), level: level, message: message)
|
|
entries.append(entry)
|
|
if entries.count > maxEntries {
|
|
entries.removeFirst(entries.count - maxEntries)
|
|
}
|
|
persist()
|
|
}
|
|
|
|
func clear() {
|
|
entries.removeAll()
|
|
UserDefaults.standard.removeObject(forKey: persistKey)
|
|
}
|
|
|
|
/// Plain-text rendering for sharing / copy.
|
|
var text: String {
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime]
|
|
return entries.map { entry in
|
|
"[\(f.string(from: entry.timestamp))] \(entry.level.rawValue) \(entry.message)"
|
|
}.joined(separator: "\n")
|
|
}
|
|
|
|
private func persist() {
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime]
|
|
let payload = entries.map { entry in
|
|
["t": f.string(from: entry.timestamp),
|
|
"l": entry.level.rawValue,
|
|
"m": entry.message]
|
|
}
|
|
if let data = try? JSONSerialization.data(withJSONObject: payload) {
|
|
UserDefaults.standard.set(data, forKey: persistKey)
|
|
}
|
|
}
|
|
|
|
private func load() {
|
|
guard let data = UserDefaults.standard.data(forKey: persistKey),
|
|
let payload = try? JSONSerialization.jsonObject(with: data) as? [[String: String]] else {
|
|
return
|
|
}
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime]
|
|
entries = payload.compactMap { dict in
|
|
guard let t = dict["t"], let date = f.date(from: t),
|
|
let l = dict["l"], let m = dict["m"],
|
|
let level = Entry.Level(rawValue: l) else { return nil }
|
|
return Entry(id: UUID(), timestamp: date, level: level, message: m)
|
|
}
|
|
}
|
|
}
|