fix(#79): centralize retrofit error handling with NetworkResult

This commit is contained in:
Hy4ri committed 2026-06-19 22:58:33 +03:00
1 parent 43df3c6e73
commit 508aea8bfa
22 files changed
+743 -738

No files matched your search

@@ -0,0 +1,87 @@
package com.m57.hermescontrol.data.remote
import retrofit2.Response
import java.io.IOException
sealed interface NetworkResult<out T> {
data class Success<out T>(val data: T) : NetworkResult<T>
data class Failure(val error: NetworkError) : NetworkResult<Nothing>
}
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 <reified T> safeApiCall(
retries: Int = 3,
crossinline call: suspend () -> Response<T>,
): NetworkResult<T> {
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"),
),
)
}
@@ -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}",
)
}
}
}
}
@@ -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}") }
}
}
}
}
@@ -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}",
)
}
}
}
}
@@ -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}",
)
}
}
}
}
@@ -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) }
}
}
}
@@ -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}")
}
}
}
@@ -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<Unit>,
apiCall: suspend () -> NetworkResult<Unit>,
) {
_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}",
)
}
}
}
}
@@ -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}")
}
}
}
@@ -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}") }
}
}
}
}
@@ -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}") }
}
}
}
@@ -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}",
)
}
}
}
}
@@ -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}") }
}
}
}
@@ -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<Unit>,
apiCall: suspend () -> NetworkResult<Unit>,
) {
_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}",
)
}
}
}
}
@@ -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}") }
}
}
}
}
@@ -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}",
)
}
}
}
}
}
@@ -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"}",
)
}
}
}
}
@@ -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}")
}
}
}
@@ -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}") }
}
}
}
@@ -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}")
}
}
}
@@ -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}",
)
}
}
}
}