Преглед изворни кода

Release/0.1.6 (#15)

* pumpUpdateInProgress set false on pump error

* another fix

* Application group

* get glucose from shared group

* Pump history and carbs deletion

* Bump version
Ivan пре 5 година
родитељ
комит
9381d04cf6

+ 40 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -9,10 +9,12 @@
 /* Begin PBXBuildFile section */
 		041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */; };
 		0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */; };
+		0D9A5E34A899219C5C4CDFAF /* DataTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* DataTableViewModel.swift */; };
 		17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8D5F457B5AFF763F8CF3DF /* CREditorProvider.swift */; };
 		19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAEF7B34EEC71B3A7B800C /* BolusBuilder.swift */; };
 		1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505E09DC17A0C3D0AF4B66FE /* ISFEditorViewModel.swift */; };
 		1D086541F369D339A74893AC /* BasalProfileEditorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA56D2DCAB9E0A8AF24D984 /* BasalProfileEditorBuilder.swift */; };
+		1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* DataTableProvider.swift */; };
 		1FF95E8F785B28961EFDE5A9 /* ManualTempBasalBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A480F6EA37954BDE0DB4B64C /* ManualTempBasalBuilder.swift */; };
 		23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19984D62EFC0035A9E9644D /* BolusProvider.swift */; };
 		25548F1F0AA8E42FF5F96DBA /* PumpSettingsEditorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CAE3534904CDCA0F367017 /* PumpSettingsEditorBuilder.swift */; };
@@ -220,12 +222,14 @@
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
 		7F7017AA5C69838FB7E6FECE /* TargetsEditorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3409A5984BB4171EC484266B /* TargetsEditorBuilder.swift */; };
+		7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */; };
 		88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */; };
 		891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF98E22A39CD656A230704 /* AutotuneConfigProvider.swift */; };
 		8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */; };
 		8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */; };
 		91732A8060347C0E67024D80 /* AutotuneConfigBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B260840169E712C05ACC1F /* AutotuneConfigBuilder.swift */; };
 		919DBD08F13BAFB180DF6F47 /* AddTempTargetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C3B5FD881CA45DFDEE0EDA9 /* AddTempTargetViewModel.swift */; };
+		937407F04370AAB478C112D1 /* DataTableBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63869A4B6CAF91EB974D1581 /* DataTableBuilder.swift */; };
 		9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */; };
 		97C1388354C7133C1D5ED72A /* PreferencesEditorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E08D9D69E5B052E5C9E8BD32 /* PreferencesEditorBuilder.swift */; };
 		9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A48AE3AC813A49A517846A /* NightscoutConfigViewModel.swift */; };
@@ -242,6 +246,7 @@
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CDB87FA71A93F3739D3D338E /* NightscoutConfigBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111579A6E3AC6BFA79C4DD43 /* NightscoutConfigBuilder.swift */; };
 		D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */; };
