From f27eaf9c823034c9b4de67634c92c6f083adeefa Mon Sep 17 00:00:00 2001 From: Honey Date: Fri, 21 Aug 2026 19:46:16 +0200 Subject: [PATCH] fix(android): surface concert-fetch errors instead of swallowing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Events were fetched with runCatching{}.getOrDefault(emptyList()), so any network/deserialization failure looked identical to "no concerts". The backend + Ticketmaster work (verified), so the failure is client-side. - EventsClient: expectSuccess + timeouts so a non-2xx (e.g. an SSO/HTML redirect) becomes a real exception instead of an obscure body() failure. - ArtistViewModel: capture the failure into eventsError (+ Logcat) rather than defaulting to empty. - ConcertsSheet: show "Konzerte konnten nicht geladen werden" + the reason when the fetch failed, vs. "Keine Konzerte angekündigt" when genuinely none. Co-Authored-By: Claude Opus 4.8 --- .../com/novasonic/app/api/EventsClient.kt | 5 +++++ .../novasonic/app/ui/component/ConcertsSheet.kt | 17 +++++++++++++---- .../app/ui/screen/ArtistDetailScreen.kt | 7 ++++--- .../novasonic/app/viewmodel/ArtistViewModel.kt | 9 +++++++-- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/api/EventsClient.kt b/android/app/src/main/kotlin/com/novasonic/app/api/EventsClient.kt index 1b2bbff..2ac0ff2 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/api/EventsClient.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/api/EventsClient.kt @@ -4,6 +4,7 @@ import com.novasonic.app.api.model.ConcertEvent import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.android.* +import io.ktor.client.plugins.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.serialization.kotlinx.json.* @@ -12,6 +13,10 @@ import kotlinx.serialization.json.Json object EventsClient { private val http = HttpClient(Android) { + // Surface non-2xx as a real exception (e.g. an SSO redirect / HTML login page) instead of + // letting .body() fail obscurely — the ViewModel turns it into a visible "load failed". + expectSuccess = true + install(HttpTimeout) { requestTimeoutMillis = 15_000; connectTimeoutMillis = 10_000 } install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true; coerceInputValues = true }) } diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/component/ConcertsSheet.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/component/ConcertsSheet.kt index 41bcb9a..11e616b 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/component/ConcertsSheet.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/component/ConcertsSheet.kt @@ -31,6 +31,7 @@ fun ConcertsSheet( artistName: String, events: List, onDismiss: () -> Unit, + eventsError: String? = null, ) { val context = LocalContext.current @@ -47,17 +48,25 @@ fun ConcertsSheet( ) if (events.isEmpty()) { - Box( - contentAlignment = Alignment.Center, + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() - .padding(vertical = 40.dp), + .padding(horizontal = 20.dp, vertical = 40.dp), ) { Text( - "Keine Konzerte angekündigt", + if (eventsError != null) "Konzerte konnten nicht geladen werden" else "Keine Konzerte angekündigt", fontFamily = Outfit, fontSize = 14.sp, color = NsOnSurface2, ) + if (eventsError != null) { + Spacer(Modifier.height(6.dp)) + Text( + eventsError, + fontFamily = Outfit, fontSize = 11.sp, + color = NsOnSurface2.copy(alpha = 0.6f), + ) + } } } else { LazyColumn( diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistDetailScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistDetailScreen.kt index 51a8df1..57ff60b 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistDetailScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ArtistDetailScreen.kt @@ -92,9 +92,10 @@ fun ArtistDetailScreen( val artist = state.artist if (artist != null) { ConcertsSheet( - artistName = artist.name, - events = state.events, - onDismiss = vm::dismissEventsSheet, + artistName = artist.name, + events = state.events, + onDismiss = vm::dismissEventsSheet, + eventsError = state.eventsError, ) } } diff --git a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt index 618a99d..279b17b 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ArtistViewModel.kt @@ -41,6 +41,7 @@ data class ArtistUiState( val partialAlbum: PartialAlbumState? = null, // Concerts val events: List = emptyList(), + val eventsError: String? = null, // non-null → the fetch failed (vs. genuinely no concerts) val showEventsSheet: Boolean = false, // Deezer fan count of the matched artist (shown under the name), null if unknown val fanCount: Long? = null, @@ -113,8 +114,12 @@ class ArtistViewModel(app: Application, savedState: SavedStateHandle) : AndroidV launch { loadItunesData(artist.name, pin) } launch { val backendUrl = repo.getBackendUrl() - val ev = runCatching { EventsClient.fetchEvents(backendUrl, artist.name) }.getOrDefault(emptyList()) - _state.value = _state.value.copy(events = ev) + runCatching { EventsClient.fetchEvents(backendUrl, artist.name) } + .onSuccess { _state.value = _state.value.copy(events = it, eventsError = null) } + .onFailure { e -> + android.util.Log.w("NovaSonic", "Konzert-Fetch fehlgeschlagen für ${artist.name}", e) + _state.value = _state.value.copy(events = emptyList(), eventsError = e.message ?: e.toString()) + } } } catch (e: Exception) { _state.value = ArtistUiState(isLoading = false, itunesLoading = false, error = e.message ?: e.toString())