From 508aea8bfa03d00355e7636999d4de6c70476d9d Mon Sep 17 00:00:00 2001 From: Hy4ri Date: Fri, 19 Jun 2026 22:58:33 +0300 Subject: [PATCH] fix(#79): centralize retrofit error handling with NetworkResult --- .../data/remote/NetworkResult.kt | 87 ++++++++++ .../ui/achievements/AchievementsViewModel.kt | 36 ++-- .../ui/channels/ChannelsViewModel.kt | 42 ++--- .../hermescontrol/ui/chat/ChatViewModel.kt | 82 ++++----- .../ui/config/ConfigViewModel.kt | 62 +++---- .../ui/connect/ConnectViewModel.kt | 64 +++++--- .../ui/cron/CronJobsViewModel.kt | 84 ++++------ .../ui/gateway/GatewayViewModel.kt | 54 +++--- .../ui/kanban/KanbanViewModel.kt | 120 +++++++------- .../hermescontrol/ui/keys/KeysViewModel.kt | 63 ++++--- .../hermescontrol/ui/logs/LogsViewModel.kt | 25 +-- .../ui/mcp/McpServersViewModel.kt | 101 +++++------- .../hermescontrol/ui/model/ModelViewModel.kt | 111 +++++++------ .../ui/pairing/PairingViewModel.kt | 54 +++--- .../ui/plugins/PluginsViewModel.kt | 98 ++++++----- .../ui/profiles/ProfilesViewModel.kt | 155 +++++++++--------- .../ui/settings/SettingsViewModel.kt | 26 ++- .../ui/skills/SkillsViewModel.kt | 37 ++--- .../ui/system/SystemViewModel.kt | 64 ++++---- .../ui/toolsets/ToolsetsViewModel.kt | 42 ++--- .../ui/webhooks/WebhooksViewModel.kt | 66 ++++---- .../ui/connect/ConnectViewModelTest.kt | 8 + 22 files changed, 743 insertions(+), 738 deletions(-) create mode 100644 app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt new file mode 100644 index 00000000..7155d2cf --- /dev/null +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt @@ -0,0 +1,87 @@ +package com.m57.hermescontrol.data.remote + +import retrofit2.Response +import java.io.IOException + +sealed interface NetworkResult { + data class Success(val data: T) : NetworkResult + + data class Failure(val error: NetworkError) : NetworkResult +} + +sealed interface NetworkError { + val message: String + + data class Http(val code: Int, override val message: String) : NetworkError + + data class Connection(override val message: String, val cause: IOException) : NetworkError + + data class Unknown(override val message: String, val cause: Throwable) : NetworkError +} + +fun mapHttpError(code: Int): NetworkError { + val message = + when (code) { + 400 -> "Bad Request (HTTP 400): The server could not understand the request." + 401 -> "Unauthorized (HTTP 401): Authentication token is invalid or expired." + 403 -> "Forbidden (HTTP 403): You do not have permission to access this resource." + 404 -> "Not Found (HTTP 404): The requested resource could not be found." + 405 -> "Method Not Allowed (HTTP 405)." + 408 -> "Request Timeout (HTTP 408): The server timed out waiting for the request." + 409 -> "Conflict (HTTP 409): The request conflicts with current server state." + 429 -> "Too Many Requests (HTTP 429): Rate limit exceeded." + in 500..599 -> "Server Error (HTTP $code): The server encountered an error." + else -> "HTTP Error $code: Unexpected server response." + } + return NetworkError.Http(code, message) +} + +suspend inline fun safeApiCall( + retries: Int = 3, + crossinline call: suspend () -> Response, +): NetworkResult { + var lastException: IOException? = null + for (attempt in 0..retries) { + try { + val response = call() + if (response.isSuccessful) { + val body = response.body() + return if (body != null) { + NetworkResult.Success(body) + } else { + @Suppress("UNCHECKED_CAST") + NetworkResult.Success(null as T) + } + } else { + val code = response.code() + if (code in 500..599 && attempt < retries) { + continue + } + return NetworkResult.Failure(mapHttpError(code)) + } + } catch (e: IOException) { + lastException = e + if (attempt == retries) { + return NetworkResult.Failure( + NetworkError.Connection( + "Network connection failure: ${e.message ?: "Unknown connection error"}", + e, + ), + ) + } + } catch (e: Exception) { + return NetworkResult.Failure( + NetworkError.Unknown( + "An unexpected error occurred: ${e.localizedMessage ?: e.message}", + e, + ), + ) + } + } + return NetworkResult.Failure( + NetworkError.Connection( + "Network connection failure after $retries attempts: ${lastException?.message}", + lastException ?: IOException("Unknown connection error"), + ), + ) +} diff --git a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsViewModel.kt index 5526d1d5..6ea52e97 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.Achievement import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,32 +27,26 @@ class AchievementsViewModel : ViewModel() { fun loadAchievements() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getAchievements() - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getAchievements() } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isLoading = false, - achievements = response.body()?.achievements.orEmpty(), - ) - } - } else { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load achievements: HTTP ${response.code()}", + achievements = result.data.achievements.orEmpty(), ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load achievements: ${e.message}", - ) + is NetworkResult.Failure -> { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load achievements: ${result.error.message}", + ) + } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsViewModel.kt index 0a399801..58b6c114 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.MessagingPlatform import com.m57.hermescontrol.data.model.MessagingPlatformUpdate import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,23 +29,22 @@ class ChannelsViewModel : ViewModel() { fun loadPlatforms() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getMessagingPlatforms() - } - if (response.isSuccessful) { - _uiState.update { it.copy(isLoading = false, platforms = response.body().orEmpty()) } - } else { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getMessagingPlatforms() } + } + when (result) { + is NetworkResult.Success -> { + _uiState.update { it.copy(isLoading = false, platforms = result.data.orEmpty()) } + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load platforms: HTTP ${response.code()}", + errorMessage = "Failed to load platforms: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { it.copy(isLoading = false, errorMessage = "Failed to load platforms: ${e.message}") } } } } @@ -53,19 +54,18 @@ class ChannelsViewModel : ViewModel() { update: MessagingPlatformUpdate, ) { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.configurePlatform(platformId, update) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.configurePlatform(platformId, update) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "$platformId configured successfully") } loadPlatforms() - } else { - _uiState.update { it.copy(toastMessage = "Failed to configure platform: HTTP ${response.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to configure platform: ${e.message}") } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to configure platform: ${result.error.message}") } + } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt index 2ff75813..4a7dc694 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt @@ -8,6 +8,8 @@ import com.m57.hermescontrol.data.local.HermesDatabase import com.m57.hermescontrol.data.local.toEntity import com.m57.hermescontrol.data.local.toUiModel import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import com.m57.hermescontrol.data.ws.HermesWsClient import com.m57.hermescontrol.data.ws.WsEvent import com.m57.hermescontrol.data.ws.WsMethods @@ -423,10 +425,13 @@ class ChatViewModel( "/status" -> { viewModelScope.launch { - try { - val response = withContext(Dispatchers.IO) { ApiClient.hermesApi.getStatus() } - if (response.isSuccessful && response.body() != null) { - val body = response.body()!! + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getStatus() } + } + when (result) { + is NetworkResult.Success -> { + val body = result.data val platformsStr = body.gateway_platforms?.entries?.joinToString("\n") { (k, v) -> " • **$k**: ${v.state ?: "Unknown"}${if (v.error_code != null) " (Error: ${v.error_code})" else ""}" @@ -444,41 +449,45 @@ class ChatViewModel( $platformsStr """.trimIndent() addAssistantMessage(statusText) - } else { - addAssistantMessage("Failed to retrieve status: HTTP ${response.code()}") } - } catch (e: Exception) { - addAssistantMessage("Failed to retrieve status: ${e.message}") + is NetworkResult.Failure -> { + addAssistantMessage("Failed to retrieve status: ${result.error.message}") + } } } } "/sessions" -> { viewModelScope.launch { - try { - val response = withContext(Dispatchers.IO) { ApiClient.hermesApi.getSessions() } - if (response.isSuccessful && response.body() != null) { - val body = response.body()!! + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getSessions() } + } + when (result) { + is NetworkResult.Success -> { + val body = result.data val sessionsStr = body.sessions.joinToString("\n") { s -> "• **${s.title ?: "Untitled"}** (ID: `${s.id}`, Messages: ${s.message_count ?: 0})" } addAssistantMessage("**Sessions List:**\n$sessionsStr") - } else { - addAssistantMessage("Failed to list sessions: HTTP ${response.code()}") } - } catch (e: Exception) { - addAssistantMessage("Failed to list sessions: ${e.message}") + is NetworkResult.Failure -> { + addAssistantMessage("Failed to list sessions: ${result.error.message}") + } } } } "/stats", "/system" -> { viewModelScope.launch { - try { - val response = withContext(Dispatchers.IO) { ApiClient.hermesApi.getSystemStats() } - if (response.isSuccessful && response.body() != null) { - val body = response.body()!! + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getSystemStats() } + } + when (result) { + is NetworkResult.Success -> { + val body = result.data val cpuPct = body.cpuPercent?.let { String.format("%.1f%%", it) } ?: "N/A" val memPct = body.memoryPercent?.let { String.format("%.1f%%", it) } ?: "N/A" val uptimeVal = body.uptime ?: "N/A" @@ -490,11 +499,10 @@ class ChatViewModel( • **Uptime:** $uptimeVal """.trimIndent() addAssistantMessage(statsText) - } else { - addAssistantMessage("Failed to retrieve system stats: HTTP ${response.code()}") } - } catch (e: Exception) { - addAssistantMessage("Failed to retrieve system stats: ${e.message}") + is NetworkResult.Failure -> { + addAssistantMessage("Failed to retrieve system stats: ${result.error.message}") + } } } } @@ -584,13 +592,13 @@ class ChatViewModel( private fun loadSessionMessages(sessionId: String) { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getSessionMessages(sessionId) - } - if (response.isSuccessful) { - val messagesList = response.body()?.messages.orEmpty() + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getSessionMessages(sessionId) } + } + when (result) { + is NetworkResult.Success -> { + val messagesList = result.data.messages.orEmpty() val chatMessages = messagesList.map { msg -> val role = @@ -618,21 +626,15 @@ class ChatViewModel( isLoading = false, ) } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load messages: HTTP ${response.code()}", + errorMessage = "Failed to load messages: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load messages: ${e.message}", - ) - } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt index 238a5ead..6f9be40a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.UpdateRawConfigRequest import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -28,33 +30,27 @@ class ConfigViewModel : ViewModel() { fun loadRawConfig() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getRawConfig() - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getRawConfig() } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isLoading = false, - path = response.body()?.path, - yamlText = response.body()?.yaml, - ) - } - } else { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load config: HTTP ${response.code()}", + path = result.data.path, + yamlText = result.data.yaml, ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load config: ${e.message}", - ) + is NetworkResult.Failure -> { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load config: ${result.error.message}", + ) + } } } } @@ -63,12 +59,12 @@ class ConfigViewModel : ViewModel() { fun saveRawConfig(yamlText: String) { _uiState.update { it.copy(isSaving = true) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.updateRawConfig(UpdateRawConfigRequest(yamlText)) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.updateRawConfig(UpdateRawConfigRequest(yamlText)) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isSaving = false, @@ -76,21 +72,15 @@ class ConfigViewModel : ViewModel() { toastMessage = "Configuration saved successfully", ) } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isSaving = false, - toastMessage = "Failed to save config: HTTP ${response.code()}", + toastMessage = "Failed to save config: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isSaving = false, - toastMessage = "Failed to save config: ${e.message}", - ) - } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt index 1888bb83..aba86e87 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt @@ -4,6 +4,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkError +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -76,43 +79,50 @@ class ConnectViewModel : ViewModel() { ApiClient.rebuild() viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getStatus() - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getStatus() } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(isConnecting = false, connectionSuccess = true, errorMessage = null) } - } else { - val code = response.code() + } + is NetworkResult.Failure -> { val msg = - when (code) { - 401 -> { - AuthManager.setToken(null) - "Invalid token (401 Unauthorized)" + when (val err = result.error) { + is NetworkError.Http -> { + when (err.code) { + 401 -> { + AuthManager.setToken(null) + "Invalid token (401 Unauthorized)" + } + 403 -> "Access denied (403 Forbidden)" + else -> "Server returned HTTP ${err.code}" + } } - - 403 -> { - "Access denied (403 Forbidden)" + is NetworkError.Connection -> { + val causeMessage = err.cause.message ?: "" + when { + causeMessage.contains("timeout", true) -> "Connection timed out" + causeMessage.contains("refused", true) -> "Connection refused – is Hermes running?" + causeMessage.contains("resolve", true) -> "Could not resolve host" + else -> "Connection failed: ${err.cause.message}" + } } - - else -> { - "Server returned HTTP $code" + is NetworkError.Unknown -> { + val causeMessage = err.cause.message ?: "" + when { + causeMessage.contains("timeout", true) -> "Connection timed out" + causeMessage.contains("refused", true) -> "Connection refused – is Hermes running?" + causeMessage.contains("resolve", true) -> "Could not resolve host" + else -> "Connection failed: ${err.cause.message}" + } } } _uiState.update { it.copy(isConnecting = false, errorMessage = msg) } } - } catch (e: Exception) { - val msg = - when { - e.message?.contains("timeout", true) == true -> "Connection timed out" - e.message?.contains("refused", true) == true -> "Connection refused – is Hermes running?" - e.message?.contains("resolve", true) == true -> "Could not resolve host" - else -> "Connection failed: ${e.message}" - } - _uiState.update { it.copy(isConnecting = false, errorMessage = msg) } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt index deb749c7..96e7622f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.CronJob import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,23 +28,22 @@ class CronJobsViewModel : ViewModel() { fun loadCronJobs() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getCronJobs() - } - if (response.isSuccessful) { - _uiState.update { it.copy(isLoading = false, jobs = response.body().orEmpty()) } - } else { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getCronJobs() } + } + when (result) { + is NetworkResult.Success -> { + _uiState.update { it.copy(isLoading = false, jobs = result.data.orEmpty()) } + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load cron jobs: HTTP ${response.code()}", + errorMessage = "Failed to load cron jobs: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { it.copy(isLoading = false, errorMessage = "Failed to load cron jobs: ${e.message}") } } } } @@ -59,16 +60,12 @@ class CronJobsViewModel : ViewModel() { ) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.pauseCronJob(id) - } - if (!response.isSuccessful) { - revertJobs(originalJobs, "Failed to pause cron job: HTTP ${response.code()}") + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.pauseCronJob(id) } } - } catch (e: Exception) { - revertJobs(originalJobs, "Failed to pause cron job: ${e.message}") + if (result is NetworkResult.Failure) { + revertJobs(originalJobs, "Failed to pause cron job: ${result.error.message}") } } } @@ -85,34 +82,29 @@ class CronJobsViewModel : ViewModel() { ) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.resumeCronJob(id) - } - if (!response.isSuccessful) { - revertJobs(originalJobs, "Failed to resume cron job: HTTP ${response.code()}") + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.resumeCronJob(id) } } - } catch (e: Exception) { - revertJobs(originalJobs, "Failed to resume cron job: ${e.message}") + if (result is NetworkResult.Failure) { + revertJobs(originalJobs, "Failed to resume cron job: ${result.error.message}") } } } fun triggerCronJob(id: String) { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.triggerCronJob(id) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.triggerCronJob(id) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Job triggered successfully") } - } else { - _uiState.update { it.copy(toastMessage = "Failed to trigger cron job: HTTP ${response.code()}") } + } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to trigger cron job: ${result.error.message}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to trigger cron job: ${e.message}") } } } } @@ -124,16 +116,12 @@ class CronJobsViewModel : ViewModel() { state.copy(jobs = state.jobs.filter { it.id != id }) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.deleteCronJob(id) - } - if (!response.isSuccessful) { - revertJobs(originalJobs, "Failed to delete cron job: HTTP ${response.code()}") + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.deleteCronJob(id) } } - } catch (e: Exception) { - revertJobs(originalJobs, "Failed to delete cron job: ${e.message}") + if (result is NetworkResult.Failure) { + revertJobs(originalJobs, "Failed to delete cron job: ${result.error.message}") } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayViewModel.kt index 8fe9636b..eb1a269b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.StatusResponse import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,53 +29,47 @@ class GatewayViewModel : ViewModel() { fun loadStatus() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getStatus() - } - if (response.isSuccessful) { - _uiState.update { it.copy(isLoading = false, status = response.body()) } - } else { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getStatus() } + } + when (result) { + is NetworkResult.Success -> { + _uiState.update { it.copy(isLoading = false, status = result.data) } + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load status: HTTP ${response.code()}", + errorMessage = "Failed to load status: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load status: ${e.message}", - ) - } } } } fun startGateway() { - runGatewayAction("start") { ApiClient.hermesApi.startGateway() } + runGatewayAction("start") { safeApiCall { ApiClient.hermesApi.startGateway() } } } fun stopGateway() { - runGatewayAction("stop") { ApiClient.hermesApi.stopGateway() } + runGatewayAction("stop") { safeApiCall { ApiClient.hermesApi.stopGateway() } } } fun restartGateway() { - runGatewayAction("restart") { ApiClient.hermesApi.restartGateway() } + runGatewayAction("restart") { safeApiCall { ApiClient.hermesApi.restartGateway() } } } private fun runGatewayAction( actionName: String, - apiCall: suspend () -> retrofit2.Response, + apiCall: suspend () -> NetworkResult, ) { _uiState.update { it.copy(isActionRunning = true) } viewModelScope.launch { - try { - val response = withContext(Dispatchers.IO) { apiCall() } - if (response.isSuccessful) { + val result = withContext(Dispatchers.IO) { apiCall() } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isActionRunning = false, @@ -81,21 +77,15 @@ class GatewayViewModel : ViewModel() { ) } loadStatus() - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isActionRunning = false, - toastMessage = "Failed to $actionName gateway: HTTP ${response.code()}", + toastMessage = "Failed to $actionName gateway: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isActionRunning = false, - toastMessage = "Failed to $actionName gateway: ${e.message}", - ) - } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanViewModel.kt index 6967cbae..9f822d6c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanViewModel.kt @@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.KanbanBoard import com.m57.hermescontrol.data.model.KanbanColumn import com.m57.hermescontrol.data.model.KanbanTask import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -31,34 +33,28 @@ class KanbanViewModel : ViewModel() { fun loadBoards() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getKanbanBoards() - } - if (response.isSuccessful) { - val boards = response.body()?.boards.orEmpty() + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getKanbanBoards() } + } + when (result) { + is NetworkResult.Success -> { + val boards = result.data.boards.orEmpty() _uiState.update { it.copy(isLoading = false, boards = boards) } - val currentSlug = response.body()?.current + val currentSlug = result.data.current val currentBoard = boards.find { it.id == currentSlug } ?: boards.firstOrNull() if (currentBoard != null) { selectBoard(currentBoard) } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load Kanban boards: HTTP ${response.code()}", + errorMessage = "Failed to load Kanban boards: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load Kanban boards: ${e.message}", - ) - } } } } @@ -66,41 +62,44 @@ class KanbanViewModel : ViewModel() { fun selectBoard(board: KanbanBoard) { _uiState.update { it.copy(selectedBoard = board, isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - // Switch current board slug + val switchResult = withContext(Dispatchers.IO) { - ApiClient.hermesApi.switchKanbanBoard(board.id) + safeApiCall { ApiClient.hermesApi.switchKanbanBoard(board.id) } } - - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getKanbanBoard() - } - if (response.isSuccessful) { - val body = response.body() - val allTasks = body?.columns?.flatMap { it.tasks }.orEmpty() - _uiState.update { - it.copy( - isLoading = false, - columns = body?.columns.orEmpty(), - tasks = allTasks, - ) - } - } else { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load Kanban tasks: HTTP ${response.code()}", - ) - } - } - } catch (e: Exception) { + if (switchResult is NetworkResult.Failure) { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load Kanban tasks: ${e.message}", + errorMessage = "Failed to switch Kanban board: ${switchResult.error.message}", ) } + return@launch + } + + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getKanbanBoard() } + } + when (result) { + is NetworkResult.Success -> { + val body = result.data + val allTasks = body.columns.flatMap { it.tasks } + _uiState.update { + it.copy( + isLoading = false, + columns = body.columns, + tasks = allTasks, + ) + } + } + is NetworkResult.Failure -> { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load Kanban tasks: ${result.error.message}", + ) + } + } } } } @@ -112,9 +111,9 @@ class KanbanViewModel : ViewModel() { ) { val board = _uiState.value.selectedBoard ?: return viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.createKanbanTask( board = board.id, task = @@ -124,14 +123,15 @@ class KanbanViewModel : ViewModel() { ), ) } - if (response.isSuccessful) { + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Task created successfully") } selectBoard(board) - } else { - _uiState.update { it.copy(toastMessage = "Failed to create task: HTTP ${response.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to create task: ${e.message}") } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to create task: ${result.error.message}") } + } } } } @@ -154,16 +154,12 @@ class KanbanViewModel : ViewModel() { } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.updateKanbanTask(task.id, mapOf("status" to newStatus)) - } - if (!response.isSuccessful) { - revertTaskMove(task.id, originalStatus, "Failed to move task: HTTP ${response.code()}") + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.updateKanbanTask(task.id, mapOf("status" to newStatus)) } } - } catch (e: Exception) { - revertTaskMove(task.id, originalStatus, "Failed to move task: ${e.message}") + if (result is NetworkResult.Failure) { + revertTaskMove(task.id, originalStatus, "Failed to move task: ${result.error.message}") } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysViewModel.kt index bbdb46b2..b94255b7 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysViewModel.kt @@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.EnvVarConfig import com.m57.hermescontrol.data.model.EnvVarRevealRequest import com.m57.hermescontrol.data.model.EnvVarUpdate import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -29,51 +31,49 @@ class KeysViewModel : ViewModel() { fun loadKeys(reveal: Boolean = false) { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getEnvVars() - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getEnvVars() } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isLoading = false, - envVars = response.body().orEmpty(), + envVars = result.data.orEmpty(), ) } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load keys: HTTP ${response.code()}", + errorMessage = "Failed to load keys: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { it.copy(isLoading = false, errorMessage = "Failed to load keys: ${e.message}") } } } } fun revealKey(key: String) { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.revealEnvVar(EnvVarRevealRequest(key)) - } - if (response.isSuccessful && response.body() != null) { - val body = response.body()!! + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.revealEnvVar(EnvVarRevealRequest(key)) } + } + when (result) { + is NetworkResult.Success -> { + val body = result.data _uiState.update { state -> state.copy( revealedValues = state.revealedValues + (body.key to body.value), ) } - } else { - _uiState.update { it.copy(toastMessage = "Failed to reveal key: HTTP ${response.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to reveal key: ${e.message}") } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to reveal key: ${result.error.message}") } + } } } } @@ -91,19 +91,18 @@ class KeysViewModel : ViewModel() { value: String, ) { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.updateEnvVar(EnvVarUpdate(key, value)) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.updateEnvVar(EnvVarUpdate(key, value)) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Key updated successfully") } loadKeys() - } else { - _uiState.update { it.copy(toastMessage = "Failed to update key: HTTP ${response.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to update key: ${e.message}") } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to update key: ${result.error.message}") } + } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsViewModel.kt index a4722e7a..936348f4 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsViewModel.kt @@ -3,6 +3,8 @@ package com.m57.hermescontrol.ui.logs import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,25 +27,24 @@ class LogsViewModel : ViewModel() { fun loadLogs() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getLogs() - } - if (response.isSuccessful) { - val body = response.body() - val logsList = body?.lines ?: body?.logs ?: emptyList() + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getLogs() } + } + when (result) { + is NetworkResult.Success -> { + val body = result.data + val logsList = body.lines ?: body.logs ?: emptyList() _uiState.update { it.copy(isLoading = false, logs = logsList) } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load logs: HTTP ${response.code()}", + errorMessage = "Failed to load logs: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { it.copy(isLoading = false, errorMessage = "Failed to load logs: ${e.message}") } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt index 3d9d149a..26a12926 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.McpServer import com.m57.hermescontrol.data.model.McpServerToggleRequest import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -28,32 +30,26 @@ class McpServersViewModel : ViewModel() { fun loadServers() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getMcpServers() - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getMcpServers() } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isLoading = false, - servers = response.body()?.servers.orEmpty(), - ) - } - } else { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load MCP servers: HTTP ${response.code()}", + servers = result.data.servers.orEmpty(), ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load MCP servers: ${e.message}", - ) + is NetworkResult.Failure -> { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load MCP servers: ${result.error.message}", + ) + } } } } @@ -73,22 +69,23 @@ class McpServersViewModel : ViewModel() { } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.toggleMcpServer(server.name, McpServerToggleRequest(targetEnabled)) } - if (!response.isSuccessful) { - revertToggle(server.name, originalEnabled, "Failed to toggle server: HTTP ${response.code()}") - } else { + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( toastMessage = "Server '${server.name}' ${if (targetEnabled) "enabled" else "disabled"}", ) } } - } catch (e: Exception) { - revertToggle(server.name, originalEnabled, "Failed to toggle server: ${e.message}") + is NetworkResult.Failure -> { + revertToggle(server.name, originalEnabled, "Failed to toggle server: ${result.error.message}") + } } } } @@ -96,33 +93,27 @@ class McpServersViewModel : ViewModel() { fun testServer(name: String) { _uiState.update { it.copy(isActionRunning = true) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.testMcpServer(name) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.testMcpServer(name) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isActionRunning = false, toastMessage = "Server '$name' tested successfully", ) } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isActionRunning = false, - toastMessage = "Server '$name' test failed: HTTP ${response.code()}", + toastMessage = "Server '$name' test failed: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isActionRunning = false, - toastMessage = "Server '$name' test failed: ${e.message}", - ) - } } } } @@ -130,12 +121,12 @@ class McpServersViewModel : ViewModel() { fun deleteServer(name: String) { _uiState.update { it.copy(isActionRunning = true) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.deleteMcpServer(name) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.deleteMcpServer(name) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isActionRunning = false, @@ -143,21 +134,15 @@ class McpServersViewModel : ViewModel() { ) } loadServers() - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isActionRunning = false, - toastMessage = "Failed to delete server: HTTP ${response.code()}", + toastMessage = "Failed to delete server: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isActionRunning = false, - toastMessage = "Failed to delete server: ${e.message}", - ) - } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt index 39b60119..b33a701d 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt @@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.ModelProvider import com.m57.hermescontrol.data.model.ProfileInfo import com.m57.hermescontrol.data.model.UpdateProfileModelRequest import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -29,26 +31,30 @@ class ModelViewModel : ViewModel() { fun loadModelOptions() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getModelOptions() - } - val activeProfileNameRes = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getActiveProfile() - } - val profilesRes = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getProfiles() - } + val responseResult = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getModelOptions() } + } + val activeProfileNameResResult = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getActiveProfile() } + } + val profilesResResult = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getProfiles() } + } - if (response.isSuccessful) { + when (responseResult) { + is NetworkResult.Success -> { val activeName = - if (activeProfileNameRes.isSuccessful) activeProfileNameRes.body()?.active else null + if (activeProfileNameResResult is NetworkResult.Success) { + activeProfileNameResResult.data.active + } else { + null + } val activeProfile = - if (profilesRes.isSuccessful && activeName != null) { - profilesRes.body()?.profiles?.find { it.name == activeName } + if (profilesResResult is NetworkResult.Success && activeName != null) { + profilesResResult.data.profiles.find { it.name == activeName } } else { null } @@ -56,25 +62,19 @@ class ModelViewModel : ViewModel() { _uiState.update { it.copy( isLoading = false, - providers = response.body()?.providers.orEmpty(), + providers = responseResult.data.providers.orEmpty(), activeProfile = activeProfile, ) } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load model options: HTTP ${response.code()}", + errorMessage = "Failed to load model options: ${responseResult.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load model options: ${e.message}", - ) - } } } } @@ -84,40 +84,45 @@ class ModelViewModel : ViewModel() { modelName: String, ) { viewModelScope.launch { - try { - val activeProfileNameRes = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getActiveProfile() - } - if (!activeProfileNameRes.isSuccessful) { + val activeProfileNameResResult = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getActiveProfile() } + } + when (activeProfileNameResResult) { + is NetworkResult.Failure -> { _uiState.update { it.copy( - toastMessage = "Failed to fetch active profile: HTTP ${activeProfileNameRes.code()}", + toastMessage = + "Failed to fetch active profile: ${activeProfileNameResResult.error.message}", ) } return@launch } - val activeProfileName = activeProfileNameRes.body()?.active - if (activeProfileName == null) { - _uiState.update { it.copy(toastMessage = "No active profile found") } - return@launch - } - - val updateRes = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.updateProfileModel( - activeProfileName, - UpdateProfileModelRequest(providerSlug, modelName), - ) + is NetworkResult.Success -> { + val activeProfileName = activeProfileNameResResult.data.active + val updateResResult = + withContext(Dispatchers.IO) { + safeApiCall { + ApiClient.hermesApi.updateProfileModel( + activeProfileName, + UpdateProfileModelRequest(providerSlug, modelName), + ) + } + } + when (updateResResult) { + is NetworkResult.Success -> { + _uiState.update { it.copy(toastMessage = "Successfully set model to $modelName") } + loadModelOptions() + } + is NetworkResult.Failure -> { + _uiState.update { + it.copy( + toastMessage = "Failed to set model: ${updateResResult.error.message}", + ) + } + } } - if (updateRes.isSuccessful) { - _uiState.update { it.copy(toastMessage = "Successfully set model to $modelName") } - loadModelOptions() - } else { - _uiState.update { it.copy(toastMessage = "Failed to set model: HTTP ${updateRes.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to set model: ${e.message}") } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingViewModel.kt index 98cbd3f0..4169a5f4 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingViewModel.kt @@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.PairingApproveRequest import com.m57.hermescontrol.data.model.PairingResponse import com.m57.hermescontrol.data.model.PairingRevokeRequest import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -29,28 +31,22 @@ class PairingViewModel : ViewModel() { fun loadPairing() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getPairing() - } - if (response.isSuccessful) { - _uiState.update { it.copy(isLoading = false, pairing = response.body()) } - } else { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getPairing() } + } + when (result) { + is NetworkResult.Success -> { + _uiState.update { it.copy(isLoading = false, pairing = result.data) } + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load pairing info: HTTP ${response.code()}", + errorMessage = "Failed to load pairing info: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load pairing info: ${e.message}", - ) - } } } } @@ -60,7 +56,7 @@ class PairingViewModel : ViewModel() { code: String, ) { runAction("Approve") { - ApiClient.hermesApi.approvePairing(PairingApproveRequest(platform, code)) + safeApiCall { ApiClient.hermesApi.approvePairing(PairingApproveRequest(platform, code)) } } } @@ -69,25 +65,25 @@ class PairingViewModel : ViewModel() { userId: String, ) { runAction("Revoke") { - ApiClient.hermesApi.revokePairing(PairingRevokeRequest(platform, userId)) + safeApiCall { ApiClient.hermesApi.revokePairing(PairingRevokeRequest(platform, userId)) } } } fun clearPending() { runAction("Clear Pending") { - ApiClient.hermesApi.clearPendingPairing() + safeApiCall { ApiClient.hermesApi.clearPendingPairing() } } } private fun runAction( actionName: String, - apiCall: suspend () -> retrofit2.Response, + apiCall: suspend () -> NetworkResult, ) { _uiState.update { it.copy(isActionRunning = true) } viewModelScope.launch { - try { - val response = withContext(Dispatchers.IO) { apiCall() } - if (response.isSuccessful) { + val result = withContext(Dispatchers.IO) { apiCall() } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isActionRunning = false, @@ -95,21 +91,15 @@ class PairingViewModel : ViewModel() { ) } loadPairing() - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isActionRunning = false, - toastMessage = "Action '$actionName' failed: HTTP ${response.code()}", + toastMessage = "Action '$actionName' failed: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isActionRunning = false, - toastMessage = "Action '$actionName' failed: ${e.message}", - ) - } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt index fb5fe2ca..1969a1ba 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.PluginInfo import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,24 +28,23 @@ class PluginsViewModel : ViewModel() { fun loadPlugins() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getPlugins() - } - if (response.isSuccessful) { - val plugins = response.body()?.plugins.orEmpty() + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getPlugins() } + } + when (result) { + is NetworkResult.Success -> { + val plugins = result.data.plugins.orEmpty() _uiState.update { it.copy(isLoading = false, plugins = plugins) } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load plugins: HTTP ${response.code()}", + errorMessage = "Failed to load plugins: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { it.copy(isLoading = false, errorMessage = "Failed to load plugins: ${e.message}") } } } } @@ -67,80 +68,75 @@ class PluginsViewModel : ViewModel() { } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - if (targetEnabled) { - ApiClient.hermesApi.enablePlugin(plugin.name) - } else { - ApiClient.hermesApi.disablePlugin(plugin.name) - } + val result = + withContext(Dispatchers.IO) { + if (targetEnabled) { + safeApiCall { ApiClient.hermesApi.enablePlugin(plugin.name) } + } else { + safeApiCall { ApiClient.hermesApi.disablePlugin(plugin.name) } } - if (!response.isSuccessful) { - revertPluginToggle(plugin.name, originalEnabled, "Failed to toggle plugin: HTTP ${response.code()}") } - } catch (e: Exception) { - revertPluginToggle(plugin.name, originalEnabled, "Failed to toggle plugin: ${e.message}") + if (result is NetworkResult.Failure) { + revertPluginToggle(plugin.name, originalEnabled, "Failed to toggle plugin: ${result.error.message}") } } } fun installPlugin(name: String) { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.installPlugin( com.m57.hermescontrol.data.model .AgentPluginInstallBody(name), ) } - if (response.isSuccessful) { + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Plugin installed successfully") } loadPlugins() - } else { - _uiState.update { it.copy(toastMessage = "Failed to install plugin: HTTP ${response.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to install plugin: ${e.message}") } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to install plugin: ${result.error.message}") } + } } } } fun uninstallPlugin(name: String) { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.uninstallPlugin(name) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.uninstallPlugin(name) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Plugin uninstalled successfully") } loadPlugins() - } else { - _uiState.update { it.copy(toastMessage = "Failed to uninstall plugin: HTTP ${response.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to uninstall plugin: ${e.message}") } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to uninstall plugin: ${result.error.message}") } + } } } } fun updatePlugin(name: String) { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.updatePlugin(name) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.updatePlugin(name) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Plugin updated successfully") } loadPlugins() - } else { - _uiState.update { it.copy(toastMessage = "Failed to update plugin: HTTP ${response.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to update plugin: ${e.message}") } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to update plugin: ${result.error.message}") } + } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt index f52b3e0d..f4ea35a9 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt @@ -7,6 +7,8 @@ import com.m57.hermescontrol.data.model.SetActiveProfileRequest import com.m57.hermescontrol.data.model.UpdateProfileModelRequest import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -32,33 +34,30 @@ class ProfilesViewModel : ViewModel() { fun loadProfiles() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val profilesRes = withContext(Dispatchers.IO) { ApiClient.hermesApi.getProfiles() } - val activeRes = withContext(Dispatchers.IO) { ApiClient.hermesApi.getActiveProfile() } - - if (profilesRes.isSuccessful && activeRes.isSuccessful) { - _uiState.update { - it.copy( - isLoading = false, - profiles = profilesRes.body()?.profiles.orEmpty(), - activeProfileName = activeRes.body()?.active, - ) - } - } else { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = - "Failed to load profiles/active: HTTP " + - "${profilesRes.code()} / ${activeRes.code()}", - ) - } + val profilesResult = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getProfiles() } } - } catch (e: Exception) { + val activeResult = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getActiveProfile() } + } + + if (profilesResult is NetworkResult.Success && activeResult is NetworkResult.Success) { _uiState.update { it.copy( isLoading = false, - errorMessage = "Error loading profiles: ${e.message}", + profiles = profilesResult.data.profiles.orEmpty(), + activeProfileName = activeResult.data.active, + ) + } + } else { + val profilesError = (profilesResult as? NetworkResult.Failure)?.error?.message ?: "Success" + val activeError = (activeResult as? NetworkResult.Failure)?.error?.message ?: "Success" + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load profiles/active: Profiles: $profilesError, Active: $activeError", ) } } @@ -71,29 +70,23 @@ class ProfilesViewModel : ViewModel() { _uiState.update { it.copy(activeProfileName = name) } viewModelScope.launch { - try { - val res = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.setActiveProfile(SetActiveProfileRequest(name)) - } - if (res.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.setActiveProfile(SetActiveProfileRequest(name)) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Switched to profile $name") } loadProfiles() - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( activeProfileName = originalActive, - toastMessage = "Failed to switch profile: HTTP ${res.code()}", + toastMessage = "Failed to switch profile: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - activeProfileName = originalActive, - toastMessage = "Failed to switch profile: ${e.message}", - ) - } } } } @@ -101,32 +94,26 @@ class ProfilesViewModel : ViewModel() { fun loadSoul(profileName: String) { _uiState.update { it.copy(isLoadingSoul = true, selectedSoulContent = null) } viewModelScope.launch { - try { - val res = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getProfileSoul(profileName) - } - if (res.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getProfileSoul(profileName) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isLoadingSoul = false, - selectedSoulContent = res.body()?.content ?: "", - ) - } - } else { - _uiState.update { - it.copy( - isLoadingSoul = false, - toastMessage = "Failed to load soul: HTTP ${res.code()}", + selectedSoulContent = result.data.content, ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoadingSoul = false, - toastMessage = "Failed to load soul: ${e.message}", - ) + is NetworkResult.Failure -> { + _uiState.update { + it.copy( + isLoadingSoul = false, + toastMessage = "Failed to load soul: ${result.error.message}", + ) + } } } } @@ -137,12 +124,17 @@ class ProfilesViewModel : ViewModel() { content: String, ) { viewModelScope.launch { - try { - val res = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.updateProfileSoul(profileName, UpdateProfileSoulRequest(content)) + val result = + withContext(Dispatchers.IO) { + safeApiCall { + ApiClient.hermesApi.updateProfileSoul( + profileName, + UpdateProfileSoulRequest(content), + ) } - if (res.isSuccessful) { + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( selectedSoulContent = null, @@ -150,19 +142,14 @@ class ProfilesViewModel : ViewModel() { ) } loadProfiles() - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( - toastMessage = "Failed to save soul: HTTP ${res.code()}", + toastMessage = "Failed to save soul: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - toastMessage = "Failed to save soul: ${e.message}", - ) - } } } } @@ -173,19 +160,27 @@ class ProfilesViewModel : ViewModel() { model: String, ) { viewModelScope.launch { - try { - val res = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.updateProfileModel(profileName, UpdateProfileModelRequest(provider, model)) + val result = + withContext(Dispatchers.IO) { + safeApiCall { + ApiClient.hermesApi.updateProfileModel( + profileName, + UpdateProfileModelRequest(provider, model), + ) } - if (res.isSuccessful) { + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Model settings updated") } loadProfiles() - } else { - _uiState.update { it.copy(toastMessage = "Failed to update model settings: HTTP ${res.code()}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to update model settings: ${e.message}") } + is NetworkResult.Failure -> { + _uiState.update { + it.copy( + toastMessage = "Failed to update model settings: ${result.error.message}", + ) + } + } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt index bd3ff509..9c07d5a5 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import com.m57.hermescontrol.theme.ThemePreference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -144,30 +146,24 @@ class SettingsViewModel : ViewModel() { _uiState.update { it.copy(isTesting = true, testResult = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getStatus() - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getStatus() } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(isTesting = false, testResult = "✅ Connected successfully") } - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isTesting = false, - testResult = "❌ HTTP ${response.code()}: ${response.message()}", + testResult = "❌ ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isTesting = false, - testResult = "❌ ${e.message ?: "Connection failed"}", - ) - } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsViewModel.kt index e757b3e2..199c7945 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.Skill import com.m57.hermescontrol.data.model.ToggleSkillRequest import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,23 +29,22 @@ class SkillsViewModel : ViewModel() { fun loadSkills() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getSkills() - } - if (response.isSuccessful) { - _uiState.update { it.copy(isLoading = false, skills = response.body().orEmpty()) } - } else { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getSkills() } + } + when (result) { + is NetworkResult.Success -> { + _uiState.update { it.copy(isLoading = false, skills = result.data.orEmpty()) } + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load skills: HTTP ${response.code()}", + errorMessage = "Failed to load skills: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { it.copy(isLoading = false, errorMessage = "Failed to load skills: ${e.message}") } } } } @@ -63,16 +64,12 @@ class SkillsViewModel : ViewModel() { } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.toggleSkill(ToggleSkillRequest(skill.name, targetEnabled)) - } - if (!response.isSuccessful) { - revertSkillToggle(skill.name, originalEnabled, "Failed to toggle skill: HTTP ${response.code()}") + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.toggleSkill(ToggleSkillRequest(skill.name, targetEnabled)) } } - } catch (e: Exception) { - revertSkillToggle(skill.name, originalEnabled, "Failed to toggle skill: ${e.message}") + if (result is NetworkResult.Failure) { + revertSkillToggle(skill.name, originalEnabled, "Failed to toggle skill: ${result.error.message}") } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemViewModel.kt index 1d48fdec..8b68e5c4 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemViewModel.kt @@ -7,6 +7,8 @@ import com.m57.hermescontrol.BuildConfig import com.m57.hermescontrol.data.model.DoctorResponse import com.m57.hermescontrol.data.model.SystemStatsResponse import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -34,70 +36,60 @@ class SystemViewModel : ViewModel() { fun loadSystemData() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val statsResponse = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getSystemStats() - } - - if (statsResponse.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getSystemStats() } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( isLoading = false, - stats = statsResponse.body(), + stats = result.data, ) } // Doctor is optional; many servers do not expose it. loadDoctorReport() - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load system stats: HTTP ${statsResponse.code()}", + errorMessage = "Failed to load system stats: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load system stats: ${e.message}", - ) - } } } } private fun loadDoctorReport() { viewModelScope.launch { - try { - val doctorResponse = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.runDoctor() - } - if (doctorResponse.isSuccessful) { - _uiState.update { it.copy(doctorReport = doctorResponse.body()) } + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.runDoctor() } } - } catch (e: Exception) { - if (BuildConfig.DEBUG) Log.w(TAG, "doctor endpoint unavailable", e) + if (result is NetworkResult.Success) { + _uiState.update { it.copy(doctorReport = result.data) } + } else if (result is NetworkResult.Failure && BuildConfig.DEBUG) { + Log.w(TAG, "doctor endpoint unavailable: ${result.error.message}") } } } fun triggerBackup() { viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.triggerBackup() - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.triggerBackup() } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy(toastMessage = "Backup triggered successfully") } - } else { - _uiState.update { it.copy(toastMessage = "Failed to trigger backup: HTTP ${response.code()}") } + } + is NetworkResult.Failure -> { + _uiState.update { it.copy(toastMessage = "Failed to trigger backup: ${result.error.message}") } } - } catch (e: Exception) { - _uiState.update { it.copy(toastMessage = "Failed to trigger backup: ${e.message}") } } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsViewModel.kt index 1a79ef7a..4455f5ea 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.Toolset import com.m57.hermescontrol.data.model.ToolsetToggleRequest import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,28 +29,22 @@ class ToolsetsViewModel : ViewModel() { fun loadToolsets() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getToolsets() - } - if (response.isSuccessful) { - _uiState.update { it.copy(isLoading = false, toolsets = response.body().orEmpty()) } - } else { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getToolsets() } + } + when (result) { + is NetworkResult.Success -> { + _uiState.update { it.copy(isLoading = false, toolsets = result.data.orEmpty()) } + } + is NetworkResult.Failure -> { _uiState.update { it.copy( isLoading = false, - errorMessage = "Failed to load toolsets: HTTP ${response.code()}", + errorMessage = "Failed to load toolsets: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load toolsets: ${e.message}", - ) - } } } } @@ -68,16 +64,12 @@ class ToolsetsViewModel : ViewModel() { } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.toggleToolset(toolset.name, ToolsetToggleRequest(targetEnabled)) - } - if (!response.isSuccessful) { - revertToggle(toolset.name, originalEnabled, "Failed to toggle toolset: HTTP ${response.code()}") + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.toggleToolset(toolset.name, ToolsetToggleRequest(targetEnabled)) } } - } catch (e: Exception) { - revertToggle(toolset.name, originalEnabled, "Failed to toggle toolset: ${e.message}") + if (result is NetworkResult.Failure) { + revertToggle(toolset.name, originalEnabled, "Failed to toggle toolset: ${result.error.message}") } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksViewModel.kt index 4d7bb5ce..1d51111e 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.WebhookSubscription import com.m57.hermescontrol.data.model.WebhooksToggleRequest import com.m57.hermescontrol.data.remote.ApiClient +import com.m57.hermescontrol.data.remote.NetworkResult +import com.m57.hermescontrol.data.remote.safeApiCall import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -30,35 +32,29 @@ class WebhooksViewModel : ViewModel() { fun loadWebhooks() { _uiState.update { it.copy(isLoading = true, errorMessage = null) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.getWebhooks() - } - if (response.isSuccessful) { - val body = response.body() + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.getWebhooks() } + } + when (result) { + is NetworkResult.Success -> { + val body = result.data _uiState.update { it.copy( isLoading = false, - enabled = body?.enabled ?: false, - baseUrl = body?.base_url, - subscriptions = body?.subscriptions.orEmpty(), - ) - } - } else { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load webhooks: HTTP ${response.code()}", + enabled = body.enabled, + baseUrl = body.base_url, + subscriptions = body.subscriptions.orEmpty(), ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load webhooks: ${e.message}", - ) + is NetworkResult.Failure -> { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load webhooks: ${result.error.message}", + ) + } } } } @@ -69,33 +65,27 @@ class WebhooksViewModel : ViewModel() { _uiState.update { it.copy(enabled = targetEnabled) } viewModelScope.launch { - try { - val response = - withContext(Dispatchers.IO) { - ApiClient.hermesApi.toggleWebhooks(WebhooksToggleRequest(targetEnabled)) - } - if (response.isSuccessful) { + val result = + withContext(Dispatchers.IO) { + safeApiCall { ApiClient.hermesApi.toggleWebhooks(WebhooksToggleRequest(targetEnabled)) } + } + when (result) { + is NetworkResult.Success -> { _uiState.update { it.copy( toastMessage = "Webhooks global status ${if (targetEnabled) "enabled" else "disabled"}", ) } loadWebhooks() - } else { + } + is NetworkResult.Failure -> { _uiState.update { it.copy( enabled = originalEnabled, - toastMessage = "Failed to toggle webhooks: HTTP ${response.code()}", + toastMessage = "Failed to toggle webhooks: ${result.error.message}", ) } } - } catch (e: Exception) { - _uiState.update { - it.copy( - enabled = originalEnabled, - toastMessage = "Failed to toggle webhooks: ${e.message}", - ) - } } } } diff --git a/app/src/test/java/com/m57/hermescontrol/ui/connect/ConnectViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/connect/ConnectViewModelTest.kt index d575503a..80c87e78 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/connect/ConnectViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/connect/ConnectViewModelTest.kt @@ -143,6 +143,14 @@ class ConnectViewModelTest { val mockResponse = mockk>() every { mockResponse.isSuccessful } returns true + every { mockResponse.body() } returns + StatusResponse( + version = "1.0", + gateway_running = true, + active_sessions = 1, + auth_required = false, + gateway_platforms = emptyMap(), + ) coEvery { mockApiService.getStatus() } returns mockResponse viewModel.connect()