diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 4c2444734..bf017c1b6 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -248,6 +248,7 @@ DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */; }; DDDB86F12DF7223C00AADDAC /* DeleteAlarmSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */; }; DDDC01DD2E244B3100D9975C /* JWTManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC01DC2E244B3100D9975C /* JWTManager.swift */; }; + A1A1A10002000000A0CFEED1 /* LogRedactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A1A10002000000A0CFEED2 /* LogRedactor.swift */; }; DDDC31CC2E13A7DF009EA0F3 /* AddAlarmSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */; }; DDDC31CE2E13A811009EA0F3 /* AlarmTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */; }; DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F482D479AEF00884336 /* NoRemoteView.swift */; }; @@ -700,6 +701,7 @@ DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryTarget.swift; sourceTree = ""; }; DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAlarmSection.swift; sourceTree = ""; }; DDDC01DC2E244B3100D9975C /* JWTManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWTManager.swift; sourceTree = ""; }; + A1A1A10002000000A0CFEED2 /* LogRedactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRedactor.swift; sourceTree = ""; }; DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAlarmSheet.swift; sourceTree = ""; }; DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmTile.swift; sourceTree = ""; }; DDDF6F482D479AEF00884336 /* NoRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoRemoteView.swift; sourceTree = ""; }; @@ -1697,6 +1699,7 @@ DDF699952C5582290058A8D9 /* TextFieldWithToolBar.swift */, DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */, DDDC01DC2E244B3100D9975C /* JWTManager.swift */, + A1A1A10002000000A0CFEED2 /* LogRedactor.swift */, 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */, ); path = Helpers; @@ -2284,6 +2287,7 @@ DD4A407E2E6AFEE6007B318B /* AuthService.swift in Sources */, 654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */, DDDC01DD2E244B3100D9975C /* JWTManager.swift in Sources */, + A1A1A10002000000A0CFEED1 /* LogRedactor.swift in Sources */, DDD10F072C529DE800D76A8E /* Observable.swift in Sources */, DDFF3D852D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift in Sources */, DDCF9A882D85FD33004DF4DD /* AlarmData.swift in Sources */, diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index a6fd9f2b9..3b88bdc50 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -72,7 +72,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Observable.shared.loopFollowDeviceToken.value = tokenString - LogManager.shared.log(category: .apns, message: "Successfully registered for remote notifications with token: \(tokenString)") + LogManager.shared.log(category: .apns, message: "Successfully registered for remote notifications with token: \(LogRedactor.tail(tokenString))") } /// Called when failed to register for remote notifications @@ -82,7 +82,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { /// Called when a remote notification is received func application(_: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - LogManager.shared.log(category: .apns, message: "Received remote notification: \(userInfo)") + let userInfoKeys = userInfo.keys.compactMap { $0 as? String }.sorted() + LogManager.shared.log(category: .apns, message: "Received remote notification: keys=\(userInfoKeys)") // Check if this is a response notification from Loop or Trio if let aps = userInfo["aps"] as? [String: Any] { @@ -217,7 +218,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate { { // Log the notification let userInfo = notification.request.content.userInfo - LogManager.shared.log(category: .general, message: "Will present notification: \(userInfo)") + let userInfoKeys = userInfo.keys.compactMap { $0 as? String }.sorted() + LogManager.shared.log(category: .general, message: "Will present notification: keys=\(userInfoKeys)") // Show the notification even when app is in foreground completionHandler([.banner, .sound, .badge]) diff --git a/LoopFollow/Helpers/JWTManager.swift b/LoopFollow/Helpers/JWTManager.swift index 3e847b999..53a4d233e 100644 --- a/LoopFollow/Helpers/JWTManager.swift +++ b/LoopFollow/Helpers/JWTManager.swift @@ -45,7 +45,7 @@ class JWTManager { let signedJWT = "\(signingInput).\(signatureBase64)" cache[cacheKey] = CachedToken(jwt: signedJWT, expiresAt: Date().addingTimeInterval(ttl)) - LogManager.shared.log(category: .apns, message: "JWT generated for key \(keyId) (TTL 55 min)") + LogManager.shared.log(category: .apns, message: "JWT generated for key \(LogRedactor.keyId(keyId)) (TTL 55 min)") return signedJWT } catch { LogManager.shared.log(category: .apns, message: "Failed to sign JWT: \(error.localizedDescription)") diff --git a/LoopFollow/Helpers/LogRedactor.swift b/LoopFollow/Helpers/LogRedactor.swift new file mode 100644 index 000000000..be29edd2c --- /dev/null +++ b/LoopFollow/Helpers/LogRedactor.swift @@ -0,0 +1,170 @@ +// LoopFollow +// LogRedactor.swift + +import CryptoKit +import Foundation + +/// Helpers for masking secrets before they hit the log file. The "share logs" +/// feature exposes the on-disk log to the user, so anything sensitive that +/// flows through `LogManager.log` must be reduced to a non-recoverable form +/// while keeping enough signal (short suffix, host, fingerprint) to correlate +/// events during debugging. +enum LogRedactor { + /// Last `keep` characters of `secret`, prefixed with `…`. Matches the + /// existing `.suffix(8)` convention used in `LiveActivityManager`. + static func tail(_ secret: String, keep: Int = 8) -> String { + if secret.isEmpty { return "(empty)" } + if secret.count <= keep { return "(redacted)" } + return "…\(secret.suffix(keep))" + } + + /// First `keep` characters of `secret`, suffixed with `…`. Matches the + /// existing `.prefix(8)` convention used in `LoopAPNSService`. + static func head(_ secret: String, keep: Int = 8) -> String { + if secret.isEmpty { return "(empty)" } + if secret.count <= keep { return "(redacted)" } + return "\(secret.prefix(keep))…" + } + + /// Known managed-Nightscout host suffixes. When a URL's host ends in one + /// of these, the leading subdomain (which identifies the user) is masked + /// and the suffix is kept so engineers can tell which platform the user + /// is on. Anything else is treated as self-hosted and reduced to the TLD. + private static let knownHostSuffixes: [String] = [ + "nightscoutpro.com", + "10be.de", + "herokuapp.com", + ] + + /// Keep scheme + a redacted host hint, drop path and query. The Nightscout + /// token rides in `?token=` and the host itself identifies the user when + /// they're on a managed platform, so we mask the subdomain and keep only + /// the platform suffix (or just the TLD for self-hosted setups). + static func url(_ raw: String) -> String { + if raw.isEmpty { return "(empty)" } + if let u = URL(string: raw), let host = u.host { + let scheme = u.scheme.map { "\($0)://" } ?? "" + return "\(scheme)\(maskHost(host))/…" + } + return "(redacted)" + } + + private static func maskHost(_ host: String) -> String { + // IPv4 / IPv6 / bracketed — drop entirely. + if host.range(of: "^\\d+\\.\\d+\\.\\d+\\.\\d+$", options: .regularExpression) != nil { return "***" } + if host.contains(":") || host.hasPrefix("[") { return "***" } + + let lower = host.lowercased() + for suffix in knownHostSuffixes { + if lower == suffix || lower.hasSuffix("." + suffix) { + return "***." + suffix + } + } + + let parts = host.split(separator: ".", omittingEmptySubsequences: false) + if parts.count >= 2, let tld = parts.last, !tld.isEmpty { + return "***." + String(tld) + } + return "***" + } + + /// Apple Developer Key ID — 10-char uppercase alphanumeric. Reveals + /// last 2 chars only. + static func keyId(_ keyId: String) -> String { + if keyId.isEmpty { return "(empty)" } + if keyId.count <= 2 { return "(redacted)" } + return "…\(keyId.suffix(2))" + } + + /// Apple Team ID — 10-char uppercase alphanumeric. Reveals last 2 chars. + static func teamId(_ teamId: String) -> String { + keyId(teamId) + } + + /// App bundle id ("com.example.MyApp"). Mask the middle component(s) but + /// keep the leading TLD and trailing app name so suffixes like + /// `.watchkitapp` or `.push-type.liveactivity` remain visible. + static func bundleId(_ id: String) -> String { + if id.isEmpty { return "(empty)" } + let parts = id.split(separator: ".", omittingEmptySubsequences: false) + guard parts.count >= 3 else { return "(redacted)" } + var masked = [String]() + masked.append(String(parts[0])) + for _ in 1 ..< parts.count - 1 { + masked.append("***") + } + masked.append(String(parts[parts.count - 1])) + return masked.joined(separator: ".") + } + + /// Username (Dexcom Share, etc.). Preserves first character and any + /// `@domain` suffix shape so engineers can tell email-shaped from not. + static func username(_ name: String) -> String { + if name.isEmpty { return "(empty)" } + if name.contains("@") { + let parts = name.split(separator: "@", maxSplits: 1).map(String.init) + let local = parts[0] + let domain = parts.count > 1 ? parts[1] : "" + let firstLocal = local.first.map(String.init) ?? "?" + let firstDomain = domain.first.map(String.init) ?? "?" + return "\(firstLocal)***@\(firstDomain)***" + } + let first = name.first.map(String.init) ?? "?" + return "\(first)***" + } + + /// Sweep an arbitrary message string for high-confidence secret shapes. + /// Idempotent. Run by `LogManager.log` on every line before write. + static func sweep(_ message: String) -> String { + var out = message + out = redactPEM(out) + out = redactTokenQuery(out) + out = redactJWT(out) + return out + } + + /// Replace any `?token=…` or `&token=…` value with `***` (case-insensitive). + private static func redactTokenQuery(_ s: String) -> String { + guard let regex = try? NSRegularExpression( + pattern: "([?&]token=)[^&\\s\"'<>]+", + options: [.caseInsensitive] + ) else { return s } + let range = NSRange(s.startIndex ..< s.endIndex, in: s) + return regex.stringByReplacingMatches(in: s, options: [], range: range, withTemplate: "$1***") + } + + /// Collapse the body of a PEM PRIVATE KEY block to `(redacted)`. + private static func redactPEM(_ s: String) -> String { + guard let regex = try? NSRegularExpression( + pattern: "-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z ]*PRIVATE KEY-----", + options: [] + ) else { return s } + let range = NSRange(s.startIndex ..< s.endIndex, in: s) + return regex.stringByReplacingMatches( + in: s, options: [], range: range, + withTemplate: "-----BEGIN PRIVATE KEY----- (redacted) -----END PRIVATE KEY-----" + ) + } + + /// Collapse the middle segment of a JWT (`ey…\.ey…\.…`). + private static func redactJWT(_ s: String) -> String { + guard let regex = try? NSRegularExpression( + pattern: "ey[A-Za-z0-9_-]{8,}\\.ey[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}", + options: [] + ) else { return s } + let range = NSRange(s.startIndex ..< s.endIndex, in: s) + return regex.stringByReplacingMatches(in: s, options: [], range: range, withTemplate: "ey……") + } + + /// Non-reversible fingerprint for opaque blobs we can't safely log + /// (settings JSON, scanned QR code contents, etc.). + static func fingerprint(_ data: Data) -> String { + let digest = SHA256.hash(data: data) + let hex = digest.compactMap { String(format: "%02x", $0) }.joined() + return "\(data.count) bytes, sha256=\(hex.prefix(8))…" + } + + static func fingerprint(_ string: String) -> String { + fingerprint(Data(string.utf8)) + } +} diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 674de1923..34f8bcb08 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -86,25 +86,22 @@ class NightscoutUtils { completion(.success(decodedObject)) } } catch let decodingError as DecodingError { - print("[ERROR] Failed to decode \(T.self):") + let typeName = String(describing: T.self) switch decodingError { case let .typeMismatch(type, context): - print("Type mismatch for type \(type), context: \(context.debugDescription)") - print("Coding path:", context.codingPath) + LogManager.shared.log(category: .nightscout, message: "Decode \(typeName) typeMismatch: \(type) at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))", isDebug: true) case let .valueNotFound(type, context): - print("Value not found for type \(type), context: \(context.debugDescription)") - print("Coding path:", context.codingPath) + LogManager.shared.log(category: .nightscout, message: "Decode \(typeName) valueNotFound: \(type) at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))", isDebug: true) case let .keyNotFound(key, context): - print("Key '\(key.stringValue)' not found, context: \(context.debugDescription)") - print("Coding path:", context.codingPath) - case let .dataCorrupted(context): - print("Data corrupted, context: \(context.debugDescription)") + LogManager.shared.log(category: .nightscout, message: "Decode \(typeName) keyNotFound: '\(key.stringValue)' at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))", isDebug: true) + case .dataCorrupted: + LogManager.shared.log(category: .nightscout, message: "Decode \(typeName) dataCorrupted", isDebug: true) @unknown default: - print("Unknown decoding error") + LogManager.shared.log(category: .nightscout, message: "Decode \(typeName) unknown error", isDebug: true) } completion(.failure(decodingError)) } catch { - print("[ERROR] General error:", error) + LogManager.shared.log(category: .nightscout, message: "Decode \(T.self) general error: \(String(describing: type(of: error)))", isDebug: true) completion(.failure(error)) } } diff --git a/LoopFollow/Log/LogManager.swift b/LoopFollow/Log/LogManager.swift index 22403cd0f..8c2fa05d3 100644 --- a/LoopFollow/Log/LogManager.swift +++ b/LoopFollow/Log/LogManager.swift @@ -55,7 +55,8 @@ class LogManager { /// - limitIdentifier: Optional key to rate-limit similar log messages. /// - limitInterval: Time interval (in seconds) to wait before logging the same type again. func log(category: Category, message: String, isDebug: Bool = false, limitIdentifier: String? = nil, limitInterval: TimeInterval = 300) { - let logMessage = formattedLogMessage(for: category, message: message) + let safeMessage = LogRedactor.sweep(message) + let logMessage = formattedLogMessage(for: category, message: safeMessage) consoleQueue.async { print(logMessage) diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift index feca46e0d..2a13dd081 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift @@ -162,7 +162,7 @@ class LoopAPNSService { // Encrypt and include return notification info using OTP if let returnInfo = createReturnNotificationInfo() { - LogManager.shared.log(category: .apns, message: "Created return notification info for carbs - deviceToken: \(returnInfo.deviceToken.prefix(8))..., bundleId: \(returnInfo.bundleId)") + LogManager.shared.log(category: .apns, message: "Created return notification info for carbs - deviceToken: \(LogRedactor.head(returnInfo.deviceToken)), bundleId: \(LogRedactor.bundleId(returnInfo.bundleId))") if let encryptedReturnInfo = encryptReturnNotificationInfo(returnInfo: returnInfo, otpCode: String(payload.otp)) { finalPayload["encrypted_return_notification"] = encryptedReturnInfo LogManager.shared.log(category: .apns, message: "Added encrypted_return_notification to carbs payload, length: \(encryptedReturnInfo.count)") @@ -227,7 +227,7 @@ class LoopAPNSService { // Encrypt and include return notification info using OTP if let returnInfo = createReturnNotificationInfo() { - LogManager.shared.log(category: .apns, message: "Created return notification info for carbs - deviceToken: \(returnInfo.deviceToken.prefix(8))..., bundleId: \(returnInfo.bundleId)") + LogManager.shared.log(category: .apns, message: "Created return notification info for carbs - deviceToken: \(LogRedactor.head(returnInfo.deviceToken)), bundleId: \(LogRedactor.bundleId(returnInfo.bundleId))") if let encryptedReturnInfo = encryptReturnNotificationInfo(returnInfo: returnInfo, otpCode: String(payload.otp)) { finalPayload["encrypted_return_notification"] = encryptedReturnInfo LogManager.shared.log(category: .apns, message: "Added encrypted_return_notification to carbs payload, length: \(encryptedReturnInfo.count)") diff --git a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift index d5ea2fc4e..c05f041a2 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift @@ -201,7 +201,7 @@ class RemoteSettingsViewModel: ObservableObject { self.remoteType = .loopAPNS self.isLoopDevice = true self.isTrioDevice = false - LogManager.shared.log(category: .apns, message: "Loop APNS QR code scanned: \(code)") + LogManager.shared.log(category: .apns, message: "Loop APNS QR code scanned: \(LogRedactor.fingerprint(code))") case let .failure(error): self.loopAPNSErrorMessage = "Scanning failed: \(error.localizedDescription)" } diff --git a/LoopFollow/Remote/TRC/PushNotificationManager.swift b/LoopFollow/Remote/TRC/PushNotificationManager.swift index 04c9977c2..aa1f661a2 100644 --- a/LoopFollow/Remote/TRC/PushNotificationManager.swift +++ b/LoopFollow/Remote/TRC/PushNotificationManager.swift @@ -276,12 +276,11 @@ class PushNotificationManager { } if let httpResponse = response as? HTTPURLResponse { - print("Push notification sent.") - print("Status code: \(httpResponse.statusCode)") + LogManager.shared.log(category: .apns, message: "Push notification sent. Status code: \(httpResponse.statusCode)", isDebug: true) var responseBodyMessage = "" if let data = data, let responseBody = String(data: data, encoding: .utf8) { - print("Response body: \(responseBody)") + LogManager.shared.log(category: .apns, message: "Response body: \(responseBody)", isDebug: true) if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let reason = json["reason"] as? String { diff --git a/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift b/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift index f2eeb167f..cea659f85 100644 --- a/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift +++ b/LoopFollow/Settings/ImportExport/ImportExportSettingsViewModel.swift @@ -66,7 +66,7 @@ class ImportExportSettingsViewModel: ObservableObject { private func processImportedSettings(_ jsonString: String) { do { - LogManager.shared.log(category: .general, message: "Processing QR code data: \(jsonString.prefix(200))...") + LogManager.shared.log(category: .general, message: "Processing QR code data: \(LogRedactor.fingerprint(jsonString))") guard let data = jsonString.data(using: .utf8) else { qrCodeErrorMessage = "Invalid QR code data" @@ -78,7 +78,8 @@ class ImportExportSettingsViewModel: ObservableObject { // Try to decode as JSON first to see what we get do { let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) - LogManager.shared.log(category: .general, message: "JSON parsing successful: \(jsonObject)") + let topLevelKeys = (jsonObject as? [String: Any]).map { Array($0.keys).sorted() } ?? [] + LogManager.shared.log(category: .general, message: "JSON parsing successful: keys=\(topLevelKeys)") } catch { LogManager.shared.log(category: .general, message: "JSON parsing failed: \(error.localizedDescription)") } @@ -263,7 +264,7 @@ class ImportExportSettingsViewModel: ObservableObject { (remoteType != nil && !remoteType!.isEmpty && remoteType != "None") || alarmCount > 0 - LogManager.shared.log(category: .general, message: "Import preview check - nightscoutURL: \(nightscoutURL ?? "nil"), dexcomUsername: \(dexcomUsername ?? "nil"), remoteType: \(remoteType ?? "nil"), alarmCount: \(alarmCount), hasAnySettings: \(hasAnySettings)") + LogManager.shared.log(category: .general, message: "Import preview check - nightscoutURL: \(nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), dexcomUsername: \(dexcomUsername.map { LogRedactor.username($0) } ?? "nil"), remoteType: \(remoteType ?? "nil"), alarmCount: \(alarmCount), hasAnySettings: \(hasAnySettings)") if hasAnySettings { LogManager.shared.log(category: .general, message: "Creating import preview with settings") @@ -274,7 +275,7 @@ class ImportExportSettingsViewModel: ObservableObject { alarmCount: alarmCount, alarmNames: alarmNames ) - LogManager.shared.log(category: .general, message: "Created importPreview - nightscoutURL: \(importPreview?.nightscoutURL ?? "nil"), remoteType: \(importPreview?.remoteType ?? "nil"), alarmCount: \(importPreview?.alarmCount ?? 0)") + LogManager.shared.log(category: .general, message: "Created importPreview - nightscoutURL: \(importPreview?.nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), remoteType: \(importPreview?.remoteType ?? "nil"), alarmCount: \(importPreview?.alarmCount ?? 0)") showImportConfirmation = true LogManager.shared.log(category: .general, message: "Set showImportConfirmation = true") } else { diff --git a/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift b/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift index 188379f80..c4e2a6b20 100644 --- a/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift +++ b/LoopFollow/Settings/ImportExport/SettingsMigrationManager.swift @@ -14,11 +14,10 @@ class SettingsMigrationManager { // Try to decode with the current version do { let currentSettings = try JSONDecoder().decode(CombinedSettingsExport.self, from: data) - print("✅ Successfully decoded CombinedSettingsExport") + LogManager.shared.log(category: .general, message: "Successfully decoded CombinedSettingsExport", isDebug: true) return currentSettings } catch { - print("❌ Failed to decode CombinedSettingsExport: \(error)") - print("❌ Error details: \(error.localizedDescription)") + LogManager.shared.log(category: .general, message: "Failed to decode CombinedSettingsExport: \(String(describing: type(of: error)))", isDebug: true) // Try to decode as individual components return tryDecodeIndividualComponents(data) @@ -28,7 +27,7 @@ class SettingsMigrationManager { private static func tryDecodeIndividualComponents(_ data: Data) -> CombinedSettingsExport? { // Try to decode as AlarmSettingsExport if let alarmSettings = try? JSONDecoder().decode(AlarmSettingsExport.self, from: data) { - print("✅ Successfully decoded as AlarmSettingsExport") + LogManager.shared.log(category: .general, message: "Successfully decoded as AlarmSettingsExport", isDebug: true) return CombinedSettingsExport( alarms: alarmSettings, exportType: "Alarm Settings" @@ -37,7 +36,7 @@ class SettingsMigrationManager { // Try to decode as NightscoutSettingsExport if let nightscoutSettings = try? JSONDecoder().decode(NightscoutSettingsExport.self, from: data) { - print("✅ Successfully decoded as NightscoutSettingsExport") + LogManager.shared.log(category: .general, message: "Successfully decoded as NightscoutSettingsExport", isDebug: true) return CombinedSettingsExport( nightscout: nightscoutSettings, exportType: "Nightscout Settings" @@ -46,7 +45,7 @@ class SettingsMigrationManager { // Try to decode as DexcomSettingsExport if let dexcomSettings = try? JSONDecoder().decode(DexcomSettingsExport.self, from: data) { - print("✅ Successfully decoded as DexcomSettingsExport") + LogManager.shared.log(category: .general, message: "Successfully decoded as DexcomSettingsExport", isDebug: true) return CombinedSettingsExport( dexcom: dexcomSettings, exportType: "Dexcom Settings" @@ -55,14 +54,14 @@ class SettingsMigrationManager { // Try to decode as RemoteSettingsExport if let remoteSettings = try? JSONDecoder().decode(RemoteSettingsExport.self, from: data) { - print("✅ Successfully decoded as RemoteSettingsExport") + LogManager.shared.log(category: .general, message: "Successfully decoded as RemoteSettingsExport", isDebug: true) return CombinedSettingsExport( remote: remoteSettings, exportType: "Remote Settings" ) } - print("❌ Failed to decode as any known component") + LogManager.shared.log(category: .general, message: "Failed to decode as any known component", isDebug: true) return nil } diff --git a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift index b7d5982f6..bc2c1a98d 100644 --- a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift +++ b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift @@ -37,8 +37,6 @@ class ObservableUserDefaultsValue: ObservableObje // Notify UserDefaultsValueGroups that value has changed UserDefaultsValueGroups.valueChanged(self) - - print("Value for \(self.key) changed to \(self.value)") // Logging } } } diff --git a/LoopFollow/Storage/Framework/ObservableValue.swift b/LoopFollow/Storage/Framework/ObservableValue.swift index c03448f1e..e690b4ac2 100644 --- a/LoopFollow/Storage/Framework/ObservableValue.swift +++ b/LoopFollow/Storage/Framework/ObservableValue.swift @@ -14,7 +14,6 @@ class ObservableValue: ObservableObject { } func set(_ newValue: T) { - print("Setting new value: \(newValue)") DispatchQueue.main.async { self.value = newValue } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index ddfb2f3f0..90bf7d660 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -1070,12 +1070,12 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // after a reboot), all StorageValues were cached from encrypted UserDefaults and hold // their defaults. Reload everything from disk now that the device is unlocked, firing // Combine observers only for values that actually changed. - LogManager.shared.log(category: .general, message: "appCameToForeground: needsBFUReload=\(Storage.shared.needsBFUReload), url='\(Storage.shared.url.value)'") + LogManager.shared.log(category: .general, message: "appCameToForeground: needsBFUReload=\(Storage.shared.needsBFUReload), url='\(LogRedactor.url(Storage.shared.url.value))'") if Storage.shared.needsBFUReload { Storage.shared.needsBFUReload = false LogManager.shared.log(category: .general, message: "BFU reload triggered — reloading all StorageValues") Storage.shared.reloadAll() - LogManager.shared.log(category: .general, message: "BFU reload complete: url='\(Storage.shared.url.value)'") + LogManager.shared.log(category: .general, message: "BFU reload complete: url='\(LogRedactor.url(Storage.shared.url.value))'") // Show the loading overlay so the user sees feedback during the 2-5s // while tasks re-run with the now-correct credentials. loadingStates = ["bg": false, "profile": false, "deviceStatus": false] diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index cecb29c68..96a4b7f47 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -120,7 +120,7 @@ extension NightscoutViewController: WKNavigationDelegate, WKUIDelegate { return false } - NSLog("Should start: \(url.absoluteString)") + LogManager.shared.log(category: .nightscout, message: "Web shouldStart: \(LogRedactor.url(url.absoluteString))", isDebug: true) return true }