+		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		D76333C9256787610B3B4875 /* AutotuneConfigViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D295A3F870E826BE371C0BB5 /* AutotuneConfigViewModel.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
@@ -490,7 +495,9 @@
 		5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetDataFlow.swift; sourceTree = "<group>"; };
 		5D5B4F8B4194BB7E260EF251 /* ConfigEditorViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorViewModel.swift; sourceTree = "<group>"; };
 		5F48C3AC770D4CCD0EA2B0C2 /* AddCarbsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsDataFlow.swift; sourceTree = "<group>"; };
+		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
 		618E62C9757B2F95431B5DC0 /* AddCarbsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsProvider.swift; sourceTree = "<group>"; };
+		63869A4B6CAF91EB974D1581 /* DataTableBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableBuilder.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CREditorViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorViewModel.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
 		680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalProvider.swift; sourceTree = "<group>"; };
@@ -502,16 +509,19 @@
 		7E22146D3DF4853786C78132 /* CREditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorDataFlow.swift; sourceTree = "<group>"; };
 		86FC1CFD647CF34508AF9A3B /* AddCarbsRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsRootView.swift; sourceTree = "<group>"; };
 		8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigRootView.swift; sourceTree = "<group>"; };
+		881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableRootView.swift; sourceTree = "<group>"; };
 		8A965332F237348B119FB858 /* PreferencesEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorRootView.swift; sourceTree = "<group>"; };
 		8C3B5FD881CA45DFDEE0EDA9 /* AddTempTargetViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetViewModel.swift; sourceTree = "<group>"; };
 		8CF5ACEE1F0859670E71B2C0 /* AutotuneConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigRootView.swift; sourceTree = "<group>"; };
 		8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigDataFlow.swift; sourceTree = "<group>"; };
 		91B260840169E712C05ACC1F /* AutotuneConfigBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigBuilder.swift; sourceTree = "<group>"; };
 		920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorRootView.swift; sourceTree = "<group>"; };
+		9455FA2D92E77A6C4AFED8A3 /* DataTableViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableViewModel.swift; sourceTree = "<group>"; };
 		96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalDataFlow.swift; sourceTree = "<group>"; };
 		9C8D5F457B5AFF763F8CF3DF /* CREditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorProvider.swift; sourceTree = "<group>"; };
 		9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorProvider.swift; sourceTree = "<group>"; };
 		A0A48AE3AC813A49A517846A /* NightscoutConfigViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigViewModel.swift; sourceTree = "<group>"; };
+		A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableDataFlow.swift; sourceTree = "<group>"; };
 		A480F6EA37954BDE0DB4B64C /* ManualTempBasalBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalBuilder.swift; sourceTree = "<group>"; };
 		A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigProvider.swift; sourceTree = "<group>"; };
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorViewModel.swift; sourceTree = "<group>"; };
@@ -584,6 +594,14 @@
 			path = ConfigEditor;
 			sourceTree = "<group>";
 		};
+		0EE66DD474AFFD4FD787D5B9 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		18B49BC9587A59E3A347C1CD /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -612,6 +630,7 @@
 				C2C98283C436DB934D7E7994 /* Bolus */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				E42231DBF0DBE2B4B92D1B15 /* CREditor */,
+				9E56E3626FAD933385101B76 /* DataTable */,
 				3811DE2725C9D49500A708ED /* Home */,
 				D8F047E14D567F2B5DBEFD96 /* ISFEditor */,
 				3811DE7025C9D6D300A708ED /* Login */,
@@ -1333,6 +1352,18 @@
 			path = PumpConfig;
 			sourceTree = "<group>";
 		};
