Files
ios-gitea-prs/GiteaPR/Logging/LogStore.swift
T
Hermes b51b2ee5c8
Build iOS App / Build .ipa and publish source (push) Failing after 5m44s
Add in-app diagnostic logging + resilient inbox; bump to 1.2
- 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.
2026-08-12 18:46:08 +00:00

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)
}
}
}