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