+		9E56E3626FAD933385101B76 /* DataTable */ = {
+			isa = PBXGroup;
+			children = (
+				63869A4B6CAF91EB974D1581 /* DataTableBuilder.swift */,
+				A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */,
+				60744C3E9BB3652895C908CC /* DataTableProvider.swift */,
+				9455FA2D92E77A6C4AFED8A3 /* DataTableViewModel.swift */,
+				0EE66DD474AFFD4FD787D5B9 /* View */,
+			);
+			path = DataTable;
+			sourceTree = "<group>";
+		};
 		A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */ = {
 			isa = PBXGroup;
 			children = (
@@ -1780,6 +1811,11 @@
 				891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */,
 				D76333C9256787610B3B4875 /* AutotuneConfigViewModel.swift in Sources */,
 				A05235B9112E677ED03B6E8E /* AutotuneConfigRootView.swift in Sources */,
+				937407F04370AAB478C112D1 /* DataTableBuilder.swift in Sources */,
+				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
+				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,
+				0D9A5E34A899219C5C4CDFAF /* DataTableViewModel.swift in Sources */,
+				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -1925,13 +1961,14 @@
 		388E596825AD948E0019842D /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				APP_GROUP_ID = "group.$(PRODUCT_BUNDLE_IDENTIFIER)";
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "";
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = BA7ZHP4963;
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
@@ -1950,13 +1987,14 @@
 		388E596925AD948E0019842D /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				APP_GROUP_ID = "group.$(PRODUCT_BUNDLE_IDENTIFIER)";
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "";
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = BA7ZHP4963;
 				ENABLE_PREVIEWS = YES;
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;

+ 1 - 1
FreeAPS/Resources/Config.xcconfig

@@ -1 +1 @@
-BUILD_VERSION = 0.1.5
+BUILD_VERSION = 0.1.6

+ 6 - 1
FreeAPS/Resources/FreeAPS.entitlements

@@ -1,5 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
-<dict/>
+<dict>
+	<key>com.apple.security.application-groups</key>
+	<array>
+		<string>$(APP_GROUP_ID)</string>
+	</array>
+</dict>
 </plist>

+ 2 - 0
FreeAPS/Resources/Info.plist

@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>AppGroupID</key>
+	<string>$(APP_GROUP_ID)</string>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 	<key>CFBundleExecutable</key>

+ 2 - 0
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -163,6 +163,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
     func pumpManagerBLEHeartbeatDidFire(_: PumpManager) {
         debug(.deviceManager, "Pump Heartbeat")
+        pumpUpdateInProgress = false
         heartbeat(date: Date(), force: false)
     }
 
@@ -210,6 +211,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
     func pumpManager(_: PumpManager, didError error: PumpManagerError) {
         info(.deviceManager, "error: \(error.localizedDescription)")
+        pumpUpdateInProgress = false
     }
 
     func pumpManager(

+ 70 - 1
FreeAPS/Sources/APS/GlucoseManager.swift

@@ -28,7 +28,12 @@ final class BaseGlucoseManager: GlucoseManager, Injectable {
                 return Publishers.CombineLatest3(
                     Just(date),
                     Just(self.glucoseStogare.syncDate()),
-                    self.nightscoutManager.fetchGlucose()
+                    Publishers.CombineLatest(
+                        self.nightscoutManager.fetchGlucose(),
+                        self.fetchGlucoseFromSgaredGroup()
+                    )
+                    .map { [$0, $1].flatMap { $0 } }
+                    .eraseToAnyPublisher()
                 )
                 .eraseToAnyPublisher()
             }
@@ -44,4 +49,68 @@ final class BaseGlucoseManager: GlucoseManager, Injectable {
             .store(in: &lifetime)
         timer.resume()
     }
+
+    private func fetchGlucoseFromSgaredGroup() -> AnyPublisher<[BloodGlucose], Never> {
+        guard let suiteName = Bundle.main.appGroupSuiteName,
+              let sharedDefaults = UserDefaults(suiteName: suiteName)
+        else {
+            return Just([]).eraseToAnyPublisher()
+        }
+
+        return Just(fetchLastBGs(60, sharedDefaults)).eraseToAnyPublisher()
+    }
+
+    private func fetchLastBGs(_ count: Int, _ sharedDefaults: UserDefaults) -> [BloodGlucose] {
+        guard let sharedData = sharedDefaults.data(forKey: "latestReadings") else {
+            return []
+        }
+
+        let decoded = try? JSONSerialization.jsonObject(with: sharedData, options: [])
+        guard let sgvs = decoded as? [AnyObject] else {
+            return []
+        }
+
+        var results: [BloodGlucose] = []
+        for sgv in sgvs.prefix(count) {
+            guard
+                let glucose = sgv["Value"] as? Int,
+                let direction = sgv["direction"] as? String,
+                let timestamp = sgv["DT"] as? String,
+                let date = parseDate(timestamp)
+            else { continue }
+
+            results.append(
+                BloodGlucose(
+                    _id: UUID().uuidString,
+                    sgv: glucose,
+                    direction: BloodGlucose.Direction(rawValue: direction),
+                    date: Decimal(Int(date.timeIntervalSince1970 * 1000)),
+                    dateString: date,
+                    filtered: nil,
+                    noise: nil,
+                    glucose: glucose
+                )
+            )
+        }
+        return results
+    }
+
+    private func parseDate(_ timestamp: String) -> Date? {
+        // timestamp looks like "/Date(1462404576000)/"
+        guard let re = try? NSRegularExpression(pattern: "\\((.*)\\)"),
+              let match = re.firstMatch(in: timestamp, range: NSMakeRange(0, timestamp.count))
+        else {
+            return nil
+        }
+
+        let matchRange = match.range(at: 1)
+        let epoch = Double((timestamp as NSString).substring(with: matchRange))! / 1000
+        return Date(timeIntervalSince1970: epoch)
+    }
+}
+
+public extension Bundle {
+    var appGroupSuiteName: String? {
+        object(forInfoDictionaryKey: "AppGroupID") as? String
+    }
 }

+ 15 - 0
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -11,6 +11,7 @@ protocol CarbsStorage {
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
+    func deleteCarbs(at date: Date)
 }
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -52,6 +53,20 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
     }
 
+    func deleteCarbs(at date: Date) {
+        processQueue.sync {
+            var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
+            guard let entryIndex = allValues.firstIndex(where: { $0.createdAt == date }) else {
+                return
+            }
+            allValues.remove(at: entryIndex)
+            storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
+            broadcaster.notify(CarbsObserver.self, on: processQueue) {
+                $0.carbsDidUpdate(allValues)
+            }
+        }
+    }
+
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
         let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
 

+ 3 - 0
FreeAPS/Sources/Modules/DataTable/DataTableBuilder.swift

@@ -0,0 +1,3 @@
+extension DataTable {
+    final class Builder: BaseModuleBuilder<RootView, ViewModel<Provider>, Provider> {}
+}

+ 120 - 0
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -0,0 +1,120 @@
+import Foundation
+import SwiftUI
+
+enum DataTable {
+    enum Config {}
+
+    enum DataType: String, Equatable {
+        case carbs
+        case bolus
+        case tempBasal
+        case tempTarget
+
+        var name: String {
+            switch self {
+            case .carbs:
+                return "Carbs"
+            case .bolus:
+                return "Bolus"
+            case .tempBasal:
+                return "Temp Basal"
+            case .tempTarget:
+                return "Temp Target"
+            }
+        }
+    }
+
+    class Item: Identifiable, Hashable, Equatable {
+        let id = UUID()
+        let units: GlucoseUnits
+        let type: DataType
+        let date: Date
+        let amount: Decimal
+        let secondAmount: Decimal?
+        let duration: Decimal?
+
+        private var numberFormater: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        init(
+            units: GlucoseUnits,
+            type: DataType,
+            date: Date,
+            amount: Decimal,
+            secondAmount: Decimal? = nil,
+            duration: Decimal? = nil
+        ) {
+            self.units = units
+            self.type = type
+            self.date = date
+            self.amount = amount
+            self.secondAmount = secondAmount
+            self.duration = duration
+        }
+
+        static func == (lhs: Item, rhs: Item) -> Bool {
+            lhs.id == rhs.id
+        }
+
+        func hash(into hasher: inout Hasher) {
+            hasher.combine(id)
+        }
+
+        var amountText: String {
+            switch type {
+            case .carbs:
+                return numberFormater.string(from: amount as NSNumber)! + " g"
+            case .bolus:
+                return numberFormater.string(from: amount as NSNumber)! + " U"
+            case .tempBasal:
+                return numberFormater.string(from: amount as NSNumber)! + " U/hr"
+            case .tempTarget:
+                var converted = amount
+                if units == .mmolL {
+                    converted = converted.asMmolL
+                }
+
+                guard var secondAmount = secondAmount else {
+                    return numberFormater.string(from: converted as NSNumber)! + " \(units.rawValue)"
+                }
+                if units == .mmolL {
+                    secondAmount = secondAmount.asMmolL
+                }
+
+                return numberFormater.string(from: converted as NSNumber)! + " - " + numberFormater
+                    .string(from: secondAmount as NSNumber)! + " \(units.rawValue)"
+            }
+        }
+
+        var color: Color {
+            switch type {
+            case .carbs:
+                return .loopYellow
+            case .bolus:
+                return .insulin
+            case .tempBasal:
+                return Color.insulin.opacity(0.5)
+            case .tempTarget:
+                return .loopGray
+            }
+        }
+
+        var durationText: String? {
+            guard let duration = duration else {
+                return nil
+            }
+            return numberFormater.string(from: duration as NSNumber)! + " min"
+        }
+    }
+}
+
+protocol DataTableProvider: Provider {
+    func pumpHistory() -> [PumpHistoryEvent]
+    func tempTargets() -> [TempTarget]
+    func carbs() -> [CarbsEntry]
+    func deleteCarbs(at date: Date)
+}

+ 26 - 0
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -0,0 +1,26 @@
+import Foundation
+
+extension DataTable {
+    final class Provider: BaseProvider, DataTableProvider {
+        @Injected() var pumpHistoryStorage: PumpHistoryStorage!
+        @Injected() var tempTargetsStorage: TempTargetsStorage!
+        @Injected() var carbsStorage: CarbsStorage!
+        @Injected() var nightscoutManager: NightscoutManager!
+
+        func pumpHistory() -> [PumpHistoryEvent] {
+            pumpHistoryStorage.recent()
+        }
+
+        func tempTargets() -> [TempTarget] {
+            tempTargetsStorage.recent()
+        }
+
+        func carbs() -> [CarbsEntry] {
+            carbsStorage.recent()
+        }
+
+        func deleteCarbs(at date: Date) {
+            nightscoutManager.deleteCarbs(at: date)
+        }
+    }
+}

+ 93 - 0
FreeAPS/Sources/Modules/DataTable/DataTableViewModel.swift

@@ -0,0 +1,93 @@
+import SwiftUI
+
+extension DataTable {
+    class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: DataTableProvider {
+        @Injected() var broadcaster: Broadcaster!
+        @Injected() var settingsManager: SettingsManager!
+        @Published var items: [Item] = []
+
+        override func subscribe() {
+            setupItems()
+            broadcaster.register(SettingsObserver.self, observer: self)
+            broadcaster.register(PumpHistoryObserver.self, observer: self)
+            broadcaster.register(TempTargetsObserver.self, observer: self)
+            broadcaster.register(CarbsObserver.self, observer: self)
+        }
+
+        private func setupItems() {
+            DispatchQueue.main.async {
+                let units = self.settingsManager.settings.units
+
+                let carbs = self.provider.carbs().map {
+                    Item(units: units, type: .carbs, date: $0.createdAt, amount: $0.carbs)
+                }
+
+                let boluses = self.provider.pumpHistory()
+                    .filter { $0.type == .bolus }
+                    .map {
+                        Item(units: units, type: .bolus, date: $0.timestamp, amount: $0.amount ?? 0)
+                    }
+
+                let tempBasals = self.provider.pumpHistory()
+                    .filter { $0.type == .tempBasal || $0.type == .tempBasalDuration }
+                    .chunks(ofCount: 2)
+                    .compactMap { chunk -> Item? in
+                        let chunk = Array(chunk)
+                        guard chunk.count == 2, chunk[0].type == .tempBasal,
+                              chunk[1].type == .tempBasalDuration else { return nil }
+                        return Item(
+                            units: units,
+                            type: .tempBasal,
+                            date: chunk[0].timestamp,
+                            amount: chunk[0].rate ?? 0,
+                            secondAmount: nil,
+                            duration: Decimal(chunk[1].durationMin ?? 0)
+                        )
+                    }
+
+                let tempTargets = self.provider.tempTargets()
+                    .map {
+                        Item(
+                            units: units,
+                            type: .tempTarget,
+                            date: $0.createdAt,
+                            amount: $0.targetBottom,
+                            secondAmount: $0.targetTop,
+                            duration: $0.duration
+                        )
+                    }
+
+                self.items = [carbs, boluses, tempBasals, tempTargets]
+                    .flatMap { $0 }
+                    .sorted { $0.date > $1.date }
+            }
+        }
+
+        func deleteCarbs(at date: Date) {
+            provider.deleteCarbs(at: date)
+        }
+    }
+}
+
+extension DataTable.ViewModel:
+    SettingsObserver,
+    PumpHistoryObserver,
+    TempTargetsObserver,
+    CarbsObserver
+{
+    func settingsDidChange(_: FreeAPSSettings) {
+        setupItems()
+    }
+
+    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
+        setupItems()
+    }
+
+    func tempTargetsDidUpdate(_: [TempTarget]) {
+        setupItems()
+    }
+
+    func carbsDidUpdate(_: [CarbsEntry]) {
+        setupItems()
+    }
+}

