Merge pull request #113 from Hy4ri/refactor/issue-79-retrofit-error-handling
fix(#79): centralize retrofit error handling with NetworkResult
This commit is contained in:
22 files changed
+615
-610
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 androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.model.Achievement
|
import com.m57.hermescontrol.data.model.Achievement
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -25,33 +27,27 @@ class AchievementsViewModel : ViewModel() {
|
|||||||
fun loadAchievements() {
|
fun loadAchievements() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getAchievements()
|
safeApiCall { ApiClient.hermesApi.getAchievements() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
achievements = response.body()?.achievements.orEmpty(),
|
achievements = result.data.achievements.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Failed to load achievements: HTTP ${response.code()}",
|
errorMessage = "Failed to load achievements: ${result.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load achievements: ${e.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.m57.hermescontrol.data.model.MessagingPlatform
|
import com.m57.hermescontrol.data.model.MessagingPlatform
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -27,23 +29,22 @@ class ChannelsViewModel : ViewModel() {
|
|||||||
fun loadPlatforms() {
|
fun loadPlatforms() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getMessagingPlatforms()
|
safeApiCall { ApiClient.hermesApi.getMessagingPlatforms() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
_uiState.update { it.copy(isLoading = false, platforms = response.body().orEmpty()) }
|
is NetworkResult.Success -> {
|
||||||
} else {
|
_uiState.update { it.copy(isLoading = false, platforms = result.data.orEmpty()) }
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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,
|
update: MessagingPlatformUpdate,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.configurePlatform(platformId, update)
|
safeApiCall { ApiClient.hermesApi.configurePlatform(platformId, update) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "$platformId configured successfully") }
|
_uiState.update { it.copy(toastMessage = "$platformId configured successfully") }
|
||||||
loadPlatforms()
|
loadPlatforms()
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to configure platform: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to configure platform: ${e.message}") }
|
_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.toEntity
|
||||||
import com.m57.hermescontrol.data.local.toUiModel
|
import com.m57.hermescontrol.data.local.toUiModel
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.HermesWsClient
|
||||||
import com.m57.hermescontrol.data.ws.WsEvent
|
import com.m57.hermescontrol.data.ws.WsEvent
|
||||||
import com.m57.hermescontrol.data.ws.WsMethods
|
import com.m57.hermescontrol.data.ws.WsMethods
|
||||||
@@ -423,10 +425,13 @@ class ChatViewModel(
|
|||||||
|
|
||||||
"/status" -> {
|
"/status" -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response = withContext(Dispatchers.IO) { ApiClient.hermesApi.getStatus() }
|
withContext(Dispatchers.IO) {
|
||||||
if (response.isSuccessful && response.body() != null) {
|
safeApiCall { ApiClient.hermesApi.getStatus() }
|
||||||
val body = response.body()!!
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
val body = result.data
|
||||||
val platformsStr =
|
val platformsStr =
|
||||||
body.gateway_platforms?.entries?.joinToString("\n") { (k, v) ->
|
body.gateway_platforms?.entries?.joinToString("\n") { (k, v) ->
|
||||||
" • **$k**: ${v.state ?: "Unknown"}${if (v.error_code != null) " (Error: ${v.error_code})" else ""}"
|
" • **$k**: ${v.state ?: "Unknown"}${if (v.error_code != null) " (Error: ${v.error_code})" else ""}"
|
||||||
@@ -444,41 +449,45 @@ class ChatViewModel(
|
|||||||
$platformsStr
|
$platformsStr
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
addAssistantMessage(statusText)
|
addAssistantMessage(statusText)
|
||||||
} else {
|
|
||||||
addAssistantMessage("Failed to retrieve status: HTTP ${response.code()}")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
addAssistantMessage("Failed to retrieve status: ${e.message}")
|
addAssistantMessage("Failed to retrieve status: ${result.error.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"/sessions" -> {
|
"/sessions" -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response = withContext(Dispatchers.IO) { ApiClient.hermesApi.getSessions() }
|
withContext(Dispatchers.IO) {
|
||||||
if (response.isSuccessful && response.body() != null) {
|
safeApiCall { ApiClient.hermesApi.getSessions() }
|
||||||
val body = response.body()!!
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
val body = result.data
|
||||||
val sessionsStr =
|
val sessionsStr =
|
||||||
body.sessions.joinToString("\n") { s ->
|
body.sessions.joinToString("\n") { s ->
|
||||||
"• **${s.title ?: "Untitled"}** (ID: `${s.id}`, Messages: ${s.message_count ?: 0})"
|
"• **${s.title ?: "Untitled"}** (ID: `${s.id}`, Messages: ${s.message_count ?: 0})"
|
||||||
}
|
}
|
||||||
addAssistantMessage("**Sessions List:**\n$sessionsStr")
|
addAssistantMessage("**Sessions List:**\n$sessionsStr")
|
||||||
} else {
|
|
||||||
addAssistantMessage("Failed to list sessions: HTTP ${response.code()}")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
addAssistantMessage("Failed to list sessions: ${e.message}")
|
addAssistantMessage("Failed to list sessions: ${result.error.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"/stats", "/system" -> {
|
"/stats", "/system" -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response = withContext(Dispatchers.IO) { ApiClient.hermesApi.getSystemStats() }
|
withContext(Dispatchers.IO) {
|
||||||
if (response.isSuccessful && response.body() != null) {
|
safeApiCall { ApiClient.hermesApi.getSystemStats() }
|
||||||
val body = response.body()!!
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
val body = result.data
|
||||||
val cpuPct = body.cpuPercent?.let { String.format("%.1f%%", it) } ?: "N/A"
|
val cpuPct = body.cpuPercent?.let { String.format("%.1f%%", it) } ?: "N/A"
|
||||||
val memPct = body.memoryPercent?.let { String.format("%.1f%%", it) } ?: "N/A"
|
val memPct = body.memoryPercent?.let { String.format("%.1f%%", it) } ?: "N/A"
|
||||||
val uptimeVal = body.uptime ?: "N/A"
|
val uptimeVal = body.uptime ?: "N/A"
|
||||||
@@ -490,11 +499,10 @@ class ChatViewModel(
|
|||||||
• **Uptime:** $uptimeVal
|
• **Uptime:** $uptimeVal
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
addAssistantMessage(statsText)
|
addAssistantMessage(statsText)
|
||||||
} else {
|
|
||||||
addAssistantMessage("Failed to retrieve system stats: HTTP ${response.code()}")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
addAssistantMessage("Failed to retrieve system stats: ${e.message}")
|
addAssistantMessage("Failed to retrieve system stats: ${result.error.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,13 +592,13 @@ class ChatViewModel(
|
|||||||
|
|
||||||
private fun loadSessionMessages(sessionId: String) {
|
private fun loadSessionMessages(sessionId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getSessionMessages(sessionId)
|
safeApiCall { ApiClient.hermesApi.getSessionMessages(sessionId) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
val messagesList = response.body()?.messages.orEmpty()
|
is NetworkResult.Success -> {
|
||||||
|
val messagesList = result.data.messages.orEmpty()
|
||||||
val chatMessages =
|
val chatMessages =
|
||||||
messagesList.map { msg ->
|
messagesList.map { msg ->
|
||||||
val role =
|
val role =
|
||||||
@@ -618,21 +626,15 @@ class ChatViewModel(
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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 androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.model.UpdateRawConfigRequest
|
import com.m57.hermescontrol.data.model.UpdateRawConfigRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -28,34 +30,28 @@ class ConfigViewModel : ViewModel() {
|
|||||||
fun loadRawConfig() {
|
fun loadRawConfig() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getRawConfig()
|
safeApiCall { ApiClient.hermesApi.getRawConfig() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
path = response.body()?.path,
|
path = result.data.path,
|
||||||
yamlText = response.body()?.yaml,
|
yamlText = result.data.yaml,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Failed to load config: HTTP ${response.code()}",
|
errorMessage = "Failed to load config: ${result.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load config: ${e.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,12 +59,12 @@ class ConfigViewModel : ViewModel() {
|
|||||||
fun saveRawConfig(yamlText: String) {
|
fun saveRawConfig(yamlText: String) {
|
||||||
_uiState.update { it.copy(isSaving = true) }
|
_uiState.update { it.copy(isSaving = true) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.updateRawConfig(UpdateRawConfigRequest(yamlText))
|
safeApiCall { ApiClient.hermesApi.updateRawConfig(UpdateRawConfigRequest(yamlText)) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
@@ -76,21 +72,15 @@ class ConfigViewModel : ViewModel() {
|
|||||||
toastMessage = "Configuration saved successfully",
|
toastMessage = "Configuration saved successfully",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isSaving = false,
|
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 androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -76,46 +79,53 @@ class ConnectViewModel : ViewModel() {
|
|||||||
ApiClient.rebuild()
|
ApiClient.rebuild()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getStatus()
|
safeApiCall { ApiClient.hermesApi.getStatus() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(isConnecting = false, connectionSuccess = true, errorMessage = null)
|
it.copy(isConnecting = false, connectionSuccess = true, errorMessage = null)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
val code = response.code()
|
is NetworkResult.Failure -> {
|
||||||
val msg =
|
val msg =
|
||||||
when (code) {
|
when (val err = result.error) {
|
||||||
|
is NetworkError.Http -> {
|
||||||
|
when (err.code) {
|
||||||
401 -> {
|
401 -> {
|
||||||
AuthManager.setToken(null)
|
AuthManager.setToken(null)
|
||||||
"Invalid token (401 Unauthorized)"
|
"Invalid token (401 Unauthorized)"
|
||||||
}
|
}
|
||||||
|
403 -> "Access denied (403 Forbidden)"
|
||||||
403 -> {
|
else -> "Server returned HTTP ${err.code}"
|
||||||
"Access denied (403 Forbidden)"
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
"Server returned HTTP $code"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_uiState.update { it.copy(isConnecting = false, errorMessage = msg) }
|
is NetworkError.Connection -> {
|
||||||
}
|
val causeMessage = err.cause.message ?: ""
|
||||||
} catch (e: Exception) {
|
|
||||||
val msg =
|
|
||||||
when {
|
when {
|
||||||
e.message?.contains("timeout", true) == true -> "Connection timed out"
|
causeMessage.contains("timeout", true) -> "Connection timed out"
|
||||||
e.message?.contains("refused", true) == true -> "Connection refused – is Hermes running?"
|
causeMessage.contains("refused", true) -> "Connection refused – is Hermes running?"
|
||||||
e.message?.contains("resolve", true) == true -> "Could not resolve host"
|
causeMessage.contains("resolve", true) -> "Could not resolve host"
|
||||||
else -> "Connection failed: ${e.message}"
|
else -> "Connection failed: ${err.cause.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) }
|
_uiState.update { it.copy(isConnecting = false, errorMessage = msg) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onPairingString(value: String) {
|
fun onPairingString(value: String) {
|
||||||
val trimmed = value.trim()
|
val trimmed = value.trim()
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.model.CronJob
|
import com.m57.hermescontrol.data.model.CronJob
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -26,23 +28,22 @@ class CronJobsViewModel : ViewModel() {
|
|||||||
fun loadCronJobs() {
|
fun loadCronJobs() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getCronJobs()
|
safeApiCall { ApiClient.hermesApi.getCronJobs() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
_uiState.update { it.copy(isLoading = false, jobs = response.body().orEmpty()) }
|
is NetworkResult.Success -> {
|
||||||
} else {
|
_uiState.update { it.copy(isLoading = false, jobs = result.data.orEmpty()) }
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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 {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.pauseCronJob(id)
|
safeApiCall { ApiClient.hermesApi.pauseCronJob(id) }
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (result is NetworkResult.Failure) {
|
||||||
revertJobs(originalJobs, "Failed to pause cron job: HTTP ${response.code()}")
|
revertJobs(originalJobs, "Failed to pause cron job: ${result.error.message}")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revertJobs(originalJobs, "Failed to pause cron job: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,34 +82,29 @@ class CronJobsViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.resumeCronJob(id)
|
safeApiCall { ApiClient.hermesApi.resumeCronJob(id) }
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (result is NetworkResult.Failure) {
|
||||||
revertJobs(originalJobs, "Failed to resume cron job: HTTP ${response.code()}")
|
revertJobs(originalJobs, "Failed to resume cron job: ${result.error.message}")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revertJobs(originalJobs, "Failed to resume cron job: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun triggerCronJob(id: String) {
|
fun triggerCronJob(id: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.triggerCronJob(id)
|
safeApiCall { ApiClient.hermesApi.triggerCronJob(id) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Job triggered successfully") }
|
_uiState.update { it.copy(toastMessage = "Job triggered successfully") }
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to trigger cron job: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to trigger cron job: ${e.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to trigger cron job: ${result.error.message}") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,16 +116,12 @@ class CronJobsViewModel : ViewModel() {
|
|||||||
state.copy(jobs = state.jobs.filter { it.id != id })
|
state.copy(jobs = state.jobs.filter { it.id != id })
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.deleteCronJob(id)
|
safeApiCall { ApiClient.hermesApi.deleteCronJob(id) }
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (result is NetworkResult.Failure) {
|
||||||
revertJobs(originalJobs, "Failed to delete cron job: HTTP ${response.code()}")
|
revertJobs(originalJobs, "Failed to delete cron job: ${result.error.message}")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revertJobs(originalJobs, "Failed to delete cron job: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.model.StatusResponse
|
import com.m57.hermescontrol.data.model.StatusResponse
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -27,53 +29,47 @@ class GatewayViewModel : ViewModel() {
|
|||||||
fun loadStatus() {
|
fun loadStatus() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getStatus()
|
safeApiCall { ApiClient.hermesApi.getStatus() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
_uiState.update { it.copy(isLoading = false, status = response.body()) }
|
is NetworkResult.Success -> {
|
||||||
} else {
|
_uiState.update { it.copy(isLoading = false, status = result.data) }
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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() {
|
fun startGateway() {
|
||||||
runGatewayAction("start") { ApiClient.hermesApi.startGateway() }
|
runGatewayAction("start") { safeApiCall { ApiClient.hermesApi.startGateway() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopGateway() {
|
fun stopGateway() {
|
||||||
runGatewayAction("stop") { ApiClient.hermesApi.stopGateway() }
|
runGatewayAction("stop") { safeApiCall { ApiClient.hermesApi.stopGateway() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartGateway() {
|
fun restartGateway() {
|
||||||
runGatewayAction("restart") { ApiClient.hermesApi.restartGateway() }
|
runGatewayAction("restart") { safeApiCall { ApiClient.hermesApi.restartGateway() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runGatewayAction(
|
private fun runGatewayAction(
|
||||||
actionName: String,
|
actionName: String,
|
||||||
apiCall: suspend () -> retrofit2.Response<Unit>,
|
apiCall: suspend () -> NetworkResult<Unit>,
|
||||||
) {
|
) {
|
||||||
_uiState.update { it.copy(isActionRunning = true) }
|
_uiState.update { it.copy(isActionRunning = true) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result = withContext(Dispatchers.IO) { apiCall() }
|
||||||
val response = withContext(Dispatchers.IO) { apiCall() }
|
when (result) {
|
||||||
if (response.isSuccessful) {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActionRunning = false,
|
isActionRunning = false,
|
||||||
@@ -81,21 +77,15 @@ class GatewayViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadStatus()
|
loadStatus()
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActionRunning = false,
|
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.KanbanColumn
|
||||||
import com.m57.hermescontrol.data.model.KanbanTask
|
import com.m57.hermescontrol.data.model.KanbanTask
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -31,34 +33,28 @@ class KanbanViewModel : ViewModel() {
|
|||||||
fun loadBoards() {
|
fun loadBoards() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getKanbanBoards()
|
safeApiCall { ApiClient.hermesApi.getKanbanBoards() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
val boards = response.body()?.boards.orEmpty()
|
is NetworkResult.Success -> {
|
||||||
|
val boards = result.data.boards.orEmpty()
|
||||||
_uiState.update { it.copy(isLoading = false, boards = boards) }
|
_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()
|
val currentBoard = boards.find { it.id == currentSlug } ?: boards.firstOrNull()
|
||||||
if (currentBoard != null) {
|
if (currentBoard != null) {
|
||||||
selectBoard(currentBoard)
|
selectBoard(currentBoard)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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) {
|
fun selectBoard(board: KanbanBoard) {
|
||||||
_uiState.update { it.copy(selectedBoard = board, isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(selectedBoard = board, isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val switchResult =
|
||||||
// Switch current board slug
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.switchKanbanBoard(board.id)
|
safeApiCall { ApiClient.hermesApi.switchKanbanBoard(board.id) }
|
||||||
}
|
}
|
||||||
|
if (switchResult is NetworkResult.Failure) {
|
||||||
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 {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
columns = body?.columns.orEmpty(),
|
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,
|
tasks = allTasks,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Failed to load Kanban tasks: HTTP ${response.code()}",
|
errorMessage = "Failed to load Kanban tasks: ${result.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load Kanban tasks: ${e.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,9 +111,9 @@ class KanbanViewModel : ViewModel() {
|
|||||||
) {
|
) {
|
||||||
val board = _uiState.value.selectedBoard ?: return
|
val board = _uiState.value.selectedBoard ?: return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
ApiClient.hermesApi.createKanbanTask(
|
ApiClient.hermesApi.createKanbanTask(
|
||||||
board = board.id,
|
board = board.id,
|
||||||
task =
|
task =
|
||||||
@@ -124,14 +123,15 @@ class KanbanViewModel : ViewModel() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Task created successfully") }
|
_uiState.update { it.copy(toastMessage = "Task created successfully") }
|
||||||
selectBoard(board)
|
selectBoard(board)
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to create task: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to create task: ${e.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to create task: ${result.error.message}") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,16 +154,12 @@ class KanbanViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.updateKanbanTask(task.id, mapOf("status" to newStatus))
|
safeApiCall { ApiClient.hermesApi.updateKanbanTask(task.id, mapOf("status" to newStatus)) }
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (result is NetworkResult.Failure) {
|
||||||
revertTaskMove(task.id, originalStatus, "Failed to move task: HTTP ${response.code()}")
|
revertTaskMove(task.id, originalStatus, "Failed to move task: ${result.error.message}")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revertTaskMove(task.id, originalStatus, "Failed to move task: ${e.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.EnvVarRevealRequest
|
||||||
import com.m57.hermescontrol.data.model.EnvVarUpdate
|
import com.m57.hermescontrol.data.model.EnvVarUpdate
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -29,51 +31,49 @@ class KeysViewModel : ViewModel() {
|
|||||||
fun loadKeys(reveal: Boolean = false) {
|
fun loadKeys(reveal: Boolean = false) {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getEnvVars()
|
safeApiCall { ApiClient.hermesApi.getEnvVars() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
envVars = response.body().orEmpty(),
|
envVars = result.data.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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) {
|
fun revealKey(key: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.revealEnvVar(EnvVarRevealRequest(key))
|
safeApiCall { ApiClient.hermesApi.revealEnvVar(EnvVarRevealRequest(key)) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful && response.body() != null) {
|
when (result) {
|
||||||
val body = response.body()!!
|
is NetworkResult.Success -> {
|
||||||
|
val body = result.data
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
revealedValues = state.revealedValues + (body.key to body.value),
|
revealedValues = state.revealedValues + (body.key to body.value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to reveal key: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to reveal key: ${e.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to reveal key: ${result.error.message}") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,19 +91,18 @@ class KeysViewModel : ViewModel() {
|
|||||||
value: String,
|
value: String,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.updateEnvVar(EnvVarUpdate(key, value))
|
safeApiCall { ApiClient.hermesApi.updateEnvVar(EnvVarUpdate(key, value)) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Key updated successfully") }
|
_uiState.update { it.copy(toastMessage = "Key updated successfully") }
|
||||||
loadKeys()
|
loadKeys()
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update key: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update key: ${e.message}") }
|
_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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -25,25 +27,24 @@ class LogsViewModel : ViewModel() {
|
|||||||
fun loadLogs() {
|
fun loadLogs() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getLogs()
|
safeApiCall { ApiClient.hermesApi.getLogs() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
val body = response.body()
|
is NetworkResult.Success -> {
|
||||||
val logsList = body?.lines ?: body?.logs ?: emptyList()
|
val body = result.data
|
||||||
|
val logsList = body.lines ?: body.logs ?: emptyList()
|
||||||
_uiState.update { it.copy(isLoading = false, logs = logsList) }
|
_uiState.update { it.copy(isLoading = false, logs = logsList) }
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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.McpServer
|
||||||
import com.m57.hermescontrol.data.model.McpServerToggleRequest
|
import com.m57.hermescontrol.data.model.McpServerToggleRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -28,33 +30,27 @@ class McpServersViewModel : ViewModel() {
|
|||||||
fun loadServers() {
|
fun loadServers() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getMcpServers()
|
safeApiCall { ApiClient.hermesApi.getMcpServers() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
servers = response.body()?.servers.orEmpty(),
|
servers = result.data.servers.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Failed to load MCP servers: HTTP ${response.code()}",
|
errorMessage = "Failed to load MCP servers: ${result.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load MCP servers: ${e.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,22 +69,23 @@ class McpServersViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
ApiClient.hermesApi.toggleMcpServer(server.name, McpServerToggleRequest(targetEnabled))
|
ApiClient.hermesApi.toggleMcpServer(server.name, McpServerToggleRequest(targetEnabled))
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
}
|
||||||
revertToggle(server.name, originalEnabled, "Failed to toggle server: HTTP ${response.code()}")
|
when (result) {
|
||||||
} else {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
toastMessage = "Server '${server.name}' ${if (targetEnabled) "enabled" else "disabled"}",
|
toastMessage = "Server '${server.name}' ${if (targetEnabled) "enabled" else "disabled"}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
revertToggle(server.name, originalEnabled, "Failed to toggle server: ${e.message}")
|
revertToggle(server.name, originalEnabled, "Failed to toggle server: ${result.error.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,33 +93,27 @@ class McpServersViewModel : ViewModel() {
|
|||||||
fun testServer(name: String) {
|
fun testServer(name: String) {
|
||||||
_uiState.update { it.copy(isActionRunning = true) }
|
_uiState.update { it.copy(isActionRunning = true) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.testMcpServer(name)
|
safeApiCall { ApiClient.hermesApi.testMcpServer(name) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActionRunning = false,
|
isActionRunning = false,
|
||||||
toastMessage = "Server '$name' tested successfully",
|
toastMessage = "Server '$name' tested successfully",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActionRunning = false,
|
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) {
|
fun deleteServer(name: String) {
|
||||||
_uiState.update { it.copy(isActionRunning = true) }
|
_uiState.update { it.copy(isActionRunning = true) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.deleteMcpServer(name)
|
safeApiCall { ApiClient.hermesApi.deleteMcpServer(name) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActionRunning = false,
|
isActionRunning = false,
|
||||||
@@ -143,21 +134,15 @@ class McpServersViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadServers()
|
loadServers()
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActionRunning = false,
|
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.ProfileInfo
|
||||||
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
|
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -29,26 +31,30 @@ class ModelViewModel : ViewModel() {
|
|||||||
fun loadModelOptions() {
|
fun loadModelOptions() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val responseResult =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getModelOptions()
|
safeApiCall { ApiClient.hermesApi.getModelOptions() }
|
||||||
}
|
}
|
||||||
val activeProfileNameRes =
|
val activeProfileNameResResult =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getActiveProfile()
|
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
||||||
}
|
}
|
||||||
val profilesRes =
|
val profilesResResult =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getProfiles()
|
safeApiCall { ApiClient.hermesApi.getProfiles() }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
when (responseResult) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
val activeName =
|
val activeName =
|
||||||
if (activeProfileNameRes.isSuccessful) activeProfileNameRes.body()?.active else null
|
if (activeProfileNameResResult is NetworkResult.Success) {
|
||||||
|
activeProfileNameResResult.data.active
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
val activeProfile =
|
val activeProfile =
|
||||||
if (profilesRes.isSuccessful && activeName != null) {
|
if (profilesResResult is NetworkResult.Success && activeName != null) {
|
||||||
profilesRes.body()?.profiles?.find { it.name == activeName }
|
profilesResResult.data.profiles.find { it.name == activeName }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -56,25 +62,19 @@ class ModelViewModel : ViewModel() {
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
providers = response.body()?.providers.orEmpty(),
|
providers = responseResult.data.providers.orEmpty(),
|
||||||
activeProfile = activeProfile,
|
activeProfile = activeProfile,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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,
|
modelName: String,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val activeProfileNameResResult =
|
||||||
val activeProfileNameRes =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getActiveProfile()
|
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
||||||
}
|
}
|
||||||
if (!activeProfileNameRes.isSuccessful) {
|
when (activeProfileNameResResult) {
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
toastMessage = "Failed to fetch active profile: HTTP ${activeProfileNameRes.code()}",
|
toastMessage =
|
||||||
|
"Failed to fetch active profile: ${activeProfileNameResResult.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val activeProfileName = activeProfileNameRes.body()?.active
|
is NetworkResult.Success -> {
|
||||||
if (activeProfileName == null) {
|
val activeProfileName = activeProfileNameResResult.data.active
|
||||||
_uiState.update { it.copy(toastMessage = "No active profile found") }
|
val updateResResult =
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val updateRes =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
ApiClient.hermesApi.updateProfileModel(
|
ApiClient.hermesApi.updateProfileModel(
|
||||||
activeProfileName,
|
activeProfileName,
|
||||||
UpdateProfileModelRequest(providerSlug, modelName),
|
UpdateProfileModelRequest(providerSlug, modelName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (updateRes.isSuccessful) {
|
}
|
||||||
|
when (updateResResult) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Successfully set model to $modelName") }
|
_uiState.update { it.copy(toastMessage = "Successfully set model to $modelName") }
|
||||||
loadModelOptions()
|
loadModelOptions()
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to set model: HTTP ${updateRes.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to set model: ${e.message}") }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Failed to set model: ${updateResResult.error.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.PairingResponse
|
||||||
import com.m57.hermescontrol.data.model.PairingRevokeRequest
|
import com.m57.hermescontrol.data.model.PairingRevokeRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -29,28 +31,22 @@ class PairingViewModel : ViewModel() {
|
|||||||
fun loadPairing() {
|
fun loadPairing() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getPairing()
|
safeApiCall { ApiClient.hermesApi.getPairing() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
_uiState.update { it.copy(isLoading = false, pairing = response.body()) }
|
is NetworkResult.Success -> {
|
||||||
} else {
|
_uiState.update { it.copy(isLoading = false, pairing = result.data) }
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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,
|
code: String,
|
||||||
) {
|
) {
|
||||||
runAction("Approve") {
|
runAction("Approve") {
|
||||||
ApiClient.hermesApi.approvePairing(PairingApproveRequest(platform, code))
|
safeApiCall { ApiClient.hermesApi.approvePairing(PairingApproveRequest(platform, code)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,25 +65,25 @@ class PairingViewModel : ViewModel() {
|
|||||||
userId: String,
|
userId: String,
|
||||||
) {
|
) {
|
||||||
runAction("Revoke") {
|
runAction("Revoke") {
|
||||||
ApiClient.hermesApi.revokePairing(PairingRevokeRequest(platform, userId))
|
safeApiCall { ApiClient.hermesApi.revokePairing(PairingRevokeRequest(platform, userId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearPending() {
|
fun clearPending() {
|
||||||
runAction("Clear Pending") {
|
runAction("Clear Pending") {
|
||||||
ApiClient.hermesApi.clearPendingPairing()
|
safeApiCall { ApiClient.hermesApi.clearPendingPairing() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runAction(
|
private fun runAction(
|
||||||
actionName: String,
|
actionName: String,
|
||||||
apiCall: suspend () -> retrofit2.Response<Unit>,
|
apiCall: suspend () -> NetworkResult<Unit>,
|
||||||
) {
|
) {
|
||||||
_uiState.update { it.copy(isActionRunning = true) }
|
_uiState.update { it.copy(isActionRunning = true) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result = withContext(Dispatchers.IO) { apiCall() }
|
||||||
val response = withContext(Dispatchers.IO) { apiCall() }
|
when (result) {
|
||||||
if (response.isSuccessful) {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActionRunning = false,
|
isActionRunning = false,
|
||||||
@@ -95,21 +91,15 @@ class PairingViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadPairing()
|
loadPairing()
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isActionRunning = false,
|
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 androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.model.PluginInfo
|
import com.m57.hermescontrol.data.model.PluginInfo
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -26,24 +28,23 @@ class PluginsViewModel : ViewModel() {
|
|||||||
fun loadPlugins() {
|
fun loadPlugins() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getPlugins()
|
safeApiCall { ApiClient.hermesApi.getPlugins() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
val plugins = response.body()?.plugins.orEmpty()
|
is NetworkResult.Success -> {
|
||||||
|
val plugins = result.data.plugins.orEmpty()
|
||||||
_uiState.update { it.copy(isLoading = false, plugins = plugins) }
|
_uiState.update { it.copy(isLoading = false, plugins = plugins) }
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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 {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (targetEnabled) {
|
if (targetEnabled) {
|
||||||
ApiClient.hermesApi.enablePlugin(plugin.name)
|
safeApiCall { ApiClient.hermesApi.enablePlugin(plugin.name) }
|
||||||
} else {
|
} else {
|
||||||
ApiClient.hermesApi.disablePlugin(plugin.name)
|
safeApiCall { ApiClient.hermesApi.disablePlugin(plugin.name) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (result is NetworkResult.Failure) {
|
||||||
revertPluginToggle(plugin.name, originalEnabled, "Failed to toggle plugin: HTTP ${response.code()}")
|
revertPluginToggle(plugin.name, originalEnabled, "Failed to toggle plugin: ${result.error.message}")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revertPluginToggle(plugin.name, originalEnabled, "Failed to toggle plugin: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installPlugin(name: String) {
|
fun installPlugin(name: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
ApiClient.hermesApi.installPlugin(
|
ApiClient.hermesApi.installPlugin(
|
||||||
com.m57.hermescontrol.data.model
|
com.m57.hermescontrol.data.model
|
||||||
.AgentPluginInstallBody(name),
|
.AgentPluginInstallBody(name),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Plugin installed successfully") }
|
_uiState.update { it.copy(toastMessage = "Plugin installed successfully") }
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to install plugin: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to install plugin: ${e.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to install plugin: ${result.error.message}") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uninstallPlugin(name: String) {
|
fun uninstallPlugin(name: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.uninstallPlugin(name)
|
safeApiCall { ApiClient.hermesApi.uninstallPlugin(name) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Plugin uninstalled successfully") }
|
_uiState.update { it.copy(toastMessage = "Plugin uninstalled successfully") }
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to uninstall plugin: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to uninstall plugin: ${e.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to uninstall plugin: ${result.error.message}") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePlugin(name: String) {
|
fun updatePlugin(name: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.updatePlugin(name)
|
safeApiCall { ApiClient.hermesApi.updatePlugin(name) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Plugin updated successfully") }
|
_uiState.update { it.copy(toastMessage = "Plugin updated successfully") }
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update plugin: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update plugin: ${e.message}") }
|
_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.UpdateProfileModelRequest
|
||||||
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
|
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -32,33 +34,30 @@ class ProfilesViewModel : ViewModel() {
|
|||||||
fun loadProfiles() {
|
fun loadProfiles() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val profilesResult =
|
||||||
val profilesRes = withContext(Dispatchers.IO) { ApiClient.hermesApi.getProfiles() }
|
withContext(Dispatchers.IO) {
|
||||||
val activeRes = withContext(Dispatchers.IO) { ApiClient.hermesApi.getActiveProfile() }
|
safeApiCall { ApiClient.hermesApi.getProfiles() }
|
||||||
|
}
|
||||||
|
val activeResult =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
||||||
|
}
|
||||||
|
|
||||||
if (profilesRes.isSuccessful && activeRes.isSuccessful) {
|
if (profilesResult is NetworkResult.Success && activeResult is NetworkResult.Success) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
profiles = profilesRes.body()?.profiles.orEmpty(),
|
profiles = profilesResult.data.profiles.orEmpty(),
|
||||||
activeProfileName = activeRes.body()?.active,
|
activeProfileName = activeResult.data.active,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val profilesError = (profilesResult as? NetworkResult.Failure)?.error?.message ?: "Success"
|
||||||
|
val activeError = (activeResult as? NetworkResult.Failure)?.error?.message ?: "Success"
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage =
|
errorMessage = "Failed to load profiles/active: Profiles: $profilesError, Active: $activeError",
|
||||||
"Failed to load profiles/active: HTTP " +
|
|
||||||
"${profilesRes.code()} / ${activeRes.code()}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Error loading profiles: ${e.message}",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,29 +70,23 @@ class ProfilesViewModel : ViewModel() {
|
|||||||
_uiState.update { it.copy(activeProfileName = name) }
|
_uiState.update { it.copy(activeProfileName = name) }
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val res =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.setActiveProfile(SetActiveProfileRequest(name))
|
safeApiCall { ApiClient.hermesApi.setActiveProfile(SetActiveProfileRequest(name)) }
|
||||||
}
|
}
|
||||||
if (res.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Switched to profile $name") }
|
_uiState.update { it.copy(toastMessage = "Switched to profile $name") }
|
||||||
loadProfiles()
|
loadProfiles()
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
activeProfileName = originalActive,
|
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,33 +94,27 @@ class ProfilesViewModel : ViewModel() {
|
|||||||
fun loadSoul(profileName: String) {
|
fun loadSoul(profileName: String) {
|
||||||
_uiState.update { it.copy(isLoadingSoul = true, selectedSoulContent = null) }
|
_uiState.update { it.copy(isLoadingSoul = true, selectedSoulContent = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val res =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getProfileSoul(profileName)
|
safeApiCall { ApiClient.hermesApi.getProfileSoul(profileName) }
|
||||||
}
|
}
|
||||||
if (res.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoadingSoul = false,
|
isLoadingSoul = false,
|
||||||
selectedSoulContent = res.body()?.content ?: "",
|
selectedSoulContent = result.data.content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoadingSoul = false,
|
isLoadingSoul = false,
|
||||||
toastMessage = "Failed to load soul: HTTP ${res.code()}",
|
toastMessage = "Failed to load soul: ${result.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoadingSoul = false,
|
|
||||||
toastMessage = "Failed to load soul: ${e.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,12 +124,17 @@ class ProfilesViewModel : ViewModel() {
|
|||||||
content: String,
|
content: String,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val res =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.updateProfileSoul(profileName, UpdateProfileSoulRequest(content))
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.updateProfileSoul(
|
||||||
|
profileName,
|
||||||
|
UpdateProfileSoulRequest(content),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (res.isSuccessful) {
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
selectedSoulContent = null,
|
selectedSoulContent = null,
|
||||||
@@ -150,19 +142,14 @@ class ProfilesViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadProfiles()
|
loadProfiles()
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
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,
|
model: String,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val res =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.updateProfileModel(profileName, UpdateProfileModelRequest(provider, model))
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.updateProfileModel(
|
||||||
|
profileName,
|
||||||
|
UpdateProfileModelRequest(provider, model),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (res.isSuccessful) {
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Model settings updated") }
|
_uiState.update { it.copy(toastMessage = "Model settings updated") }
|
||||||
loadProfiles()
|
loadProfiles()
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update model settings: HTTP ${res.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update model settings: ${e.message}") }
|
_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 androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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 com.m57.hermescontrol.theme.ThemePreference
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -144,30 +146,24 @@ class SettingsViewModel : ViewModel() {
|
|||||||
_uiState.update { it.copy(isTesting = true, testResult = null) }
|
_uiState.update { it.copy(isTesting = true, testResult = null) }
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getStatus()
|
safeApiCall { ApiClient.hermesApi.getStatus() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(isTesting = false, testResult = "✅ Connected successfully")
|
it.copy(isTesting = false, testResult = "✅ Connected successfully")
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isTesting = false,
|
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.Skill
|
||||||
import com.m57.hermescontrol.data.model.ToggleSkillRequest
|
import com.m57.hermescontrol.data.model.ToggleSkillRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -27,23 +29,22 @@ class SkillsViewModel : ViewModel() {
|
|||||||
fun loadSkills() {
|
fun loadSkills() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getSkills()
|
safeApiCall { ApiClient.hermesApi.getSkills() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
_uiState.update { it.copy(isLoading = false, skills = response.body().orEmpty()) }
|
is NetworkResult.Success -> {
|
||||||
} else {
|
_uiState.update { it.copy(isLoading = false, skills = result.data.orEmpty()) }
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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 {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.toggleSkill(ToggleSkillRequest(skill.name, targetEnabled))
|
safeApiCall { ApiClient.hermesApi.toggleSkill(ToggleSkillRequest(skill.name, targetEnabled)) }
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (result is NetworkResult.Failure) {
|
||||||
revertSkillToggle(skill.name, originalEnabled, "Failed to toggle skill: HTTP ${response.code()}")
|
revertSkillToggle(skill.name, originalEnabled, "Failed to toggle skill: ${result.error.message}")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revertSkillToggle(skill.name, originalEnabled, "Failed to toggle skill: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import com.m57.hermescontrol.BuildConfig
|
|||||||
import com.m57.hermescontrol.data.model.DoctorResponse
|
import com.m57.hermescontrol.data.model.DoctorResponse
|
||||||
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -34,70 +36,60 @@ class SystemViewModel : ViewModel() {
|
|||||||
fun loadSystemData() {
|
fun loadSystemData() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val statsResponse =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getSystemStats()
|
safeApiCall { ApiClient.hermesApi.getSystemStats() }
|
||||||
}
|
}
|
||||||
|
when (result) {
|
||||||
if (statsResponse.isSuccessful) {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
stats = statsResponse.body(),
|
stats = result.data,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Doctor is optional; many servers do not expose it.
|
// Doctor is optional; many servers do not expose it.
|
||||||
loadDoctorReport()
|
loadDoctorReport()
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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() {
|
private fun loadDoctorReport() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val doctorResponse =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.runDoctor()
|
safeApiCall { ApiClient.hermesApi.runDoctor() }
|
||||||
}
|
}
|
||||||
if (doctorResponse.isSuccessful) {
|
if (result is NetworkResult.Success) {
|
||||||
_uiState.update { it.copy(doctorReport = doctorResponse.body()) }
|
_uiState.update { it.copy(doctorReport = result.data) }
|
||||||
}
|
} else if (result is NetworkResult.Failure && BuildConfig.DEBUG) {
|
||||||
} catch (e: Exception) {
|
Log.w(TAG, "doctor endpoint unavailable: ${result.error.message}")
|
||||||
if (BuildConfig.DEBUG) Log.w(TAG, "doctor endpoint unavailable", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun triggerBackup() {
|
fun triggerBackup() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.triggerBackup()
|
safeApiCall { ApiClient.hermesApi.triggerBackup() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Backup triggered successfully") }
|
_uiState.update { it.copy(toastMessage = "Backup triggered successfully") }
|
||||||
} else {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to trigger backup: HTTP ${response.code()}") }
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to trigger backup: ${e.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to trigger backup: ${result.error.message}") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.m57.hermescontrol.data.model.Toolset
|
import com.m57.hermescontrol.data.model.Toolset
|
||||||
import com.m57.hermescontrol.data.model.ToolsetToggleRequest
|
import com.m57.hermescontrol.data.model.ToolsetToggleRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -27,28 +29,22 @@ class ToolsetsViewModel : ViewModel() {
|
|||||||
fun loadToolsets() {
|
fun loadToolsets() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getToolsets()
|
safeApiCall { ApiClient.hermesApi.getToolsets() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
_uiState.update { it.copy(isLoading = false, toolsets = response.body().orEmpty()) }
|
is NetworkResult.Success -> {
|
||||||
} else {
|
_uiState.update { it.copy(isLoading = false, toolsets = result.data.orEmpty()) }
|
||||||
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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 {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.toggleToolset(toolset.name, ToolsetToggleRequest(targetEnabled))
|
safeApiCall { ApiClient.hermesApi.toggleToolset(toolset.name, ToolsetToggleRequest(targetEnabled)) }
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (result is NetworkResult.Failure) {
|
||||||
revertToggle(toolset.name, originalEnabled, "Failed to toggle toolset: HTTP ${response.code()}")
|
revertToggle(toolset.name, originalEnabled, "Failed to toggle toolset: ${result.error.message}")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revertToggle(toolset.name, originalEnabled, "Failed to toggle toolset: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.m57.hermescontrol.data.model.WebhookSubscription
|
import com.m57.hermescontrol.data.model.WebhookSubscription
|
||||||
import com.m57.hermescontrol.data.model.WebhooksToggleRequest
|
import com.m57.hermescontrol.data.model.WebhooksToggleRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -30,36 +32,30 @@ class WebhooksViewModel : ViewModel() {
|
|||||||
fun loadWebhooks() {
|
fun loadWebhooks() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getWebhooks()
|
safeApiCall { ApiClient.hermesApi.getWebhooks() }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
val body = response.body()
|
is NetworkResult.Success -> {
|
||||||
|
val body = result.data
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
enabled = body?.enabled ?: false,
|
enabled = body.enabled,
|
||||||
baseUrl = body?.base_url,
|
baseUrl = body.base_url,
|
||||||
subscriptions = body?.subscriptions.orEmpty(),
|
subscriptions = body.subscriptions.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
errorMessage = "Failed to load webhooks: HTTP ${response.code()}",
|
errorMessage = "Failed to load webhooks: ${result.error.message}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load webhooks: ${e.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,33 +65,27 @@ class WebhooksViewModel : ViewModel() {
|
|||||||
_uiState.update { it.copy(enabled = targetEnabled) }
|
_uiState.update { it.copy(enabled = targetEnabled) }
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val result =
|
||||||
val response =
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.toggleWebhooks(WebhooksToggleRequest(targetEnabled))
|
safeApiCall { ApiClient.hermesApi.toggleWebhooks(WebhooksToggleRequest(targetEnabled)) }
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
toastMessage = "Webhooks global status ${if (targetEnabled) "enabled" else "disabled"}",
|
toastMessage = "Webhooks global status ${if (targetEnabled) "enabled" else "disabled"}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadWebhooks()
|
loadWebhooks()
|
||||||
} else {
|
}
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
enabled = originalEnabled,
|
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}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,14 @@ class ConnectViewModelTest {
|
|||||||
|
|
||||||
val mockResponse = mockk<Response<StatusResponse>>()
|
val mockResponse = mockk<Response<StatusResponse>>()
|
||||||
every { mockResponse.isSuccessful } returns true
|
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
|
coEvery { mockApiService.getStatus() } returns mockResponse
|
||||||
|
|
||||||
viewModel.connect()
|
viewModel.connect()
|
||||||
|
|||||||
Reference in new issue
Block a user