Debug: 100 m fence delivery test + test-button trace logging
Settings → Debug → 'Debug fence (delivery test)': registers a 100 m
CLCircularRegion at the current fix that exists solely to prove iOS
delivers region events on this install. Entering it ticks 'Region events
this session', logs 'debug fence entered' in the Alert trace, and fires a
plain notification — zero alert gates. Cleared via button; survives the
station re-registration loop (update() now skips debug-fence regions);
cleared when alerts are disabled.
The Test alert / Plain test buttons now also write to the Alert trace
('test alert scheduled — …'), so the trace reflects every probe, not just
live geofence entries. 83 tests, Release build green.
This commit is contained in:
@@ -96,6 +96,11 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
/// How many `didEnterRegion` callbacks this session — proof the geofences
|
||||
/// actually fire (vs. a gate silently blocking every alert).
|
||||
@Published private(set) var regionEventCount = 0
|
||||
/// Identifier of the armed debug fence (Settings → Debug → "Register
|
||||
/// 100 m fence"), nil when not armed. The fence is a 100 m circle at the
|
||||
/// current location that exists only to prove iOS delivers region events
|
||||
/// on this install; entering it fires a plain notification, no gates.
|
||||
@Published private(set) var debugFenceIdentifier: String?
|
||||
|
||||
private let manager = CLLocationManager()
|
||||
private var stations: [FuelStation] = []
|
||||
@@ -146,7 +151,10 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
|
||||
refreshDebugStatus()
|
||||
|
||||
for region in manager.monitoredRegions {
|
||||
// Drop every region EXCEPT the debug fence (if armed) — the fence is
|
||||
// a fixed 100 m circle at a fixed point that must survive re-registration
|
||||
// churn; stations re-register from scratch every pass.
|
||||
for region in manager.monitoredRegions where !region.identifier.hasPrefix("debug-fence") {
|
||||
manager.stopMonitoring(for: region)
|
||||
}
|
||||
monitoredStationIDs = []
|
||||
@@ -200,6 +208,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
manager.stopMonitoring(for: region)
|
||||
}
|
||||
monitoredStationIDs = []
|
||||
debugFenceIdentifier = nil
|
||||
} else {
|
||||
requestPermissions()
|
||||
// Re-register geofences from restored cache immediately.
|
||||
@@ -215,6 +224,42 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
if alertLog.count > 8 { alertLog.removeLast() }
|
||||
}
|
||||
|
||||
// MARK: - Debug fence
|
||||
|
||||
/// Debug-only (Settings → Debug → "Register 100 m fence"): arms a 100 m
|
||||
/// circle around the last known fix purely to prove iOS delivers region
|
||||
/// events on this install. Stepping 100 m away and back should tick
|
||||
/// "Region events this session" and fire a plain notification — no alert
|
||||
/// gates apply. If that produces nothing, region delivery is broken at the
|
||||
/// system level and no amount of alert-logic fixing will help.
|
||||
func registerDebugFenceAroundMe() {
|
||||
guard let fix = FuelStore.loadLocationWithDate() else {
|
||||
logAlert(.error, "debug fence — no location fix yet (wait for one, then retry)")
|
||||
return
|
||||
}
|
||||
clearDebugFence()
|
||||
let id = "debug-fence-\(UUID().uuidString.prefix(8))"
|
||||
let region = CLCircularRegion(
|
||||
center: CLLocationCoordinate2D(latitude: fix.coordinate.lat, longitude: fix.coordinate.lng),
|
||||
radius: 100,
|
||||
identifier: id
|
||||
)
|
||||
region.notifyOnEntry = true
|
||||
region.notifyOnExit = false
|
||||
manager.startMonitoring(for: region)
|
||||
debugFenceIdentifier = id
|
||||
logAlert(.entry, "debug fence armed — 100 m circle at current location (step away and back in)")
|
||||
}
|
||||
|
||||
func clearDebugFence() {
|
||||
guard let id = debugFenceIdentifier else { return }
|
||||
for region in manager.monitoredRegions where region.identifier == id {
|
||||
manager.stopMonitoring(for: region)
|
||||
}
|
||||
debugFenceIdentifier = nil
|
||||
logAlert(.gate, "debug fence cleared")
|
||||
}
|
||||
|
||||
/// Recomputes the Settings → Debug location snapshot from current state.
|
||||
/// "In range" uses the SAME criteria as live alerts: stations selling the
|
||||
/// monitored fuel within the alert radius of the last known location.
|
||||
@@ -288,6 +333,15 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
// "delivery fine, a gate blocked the alert".
|
||||
self.regionEventCount += 1
|
||||
self.logAlert(.entry, "didEnterRegion — \(region.identifier)")
|
||||
// Debug fence (Settings → Debug): exists only to prove iOS
|
||||
// delivers region events on this install. Entering it fires a
|
||||
// plain notification with no alert gates — if the counter ticks
|
||||
// and a banner shows after stepping out and back, delivery works.
|
||||
if region.identifier.hasPrefix("debug-fence") {
|
||||
self.logAlert(.fired, "debug fence entered — iOS delivered the region event (100 m)")
|
||||
self.sendPlainTestNotification()
|
||||
return
|
||||
}
|
||||
self.handleEntry(region)
|
||||
}
|
||||
}
|
||||
@@ -585,6 +639,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
content.body = "\(choices.count) stations at \(String(format: "%.1fp", price)) within \(unit.format(radiusKM)). Pick one for directions."
|
||||
let baseResult = "\(fuel.displayName) · \(choices.count) tied at \(String(format: "%.1fp", price)) · radius \(unit.format(radiusKM))"
|
||||
lastTestResult = baseResult
|
||||
logAlert(.fired, "test alert scheduled — \(choices.count) tied at \(String(format: "%.1fp", price))")
|
||||
addAlertRequest(content: content, station: candidate, ties: choices) { status in
|
||||
self.lastTestResult = "\(baseResult) · \(status)"
|
||||
}
|
||||
@@ -593,6 +648,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
content.body = "\(candidate.name) · \(distanceText) away · \(String(format: "%.1fp", price)). Tap for directions."
|
||||
let baseResult = "\(fuel.displayName) · \(brand) · \(String(format: "%.1fp", price)) · \(distanceText) away · radius \(unit.format(radiusKM))"
|
||||
lastTestResult = baseResult
|
||||
logAlert(.fired, "test alert scheduled — \(brand) · \(String(format: "%.1fp", price))")
|
||||
addAlertRequest(content: content, station: candidate) { status in
|
||||
self.lastTestResult = "\(baseResult) · \(status)"
|
||||
}
|
||||
@@ -609,6 +665,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
content.sound = .default
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
logAlert(.fired, "test alert scheduled — no \(fuel.displayName.lowercased()) candidate, fallback copy sent")
|
||||
lastTestResult = sellers.isEmpty
|
||||
? "No stations loaded — open the Stations tab first"
|
||||
: "No \(fuel.displayName.lowercased()) seller within \(unit.format(radiusKM))"
|
||||
@@ -642,6 +699,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
content.title = "\(brand) — plain test notification"
|
||||
content.body = "\(nearest.name) · \(distanceText) away · \(String(format: "%.1fp", price)). Tap for directions."
|
||||
content.sound = .default
|
||||
logAlert(.fired, "plain test notification scheduled — \(brand)")
|
||||
addAlertRequest(content: content, station: nearest)
|
||||
} else {
|
||||
content.title = "FuelBoard test notification"
|
||||
@@ -649,6 +707,7 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
content.sound = .default
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
logAlert(.fired, "plain test notification scheduled — no station data")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user