Showing 6 changed files with 775 additions and 677 deletions
+4 -0
USB Meter.xcodeproj/project.pbxproj
@@ -60,6 +60,7 @@
60 60
 		C10000043C8E4A7A00A10004 /* ChargedDeviceEditorSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000143C8E4A7A00A10014 /* ChargedDeviceEditorSheetView.swift */; };
61 61
 		C10000053C8E4A7A00A10005 /* ChargedDeviceLibrarySheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000153C8E4A7A00A10015 /* ChargedDeviceLibrarySheetView.swift */; };
62 62
 		C10000063C8E4A7A00A10006 /* ChargedDeviceDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000163C8E4A7A00A10016 /* ChargedDeviceDetailView.swift */; };
63
+		C1CCSS013C8E4A7A00CCSS01 /* ChargeSessionCompletionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1CCSS023C8E4A7A00CCSS02 /* ChargeSessionCompletionSheetView.swift */; };
63 64
 		C10000073C8E4A7A00A10007 /* SidebarChargedDevicesSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */; };
64 65
 		C10000083C8E4A7A00A10008 /* BatteryCheckpointEditorSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000183C8E4A7A00A10018 /* BatteryCheckpointEditorSheetView.swift */; };
65 66
 		C10000093C8E4A7A00A10009 /* CKModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C10000213C8E4A7A00A10021 /* CKModel.xcdatamodeld */; };
@@ -174,6 +175,7 @@
174 175
 		C10000143C8E4A7A00A10014 /* ChargedDeviceEditorSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceEditorSheetView.swift; sourceTree = "<group>"; };
175 176
 		C10000153C8E4A7A00A10015 /* ChargedDeviceLibrarySheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceLibrarySheetView.swift; sourceTree = "<group>"; };
176 177
 		C10000163C8E4A7A00A10016 /* ChargedDeviceDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceDetailView.swift; sourceTree = "<group>"; };
178
+		C1CCSS023C8E4A7A00CCSS02 /* ChargeSessionCompletionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargeSessionCompletionSheetView.swift; sourceTree = "<group>"; };
177 179
 		C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarChargedDevicesSectionView.swift; sourceTree = "<group>"; };
178 180
 		C10000183C8E4A7A00A10018 /* BatteryCheckpointEditorSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryCheckpointEditorSheetView.swift; sourceTree = "<group>"; };
179 181
 		C10000193C8E4A7A00A10019 /* USB_Meter 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 4.xcdatamodel"; sourceTree = "<group>"; };
@@ -488,6 +490,7 @@
488 490
 			children = (
489 491
 				C10000163C8E4A7A00A10016 /* ChargedDeviceDetailView.swift */,
490 492
 				C10000143C8E4A7A00A10014 /* ChargedDeviceEditorSheetView.swift */,
493
+				C1CCSS023C8E4A7A00CCSS02 /* ChargeSessionCompletionSheetView.swift */,
491 494
 				C10000153C8E4A7A00A10015 /* ChargedDeviceLibrarySheetView.swift */,
492 495
 				C10000133C8E4A7A00A10013 /* ChargedDeviceQRCodeView.swift */,
493 496
 				C10000183C8E4A7A00A10018 /* BatteryCheckpointEditorSheetView.swift */,
@@ -777,6 +780,7 @@
777 780
 				C10000043C8E4A7A00A10004 /* ChargedDeviceEditorSheetView.swift in Sources */,
778 781
 				C10000053C8E4A7A00A10005 /* ChargedDeviceLibrarySheetView.swift in Sources */,
779 782
 				C10000063C8E4A7A00A10006 /* ChargedDeviceDetailView.swift in Sources */,
783
+				C1CCSS013C8E4A7A00CCSS01 /* ChargeSessionCompletionSheetView.swift in Sources */,
780 784
 				C10000073C8E4A7A00A10007 /* SidebarChargedDevicesSectionView.swift in Sources */,
781 785
 				C10000083C8E4A7A00A10008 /* BatteryCheckpointEditorSheetView.swift in Sources */,
782 786
 				C10000093C8E4A7A00A10009 /* CKModel.xcdatamodeld in Sources */,
+3 -5
USB Meter/Model/AppData.swift
@@ -505,14 +505,12 @@ final class AppData : ObservableObject {
505 505
     }
506 506
 
507 507
     @discardableResult
