@@ -241,6 +241,17 @@ public async Task ProcessWeatherAlertSourceAsync(Guid sourceId, CancellationToke
241241 source . ErrorMessage = null ;
242242 await _weatherAlertSourceRepository . UpdateAsync ( source , ct , true ) ;
243243 }
244+ catch ( PermanentWeatherAlertException ex )
245+ {
246+ // Permanent failures (invalid zone codes, etc.) — record the error
247+ // and mark the source as failed. Don't rethrow so the poller continues
248+ // without noisy Fatal logs. The source will be retried only after the
249+ // user edits and saves it.
250+ source . LastPollUtc = DateTime . UtcNow ;
251+ source . IsFailure = true ;
252+ source . ErrorMessage = ex . Message ;
253+ await _weatherAlertSourceRepository . UpdateAsync ( source , ct , true ) ;
254+ }
244255 catch ( TransientWeatherAlertException ex )
245256 {
246257 // Transient failures (rate-limit, server errors) — record the error
@@ -268,6 +279,12 @@ public async Task ProcessAllActiveSourcesAsync(CancellationToken ct = default)
268279
269280 foreach ( var source in sources )
270281 {
282+ // Skip sources with permanent failures (e.g. invalid zone codes).
283+ // They will be retried only after the user edits and saves the source.
284+ if ( source . IsFailure && ! string . IsNullOrEmpty ( source . ErrorMessage ) &&
285+ ! source . ErrorMessage . StartsWith ( "Transient:" ) )
286+ continue ;
287+
271288 // Check if it's time to poll based on interval
272289 if ( source . LastPollUtc . HasValue )
273290 {
0 commit comments