From 2ae6cce9290ee674bf6a531daf6384cf6e6f907a Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Mon, 29 Jun 2026 00:45:16 +0300 Subject: [PATCH] =?UTF-8?q?fix(#372):=20plugin=20install/uninstall=20400?= =?UTF-8?q?=20=E2=80=94=20enable=20instead=20of=20install=20for=20on-disk?= =?UTF-8?q?=20plugins,=20gate=20uninstall=20behind=20canRemove,=20parse=20?= =?UTF-8?q?FastAPI=20error=20bodies=20(#416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace installPlugin(name) with activatePlugin(plugin) that calls enable endpoint instead of the install endpoint for plugins already on disk (bundled/inactive). - Gate Uninstall button behind plugin.canRemove — bundled plugins don't show it. - Update PluginsScreen to call activatePlugin(plugin) instead of installPlugin(name). - Parse FastAPI 'detail' field from 400 response bodies in mapHttpError/safeApiCall so users see the actual error message instead of a generic placeholder. - Import ButtonDefaults to fix max-line-length lint warning. Co-authored-by: M57 --- .../data/remote/NetworkResult.kt | 30 +++++++++++++++++-- .../hermescontrol/ui/plugins/PluginsScreen.kt | 23 +++++++------- .../ui/plugins/PluginsViewModel.kt | 13 +++----- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt index e1ebad9c..494c1b28 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/NetworkResult.kt @@ -1,5 +1,6 @@ package com.m57.hermescontrol.data.remote +import com.google.gson.JsonParser import kotlinx.coroutines.delay import retrofit2.Response import java.io.IOException @@ -37,12 +38,28 @@ sealed interface NetworkError { ) : NetworkError } -fun mapHttpError(code: Int): NetworkError { +fun mapHttpError( + code: Int, + errorBody: String? = null, +): NetworkError { if (code == 401) { return NetworkError.AuthExpired() } + // Try to parse FastAPI-style error detail from response body + val detail = + if (errorBody != null) { + try { + val parsed = com.google.gson.JsonParser.parseString(errorBody).asJsonObject + parsed.get("detail")?.asString + } catch (_: Exception) { + null + } + } else { + null + } + val message = - when (code) { + detail ?: when (code) { 400 -> "Bad Request (HTTP 400): The server could not understand the request." 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." @@ -88,12 +105,19 @@ suspend inline fun safeApiCall( } } else { val code = response.code() + // Read error body for detail message + val errorBody = + try { + response.errorBody()?.string() + } catch (_: Exception) { + null + } if ((code in 500..599 || code == 429) && attempt < retries) { val backoff = jitteredBackoff(500L * (1 shl attempt)) delay(backoff) continue } - return NetworkResult.Failure(mapHttpError(code)) + return NetworkResult.Failure(mapHttpError(code, errorBody)) } } catch (e: IOException) { lastException = e diff --git a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt index 28e6c962..9721ab17 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -159,18 +160,20 @@ fun PluginsScreen( OutlinedButton(onClick = { viewModel.updatePlugin(plugin.name) }) { Text(stringResource(R.string.plugins_action_update)) } - Spacer(modifier = Modifier.width(8.dp)) - OutlinedButton( - onClick = { viewModel.uninstallPlugin(plugin.name) }, - colors = - androidx.compose.material3.ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.error, - ), - ) { - Text(stringResource(R.string.plugins_action_uninstall)) + if (plugin.canRemove) { + Spacer(modifier = Modifier.width(8.dp)) + OutlinedButton( + onClick = { viewModel.uninstallPlugin(plugin.name) }, + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Text(stringResource(R.string.plugins_action_uninstall)) + } } } else { - Button(onClick = { viewModel.installPlugin(plugin.name) }) { + Button(onClick = { viewModel.activatePlugin(plugin) }) { Text(stringResource(R.string.plugins_action_install)) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt index 1789eacd..3c71265e 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt @@ -79,25 +79,20 @@ class PluginsViewModel : ViewModel(), ToastHost { } } - fun installPlugin(name: String) { + fun activatePlugin(plugin: PluginInfo) { viewModelScope.launch { val result = withContext(Dispatchers.IO) { - safeApiCall { - ApiClient.hermesApi.installPlugin( - com.m57.hermescontrol.data.model - .AgentPluginInstallBody(name), - ) - } + safeApiCall { ApiClient.hermesApi.enablePlugin(plugin.name) } } when (result) { is NetworkResult.Success -> { - _uiState.update { it.copy(toastMessage = "Plugin installed successfully") } + _uiState.update { it.copy(toastMessage = "Plugin enabled successfully") } loadPlugins() } is NetworkResult.Failure -> { - _uiState.update { it.copy(toastMessage = "Failed to install plugin: ${result.error.message}") } + _uiState.update { it.copy(toastMessage = "Failed to enable plugin: ${result.error.message}") } } } }