+ 65 - 0
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -0,0 +1,65 @@
+import SwiftUI
+
+extension DataTable {
+    struct RootView: BaseView {
+        @EnvironmentObject var viewModel: ViewModel<Provider>
+        @State private var isRemoveCarbsAlertPresented = false
+        @State private var removeCarbsAlert: Alert?
+
+        private var dateFormatter: DateFormatter {
+            let formatter = DateFormatter()
+            formatter.timeStyle = .short
+            return formatter
+        }
+
+        var body: some View {
+            Form {
+                list
+            }
+            .navigationTitle("History")
+            .navigationBarTitleDisplayMode(.automatic)
+            .navigationBarItems(
+                leading: Button("Close", action: viewModel.hideModal)
+            )
+        }
+
+        private var list: some View {
+            List {
+                ForEach(viewModel.items.indexed(), id: \.1.id) { _, item in
+                    HStack {
+                        Image(systemName: "circle.fill").foregroundColor(item.color)
+                        Text(dateFormatter.string(from: item.date))
+                            .moveDisabled(true)
+                        Text(item.type.name)
+                        Text(item.amountText).foregroundColor(.secondary)
+                        if let duration = item.durationText {
+                            Text(duration).foregroundColor(.secondary)
+                        }
+
+                        if item.type == .carbs {
+                            Spacer()
+                            Image(systemName: "xmark.circle").foregroundColor(.secondary)
+                                .contentShape(Rectangle())
+                                .padding(.vertical)
+                                .onTapGesture {
+                                    removeCarbsAlert = Alert(
+                                        title: Text("Delete carbs?"),
+                                        message: Text(item.amountText),
+                                        primaryButton: .destructive(
+                                            Text("Delete"),
+                                            action: { viewModel.deleteCarbs(at: item.date) }
+                                        ),
+                                        secondaryButton: .cancel()
+                                    )
+                                    isRemoveCarbsAlertPresented = true
+                                }
+                                .alert(isPresented: $isRemoveCarbsAlertPresented) {
+                                    removeCarbsAlert!
+                                }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 1 - 0
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -41,6 +41,7 @@ struct CurrentGlucoseView: View {
                         ?? "--"
                 )
                 .font(.system(size: 24, weight: .bold))
+                .minimumScaleFactor(0.5)
                 image.padding(.bottom, 2)
 
             }.padding(.leading, 4)

+ 2 - 0
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -178,6 +178,8 @@ extension Home {
                         units: viewModel.units
                     )
                     .padding(.bottom)
+                    .modal(for: .dataTable, from: self)
+
                     legendPanal
 
                     ZStack {

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -22,6 +22,7 @@ enum Screen: Identifiable {
     case bolus
     case manualTempBasal
     case autotuneConfig
+    case dataTable
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -69,6 +70,8 @@ extension Screen {
             return ManualTempBasal.Builder(resolver: resolver).buildView()
         case .autotuneConfig:
             return AutotuneConfig.Builder(resolver: resolver).buildView()
+        case .dataTable:
+            return DataTable.Builder(resolver: resolver).buildView()
         }
     }
 

+ 29 - 0
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -127,6 +127,35 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
+    func deleteCarbs(at date: Date) -> AnyPublisher<Void, Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+        components.queryItems = [
+            URLQueryItem(name: "find[carbs][$exists]", value: "true"),
+            URLQueryItem(
+                name: "find[created_at][$eq]",
+                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+            )
+        ]
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+        request.httpMethod = "DELETE"
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .map { _ in () }
+            .eraseToAnyPublisher()
+    }
+
     func fetchTempTargets(sinceDate: Date? = nil) -> AnyPublisher<[TempTarget], Swift.Error> {
         var components = URLComponents()
         components.scheme = url.scheme

+ 20 - 0
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -8,6 +8,7 @@ protocol NightscoutManager {
     func fetchCarbs() -> AnyPublisher<Void, Never>
     func fetchTempTargets() -> AnyPublisher<Void, Never>
     func fetchAnnouncements() -> AnyPublisher<Void, Never>
+    func deleteCarbs(at date: Date)
     func uploadStatus()
     var cgmURL: URL? { get }
 }
@@ -131,6 +132,25 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }.eraseToAnyPublisher()
     }
 
+    func deleteCarbs(at date: Date) {
+        guard let nightscout = nightscoutAPI, isUploadEnabled else {
+            carbsStorage.deleteCarbs(at: date)
+            return
+        }
+
+        nightscout.deleteCarbs(at: date)
+            .sink { completion in
+                switch completion {
+                case .finished:
+                    self.carbsStorage.deleteCarbs(at: date)
+                    debug(.nightscout, "Carbs deleted")
+                case let .failure(error):
+                    debug(.nightscout, error.localizedDescription)
+                }
+            } receiveValue: {}
+            .store(in: &lifetime)
+    }
+
     func uploadStatus() {
         let iob = storage.retrieve(OpenAPS.Monitor.iob, as: [IOBEntry].self)
         var suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)