// // SyncLogger.swift // SportsTime // // File-based logger for sync operations. // Writes to Documents/sync_log.txt for viewing in Settings. // import Foundation final class SyncLogger: @unchecked Sendable { static let shared = SyncLogger() private let fileURL: URL private let maxLines = 500 private let queue = DispatchQueue(label: "com.88oakapps.SportsTime.synclogger") private init() { let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory fileURL = docs.appendingPathComponent("sync_log.txt") } // MARK: - Public API func log(_ message: String) { let timestamp = ISO8601DateFormatter().string(from: Date()) let line = "[\(timestamp)] \(message)\n" // Also print to console #if DEBUG print(message) #endif queue.async { [weak self] in self?.appendToFile(line) } } func readLog() -> String { queue.sync { guard FileManager.default.fileExists(atPath: fileURL.path) else { return "No sync logs yet." } return (try? String(contentsOf: fileURL, encoding: .utf8)) ?? "Failed to read log." } } func clearLog() { queue.async { [weak self] in guard let self = self else { return } try? FileManager.default.removeItem(at: self.fileURL) } } // MARK: - Private private func appendToFile(_ line: String) { if !FileManager.default.fileExists(atPath: fileURL.path) { FileManager.default.createFile(atPath: fileURL.path, contents: nil) } guard let handle = try? FileHandle(forWritingTo: fileURL) else { return } defer { try? handle.close() } handle.seekToEndOfFile() if let data = line.data(using: .utf8) { handle.write(data) } // Trim if too large trimIfNeeded() } private func trimIfNeeded() { guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { return } let lines = content.components(separatedBy: "\n") if lines.count > maxLines { let trimmed = lines.suffix(maxLines).joined(separator: "\n") try? trimmed.write(to: fileURL, atomically: true, encoding: .utf8) } } }