@@ -55,54 +55,98 @@ constructor(
5555 */
5656 suspend fun getDeviceHardwareByModel (hwModel : Int , forceRefresh : Boolean = false): Result <DeviceHardware ?> =
5757 withContext(Dispatchers .IO ) {
58+ Timber .d(
59+ " DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=%d, forceRefresh=%b)" ,
60+ hwModel,
61+ forceRefresh,
62+ )
63+
5864 if (forceRefresh) {
65+ Timber .d(" DeviceHardwareRepository: forceRefresh=true, clearing local device hardware cache" )
5966 localDataSource.deleteAllDeviceHardware()
6067 } else {
6168 // 1. Attempt to retrieve from cache first
6269 val cachedEntity = localDataSource.getByHwModel(hwModel)
6370 if (cachedEntity != null && ! cachedEntity.isStale()) {
64- Timber .d(" Using fresh cached device hardware for model $ hwModel" )
71+ Timber .d(" DeviceHardwareRepository: using fresh cached device hardware for hwModel=%d " , hwModel )
6572 return @withContext Result .success(cachedEntity.asExternalModel())
6673 }
74+ Timber .d(" DeviceHardwareRepository: no fresh cache for hwModel=%d, attempting remote fetch" , hwModel)
6775 }
6876
6977 // 2. Fetch from remote API
7078 runCatching {
71- Timber .d(" Fetching device hardware from remote API. " )
79+ Timber .d(" DeviceHardwareRepository: fetching device hardware from remote API" )
7280 val remoteHardware = remoteDataSource.getAllDeviceHardware()
81+ Timber .d(
82+ " DeviceHardwareRepository: remote API returned %d device hardware entries" ,
83+ remoteHardware.size,
84+ )
7385
7486 localDataSource.insertAllDeviceHardware(remoteHardware)
75- localDataSource.getByHwModel(hwModel)?.asExternalModel()
87+ val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
88+ Timber .d(
89+ " DeviceHardwareRepository: lookup after remote fetch for hwModel=%d %s" ,
90+ hwModel,
91+ if (fromDb != null ) " succeeded" else " returned null" ,
92+ )
93+ fromDb
7694 }
7795 .onSuccess {
7896 // Successfully fetched and found the model
7997 return @withContext Result .success(it)
8098 }
8199 .onFailure { e ->
82- Timber .w(" Failed to fetch device hardware from server: ${e.message} " )
100+ Timber .w(
101+ e,
102+ " DeviceHardwareRepository: failed to fetch device hardware from server for hwModel=%d" ,
103+ hwModel,
104+ )
83105
84- // 3. Attempt to use stale cache as a fallback
106+ // 3. Attempt to use stale cache as a fallback, but only if it looks complete.
85107 val staleEntity = localDataSource.getByHwModel(hwModel)
86- if (staleEntity != null ) {
87- Timber .d(" Using stale cached device hardware for model $ hwModel" )
108+ if (staleEntity != null && ! staleEntity.isIncomplete() ) {
109+ Timber .d(" DeviceHardwareRepository: using stale cached device hardware for hwModel=%d " , hwModel )
88110 return @withContext Result .success(staleEntity.asExternalModel())
89111 }
90112
91- // 4. Fallback to bundled JSON if cache is empty
92- Timber .d(" Cache is empty, falling back to bundled JSON asset." )
113+ // 4. Fallback to bundled JSON if cache is empty or incomplete
114+ Timber .d(
115+ " DeviceHardwareRepository: cache %s for hwModel=%d, falling back to bundled JSON asset" ,
116+ if (staleEntity == null ) " empty" else " incomplete" ,
117+ hwModel,
118+ )
93119 return @withContext loadFromBundledJson(hwModel)
94120 }
95121 }
96122
97123 private suspend fun loadFromBundledJson (hwModel : Int ): Result <DeviceHardware ?> = runCatching {
124+ Timber .d(" DeviceHardwareRepository: loading device hardware from bundled JSON for hwModel=%d" , hwModel)
98125 val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
126+ Timber .d(" DeviceHardwareRepository: bundled JSON returned %d device hardware entries" , jsonHardware.size)
127+
99128 localDataSource.insertAllDeviceHardware(jsonHardware)
100- localDataSource.getByHwModel(hwModel)?.asExternalModel()
129+ val fromDb = localDataSource.getByHwModel(hwModel)?.asExternalModel()
130+ Timber .d(
131+ " DeviceHardwareRepository: lookup after JSON load for hwModel=%d %s" ,
132+ hwModel,
133+ if (fromDb != null ) " succeeded" else " returned null" ,
134+ )
135+ fromDb
101136 }
102137
103- /* * Extension function to check if the cached entity is stale. */
138+ /* * Returns true if the cached entity is missing important fields and should be refreshed. */
139+ private fun DeviceHardwareEntity.isIncomplete (): Boolean =
140+ displayName.isBlank() || platformioTarget.isBlank() || images.isNullOrEmpty()
141+
142+ /* *
143+ * Extension function to check if the cached entity is stale.
144+ *
145+ * We treat entries with missing critical fields (e.g., no images or target) as stale so that they can be
146+ * automatically healed from newer JSON snapshots even if their timestamp is recent.
147+ */
104148 private fun DeviceHardwareEntity.isStale (): Boolean =
105- (System .currentTimeMillis() - this .lastUpdated) > CACHE_EXPIRATION_TIME_MS
149+ isIncomplete() || (System .currentTimeMillis() - this .lastUpdated) > CACHE_EXPIRATION_TIME_MS
106150
107151 companion object {
108152 private val CACHE_EXPIRATION_TIME_MS = TimeUnit .DAYS .toMillis(1 )
0 commit comments