Debug: live alert trace — surface every gate decision from the alert chain
ProximityMonitor now records each stage the live chain reaches (Settings → Debug → Alert trace, in-memory, newest-first, capped at 8): - didEnterRegion arrival (regionEventCount + entry line) — distinguishes 'geofence never fires' from 'gate blocked the alert' - gate misses: station not in loaded list, hourly dedup (with minutes ago), no fuel sellers within radius after fresh fetch, entered-not-cheapest (with the cheaper station + price) - the previously-silent fresh-fetch catch now logs the error - 'alert scheduled' once UNUserNotificationCenter.add is called — if that line appears but no banner shows, the failure is delivery/permission, not alert logic SettingsView shows the trace (symbol per kind) + region-event count; the existing Test-alert button already probes notification permission and reports denial inline. 83 tests, Release build green.
This commit is contained in:
@@ -41,6 +41,11 @@ struct SettingsView: View {
|
||||
/// Live geofence count (monitor.monitoredStationIDs.count) — Debug-only
|
||||
/// status; the user-facing Alerts tab no longer exposes fence plumbing.
|
||||
var monitoredCount: Int = 0
|
||||
/// Live alert trace (monitor.alertLog) — every stage the alert chain
|
||||
/// reached this session (region event / gate / scheduled). Debug-only.
|
||||
var alertLog: [AlertLogEntry] = []
|
||||
/// Region events received this session (monitor.regionEventCount).
|
||||
var regionEventCount: Int = 0
|
||||
|
||||
@StateObject private var tipStore = TipStore()
|
||||
@State private var showTipAlert = false
|
||||
@@ -228,6 +233,34 @@ struct SettingsView: View {
|
||||
} header: {
|
||||
Text("Geofence monitoring")
|
||||
}
|
||||
|
||||
Section {
|
||||
LabeledContent("Region events this session", value: "\(regionEventCount)")
|
||||
if alertLog.isEmpty {
|
||||
Text("No alert activity yet — drive across a geofence boundary, or tap the test-alert button above to exercise the chain without a geofence.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(alertLog) { entry in
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Image(systemName: alertSymbol(for: entry.kind))
|
||||
.foregroundStyle(alertColor(for: entry.kind))
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(entry.text)
|
||||
.font(.caption2)
|
||||
.textSelection(.enabled)
|
||||
Text(entry.date, format: .dateTime.hour().minute().second())
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Alert trace")
|
||||
} footer: {
|
||||
Text("One line per stage the live chain reached: a geofence entry, each gate that passed or blocked the alert (dedup, cheapest-within-radius, fresh fetch), then the scheduled alert. If the trace shows 'alert scheduled' but no banner appears, the failure is in notification delivery itself (permission or background delivery), not the alert logic.")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -442,6 +475,26 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Alert trace rendering
|
||||
|
||||
private func alertSymbol(for kind: AlertLogEntry.Kind) -> String {
|
||||
switch kind {
|
||||
case .entry: return "location.circle.fill"
|
||||
case .gate: return "arrow.right.circle"
|
||||
case .error: return "xmark.octagon.fill"
|
||||
case .fired: return "bell.badge.fill"
|
||||
}
|
||||
}
|
||||
|
||||
private func alertColor(for kind: AlertLogEntry.Kind) -> Color {
|
||||
switch kind {
|
||||
case .entry: return .green
|
||||
case .gate: return .orange
|
||||
case .error: return .red
|
||||
case .fired: return .blue
|
||||
}
|
||||
}
|
||||
|
||||
private func sendTestAlert() {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
Task { @MainActor in
|
||||
|
||||
Reference in New Issue
Block a user