From dda2d365c3eb1cf6542a1d8908ab4aae4467b33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 29 Apr 2026 13:30:34 +0200 Subject: [PATCH] Default debug logging on and prompt for description when sharing logs Flip the debugLogLevel default to true and add a migration step so existing users with it stored as false also get it enabled, ensuring shared logs contain useful detail when reporting problems. When the user taps Share Logs, present a sheet asking for a short description of the issue. The description is written to a ShareNotice_.txt file (date, app version, branch+sha, user description) and included alongside the log files in the iOS share sheet. --- LoopFollow.xcodeproj/project.pbxproj | 4 ++ LoopFollow/Settings/ShareLogNoticeView.swift | 37 ++++++++++++ LoopFollow/Storage/Storage+Migrate.swift | 7 +++ LoopFollow/Storage/Storage.swift | 2 +- .../ViewControllers/MainViewController.swift | 5 ++ .../MoreMenuViewController.swift | 56 ++++++++++++++++++- 6 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 LoopFollow/Settings/ShareLogNoticeView.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 4c2444734..645d31549 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 6589CC6D2E9E7D1600BB18FE /* CalendarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC582E9E7D1600BB18FE /* CalendarSettingsView.swift */; }; 6589CC6E2E9E7D1600BB18FE /* SettingsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */; }; 6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC572E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift */; }; + 6589CC712E9E7D1600BB18FE /* ShareLogNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */; }; 6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */; }; 6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */; }; 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; }; @@ -506,6 +507,7 @@ 6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsView.swift; sourceTree = ""; }; 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMenuView.swift; sourceTree = ""; }; 6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = ""; }; + 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogNoticeView.swift; sourceTree = ""; }; 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSelectionView.swift; sourceTree = ""; }; 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationManager.swift; sourceTree = ""; }; 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = ""; }; @@ -968,6 +970,7 @@ 6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */, 657F98172F043D8100F732BD /* HomeContentView.swift */, 6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */, + 6589CC702E9E7D1600BB18FE /* ShareLogNoticeView.swift */, 6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */, ); path = Settings; @@ -2249,6 +2252,7 @@ 6589CC6E2E9E7D1600BB18FE /* SettingsMenuView.swift in Sources */, 657F98182F043D8100F732BD /* HomeContentView.swift in Sources */, 6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */, + 6589CC712E9E7D1600BB18FE /* ShareLogNoticeView.swift in Sources */, DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */, DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */, DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */, diff --git a/LoopFollow/Settings/ShareLogNoticeView.swift b/LoopFollow/Settings/ShareLogNoticeView.swift new file mode 100644 index 000000000..5b6b3fe3b --- /dev/null +++ b/LoopFollow/Settings/ShareLogNoticeView.swift @@ -0,0 +1,37 @@ +// LoopFollow +// ShareLogNoticeView.swift + +import SwiftUI + +struct ShareLogNoticeView: View { + @State private var noticeText: String = "" + let onCancel: () -> Void + let onShare: (String) -> Void + + var body: some View { + NavigationView { + Form { + Section { + Text("Thanks for sharing these logs to help us find the problem. Please describe it in as much detail as possible — what time did it happen, what did you do, and what did you expect to happen that didn't?") + .font(.callout) + .foregroundColor(.secondary) + } + + Section(header: Text("Description")) { + TextEditor(text: $noticeText) + .frame(minHeight: 180) + } + } + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) + .navigationBarTitle("Share Logs", displayMode: .inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel", action: onCancel) + } + ToolbarItem(placement: .confirmationAction) { + Button("Share") { onShare(noticeText) } + } + } + } + } +} diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 70cd30dc1..fbd596ad9 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -48,6 +48,13 @@ extension Storage { UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: legacyNotificationIDs) } + func migrateStep8() { + // Default for debugLogLevel changed from false to true so users ship useful + // logs when they report a problem. Force-enable for existing users. + LogManager.shared.log(category: .general, message: "Running migrateStep8 — enabling debug log level") + debugLogLevel.value = true + } + func migrateStep6() { // APNs credential separation LogManager.shared.log(category: .general, message: "Running migrateStep6 — APNs credential separation") diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 5dea5ebb1..e7b09931f 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -39,7 +39,7 @@ class Storage { var selectedBLEDevice = StorageValue(key: "selectedBLEDevice", defaultValue: nil) - var debugLogLevel = StorageValue(key: "debugLogLevel", defaultValue: false) + var debugLogLevel = StorageValue(key: "debugLogLevel", defaultValue: true) var contactTrend = StorageValue(key: "contactTrend", defaultValue: .off) var contactDelta = StorageValue(key: "contactDelta", defaultValue: .off) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index ddfb2f3f0..07b1cfa33 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -1056,6 +1056,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele Storage.shared.migrateStep7() Storage.shared.migrationStep.value = 7 } + + if Storage.shared.migrationStep.value < 8 { + Storage.shared.migrateStep8() + Storage.shared.migrationStep.value = 8 + } } @objc func appDidBecomeActive() { diff --git a/LoopFollow/ViewControllers/MoreMenuViewController.swift b/LoopFollow/ViewControllers/MoreMenuViewController.swift index 2693cde33..bbd24292e 100644 --- a/LoopFollow/ViewControllers/MoreMenuViewController.swift +++ b/LoopFollow/ViewControllers/MoreMenuViewController.swift @@ -343,10 +343,64 @@ class MoreMenuViewController: UIViewController { presentSimpleAlert(title: "No Logs Available", message: "There are no logs to share.") return } - let avc = UIActivityViewController(activityItems: files, applicationActivities: nil) + + let noticeView = ShareLogNoticeView( + onCancel: { [weak self] in + self?.dismiss(animated: true) + }, + onShare: { [weak self] noticeText in + self?.dismiss(animated: true) { + self?.presentLogShareSheet(noticeText: noticeText, logFiles: files) + } + } + ) + let host = UIHostingController(rootView: noticeView) + host.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + host.modalPresentationStyle = .formSheet + present(host, animated: true) + } + + private func presentLogShareSheet(noticeText: String, logFiles: [URL]) { + var items: [Any] = logFiles + if let noticeURL = writeShareNoticeFile(text: noticeText) { + items.insert(noticeURL, at: 0) + } + let avc = UIActivityViewController(activityItems: items, applicationActivities: nil) present(avc, animated: true) } + private func writeShareNoticeFile(text: String) -> URL? { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd_HHmm" + let timestamp = formatter.string(from: Date()) + + let version = AppVersionManager().version() + let branchAndSha = BuildDetails.default.branchAndSha + + let body = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + ? "(no description provided)" + : text + + let contents = """ + LoopFollow Log Share Notice + Date: \(ISO8601DateFormatter().string(from: Date())) + App version: \(version) (\(branchAndSha)) + + User description: + \(body) + """ + + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("ShareNotice_\(timestamp).txt") + do { + try contents.write(to: url, atomically: true, encoding: .utf8) + return url + } catch { + LogManager.shared.log(category: .general, message: "Failed to write share notice file: \(error)") + return nil + } + } + private func openURL(_ urlString: String) { if let url = URL(string: urlString) { UIApplication.shared.open(url)