508
-    func stopChargeSession(sessionID: UUID, finalBatteryPercent: Double) -> Bool {
508
+    func stopChargeSession(sessionID: UUID, finalBatteryPercent: Double? = nil) -> Bool {
509 509
         let didSave = chargeInsightsStore?.stopSession(
510 510
             id: sessionID,
511 511
             finalBatteryPercent: finalBatteryPercent
512 512
         ) ?? false
513
-        if didSave {
514
-            reloadChargedDevices()
515
-        }
513
+        reloadChargedDevices()
516 514
         return didSave
517 515
     }
518 516
 
@@ -841,7 +839,7 @@ final class AppData : ObservableObject {
841 839
         return chargedDevices
842 840
             .lazy
843 841
             .compactMap(\.activeSession)
844
-            .first(where: { $0.status == .active && $0.meterMACAddress == normalizedMAC })
842
+            .first(where: { $0.status.isOpen && $0.meterMACAddress == normalizedMAC })
845 843
     }
846 844
 
847 845
     private func reloadChargedDevices() {
+2 -0
USB Meter/Model/CKModel.xcdatamodeld/USB_Meter 12.xcdatamodel/contents
@@ -52,6 +52,8 @@
52 52
         <attribute name="meterEnergyBaselineWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
53 53
         <attribute name="meterChargeBaselineAh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
54 54
         <attribute name="meterDurationBaselineSeconds" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
55
+        <attribute name="meterLastEnergyWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
56
+        <attribute name="meterLastChargeAh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
55 57
         <attribute name="meterLastDurationSeconds" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
56 58
         <attribute name="measuredEnergyWh" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
57 59
         <attribute name="effectiveBatteryEnergyWh" optional="YES" attributeType="Double" usesScalarValueType="YES"/>
+37 -7
USB Meter/Model/ChargeInsightsStore.swift
@@ -537,10 +537,12 @@ final class ChargeInsightsStore {
537 537
     @discardableResult
538 538
     func stopSession(
539 539
         id sessionID: UUID,
540
-        finalBatteryPercent: Double
540
+        finalBatteryPercent: Double? = nil
541 541
     ) -> Bool {
542
-        guard finalBatteryPercent.isFinite, finalBatteryPercent >= 0, finalBatteryPercent <= 100 else {
543
-            return false
542
+        if let finalBatteryPercent {
543
+            guard finalBatteryPercent.isFinite, finalBatteryPercent >= 0, finalBatteryPercent <= 100 else {
544
+                return false
545
+            }
544 546
         }
545 547
 
546 548
         var didSave = false
@@ -723,6 +725,7 @@ final class ChargeInsightsStore {
723 725
             }
724 726
 
725 727
             clearCompletionConfirmationState(for: session)
728
+            session.setValue(nil, forKey: "belowThresholdSince")
726 729
             session.setValue(Date().addingTimeInterval(completionConfirmationCooldown), forKey: "completionConfirmationCooldownUntil")
727 730
             session.setValue(Date(), forKey: "updatedAt")
728 731
             didSave = saveContext()
@@ -1018,7 +1021,7 @@ final class ChargeInsightsStore {
1018 1021
         var summary: ChargeSessionSummary?
1019 1022
 
1020 1023
         context.performAndWait {
1021
-            guard let session = fetchActiveSessionObject(forMeterMACAddress: normalizedMAC),
1024
+            guard let session = fetchOpenSessionObject(forMeterMACAddress: normalizedMAC),
1022 1025
                   let sessionID = stringValue(session, key: "id") else {
1023 1026
                 return
1024 1027
             }
@@ -1351,7 +1354,11 @@ final class ChargeInsightsStore {
1351 1354
         return sample
1352 1355
     }
1353 1356
 
1354
-    private func maybeTriggerTargetBatteryAlert(for session: NSManagedObject, observedAt: Date) {
1357
+    private func maybeTriggerTargetBatteryAlert(
1358
+        for session: NSManagedObject,
1359
+        observedAt: Date,
1360
+        completionFallbackPercent: Double? = nil
1361
+    ) {
1355 1362
         guard dateValue(session, key: "targetBatteryAlertTriggeredAt") == nil else {
1356 1363
             return
1357 1364
         }
@@ -1362,6 +1369,7 @@ final class ChargeInsightsStore {
1362 1369
 
1363 1370
         let predictedBatteryPercent = predictedBatteryPercent(for: session)
1364 1371
             ?? optionalDoubleValue(session, key: "endBatteryPercent")
1372
+            ?? completionFallbackPercent
1365 1373
 
1366 1374
         guard let predictedBatteryPercent, predictedBatteryPercent >= targetBatteryPercent else {
1367 1375
             return
@@ -1497,6 +1505,14 @@ final class ChargeInsightsStore {
1497 1505
         session.setValue(nil, forKey: "completionConfirmationCooldownUntil")
1498 1506
         updateCapacityEstimate(for: session)
1499 1507
         session.setValue(observedAt, forKey: "updatedAt")
1508
+
1509
+        if status == .completed {
1510
+            maybeTriggerTargetBatteryAlert(
1511
+                for: session,
1512
+                observedAt: observedAt,
1513
+                completionFallbackPercent: defaultCompletionPercentThreshold
1514
+            )
1515
+        }
1500 1516
     }
1501 1517
 
1502 1518
     private func predictedBatteryPercent(for session: NSManagedObject) -> Double? {
@@ -1509,8 +1525,22 @@ final class ChargeInsightsStore {
1509 1525
             return nil
1510 1526
         }
1511 1527
 
1512
-        let measuredEnergyWh = optionalDoubleValue(session, key: "effectiveBatteryEnergyWh")
1513
-            ?? doubleValue(session, key: "measuredEnergyWh")
1528
+        // Compute effective battery energy dynamically so the prediction uses the
1529
+        // most current measuredEnergyWh rather than the stale effectiveBatteryEnergyWh
1530
+        // (which is only refreshed at session start, checkpoint insertion, and finish).
1531
+        let rawMeasuredEnergyWh = doubleValue(session, key: "measuredEnergyWh")
1532
+        let measuredEnergyWh: Double
1533
+        switch chargingTransportMode(for: session) {
1534
+        case .wired:
1535
+            measuredEnergyWh = rawMeasuredEnergyWh
1536
+        case .wireless:
1537
+            if let factor = optionalDoubleValue(session, key: "wirelessEfficiencyFactor"), factor > 0 {
1538
+                measuredEnergyWh = rawMeasuredEnergyWh * factor
1539
+            } else {
1540
+                measuredEnergyWh = optionalDoubleValue(session, key: "effectiveBatteryEnergyWh")
1541
+                    ?? rawMeasuredEnergyWh
1542
+            }
1543
+        }
1514 1544
         let sessionID = stringValue(session, key: "id") ?? ""
1515 1545
 
1516 1546
         struct Anchor {
+86 -0
USB Meter/Views/ChargedDevices/ChargeSessionCompletionSheetView.swift
@@ -0,0 +1,86 @@
1
+//
2
+//  ChargeSessionCompletionSheetView.swift
3
+//  USB Meter
4
+//
5
+
6
+import SwiftUI
7
+
8
+struct ChargeSessionCompletionSheetView: View {
9
+    @EnvironmentObject private var appData: AppData
10
+    @Environment(\.dismiss) private var dismiss
11
+
12
+    let sessionID: UUID
13
+    let title: String
14
+    let confirmTitle: String
15
+    let explanation: String
16
+
17
+    @State private var batteryPercent = ""
18
+
19
+    var body: some View {
20
+        NavigationView {
21
+            Form {
22
+                Section(
23
+                    header: ContextInfoHeader(
24
+                        title: "Final Checkpoint",
25
+                        message: explanation
26
+                    )
27
+                ) {
28
+                    TextField("Battery %", text: $batteryPercent)
29
+                        .keyboardType(.decimalPad)
30
+                }
31
+
32
+                Section {
33
+                    if let sessionWarning {
34
+                        Text(sessionWarning)
35
+                            .font(.footnote)
36
+                            .foregroundColor(.orange)
37
+                    } else if (parsedBatteryPercent ?? 0) >= 99.5 {
38
+                        Text("A final checkpoint at 100% lets the app learn the stop current for this exact charging type when the session data is reliable.")
39
+                            .font(.footnote)
40
+                            .foregroundColor(.secondary)
41
+                    }
42
+                }
43
+            }
44
+            .navigationTitle(title)
45
+            .navigationBarTitleDisplayMode(.inline)
46
+            .toolbar {
47
+                ToolbarItem(placement: .cancellationAction) {
48
+                    Button("Cancel") {
49
+                        dismiss()
50
+                    }
51
+                }
52
+                ToolbarItem(placement: .confirmationAction) {
53
+                    Button(confirmTitle) {
54
+                        guard let percent = parsedBatteryPercent else { return }
55
+                        if appData.stopChargeSession(sessionID: sessionID, finalBatteryPercent: percent) {
56
+                            dismiss()
57
+                        }
58
+                    }
59
+                    .disabled(parsedBatteryPercent == nil)
60
+                }
61
+            }
62
+        }
63
+        .navigationViewStyle(StackNavigationViewStyle())
64
+    }
65
+
66
+    private var parsedBatteryPercent: Double? {
67
+        let normalized = batteryPercent
68
+            .trimmingCharacters(in: .whitespacesAndNewlines)
69
+            .replacingOccurrences(of: ",", with: ".")
70
+        guard let value = Double(normalized), value >= 0, value <= 100 else { return nil }
71
+        return value
72
+    }
73
+
74
+    private var sessionWarning: String? {
75
+        guard let session = appData.chargedDevices
76
+            .flatMap(\.sessions)
77
+            .first(where: { $0.id == sessionID }),
78
+              session.chargingTransportMode == .wireless,
79
+              let chargerID = session.chargerID,
80
+              let charger = appData.chargedDeviceSummary(id: chargerID),
81
+              charger.chargerIdleCurrentAmps == nil else {
82
+            return nil
83
+        }
84
+        return "This charger has no idle-current measurement, so the final checkpoint will stop the session but will not learn a wireless stop threshold yet."
85
+    }
86
+}
+643 -665
USB Meter/Views/Meter/Tabs/ChargeRecord/MeterChargeRecordTabView.swift
@@ -26,30 +26,17 @@ struct MeterChargeRecordContentView: View {
26 26
 
27 27
         var title: String {
28 28
             switch self {
29
-            case .known:
30
-                return "Known"
31
-            case .unknown:
32
-                return "Unknown"
33
-            case .flat:
34
-                return "Flat"
29
+            case .known:   return "Known"
30
+            case .unknown: return "Unknown"
31
+            case .flat:    return "Flat"
35 32
             }
36 33
         }
37 34
     }
38 35
 
39
-    @EnvironmentObject private var appData: AppData
40
-    @EnvironmentObject private var usbMeter: Meter
41
-
42
-    @State private var chargedDeviceLibraryVisibility = false
43
-    @State private var chargerLibraryVisibility = false
44
-    @State private var editingChargedDevice: ChargedDeviceSummary?
45
-    @State private var targetNotificationEditorVisibility = false
46
-    @State private var pendingStopRequest: ChargeSessionStopRequest?
47
-    @State private var pendingCheckpointDeletion: ChargeCheckpointSummary?
48
-    @State private var draftChargingTransportMode: ChargingTransportMode?
49
-    @State private var draftChargingStateMode: ChargingStateMode?
50
-    @State private var initialCheckpointMode: InitialCheckpointMode = .known
51
-    @State private var initialCheckpoint = ""
52
-    @State private var showsMeterTotalsInfo = false
36
+    private enum ActiveMode: Hashable {
37
+        case chargeSession
38
+        case standbyPower
39
+    }
53 40
 
54 41
     private enum SessionStartRequirement: Identifiable {
55 42
         case existingSession
@@ -62,48 +49,73 @@ struct MeterChargeRecordContentView: View {
62 49
 
63 50
         var id: String {
64 51
             switch self {
65
-            case .existingSession:
66
-                return "existing-session"
67
-            case .device:
68
-                return "device"
69
-            case .chargingType:
70
-                return "charging-type"
71
-            case .chargingMode:
72
-                return "charging-mode"
73
-            case .charger:
74
-                return "charger"
75
-            case .initialCheckpointEmpty:
76
-                return "initial-checkpoint-empty"
77
-            case .initialCheckpointInvalid:
78
-                return "initial-checkpoint-invalid"
52
+            case .existingSession:         return "existing-session"
53
+            case .device:                  return "device"
54
+            case .chargingType:            return "charging-type"
55
+            case .chargingMode:            return "charging-mode"
56
+            case .charger:                 return "charger"
57
+            case .initialCheckpointEmpty:  return "initial-checkpoint-empty"
58
+            case .initialCheckpointInvalid:return "initial-checkpoint-invalid"
79 59
             }
80 60
         }
81 61
 
82 62
         var message: String {
83 63
             switch self {
84
-            case .existingSession:
85
-                return "Stop or pause the current session before starting another one."
86
-            case .device:
87
-                return "Select the device that is charging."
88
-            case .chargingType:
89
-                return "Choose the charging type for this session."
90
-            case .chargingMode:
91
-                return "Choose whether the device is on or off for this session."
92
-            case .charger:
93
-                return "Select the wireless charger used in this session."
94
-            case .initialCheckpointEmpty:
95
-                return "Enter the initial battery percentage."
96
-            case .initialCheckpointInvalid:
97
-                return "Initial battery percentage must be between 0 and 100."
64
+            case .existingSession:          return "Stop or pause the current session before starting another one."
65
+            case .device:                   return "Select the device that is charging."
66
+            case .chargingType:             return "Choose the charging type for this session."
67
+            case .chargingMode:             return "Choose whether the device is on or off for this session."
68
+            case .charger:                  return "Select the wireless charger used in this session."
69
+            case .initialCheckpointEmpty:   return "Enter the initial battery percentage."
70
+            case .initialCheckpointInvalid: return "Initial battery percentage must be between 0 and 100."
71
+            }
72
+        }
73
+    }
74
+
75
+    private enum FinalCheckpoint: Hashable {
76
+        case full
77
+        case skip
78
+        case custom
79
+
80
+        var label: String {
81
+            switch self {
82
+            case .full:   return "Full"
83
+            case .skip:   return "Skip"
84
+            case .custom: return "Other %"
85
+            }
86
+        }
87
+
88
+        var icon: String {
89
+            switch self {
90
+            case .full:   return "battery.100percent"
91
+            case .skip:   return "minus.circle"
92
+            case .custom: return "pencil"
98 93
             }
99 94
         }
100 95
     }
101 96
 
97
+    @EnvironmentObject private var appData: AppData
98
+    @EnvironmentObject private var usbMeter: Meter
99
+
100
+    @State private var chargedDeviceLibraryVisibility = false
101
+    @State private var chargerLibraryVisibility = false
102
+    @State private var showingInlineTargetEditor = false
103
+    @State private var draftTargetText = ""
104
+    @State private var showingStopConfirm = false
105
+    @State private var finalCheckpointMode: FinalCheckpoint = .full
106
+    @State private var finalCheckpointText = ""
107
+    @State private var pendingCheckpointDeletion: ChargeCheckpointSummary?
108
+    @State private var draftChargingTransportMode: ChargingTransportMode?
109
+    @State private var draftChargingStateMode: ChargingStateMode?
110
+    @State private var initialCheckpointMode: InitialCheckpointMode = .known
111
+    @State private var initialCheckpoint = ""
112
+    @State private var showsMeterTotalsInfo = false
113
+    @State private var activeMode: ActiveMode = .chargeSession
114
+
102 115
     var body: some View {
103 116
         ScrollView {
104
-            VStack(spacing: 16) {
105
-                headerCard
106
-                sessionSetupCard
117
+            VStack(spacing: 14) {
118
+                statusHeader
107 119
 
108 120
                 if let openChargeSession {
109 121
                     chargingMonitorCard(openChargeSession)
@@ -112,11 +124,22 @@ struct MeterChargeRecordContentView: View {
112 124
                         meterTotalsCard
113 125
                     }
114 126
 
115
-                    if let sessionChartTimeRange {
116
-                        sessionChartCard(timeRange: sessionChartTimeRange, session: openChargeSession)
127
+                    if let range = sessionChartTimeRange {
128
+                        sessionChartCard(timeRange: range, session: openChargeSession)
129
+                    }
130
+                } else {
131
+                    modePicker
132
+
133
+                    switch activeMode {
134
+                    case .chargeSession:
135
+                        chargeSessionSetupCard
136
+                    case .standbyPower:
137
+                        standbyPowerCard
138
+                    }
139
+
140
+                    if showsMeterTotalsCard {
141
+                        meterTotalsCard
117 142
                     }
118
-                } else if showsMeterTotalsCard {
119
-                    meterTotalsCard
120 143
                 }
121 144
             }
122 145
             .padding()
@@ -147,32 +170,6 @@ struct MeterChargeRecordContentView: View {
147 170
             )
148 171
             .environmentObject(appData)
149 172
         }
150
-        .sheet(item: $editingChargedDevice) { chargedDevice in
151
-            ChargedDeviceEditorSheetView(
152
-                meterMACAddress: nil,
153
-                kind: chargedDevice.kind,
154
-                chargedDevice: chargedDevice
155
-            )
156
-            .environmentObject(appData)
157
-        }
158
-        .sheet(isPresented: $targetNotificationEditorVisibility) {
159
-            if let openChargeSession {
160
-                BatteryTargetNotificationEditorSheetView(
161
-                    sessionID: openChargeSession.id,
162
-                    initialTargetPercent: openChargeSession.targetBatteryPercent
163
-                )
164
-                .environmentObject(appData)
165
-            }
166
-        }
167
-        .sheet(item: $pendingStopRequest) { request in
168
-            ChargeSessionCompletionSheetView(
169
-                sessionID: request.sessionID,
170
-                title: request.title,
171
-                confirmTitle: request.confirmTitle,
172
-                explanation: request.explanation
173
-            )
174
-            .environmentObject(appData)
175
-        }
176 173
         .alert(item: $pendingCheckpointDeletion) { checkpoint in
177 174
             Alert(
178 175
                 title: Text("Delete Battery Checkpoint"),
@@ -196,9 +193,13 @@ struct MeterChargeRecordContentView: View {
196 193
         }
197 194
         .onChange(of: openChargeSession?.id) { _ in
198 195
             syncDraftSelections()
196
+            showingInlineTargetEditor = false
197
+            draftTargetText = ""
199 198
         }
200 199
     }
201 200
 
201
+    // MARK: - Computed Properties
202
+
202 203
     private var meterMACAddress: String {
203 204
         usbMeter.btSerial.macAddress.description
204 205
     }
@@ -232,15 +233,11 @@ struct MeterChargeRecordContentView: View {
232 233
     }
233 234
 
234 235
     private var initialCheckpointValue: Double? {
235
-        guard initialCheckpointMode == .known else {
236
-            return nil
237
-        }
236
+        guard initialCheckpointMode == .known else { return nil }
238 237
         let normalized = initialCheckpoint
239 238
             .trimmingCharacters(in: .whitespacesAndNewlines)
240 239
             .replacingOccurrences(of: ",", with: ".")
241
-        guard let value = Double(normalized), value >= 0, value <= 100 else {
242
-            return nil
243
-        }
240
+        guard let value = Double(normalized), value >= 0, value <= 100 else { return nil }
244 241
         return value
245 242
     }
246 243
 
@@ -310,448 +307,293 @@ struct MeterChargeRecordContentView: View {
310 307
     }
311 308
 
312 309
     private var headerStatusTitle: String {
313
-        guard let openChargeSession else {
314
-            return "Idle"
315
-        }
310
+        guard let openChargeSession else { return "Idle" }
316 311
         return openChargeSession.status.title
317 312
     }
318 313
 
319 314
     private var headerStatusColor: Color {
320
-        guard let openChargeSession else {
321
-            return .secondary
322
-        }
323
-
315
+        guard let openChargeSession else { return .secondary }
324 316
         switch openChargeSession.status {
325
-        case .active:
326
-            return .red
327
-        case .paused:
328
-            return .orange
329
-        case .completed:
330
-            return .green
331
-        case .abandoned:
332
-            return .secondary
317
+        case .active:    return .red
318
+        case .paused:    return .orange
319
+        case .completed: return .green
320
+        case .abandoned: return .secondary
333 321
         }
334 322
     }
335 323
 
336 324
     private var sessionChartTimeRange: ClosedRange<Date>? {
337
-        guard let openChargeSession else {
338
-            return nil
339
-        }
340
-
325
+        guard let openChargeSession else { return nil }
341 326
         let end = openChargeSession.pausedAt ?? openChargeSession.lastObservedAt
342 327
         return openChargeSession.startedAt...max(end, openChargeSession.startedAt)
343 328
     }
344 329
 
345
-    private var headerCard: some View {
346
-        VStack(alignment: .leading, spacing: 8) {
347
-            HStack {
348
-                Text("Charging Session")
349
-                    .font(.system(.title3, design: .rounded).weight(.bold))
350
-                ContextInfoButton(
351
-                    title: "Charging Session",
352
-                    message: "Recording starts only after the device, charging type, charging mode, and initial checkpoint are explicit."
353
-                )
354
-                Spacer()
355
-                Text(headerStatusTitle)
356
-                    .font(.caption.weight(.bold))
357
-                    .foregroundColor(headerStatusColor)
358
-                    .padding(.horizontal, 10)
359
-                    .padding(.vertical, 6)
360
-                    .meterCard(
361
-                        tint: headerStatusColor,
362
-                        fillOpacity: 0.18,
363
-                        strokeOpacity: 0.24,
364
-                        cornerRadius: 999
365
-                    )
366
-            }
330
+    private var showsWirelessChargerSection: Bool {
331
+        let transportMode = selectedDraftTransportMode ?? selectedChargedDevice?.supportedChargingModes.first
332
+        return transportMode == .wireless
333
+    }
367 334
 
368
-        }
369
-        .frame(maxWidth: .infinity)
370
-        .padding(18)
335
+    // MARK: - Status Header
336
+
337
+    private var statusHeader: some View {
338
+        HStack {
339
+            Image(systemName: "bolt.fill")
340
+                .foregroundColor(.pink)
341
+            Text("Charging Session")
342
+                .font(.system(.title3, design: .rounded).weight(.bold))
343
+            Spacer()
344
+            Text(headerStatusTitle)
345
+                .font(.caption.weight(.bold))
346
+                .foregroundColor(headerStatusColor)
347
+                .padding(.horizontal, 10)
348
+                .padding(.vertical, 6)
349
+                .meterCard(tint: headerStatusColor, fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999)
350
+        }
351
+        .padding(.horizontal, 18)
352
+        .padding(.vertical, 12)
371 353
         .meterCard(tint: .pink, fillOpacity: 0.18, strokeOpacity: 0.24)
372 354
     }
373 355
 
374
-    private var sessionSetupCard: some View {
375
-        VStack(alignment: .leading, spacing: 14) {
376
-            HStack {
377
-                Text(openChargeSession == nil ? "Session Setup" : "Session Context")
378
-                    .font(.headline)
379
-                ContextInfoButton(
380
-                    title: openChargeSession == nil ? "Session Setup" : "Session Context",
381
-                    message: "Select or create the device you are charging. Sessions, checkpoints, curve learning, and wireless charger warnings all depend on that explicit selection."
382
-                )
383
-                Spacer()
384
-                Button("Library") {
385
-                    chargedDeviceLibraryVisibility = true
386
-                }
387
-                .disabled(openChargeSession != nil)
388
-            }
389
-
390
-            if let selectedChargedDevice {
391
-                deviceSummary(selectedChargedDevice)
392
-
393
-                if openChargeSession == nil {
394
-                    setupControls(for: selectedChargedDevice)
395
-                }
396
-
397
-                Button("Edit Device") {
398
-                    editingChargedDevice = selectedChargedDevice
399
-                }
400
-                .frame(maxWidth: .infinity)
401
-                .padding(.vertical, 10)
402
-                .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
403
-                .buttonStyle(.plain)
404
-            }
356
+    // MARK: - Mode Picker
405 357
 
406
-            if selectedChargedDevice != nil {
407
-                Divider()
408
-            }
409
-            standbyMeasurementSection
358
+    private var modePicker: some View {
359
+        Picker("", selection: $activeMode) {
360
+            Label("Charge Session", systemImage: "bolt.fill").tag(ActiveMode.chargeSession)
361
+            Label("Standby Power", systemImage: "powersleep").tag(ActiveMode.standbyPower)
410 362
         }
411
-        .padding(18)
412
-        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
363
+        .pickerStyle(.segmented)
364
+        .labelsHidden()
413 365
     }
414 366
 
415
-    private func deviceSummary(_ chargedDevice: ChargedDeviceSummary) -> some View {
416
-        VStack(alignment: .leading, spacing: 12) {
417
-            HStack(alignment: .top, spacing: 14) {
418
-                ChargedDeviceQRCodeView(
419
-                    qrIdentifier: chargedDevice.qrIdentifier,
420
-                    side: 88
421
-                )
422
-
423
-                VStack(alignment: .leading, spacing: 8) {
424
-                    ChargedDeviceIdentityLabelView(
425
-                        chargedDevice: chargedDevice,
426
-                        iconPointSize: 17
427
-                    )
428
-                    .font(.headline)
429
-
430
-                    Text(chargedDevice.identityTitle)
431
-                        .font(.caption.weight(.semibold))
432
-                        .foregroundColor(.secondary)
433
-
434
-                    Text(chargedDevice.chargingStateAvailability.description)
435
-                        .font(.caption2)
436
-                        .foregroundColor(.secondary)
367
+    // MARK: - Charge Session Setup
437 368
 
438
-                    Text(chargedDevice.chargingSupportSummary)
439
-                        .font(.caption2)
369
+    private var chargeSessionSetupCard: some View {
370
+        VStack(alignment: .leading, spacing: 0) {
371
+            // Device
372
+            setupRow(icon: "iphone", iconColor: .blue) {
373
+                if let device = selectedChargedDevice {
374
+                    ChargedDeviceIdentityLabelView(chargedDevice: device, iconPointSize: 15)
375
+                        .font(.subheadline.weight(.semibold))
376
+                } else {
377
+                    Text("No device selected")
440 378
                         .foregroundColor(.secondary)
441
-
442
-                    if let estimatedCapacity = chargedDevice.estimatedBatteryCapacityWh {
443
-                        Text("Estimated capacity: \(estimatedCapacity.format(decimalDigits: 2)) Wh")
444
-                            .font(.caption2)
445
-                            .foregroundColor(.secondary)
446
-                    }
379
+                        .font(.subheadline)
447 380
                 }
448
-
449
-                Spacer(minLength: 0)
450
-            }
451
-
452
-            if showsWirelessChargerSection {
453
-                Divider()
454
-                wirelessChargerSection
381
+                Spacer(minLength: 8)
382
+                Button(selectedChargedDevice == nil ? "Select" : "Change") {
383
+                    chargedDeviceLibraryVisibility = true
384
+                }
385
+                .font(.caption.weight(.semibold))
386
+                .buttonStyle(.bordered)
387
+                .controlSize(.small)
455 388
             }
456
-        }
457
-    }
458
-
459
-    private func setupControls(for chargedDevice: ChargedDeviceSummary) -> some View {
460
-        VStack(alignment: .leading, spacing: 12) {
461
-            if requiresExplicitTransportSelection {
462
-                VStack(alignment: .leading, spacing: 8) {
463
-                    Text("Charging Type")
464
-                        .font(.subheadline.weight(.semibold))
465 389
 
390
+            // Charging type — only when device supports multiple
391
+            if requiresExplicitTransportSelection, let device = selectedChargedDevice {
392
+                Divider().padding(.leading, 46)
393
+                setupRow(icon: draftChargingTransportMode?.symbolName ?? "bolt.slash", iconColor: .orange) {
394
+                    Text("Type")
395
+                        .foregroundColor(.secondary)
396
+                        .font(.subheadline)
397
+                    Spacer()
466 398
                     compactSelectionMenu(
467 399
                         title: draftChargingTransportMode?.title ?? "Choose",
468
-                        options: chargedDevice.supportedChargingModes.map { chargingTransportMode in
400
+                        options: device.supportedChargingModes.map { mode in
469 401
                             CompactSelectionOption(
470
-                                id: chargingTransportMode.id,
471
-                                title: chargingTransportMode.title,
472
-                                isSelected: draftChargingTransportMode == chargingTransportMode,
473
-                                action: { draftChargingTransportMode = chargingTransportMode }
402
+                                id: mode.id, title: mode.title,
403
+                                isSelected: draftChargingTransportMode == mode,
404
+                                action: { draftChargingTransportMode = mode }
474 405
                             )
475 406
                         }
476 407
                     )
477
-
478
-                    if draftChargingTransportMode == nil {
479
-                        Text("Pick the charging type explicitly before starting.")
480
-                            .font(.caption2)
481
-                            .foregroundColor(.orange)
482
-                    }
483 408
                 }
484
-            } else if let chargingTransportMode = chargedDevice.supportedChargingModes.first {
485
-                Label(
486
-                    "Charging type: \(chargingTransportMode.title)",
487
-                    systemImage: chargingTransportMode.symbolName
488
-                )
489
-                .font(.subheadline.weight(.semibold))
490 409
             }
491 410
 
492
-            if requiresExplicitChargingStateSelection {
493
-                VStack(alignment: .leading, spacing: 8) {
494
-                    Text("Charging Mode")
495
-                        .font(.subheadline.weight(.semibold))
496
-
411
+            // Charging state — only when device supports multiple
412
+            if requiresExplicitChargingStateSelection, let device = selectedChargedDevice {
413
+                Divider().padding(.leading, 46)
414
+                setupRow(icon: draftChargingStateMode == .off ? "power.circle" : "power", iconColor: .purple) {
415
+                    Text("Mode")
416
+                        .foregroundColor(.secondary)
417
+                        .font(.subheadline)
418
+                    Spacer()
497 419
                     compactSelectionMenu(
498 420
                         title: draftChargingStateMode?.title ?? "Choose",
499
-                        options: chargedDevice.supportedChargingStateModes.map { chargingStateMode in
421
+                        options: device.supportedChargingStateModes.map { mode in
500 422
                             CompactSelectionOption(
501
-                                id: chargingStateMode.id,
502
-                                title: chargingStateMode.title,
503
-                                isSelected: draftChargingStateMode == chargingStateMode,
504
-                                action: { draftChargingStateMode = chargingStateMode }
423
+                                id: mode.id, title: mode.title,
424
+                                isSelected: draftChargingStateMode == mode,
425
+                                action: { draftChargingStateMode = mode }
505 426
                             )
506 427
                         }
507 428
                     )
429
+                }
430
+            }
508 431
 
509
-                    if draftChargingStateMode == nil {
510
-                        Text("Pick whether the device is on or off for this session.")
511
-                            .font(.caption2)
512
-                            .foregroundColor(.orange)
432
+            // Wireless charger — only when wireless transport
433
+            if showsWirelessChargerSection {
434
+                Divider().padding(.leading, 46)
435
+                setupRow(icon: "antenna.radiowaves.left.and.right", iconColor: .teal) {
436
+                    if let charger = selectedCharger {
437
+                        ChargedDeviceIdentityLabelView(chargedDevice: charger, iconPointSize: 15)
438
+                            .font(.subheadline.weight(.semibold))
439
+                        if charger.chargerIdleCurrentAmps == nil {
440
+                            Image(systemName: "exclamationmark.triangle.fill")
441
+                                .foregroundColor(.orange)
442
+                                .font(.caption)
443
+                        }
444
+                    } else {
445
+                        Text("No charger selected")
446
+                            .foregroundColor(.secondary)
447
+                            .font(.subheadline)
448
+                    }
449
+                    Spacer(minLength: 8)
450
+                    Button(selectedCharger == nil ? "Select" : "Change") {
451
+                        chargerLibraryVisibility = true
513 452
                     }
453
+                    .font(.caption.weight(.semibold))
454
+                    .buttonStyle(.bordered)
455
+                    .controlSize(.small)
514 456
                 }
515
-            } else if let chargingStateMode = chargedDevice.supportedChargingStateModes.first {
516
-                Label(
517
-                    "Charging mode: \(chargingStateMode.title)",
518
-                    systemImage: chargingStateMode == .off ? "power.circle" : "power"
519
-                )
520
-                .font(.subheadline.weight(.semibold))
521 457
             }
522 458
 
523
-            VStack(alignment: .leading, spacing: 8) {
524
-                HStack(spacing: 8) {
525
-                    Text("Initial Checkpoint")
526
-                        .font(.subheadline.weight(.semibold))
527
-                    ContextInfoButton(
528
-                        title: "Initial Checkpoint",
529
-                        message: "Use the battery level shown by the device right now when it is known. A known checkpoint improves battery prediction and capacity learning, but the session can also start without one when the level is unavailable."
530
-                    )
531
-                }
459
+            // Battery checkpoint
460
+            Divider().padding(.leading, 46)
461
+            setupRow(icon: "battery.75percent", iconColor: .green) {
462
+                if initialCheckpointMode == .known {
463
+                    Button { adjustInitialCheckpoint(by: -1) } label: {
464
+                        Image(systemName: "minus.circle").font(.title3)
465
+                    }
466
+                    .buttonStyle(.plain)
467
+
468
+                    TextField("—", text: $initialCheckpoint)
469
+                        .keyboardType(.decimalPad)
470
+                        .textFieldStyle(.roundedBorder)
471
+                        .frame(width: 52)
472
+                        .multilineTextAlignment(.center)
532 473
 
474
+                    Text("%")
475
+                        .font(.subheadline)
476
+                        .foregroundColor(.secondary)
477
+
478
+                    Button { adjustInitialCheckpoint(by: 1) } label: {
479
+                        Image(systemName: "plus.circle").font(.title3)
480
+                    }
481
+                    .buttonStyle(.plain)
482
+                } else {
483
+                    Text(initialCheckpointMode == .flat
484
+                         ? "Flat (device off / discharged)"
485
+                         : "Unknown")
486
+                        .font(.subheadline)
487
+                        .foregroundColor(.secondary)
488
+                }
489
+                Spacer()
533 490
                 compactSelectionMenu(
534 491
                     title: initialCheckpointMode.title,
535 492
                     options: InitialCheckpointMode.allCases.map { mode in
536 493
                         CompactSelectionOption(
537
-                            id: mode.id,
538
-                            title: mode.title,
494
+                            id: mode.id, title: mode.title,
539 495
                             isSelected: initialCheckpointMode == mode,
540 496
                             action: { initialCheckpointMode = mode }
541 497
                         )
542 498
                     }
543 499
                 )
544
-
545
-                if initialCheckpointMode == .known {
546
-                    HStack(spacing: 10) {
547
-                        Button {
548
-                            adjustInitialCheckpoint(by: -1)
549
-                        } label: {
550
-                            Image(systemName: "minus.circle")
551
-                                .font(.title3)
552
-                        }
553
-                        .buttonStyle(.plain)
554
-
555
-                        TextField("Battery %", text: $initialCheckpoint)
556
-                            .keyboardType(.decimalPad)
557
-                            .textFieldStyle(.roundedBorder)
558
-                            .frame(width: 92)
559
-
560
-                        Text("%")
561
-                            .font(.subheadline.weight(.semibold))
562
-                            .foregroundColor(.secondary)
563
-
564
-                        Button {
565
-                            adjustInitialCheckpoint(by: 1)
566
-                        } label: {
567
-                            Image(systemName: "plus.circle")
568
-                                .font(.title3)
569
-                        }
570
-                        .buttonStyle(.plain)
571
-
572
-                        Spacer()
573
-                    }
574
-                } else {
575
-                    Text(
576
-                        initialCheckpointMode == .flat
577
-                        ? "Use Flat when the device does not turn on yet. Predictions and capacity estimates stay off until you record a positive battery level."
578
-                        : "Start without an initial battery checkpoint only when the level cannot be read reliably, for example on a device without display."
579
-                    )
580
-                        .font(.caption2)
581
-                        .foregroundColor(.orange)
582
-                }
583
-
584 500
             }
585 501
 
586
-            VStack(alignment: .leading, spacing: 8) {
587
-                Text("Start Requirements")
588
-                    .font(.subheadline.weight(.semibold))
589
-
590
-                if startRequirements.isEmpty {
591
-                    Label("Everything needed to start is ready.", systemImage: "checkmark.circle.fill")
592
-                        .font(.caption)
593
-                        .foregroundColor(.green)
594
-                } else {
502
+            // Requirement errors
503
+            if startRequirements.isEmpty == false {
504
+                Divider()
505
+                VStack(alignment: .leading, spacing: 6) {
595 506
                     ForEach(startRequirements) { requirement in
596 507
                         Label(requirement.message, systemImage: "exclamationmark.circle")
597 508
                             .font(.caption)
598 509
                             .foregroundColor(.orange)
599 510
                     }
600 511
                 }
512
+                .padding(.horizontal, 14)
513
+                .padding(.vertical, 10)
601 514
             }
602 515
 
516
+            // Start button
517
+            Divider()
603 518
             Button("Start Session") {
604 519
                 startSession()
605 520
             }
606 521
             .frame(maxWidth: .infinity)
607
-            .padding(.vertical, 10)
608
-            .meterCard(tint: .green, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
522
+            .padding(.vertical, 11)
523
+            .font(.subheadline.weight(.semibold))
524
+            .foregroundColor(canStartSession ? .green : .secondary)
609 525
             .buttonStyle(.plain)
610 526
             .disabled(!canStartSession)
611 527
         }
528
+        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
612 529
     }
613 530
 
614
-    private var wirelessChargerSection: some View {
615
-        VStack(alignment: .leading, spacing: 10) {
616
-            HStack {
617
-                Text("Wireless Charger")
618
-                    .font(.subheadline.weight(.semibold))
619
-                Spacer()
620
-                Button(selectedCharger == nil ? "Select" : "Change") {
621
-                    chargerLibraryVisibility = true
622
-                }
623
-                .disabled(openChargeSession != nil)
624
-            }
625
-
626
-            if let selectedCharger {
627
-                HStack(alignment: .top, spacing: 12) {
628
-                    ChargedDeviceQRCodeView(
629
-                        qrIdentifier: selectedCharger.qrIdentifier,
630
-                        side: 62
631
-                    )
632
-
633
-                    VStack(alignment: .leading, spacing: 6) {
634
-                        ChargedDeviceIdentityLabelView(
635
-                            chargedDevice: selectedCharger,
636
-                            iconPointSize: 15
637
-                        )
638
-                        .font(.subheadline.weight(.semibold))
639
-
640
-                        if let chargerMaximumPowerWatts = selectedCharger.chargerMaximumPowerWatts {
641
-                            Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
642
-                                .font(.caption)
643
-                                .foregroundColor(.secondary)
644
-                        }
645
-
646
-                        if let chargerIdleCurrentAmps = selectedCharger.chargerIdleCurrentAmps {
647
-                            Text("Idle current: \(chargerIdleCurrentAmps.format(decimalDigits: 2)) A")
648
-                                .font(.caption2)
649
-                                .foregroundColor(.secondary)
650
-                        } else {
651
-                            Text("Idle current is missing, so wireless stop-threshold learning and auto-stop stay unavailable.")
652
-                                .font(.caption2)
653
-                                .foregroundColor(.orange)
654
-                        }
655
-                    }
656
-                }
657
-
658
-            } else {
659
-                Text("Wireless sessions need a selected charger in addition to the charged device.")
660
-                    .font(.caption)
661
-                    .foregroundColor(.secondary)
662
-            }
663
-        }
664
-    }
531
+    // MARK: - Standby Power Card
665 532
 
666
-    private var standbyMeasurementSection: some View {
667
-        VStack(alignment: .leading, spacing: 10) {
668
-            HStack {
669
-                Text("Charger Standby Power")
670
-                    .font(.subheadline.weight(.semibold))
671
-                Spacer()
672
-                Button(selectedCharger == nil ? "Select Charger" : "Change Charger") {
673
-                    chargerLibraryVisibility = true
674
-                }
675
-                .disabled(openChargeSession != nil)
676
-            }
677
-
678
-            if let selectedCharger {
679
-                HStack(alignment: .top, spacing: 12) {
680
-                    ChargedDeviceQRCodeView(
681
-                        qrIdentifier: selectedCharger.qrIdentifier,
682
-                        side: 62
683
-                    )
684
-
685
-                    VStack(alignment: .leading, spacing: 6) {
686
-                        ChargedDeviceIdentityLabelView(
687
-                            chargedDevice: selectedCharger,
688
-                            iconPointSize: 15
689
-                        )
533
+    private var standbyPowerCard: some View {
534
+        VStack(alignment: .leading, spacing: 12) {
535
+            HStack(spacing: 10) {
536
+                Image(systemName: "powersleep")
537
+                    .foregroundColor(.orange)
538
+                    .font(.title3)
539
+                VStack(alignment: .leading, spacing: 2) {
540
+                    Text("Charger Standby Power")
690 541
                         .font(.subheadline.weight(.semibold))
691
-
692
-                        Text(
693
-                            selectedCharger.latestStandbyPowerMeasurement.map {
694
-                                "Latest standby: \($0.averagePowerWatts.format(decimalDigits: 3)) W"
695
-                            } ?? "No standby baseline saved yet."
696
-                        )
542
+                    Text("Measure idle draw with no device connected.")
697 543
                         .font(.caption)
698 544
                         .foregroundColor(.secondary)
699
-                    }
700 545
                 }
546
+            }
701 547
 
702
-                NavigationLink(
703
-                    destination: ChargerStandbyPowerWizardView(
704
-                        preferredMeterMACAddress: meterMACAddress,
705
-                        preferredChargerID: selectedCharger.id
706
-                    )
707
-                ) {
708
-                    Label("New Measurement", systemImage: "plus.circle.fill")
709
-                        .font(.subheadline.weight(.semibold))
548
+            NavigationLink(
549
+                destination: ChargerStandbyPowerWizardView(
550
+                    preferredMeterMACAddress: meterMACAddress
551
+                )
552
+            ) {
553
+                HStack {
554
+                    Image(systemName: "plus.circle.fill")
710 555
                         .foregroundColor(.orange)
711
-                }
712
-                .buttonStyle(.plain)
713
-                .disabled(openChargeSession != nil)
714
-
715
-                if openChargeSession != nil {
716
-                    Text("Stop or pause the active charge session before starting a standby-power run on this meter.")
717
-                        .font(.caption)
718
-                        .foregroundColor(.secondary)
719
-                }
720
-            } else {
721
-                NavigationLink(
722
-                    destination: ChargerStandbyPowerWizardView(
723
-                        preferredMeterMACAddress: meterMACAddress
724
-                    )
725
-                ) {
726
-                    Label("New Measurement", systemImage: "plus.circle.fill")
556
+                    Text("New Measurement")
727 557
                         .font(.subheadline.weight(.semibold))
728
-                        .foregroundColor(.orange)
558
+                    Spacer()
559
+                    Image(systemName: "chevron.right")
560
+                        .font(.caption.weight(.semibold))
561
+                        .foregroundColor(.secondary)
729 562
                 }
730
-                .buttonStyle(.plain)
731
-
732
-                Text("Open the wizard and choose the charger there, or preselect one from Charge Record first.")
733
-                    .font(.caption)
734
-                    .foregroundColor(.secondary)
563
+                .padding(.vertical, 10)
564
+                .padding(.horizontal, 14)
565
+                .meterCard(tint: .orange, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 14)
735 566
             }
567
+            .buttonStyle(.plain)
736 568
         }
569
+        .padding(18)
570
+        .meterCard(tint: .orange, fillOpacity: 0.10, strokeOpacity: 0.16)
737 571
     }
738 572
 
573
+    // MARK: - Charging Monitor Card
574
+
739 575
     private func chargingMonitorCard(_ openChargeSession: ChargeSessionSummary) -> some View {
740 576
         let displayedEnergyWh = displayedSessionEnergyWh(for: openChargeSession)
741 577
         let displayedChargeAh = displayedSessionChargeAh(for: openChargeSession)
742 578
         let canAddCheckpoint = appData.canAddBatteryCheckpoint(to: openChargeSession.id)
743
-        let metricRows = sessionMetricRows(
744
-            for: openChargeSession,
745
-            displayedEnergyWh: displayedEnergyWh
746
-        )
579
+        let metricRows = sessionMetricRows(for: openChargeSession, displayedEnergyWh: displayedEnergyWh)
580
+
747 581
         return VStack(alignment: .leading, spacing: 12) {
748
-            HStack(spacing: 8) {
749
-                Text("Charging Monitor")
750
-                    .font(.headline)
751
-                ContextInfoButton(
752
-                    title: "Charging Monitor",
753
-                    message: "The monitor keeps the session explicit: it never auto-selects another device or charger, and it never starts a new session on its own."
754
-                )
582
+            // Header
583
+            HStack {
584
+                if let device = selectedChargedDevice {
585
+                    ChargedDeviceIdentityLabelView(chargedDevice: device, iconPointSize: 16)
586
+                        .font(.headline)
587
+                } else {
588
+                    Text("Charging Monitor").font(.headline)
589
+                }
590
+                Spacer()
591
+                Text(openChargeSession.status.title)
592
+                    .font(.caption.weight(.bold))
593
+                    .foregroundColor(headerStatusColor)
594
+                    .padding(.horizontal, 8)
595
+                    .padding(.vertical, 4)
596
+                    .meterCard(tint: headerStatusColor, fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999)
755 597
             }
756 598
 
757 599
             ChargeRecordMetricsTableView(
@@ -760,7 +602,7 @@ struct MeterChargeRecordContentView: View {
760 602
             )
761 603
 
762 604
             if openChargeSession.stopThresholdAmps > 0 {
763
-                Text("Meter monitor threshold: \(openChargeSession.stopThresholdAmps.format(decimalDigits: 2)) A")
605
+                Text("Stop threshold: \(openChargeSession.stopThresholdAmps.format(decimalDigits: 2)) A")
764 606
                     .font(.caption)
765 607
                     .foregroundColor(.secondary)
766 608
             }
@@ -769,46 +611,40 @@ struct MeterChargeRecordContentView: View {
769 611
                 for: openChargeSession,
770 612
                 effectiveEnergyWhOverride: displayedEnergyWh
771 613
             ) {
772
-                VStack(alignment: .leading, spacing: 4) {
773
-                    Text("Predicted battery level: \(batteryPrediction.predictedPercent.format(decimalDigits: 0))%")
614
+                HStack(spacing: 6) {
615
+                    Image(systemName: "battery.75percent")
616
+                        .foregroundColor(.green)
617
+                    Text("Predicted: \(batteryPrediction.predictedPercent.format(decimalDigits: 0))%")
774 618
                         .font(.caption.weight(.semibold))
775
-                    Text(
776
-                        "Anchored to \(batteryPrediction.anchorDescription) at \(batteryPrediction.anchorPercent.format(decimalDigits: 0))% using \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh estimated capacity."
777
-                    )
778
-                    .font(.caption2)
779
-                    .foregroundColor(.secondary)
619
+                    Text("· \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh est.")
620
+                        .font(.caption2)
621
+                        .foregroundColor(.secondary)
780 622
                 }
781 623
             }
782 624
 
783 625
             if let sessionWarning = sessionWarning(for: openChargeSession) {
784
-                Text(sessionWarning)
626
+                Label(sessionWarning, systemImage: "exclamationmark.triangle")
785 627
                     .font(.caption)
786 628
                     .foregroundColor(.orange)
787 629
             }
788 630
 
789 631
             if openChargeSession.isPaused {
790
-                Text("Paused at \(openChargeSession.pausedAt?.format() ?? openChargeSession.lastObservedAt.format()). If it stays paused for more than 10 minutes, it stops automatically.")
791
-                    .font(.caption)
792
-                    .foregroundColor(.secondary)
632
+                Label(
633
+                    "Paused \(openChargeSession.pausedAt?.format() ?? openChargeSession.lastObservedAt.format()). Auto-stops after 10 min.",
634
+                    systemImage: "pause.circle"
635
+                )
636
+                .font(.caption)
637
+                .foregroundColor(.secondary)
793 638
             }
794 639
 
795
-            if openChargeSession.requiresCompletionConfirmation {
640
+            if openChargeSession.requiresCompletionConfirmation && !showingStopConfirm {
796 641
                 completionConfirmationCard(openChargeSession)
797 642
             }
798 643
 
799
-            if let targetBatteryPercent = openChargeSession.targetBatteryPercent {
800
-                Text("Target notification: \(targetBatteryPercent.format(decimalDigits: 0))%")
801
-                    .font(.caption.weight(.semibold))
802
-            } else {
803
-                Text("No target battery notification configured.")
804
-                    .font(.caption)
805
-                    .foregroundColor(.secondary)
806
-            }
807
-
808 644
             BatteryCheckpointSectionView(
809 645
                 sessionID: openChargeSession.id,
810 646
                 checkpoints: openChargeSession.checkpoints,
811
-                message: "The checkpoint is stored on the active charge session and later used for capacity estimation and the typical charge curve.",
647
+                message: "Checkpoints are used for capacity estimation and the typical charge curve.",
812 648
                 canAddCheckpoint: canAddCheckpoint,
813 649
                 requirementMessage: appData.batteryCheckpointCaptureRequirementMessage(for: openChargeSession.id),
814 650
                 effectiveEnergyWhOverride: displayedEnergyWh,
@@ -818,106 +654,360 @@ struct MeterChargeRecordContentView: View {
818 654
                 }
819 655
             )
820 656
 
821
-            Button(openChargeSession.targetBatteryPercent == nil ? "Set Target Notification" : "Change Target Notification") {
822
-                targetNotificationEditorVisibility = true
823
-            }
824
-            .frame(maxWidth: .infinity)
825
-            .padding(.vertical, 10)
826
-            .meterCard(tint: .indigo, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
827
-            .buttonStyle(.plain)
657
+            targetSectionView(
658
+                for: openChargeSession,
659
+                predictedPercent: selectedChargedDevice?.batteryLevelPrediction(
660
+                    for: openChargeSession,
661
+                    effectiveEnergyWhOverride: displayedEnergyWh
662
+                )?.predictedPercent
663
+            )
664
+
665
+            if showingStopConfirm {
666
+                stopConfirmPanel(for: openChargeSession)
667
+            } else {
668
+                // Session controls
669
+                HStack(spacing: 10) {
670
+                    if openChargeSession.status == .active {
671
+                        Button("Pause") {
672
+                            _ = appData.pauseChargeSession(sessionID: openChargeSession.id, from: usbMeter)
673
+                        }
674
+                        .frame(maxWidth: .infinity)
675
+                        .padding(.vertical, 10)
676
+                        .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
677
+                        .buttonStyle(.plain)
678
+                    } else if openChargeSession.status == .paused {
679
+                        Button("Resume") {
680
+                            _ = appData.resumeChargeSession(sessionID: openChargeSession.id, from: usbMeter)
681
+                        }
682
+                        .frame(maxWidth: .infinity)
683
+                        .padding(.vertical, 10)
684
+                        .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
685
+                        .buttonStyle(.plain)
686
+                    }
828 687
 
829
-            if openChargeSession.targetBatteryPercent != nil {
830
-                Button("Clear Target Notification") {
831
-                    _ = appData.setTargetBatteryPercent(nil, for: openChargeSession.id)
688
+                    Button("Stop") {
689
+                        finalCheckpointMode = .full
690
+                        finalCheckpointText = ""
691
+                        showingStopConfirm = true
692
+                    }
693
+                    .frame(maxWidth: .infinity)
694
+                    .padding(.vertical, 10)
695
+                    .meterCard(tint: .red, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
696
+                    .buttonStyle(.plain)
832 697
                 }
833
-                .frame(maxWidth: .infinity)
834
-                .padding(.vertical, 10)
835
-                .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 14)
836
-                .buttonStyle(.plain)
698
+            }
699
+        }
700
+        .padding(18)
701
+        .meterCard(tint: .green, fillOpacity: 0.14, strokeOpacity: 0.20)
702
+    }
703
+
704
+    private func completionConfirmationCard(_ openChargeSession: ChargeSessionSummary) -> some View {
705
+        VStack(alignment: .leading, spacing: 10) {
706
+            Label("Charging may have stopped", systemImage: "questionmark.circle.fill")
707
+                .font(.subheadline.weight(.semibold))
708
+
709
+            if let contradictionPercent = openChargeSession.completionContradictionPercent {
710
+                Text("Current dropped but estimated level is only \(contradictionPercent.format(decimalDigits: 0))%.")
711
+                    .font(.caption)
712
+                    .foregroundColor(.secondary)
713
+            } else {
714
+                Text("Current dropped to the stop threshold but the prediction doesn't confirm a full charge yet.")
715
+                    .font(.caption)
716
+                    .foregroundColor(.secondary)
837 717
             }
838 718
 
839
-            if openChargeSession.status == .active {
840
-                Button("Pause Session") {
841
-                    _ = appData.pauseChargeSession(sessionID: openChargeSession.id, from: usbMeter)
719
+            HStack(spacing: 10) {
720
+                Button("Finish") {
721
+                    finalCheckpointMode = .full
722
+                    finalCheckpointText = ""
723
+                    showingStopConfirm = true
842 724
                 }
843 725
                 .frame(maxWidth: .infinity)
844
-                .padding(.vertical, 10)
726
+                .padding(.vertical, 9)
845 727
                 .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
846 728
                 .buttonStyle(.plain)
847
-            } else if openChargeSession.status == .paused {
848
-                Button("Resume Session") {
849
-                    _ = appData.resumeChargeSession(sessionID: openChargeSession.id, from: usbMeter)
729
+
730
+                Button("Keep Monitoring") {
731
+                    _ = appData.continueChargeSessionMonitoring(sessionID: openChargeSession.id)
850 732
                 }
851 733
                 .frame(maxWidth: .infinity)
852
-                .padding(.vertical, 10)
734
+                .padding(.vertical, 9)
853 735
                 .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
854 736
                 .buttonStyle(.plain)
855 737
             }
856
-
857
-            Button("Stop Session") {
858
-                pendingStopRequest = ChargeSessionStopRequest(
859
-                    sessionID: openChargeSession.id,
860
-                    title: "Stop Session",
861
-                    confirmTitle: "Stop",
862
-                    explanation: "Record the final battery checkpoint before closing this session."
863
-                )
864
-            }
865
-            .frame(maxWidth: .infinity)
866
-            .padding(.vertical, 10)
867
-            .meterCard(tint: .red, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
868
-            .buttonStyle(.plain)
869
-
870 738
         }
871
-        .padding(18)
872
-        .meterCard(tint: .green, fillOpacity: 0.14, strokeOpacity: 0.20)
739
+        .padding(14)
740
+        .meterCard(tint: .orange, fillOpacity: 0.10, strokeOpacity: 0.16, cornerRadius: 16)
873 741
     }
874 742
 
875
-    private func completionConfirmationCard(_ openChargeSession: ChargeSessionSummary) -> some View {
876
-        VStack(alignment: .leading, spacing: 10) {
877
-            Text("Completion Needs Confirmation")
743
+    // MARK: - Target Section
744
+
745
+    private func targetSectionView(for session: ChargeSessionSummary, predictedPercent: Double?) -> some View {
746
+        let draftBelowPrediction: Bool = {
747
+            guard let draft = parsedDraftTarget, let predicted = predictedPercent else { return false }
748
+            return draft <= predicted
749
+        }()
750
+        let savedBelowPrediction: Bool = {
751
+            guard let saved = session.targetBatteryPercent, let predicted = predictedPercent else { return false }
752
+            return saved <= predicted
753
+        }()
754
+
755
+        return HStack(alignment: .center, spacing: 8) {
756
+            Image(systemName: "bell.badge")
757
+                .foregroundColor(.indigo)
758
+                .font(.subheadline)
759
+
760
+            Text("Notify at")
878 761
                 .font(.subheadline.weight(.semibold))
879 762
 
880
-            if let contradictionPercent = openChargeSession.completionContradictionPercent {
881
-                Text("Current says charging may have stopped, but the estimated battery level is only \(contradictionPercent.format(decimalDigits: 0))%.")
882
-                    .font(.caption)
763
+            Spacer(minLength: 8)
764
+
765
+            if showingInlineTargetEditor {
766
+                Button {
767
+                    let current = parsedDraftTarget ?? session.targetBatteryPercent ?? 80
768
+                    let next = max(current - 1, 1)
769
+                    draftTargetText = next.format(decimalDigits: 0)
770
+                } label: {
771
+                    Image(systemName: "minus.circle")
772
+                        .font(.title3)
773
+                }
774
+                .buttonStyle(.plain)
775
+
776
+                TextField("—", text: $draftTargetText)
777
+                    .keyboardType(.decimalPad)
778
+                    .textFieldStyle(.roundedBorder)
779
+                    .frame(width: 48)
780
+                    .multilineTextAlignment(.center)
781
+                    .foregroundColor(draftBelowPrediction ? .orange : .primary)
782
+
783
+                Text("%")
784
+                    .font(.subheadline)
883 785
                     .foregroundColor(.secondary)
786
+
787
+                if draftBelowPrediction {
788
+                    Button {} label: {
789
+                        Image(systemName: "exclamationmark.triangle.fill")
790
+                            .font(.body.weight(.semibold))
791
+                            .foregroundColor(.orange)
792
+                    }
793
+                    .buttonStyle(.plain)
794
+                    .help("Battery is already predicted at \(predictedPercent!.format(decimalDigits: 0))% — this alert won't fire. Raise the value or add a checkpoint to correct the prediction.")
795
+                }
796
+
797
+                Button {
798
+                    let current = parsedDraftTarget ?? session.targetBatteryPercent ?? 80
799
+                    let next = min(current + 1, 100)
800
+                    draftTargetText = next.format(decimalDigits: 0)
801
+                } label: {
802
+                    Image(systemName: "plus.circle")
803
+                        .font(.title3)
804
+                }
805
+                .buttonStyle(.plain)
806
+
807
+                Button {
808
+                    if let value = parsedDraftTarget {
809
+                        _ = appData.setTargetBatteryPercent(value, for: session.id)
810
+                    }
811
+                    showingInlineTargetEditor = false
812
+                } label: {
813
+                    Image(systemName: "checkmark.circle.fill")
814
+                        .foregroundColor(parsedDraftTarget != nil ? .indigo : .secondary)
815
+                        .font(.title3)
816
+                }
817
+                .buttonStyle(.plain)
818
+                .disabled(parsedDraftTarget == nil)
819
+
820
+                Button {
821
+                    showingInlineTargetEditor = false
822
+                    draftTargetText = ""
823
+                } label: {
824
+                    Image(systemName: "xmark.circle")
825
+                        .foregroundColor(.secondary)
826
+                        .font(.title3)
827
+                }
828
+                .buttonStyle(.plain)
829
+
884 830
             } else {
885
-                Text("Current dropped to the learned stop threshold, but the battery prediction does not look like a normal finish yet.")
886
-                    .font(.caption)
887
-                    .foregroundColor(.secondary)
831
+                if let targetPercent = session.targetBatteryPercent {
832
+                    Text("\(targetPercent.format(decimalDigits: 0))%")
833
+                        .font(.subheadline.weight(.semibold))
834
+                        .foregroundColor(savedBelowPrediction ? .orange : .indigo)
835
+
836
+                    if savedBelowPrediction {
837
+                        Button {} label: {
838
+                            Image(systemName: "exclamationmark.triangle.fill")
839
+                                .font(.callout.weight(.semibold))
840
+                                .foregroundColor(.orange)
841
+                        }
842
+                        .buttonStyle(.plain)
843
+                        .help("Battery is already predicted at \(predictedPercent!.format(decimalDigits: 0))% — this alert won't fire. Raise the value or add a checkpoint to correct the prediction.")
844
+                    }
845
+
846
+                    Button {
847
+                        _ = appData.setTargetBatteryPercent(nil, for: session.id)
848
+                    } label: {
849
+                        Image(systemName: "xmark.circle.fill")
850
+                            .foregroundColor(.secondary)
851
+                            .font(.callout)
852
+                    }
853
+                    .buttonStyle(.plain)
854
+                    .help("Remove alert")
855
+                }
856
+
857
+                Button {
858
+                    draftTargetText = session.targetBatteryPercent.map {
859
+                        $0.format(decimalDigits: 0)
860
+                    } ?? "80"
861
+                    showingInlineTargetEditor = true
862
+                } label: {
863
+                    Image(systemName: session.targetBatteryPercent == nil ? "plus" : "pencil")
864
+                        .font(.caption.weight(.semibold))
865
+                        .frame(width: 30, height: 30)
866
+                        .contentShape(Rectangle())
867
+                }
868
+                .meterCard(tint: .indigo, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 10)
869
+                .buttonStyle(.plain)
870
+                .help(session.targetBatteryPercent == nil ? "Set battery alert" : "Edit battery alert")
888 871
             }
872
+        }
873
+    }
889 874
 
890
-            Button("Finish Session With Final Checkpoint") {
891
-                pendingStopRequest = ChargeSessionStopRequest(
892
-                    sessionID: openChargeSession.id,
893
-                    title: "Finish Session",
894
-                    confirmTitle: "Finish",
895
-                    explanation: "Add the final checkpoint before confirming the stop."
896
-                )
875
+    private var parsedDraftTarget: Double? {
876
+        let normalized = draftTargetText
877
+            .trimmingCharacters(in: .whitespacesAndNewlines)
878
+            .replacingOccurrences(of: ",", with: ".")
879
+        guard let value = Double(normalized), value >= 1, value <= 100 else { return nil }
880
+        return value
881
+    }
882
+
883
+    private func stopConfirmPanel(for session: ChargeSessionSummary) -> some View {
884
+        VStack(alignment: .leading, spacing: 12) {
885
+            Text("Final Checkpoint (optional)")
886
+                .font(.subheadline.weight(.semibold))
887
+
888
+            // Three compact option tiles
889
+            HStack(spacing: 8) {
890
+                ForEach([FinalCheckpoint.full, .skip, .custom], id: \.self) { mode in
891
+                    Button {
892
+                        finalCheckpointMode = mode
893
+                        if mode != .custom {
894
+                            finalCheckpointText = ""
895
+                        }
896
+                    } label: {
897
+                        VStack(spacing: 5) {
898
+                            Image(systemName: mode.icon)
899
+                                .font(.title3)
900
+                                .foregroundColor(finalCheckpointMode == mode ? .primary : .secondary)
901
+                            Text(mode.label)
902
+                                .font(.caption.weight(.semibold))
903
+                                .foregroundColor(finalCheckpointMode == mode ? .primary : .secondary)
904
+                        }
905
+                        .frame(maxWidth: .infinity)
906
+                        .padding(.vertical, 10)
907
+                        .background(
908
+                            finalCheckpointMode == mode
909
+                                ? Color.primary.opacity(0.10)
910
+                                : Color.clear
911
+                        )
912
+                        .meterCard(
913
+                            tint: finalCheckpointMode == mode ? .primary : .secondary,
914
+                            fillOpacity: finalCheckpointMode == mode ? 0.08 : 0.04,
915
+                            strokeOpacity: finalCheckpointMode == mode ? 0.20 : 0.10,
916
+                            cornerRadius: 12
917
+                        )
918
+                    }
919
+                    .buttonStyle(.plain)
920
+                }
897 921
             }
898
-            .frame(maxWidth: .infinity)
899
-            .padding(.vertical, 10)
900
-            .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
901
-            .buttonStyle(.plain)
902 922
 
903
-            Button("Keep Monitoring") {
904
-                _ = appData.continueChargeSessionMonitoring(sessionID: openChargeSession.id)
923
+            // Custom % input
924
+            if finalCheckpointMode == .custom {
925
+                HStack(spacing: 8) {
926
+                    Button { adjustFinalCheckpoint(by: -1) } label: {
927
+                        Image(systemName: "minus.circle").font(.title3)
928
+                    }
929
+                    .buttonStyle(.plain)
930
+
931
+                    TextField("—", text: $finalCheckpointText)
932
+                        .keyboardType(.decimalPad)
933
+                        .textFieldStyle(.roundedBorder)
934
+                        .frame(width: 56)
935
+                        .multilineTextAlignment(.center)
936
+
937
+                    Text("%")
938
+                        .foregroundColor(.secondary)
939
+
940
+                    Button { adjustFinalCheckpoint(by: 1) } label: {
941
+                        Image(systemName: "plus.circle").font(.title3)
942
+                    }
943
+                    .buttonStyle(.plain)
944
+
945
+                    Spacer()
946
+                }
947
+            }
948
+
949
+            // Action row
950
+            HStack(spacing: 10) {
951
+                Button("Cancel") {
952
+                    showingStopConfirm = false
953
+                    finalCheckpointText = ""
954
+                }
955
+                .frame(maxWidth: .infinity)
956
+                .padding(.vertical, 9)
957
+                .meterCard(tint: .secondary, fillOpacity: 0.10, strokeOpacity: 0.14, cornerRadius: 14)
958
+                .buttonStyle(.plain)
959
+
960
+                let stopDisabled = finalCheckpointMode == .custom
961
+                    && finalCheckpointText.isEmpty == false
962
+                    && parsedFinalCheckpoint == nil
963
+
964
+                Button("Stop Session") {
965
+                    _ = appData.stopChargeSession(
966
+                        sessionID: session.id,
967
+                        finalBatteryPercent: resolvedFinalCheckpoint
968
+                    )
969
+                    showingStopConfirm = false
970
+                    finalCheckpointText = ""
971
+                    finalCheckpointMode = .full
972
+                }
973
+                .frame(maxWidth: .infinity)
974
+                .padding(.vertical, 9)
975
+                .meterCard(tint: .red, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
976
+                .buttonStyle(.plain)
977
+                .disabled(stopDisabled)
905 978
             }
906
-            .frame(maxWidth: .infinity)
907
-            .padding(.vertical, 10)
908
-            .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
909
-            .buttonStyle(.plain)
910 979
         }
911 980
         .padding(14)
912
-        .meterCard(tint: .orange, fillOpacity: 0.10, strokeOpacity: 0.16, cornerRadius: 16)
981
+        .meterCard(tint: .red, fillOpacity: 0.06, strokeOpacity: 0.14, cornerRadius: 16)
913 982
     }
914 983
 
915
-    private func sessionChartCard(
916
-        timeRange: ClosedRange<Date>,
917
-        session: ChargeSessionSummary
918
-    ) -> some View {
984
+    private var parsedFinalCheckpoint: Double? {
985
+        let normalized = finalCheckpointText
986
+            .trimmingCharacters(in: .whitespacesAndNewlines)
987
+            .replacingOccurrences(of: ",", with: ".")
988
+        guard let value = Double(normalized), value >= 0, value <= 100 else { return nil }
989
+        return value
990
+    }
991
+
992
+    private var resolvedFinalCheckpoint: Double? {
993
+        switch finalCheckpointMode {
994
+        case .full:   return 100.0
995
+        case .skip:   return nil
996
+        case .custom: return parsedFinalCheckpoint
997
+        }
998
+    }
999
+
1000
+    private func adjustFinalCheckpoint(by delta: Double) {
1001
+        let current = parsedFinalCheckpoint ?? 0
1002
+        let next = min(max(current + delta, 0), 100)
1003
+        finalCheckpointText = next.format(decimalDigits: 0)
1004
+    }
1005
+
1006
+    private func sessionChartCard(timeRange: ClosedRange<Date>, session: ChargeSessionSummary) -> some View {
919 1007
         VStack(alignment: .leading, spacing: 12) {
920 1008
             HStack(spacing: 8) {
1009
+                Image(systemName: "chart.xyaxis.line")
1010
+                    .foregroundColor(.blue)
921 1011
                 Text("Session Chart")
922 1012
                     .font(.headline)
923 1013
                 ContextInfoButton(
@@ -996,9 +1086,22 @@ struct MeterChargeRecordContentView: View {
996 1086
         .meterCard(tint: .teal, fillOpacity: 0.14, strokeOpacity: 0.20)
997 1087
     }
998 1088
 
999
-    private var showsWirelessChargerSection: Bool {
1000
-        let transportMode = selectedDraftTransportMode ?? selectedChargedDevice?.supportedChargingModes.first
1001
-        return transportMode == .wireless
1089
+    // MARK: - Helpers
1090
+
1091
+    private func setupRow<Content: View>(
1092
+        icon: String,
1093
+        iconColor: Color = .secondary,
1094
+        @ViewBuilder content: () -> Content
1095
+    ) -> some View {
1096
+        HStack(spacing: 10) {
1097
+            Image(systemName: icon)
1098
+                .foregroundColor(iconColor)
1099
+                .font(.body.weight(.medium))
1100
+                .frame(width: 22, alignment: .center)
1101
+            content()
1102
+        }
1103
+        .padding(.horizontal, 14)
1104
+        .padding(.vertical, 11)
1002 1105
     }
1003 1106
 
1004 1107
     private func autoStopLabel(for session: ChargeSessionSummary) -> String {
@@ -1035,52 +1138,34 @@ struct MeterChargeRecordContentView: View {
1035 1138
     }
1036 1139
 
1037 1140
     private func shouldShowChargingTransport(for session: ChargeSessionSummary) -> Bool {
1038
-        guard let selectedChargedDevice else {
1039
-            return true
1040
-        }
1141
+        guard let selectedChargedDevice else { return true }
1041 1142
         return selectedChargedDevice.supportedChargingModes.count > 1
1042 1143
             || selectedChargedDevice.supportedChargingModes.contains(session.chargingTransportMode) == false
1043 1144
     }
1044 1145
 
1045 1146
     private func shouldShowChargingState(for session: ChargeSessionSummary) -> Bool {
1046
-        guard let selectedChargedDevice else {
1047
-            return true
1048
-        }
1147
+        guard let selectedChargedDevice else { return true }
1049 1148
         return selectedChargedDevice.supportedChargingStateModes.count > 1
1050 1149
             || selectedChargedDevice.supportedChargingStateModes.contains(session.chargingStateMode) == false
1051 1150
     }
1052 1151
 
1053 1152
     private func displayedSessionEnergyWh(for session: ChargeSessionSummary) -> Double {
1054 1153
         let storedEnergyWh = session.effectiveOrMeasuredEnergyWh
1055
-        guard session.status.isOpen else {
1056
-            return storedEnergyWh
1057
-        }
1058
-
1059
-        guard session.meterMACAddress == meterMACAddress else {
1060
-            return storedEnergyWh
1061
-        }
1062
-
1154
+        guard session.status.isOpen else { return storedEnergyWh }
1155
+        guard session.meterMACAddress == meterMACAddress else { return storedEnergyWh }
1063 1156
         if let baselineEnergyWh = session.meterEnergyBaselineWh {
1064 1157
             return max(storedEnergyWh, max(usbMeter.recordedWH - baselineEnergyWh, 0))
1065 1158
         }
1066
-
1067 1159
         return storedEnergyWh
1068 1160
     }
1069 1161
 
1070 1162
     private func displayedSessionChargeAh(for session: ChargeSessionSummary) -> Double {
1071 1163
         let storedChargeAh = session.measuredChargeAh
1072
-        guard session.status.isOpen else {
1073
-            return storedChargeAh
1074
-        }
1075
-
1076
-        guard session.meterMACAddress == meterMACAddress else {
1077
-            return storedChargeAh
1078
-        }
1079
-
1164
+        guard session.status.isOpen else { return storedChargeAh }
1165
+        guard session.meterMACAddress == meterMACAddress else { return storedChargeAh }
1080 1166
         if let baselineChargeAh = session.meterChargeBaselineAh {
1081 1167
             return max(storedChargeAh, max(usbMeter.recordedAH - baselineChargeAh, 0))
1082 1168
         }
1083
-
1084 1169
         return storedChargeAh
1085 1170
     }
1086 1171
 
@@ -1089,7 +1174,6 @@ struct MeterChargeRecordContentView: View {
1089 1174
         let hours = totalSeconds / 3600
1090 1175
         let minutes = (totalSeconds % 3600) / 60
1091 1176
         let seconds = totalSeconds % 60
1092
-
1093 1177
         if hours > 0 {
1094 1178
             return String(format: "%d:%02d:%02d", hours, minutes, seconds)
1095 1179
         }
@@ -1102,11 +1186,7 @@ struct MeterChargeRecordContentView: View {
1102 1186
               let charger = appData.chargedDeviceSummary(id: chargerID) else {
1103 1187
             return nil
1104 1188
         }
1105
-
1106
-        guard charger.chargerIdleCurrentAmps == nil else {
1107
-            return nil
1108
-        }
1109
-
1189
+        guard charger.chargerIdleCurrentAmps == nil else { return nil }
1110 1190
         return "The selected charger has no idle-current measurement. Wireless stop-threshold learning and precise auto-stop are unavailable for this session."
1111 1191
     }
1112 1192
 
@@ -1136,10 +1216,7 @@ struct MeterChargeRecordContentView: View {
1136 1216
     }
1137 1217
 
1138 1218
     private func adjustInitialCheckpoint(by delta: Double) {
1139
-        guard initialCheckpointMode == .known else {
1140
-            return
1141
-        }
1142
-
1219
+        guard initialCheckpointMode == .known else { return }
1143 1220
         let currentValue = initialCheckpointValue ?? 0
1144 1221
         let nextValue = min(max(currentValue + delta, 0), 100)
1145 1222
         initialCheckpoint = nextValue.format(decimalDigits: 0)
@@ -1214,109 +1291,10 @@ struct MeterChargeRecordContentView: View {
1214 1291
             }
1215 1292
             .padding(.horizontal, 12)
1216 1293
             .padding(.vertical, 9)
1217
-            .frame(width: 180, alignment: .leading)
1294
+            .frame(width: 160, alignment: .leading)
1218 1295
             .meterCard(tint: .secondary, fillOpacity: 0.08, strokeOpacity: 0.14, cornerRadius: 12)
1219 1296
         }
1220 1297
         .buttonStyle(.plain)
1221 1298
     }
1222 1299
 }
1223 1300
 
1224
-struct ChargeSessionCompletionSheetView: View {
1225
-    @EnvironmentObject private var appData: AppData
1226
-    @Environment(\.dismiss) private var dismiss
1227
-
1228
-    let sessionID: UUID
1229
-    let title: String
1230
-    let confirmTitle: String
1231
-    let explanation: String
1232
-
1233
-    @State private var batteryPercent = ""
1234
-
1235
-    var body: some View {
1236
-        NavigationView {
1237
-            Form {
1238
-                Section(
1239
-                    header: ContextInfoHeader(
1240
-                        title: "Final Checkpoint",
1241
-                        message: explanation
1242
-                    )
1243
-                ) {
1244
-                    TextField("Battery %", text: $batteryPercent)
1245
-                        .keyboardType(.decimalPad)
1246
-                }
1247
-
1248
-                Section {
1249
-                    if let sessionWarning {
1250
-                        Text(sessionWarning)
1251
-                            .font(.footnote)
1252
-                            .foregroundColor(.orange)
1253
-                    } else if (parsedBatteryPercent ?? 0) >= 99.5 {
1254
-                        Text("A final checkpoint at 100% lets the app learn the stop current for this exact charging type when the session data is reliable.")
1255
-                            .font(.footnote)
1256
-                            .foregroundColor(.secondary)
1257
-                    }
1258
-                }
1259
-            }
1260
-            .navigationTitle(title)
1261
-            .navigationBarTitleDisplayMode(.inline)
1262
-            .toolbar {
1263
-                ToolbarItem(placement: .cancellationAction) {
1264
-                    Button("Cancel") {
1265
-                        dismiss()
1266
-                    }
1267
-                }
1268
-                ToolbarItem(placement: .confirmationAction) {
1269
-                    Button(confirmTitle) {
1270
-                        guard let batteryPercent = parsedBatteryPercent else {
1271
-                            return
1272
-                        }
1273
-
1274
-                        if appData.stopChargeSession(
1275
-                            sessionID: sessionID,
1276
-                            finalBatteryPercent: batteryPercent
1277
-                        ) {
1278
-                            dismiss()
1279
-                        }
1280
-                    }
1281
-                    .disabled(parsedBatteryPercent == nil)
1282
-                }
1283
-            }
1284
-        }
1285
-        .navigationViewStyle(StackNavigationViewStyle())
1286
-    }
1287
-
1288
-    private var parsedBatteryPercent: Double? {
1289
-        let normalized = batteryPercent
1290
-            .trimmingCharacters(in: .whitespacesAndNewlines)
1291
-            .replacingOccurrences(of: ",", with: ".")
1292
-        guard let value = Double(normalized), value >= 0, value <= 100 else {
1293
-            return nil
1294
-        }
1295
-        return value
1296
-    }
1297
-
1298
-    private var sessionWarning: String? {
1299
-        guard let session = appData.chargedDevices
1300
-            .flatMap(\.sessions)
1301
-            .first(where: { $0.id == sessionID }),
1302
-              session.chargingTransportMode == .wireless,
1303
-              let chargerID = session.chargerID,
1304
-              let charger = appData.chargedDeviceSummary(id: chargerID),
1305
-              charger.chargerIdleCurrentAmps == nil else {
1306
-            return nil
1307
-        }
1308
-
1309
-        return "This charger has no idle-current measurement, so the final checkpoint will stop the session but will not learn a wireless stop threshold yet."
1310
-    }
1311
-}
1312
-
1313
-private struct ChargeSessionStopRequest: Identifiable {
1314
-    let sessionID: UUID
1315
-    let title: String
1316
-    let confirmTitle: String
1317
-    let explanation: String
1318
-
1319
-    var id: UUID {
1320
-        sessionID
1321
-    }
1322
-}