A/B test: from-scratch minimal small widget (proven medium intent)

Temporary FuelBoardTestSmallWidget kind: a small-face widget driven by
the PROVEN medium intent (fuel/sort/distance only — no favourite params,
no dependency, no custom entity query), registered first in the gallery.
Isolates small SIZE vs small INTENT: if it populates while
FuelPriceSmallWidget skeletons, the small intent's extra machinery is the
culprit; if it also skeletons, the failure is small-family/system-level.
Provider gains a beaconTag so the test widget's beacons are
distinguishable in the relay log and keychain slots. Removed once the
root cause is confirmed.
This commit is contained in:
FuelBoard Contributor
2026-08-13 20:27:03 +01:00
parent 8a9e77546f
commit 553730681f
2 changed files with 42 additions and 4 deletions
@@ -4,7 +4,10 @@ import SwiftUI
@main
struct FuelBoardWidgetsBundle: WidgetBundle {
var body: some Widget {
// Gallery order follows registration order: small first, then medium.
// Gallery order follows registration order. The A/B test widget is
// first so it's easy to find; remove it once the small-widget root
// cause is confirmed.
FuelBoardTestSmallWidget()
FuelPriceSmallWidget()
FuelPriceWidget()
FuelBoardLiveActivity()
+38 -3
View File
@@ -85,6 +85,35 @@ struct FuelPriceSmallWidget: Widget {
}
}
// A/B TEST widget (temporary): a from-scratch SMALL widget driven by the
// PROVEN medium intent no favourite params, no dependency, no custom
// query. If this populates while FuelPriceSmallWidget skeletons, the small
// SIZE is fine and the small intent's extra machinery is the culprit. If it
// ALSO skeletons, the failure is at the small-family/system level. Removed
// once the root cause is confirmed.
struct FuelBoardTestSmallWidget: Widget {
let kind = "FuelBoardTestSmallWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: FuelBoardWidgetConfigurationIntent.self,
provider: FuelPriceTimelineProvider<FuelBoardWidgetConfigurationIntent>(
beaconTag: "FuelBoardTestSmallWidget"
)
) { entry in
FuelPriceWidgetView(entry: entry)
.containerBackground(for: .widget) {
Color(.systemBackground)
}
}
.configurationDisplayName("FuelBoard Small Test")
.description("A/B test: minimal small widget using the proven medium intent.")
.supportedFamilies([.systemSmall])
.disfavoredLocations([.carPlay], for: [.systemSmall])
}
}
struct FuelPriceEntry: TimelineEntry {
let date: Date
let stations: [FuelStation] // already filtered + sorted per config
@@ -99,6 +128,12 @@ struct FuelPriceEntry: TimelineEntry {
struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & WidgetConfigValues>: AppIntentTimelineProvider {
typealias Intent = Configuration
/// Diagnostics beacon tag: empty derive from the intent type name. A
/// widget kind that shares an intent (the A/B test small widget reuses the
/// medium intent) passes an explicit tag so its beacons stay
/// distinguishable in the relay log and keychain slots.
var beaconTag: String = ""
func placeholder(in context: Context) -> FuelPriceEntry {
let unit = FuelStore.loadDistanceUnit()
let sample = SampleFuelProvider.sampleStations
@@ -128,7 +163,7 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
private func makeEntry(configuration: Configuration) async -> FuelPriceEntry {
let entry = await makeEntryCore(configuration: configuration)
Self.writeDiagBeacon(entry: entry)
writeDiagBeacon(entry: entry)
return entry
}
@@ -137,8 +172,8 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
/// access log records that a widget timeline actually ran in the
/// extension and what it produced. Deliberately outside the timeline
/// result can never affect rendering.
private static func writeDiagBeacon(entry: FuelPriceEntry) {
let intentType = String(describing: Configuration.self)
private func writeDiagBeacon(entry: FuelPriceEntry) {
let intentType = beaconTag.isEmpty ? String(describing: Configuration.self) : beaconTag
let first = entry.stations.first
let json = """
{"intent":"\(intentType)","source":"\(entry.locationSource)",\