Widget diagnostics: beacon from makeEntry (keychain + relay) + app diagnostics section

The small-widget skeleton survived four resolution fixes; instrument the
widget instead of guessing. Every makeEntry now writes its entry state
(intent type, location source, station count, fuel, sort, favourites flag,
first station) to a keychain beacon (app-readable) and fire-and-forgets a
GET to the relay's /api/v1/widget-diag so its access log records whether a
widget timeline actually runs in the extension and what it produced.
Settings → Debug gains a Widget Diagnostics section: the keychain beacon +
the installed-widget inventory from WidgetCenter. Relay gains the
/api/v1/widget-diag route (204, log-only).
This commit is contained in:
FuelBoard Contributor
2026-08-13 19:52:12 +01:00
parent 4d35b08533
commit 335a9751dc
3 changed files with 90 additions and 0 deletions
+48
View File
@@ -45,6 +45,28 @@ struct SettingsView: View {
@State private var debugMode: Bool = FuelStore.loadDebugMode()
@State private var versionTapCount = 0
@State private var lastVersionTap = Date.distantPast
/// Widget diagnostics: the extension's last makeEntry beacon (keychain)
/// + the installed-widget inventory from WidgetCenter.
@State private var widgetDiagBeacon: String?
@State private var installedWidgets: String = ""
private func refreshWidgetDiag() {
widgetDiagBeacon = FuelStore.loadWidgetDiag()
WidgetCenter.shared.getCurrentConfigurations { result in
let text: String
switch result {
case .success(let widgets):
text = widgets.isEmpty
? "No widgets installed"
: widgets.map { "\($0.kind) · \($0.family)" }.joined(separator: "\n")
case .failure(let error):
text = "Error: \(error.localizedDescription)"
}
Task { @MainActor in
installedWidgets = text
}
}
}
var body: some View {
NavigationStack {
@@ -110,6 +132,32 @@ struct SettingsView: View {
Text("The first button applies the real criteria — cheapest \(alertsFuel.displayName.lowercased()) station within \(distanceUnit.format(alertsRadiusKM)) of you — and sends the actual station's name, price and coordinates. The second fires with no criteria at all (nearest seller), still carrying a real station, just to verify a notification appears and tapping it opens directions. Cheapest in range mirrors the app list (selected fuel + distance); the in-range count mirrors the alert radius.")
}
.onAppear { onRefreshDebugStatus() }
Section {
Button {
refreshWidgetDiag()
} label: {
Label("Refresh widget diagnostics", systemImage: "arrow.clockwise")
}
if let widgetDiagBeacon {
Text(widgetDiagBeacon)
.font(.caption2)
.foregroundStyle(.secondary)
.textSelection(.enabled)
} else {
Text("No widget beacon yet — add a widget on the home screen, then refresh.")
.font(.caption2)
.foregroundStyle(.secondary)
}
Text(installedWidgets.isEmpty ? "No widgets installed" : installedWidgets)
.font(.caption2)
.foregroundStyle(.secondary)
} header: {
Text("Widget Diagnostics")
} footer: {
Text("The beacon is written by the widget extension at the end of every timeline entry (keychain, so it survives free-SideStore installs). Installed widgets come from WidgetCenter.")
}
.onAppear { refreshWidgetDiag() }
}
Section {