fixes
This commit is contained in:
1 parent
0562906634
commit
8ca4599c6a
14 files changed
+362
-166
No files matched your search
@@ -1,12 +1,7 @@
|
|||||||
package com.m57.hermescontrol.data.model
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
data class DoctorResponse(
|
data class DoctorResponse(
|
||||||
val status: String,
|
val ok: Boolean,
|
||||||
val reports: List<DoctorReport>,
|
val pid: Int?,
|
||||||
)
|
val name: String?,
|
||||||
|
|
||||||
data class DoctorReport(
|
|
||||||
val category: String,
|
|
||||||
val status: String,
|
|
||||||
val message: String,
|
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class EnvVarConfig(
|
||||||
|
@SerializedName("is_set") val isSet: Boolean,
|
||||||
|
@SerializedName("redacted_value") val redactedValue: String?,
|
||||||
|
val description: String?,
|
||||||
|
val url: String?,
|
||||||
|
val category: String?,
|
||||||
|
@SerializedName("is_password") val isPassword: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EnvVarRevealRequest(
|
||||||
|
val key: String,
|
||||||
|
val profile: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EnvVarRevealResponse(
|
||||||
|
val key: String,
|
||||||
|
val value: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EnvVarUpdate(
|
||||||
|
val key: String,
|
||||||
|
val value: String,
|
||||||
|
val profile: String? = null,
|
||||||
|
)
|
||||||
@@ -1,27 +1,38 @@
|
|||||||
package com.m57.hermescontrol.data.model
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class KanbanBoard(
|
data class KanbanBoard(
|
||||||
val id: String,
|
@SerializedName("slug") val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class KanbanTask(
|
data class KanbanTask(
|
||||||
val id: String,
|
val id: String,
|
||||||
val boardId: String,
|
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String?,
|
@SerializedName("body") val description: String?,
|
||||||
val status: String,
|
val status: String,
|
||||||
val assignedTo: String?,
|
@SerializedName("assignee") val assignedTo: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MoveTaskRequest(
|
data class CreateTaskBody(
|
||||||
val status: String,
|
val title: String,
|
||||||
|
val body: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class CreateTaskRequest(
|
data class KanbanColumn(
|
||||||
val boardId: String,
|
val name: String,
|
||||||
val title: String,
|
val tasks: List<KanbanTask>,
|
||||||
val description: String?,
|
)
|
||||||
val status: String,
|
|
||||||
|
data class KanbanBoardResponse(
|
||||||
|
val columns: List<KanbanColumn>,
|
||||||
|
val assignees: List<String>?,
|
||||||
|
val tenants: List<String>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class KanbanBoardsResponse(
|
||||||
|
val boards: List<KanbanBoard>,
|
||||||
|
val current: String,
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,27 @@
|
|||||||
package com.m57.hermescontrol.data.model
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class MessagingPlatform(
|
data class MessagingPlatform(
|
||||||
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
val configured: Boolean,
|
val configured: Boolean,
|
||||||
val details: Map<String, String>?,
|
@SerializedName("env_vars") val envVars: List<EnvVarField>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EnvVarField(
|
||||||
|
val key: String,
|
||||||
|
val required: Boolean,
|
||||||
|
@SerializedName("is_set") val isSet: Boolean,
|
||||||
|
val description: String?,
|
||||||
|
val prompt: String?,
|
||||||
|
@SerializedName("is_password") val isPassword: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MessagingPlatformUpdate(
|
||||||
|
val enabled: Boolean? = null,
|
||||||
|
val env: Map<String, String> = emptyMap(),
|
||||||
|
@SerializedName("clear_env") val clearEnv: List<String> = emptyList(),
|
||||||
|
val profile: String? = null,
|
||||||
)
|
)
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
package com.m57.hermescontrol.data.model
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class PluginInfo(
|
data class PluginInfo(
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val version: String?,
|
val version: String?,
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
val installed: Boolean,
|
val installed: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TogglePluginRequest(
|
data class TogglePluginRequest(
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class AgentPluginInstallBody(
|
||||||
|
val identifier: String,
|
||||||
|
val force: Boolean = false,
|
||||||
|
val enable: Boolean = true,
|
||||||
|
)
|
||||||
@@ -2,17 +2,24 @@ package com.m57.hermescontrol.data.remote
|
|||||||
|
|
||||||
import com.m57.hermescontrol.data.model.AchievementsResponse
|
import com.m57.hermescontrol.data.model.AchievementsResponse
|
||||||
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
||||||
import com.m57.hermescontrol.data.model.CreateTaskRequest
|
import com.m57.hermescontrol.data.model.AgentPluginInstallBody
|
||||||
|
import com.m57.hermescontrol.data.model.CreateTaskBody
|
||||||
import com.m57.hermescontrol.data.model.CronJob
|
import com.m57.hermescontrol.data.model.CronJob
|
||||||
import com.m57.hermescontrol.data.model.DoctorResponse
|
import com.m57.hermescontrol.data.model.DoctorResponse
|
||||||
|
import com.m57.hermescontrol.data.model.EnvVarConfig
|
||||||
|
import com.m57.hermescontrol.data.model.EnvVarRevealRequest
|
||||||
|
import com.m57.hermescontrol.data.model.EnvVarRevealResponse
|
||||||
|
import com.m57.hermescontrol.data.model.EnvVarUpdate
|
||||||
import com.m57.hermescontrol.data.model.KanbanBoard
|
import com.m57.hermescontrol.data.model.KanbanBoard
|
||||||
|
import com.m57.hermescontrol.data.model.KanbanBoardResponse
|
||||||
|
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
||||||
import com.m57.hermescontrol.data.model.KanbanTask
|
import com.m57.hermescontrol.data.model.KanbanTask
|
||||||
import com.m57.hermescontrol.data.model.LogResponse
|
import com.m57.hermescontrol.data.model.LogResponse
|
||||||
import com.m57.hermescontrol.data.model.McpServerToggleRequest
|
import com.m57.hermescontrol.data.model.McpServerToggleRequest
|
||||||
import com.m57.hermescontrol.data.model.McpServersResponse
|
import com.m57.hermescontrol.data.model.McpServersResponse
|
||||||
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.ModelOptionsResponse
|
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
||||||
import com.m57.hermescontrol.data.model.MoveTaskRequest
|
|
||||||
import com.m57.hermescontrol.data.model.PairingApproveRequest
|
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
|
||||||
@@ -40,9 +47,11 @@ import retrofit2.Response
|
|||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.DELETE
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.PATCH
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.PUT
|
import retrofit2.http.PUT
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface HermesApiService {
|
interface HermesApiService {
|
||||||
@GET("api/status")
|
@GET("api/status")
|
||||||
@@ -205,72 +214,82 @@ interface HermesApiService {
|
|||||||
@GET("api/logs")
|
@GET("api/logs")
|
||||||
suspend fun getLogs(): Response<LogResponse>
|
suspend fun getLogs(): Response<LogResponse>
|
||||||
|
|
||||||
@GET("api/plugins")
|
@GET("api/dashboard/plugins")
|
||||||
suspend fun getPlugins(): Response<List<PluginInfo>>
|
suspend fun getPlugins(): Response<List<PluginInfo>>
|
||||||
|
|
||||||
@POST("api/plugins/{name}/install")
|
@POST("api/dashboard/agent-plugins/install")
|
||||||
suspend fun installPlugin(
|
suspend fun installPlugin(
|
||||||
@Path("name") name: String,
|
@Body body: AgentPluginInstallBody,
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("api/plugins/{name}/uninstall")
|
@DELETE("api/dashboard/agent-plugins/{name}")
|
||||||
suspend fun uninstallPlugin(
|
suspend fun uninstallPlugin(
|
||||||
@Path("name") name: String,
|
@Path("name") name: String,
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("api/plugins/{name}/update")
|
@POST("api/dashboard/agent-plugins/{name}/update")
|
||||||
suspend fun updatePlugin(
|
suspend fun updatePlugin(
|
||||||
@Path("name") name: String,
|
@Path("name") name: String,
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
@PUT("api/plugins/{name}/toggle")
|
@POST("api/dashboard/agent-plugins/{name}/enable")
|
||||||
suspend fun togglePlugin(
|
suspend fun enablePlugin(
|
||||||
|
@Path("name") name: String,
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
@POST("api/dashboard/agent-plugins/{name}/disable")
|
||||||
|
suspend fun disablePlugin(
|
||||||
@Path("name") name: String,
|
@Path("name") name: String,
|
||||||
@Body body: TogglePluginRequest,
|
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
@GET("api/messaging/platforms")
|
@GET("api/messaging/platforms")
|
||||||
suspend fun getMessagingPlatforms(): Response<List<MessagingPlatform>>
|
suspend fun getMessagingPlatforms(): Response<List<MessagingPlatform>>
|
||||||
|
|
||||||
@POST("api/messaging/platforms/{name}/configure")
|
@PUT("api/messaging/platforms/{platform_id}")
|
||||||
suspend fun configurePlatform(
|
suspend fun configurePlatform(
|
||||||
@Path("name") name: String,
|
@Path("platform_id") platformId: String,
|
||||||
@Body config: Map<String, String>,
|
@Body config: MessagingPlatformUpdate,
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
@GET("api/env")
|
@GET("api/env")
|
||||||
suspend fun getEnvVars(): Response<Map<String, String>>
|
suspend fun getEnvVars(): Response<Map<String, EnvVarConfig>>
|
||||||
|
|
||||||
@PUT("api/env")
|
@PUT("api/env")
|
||||||
suspend fun updateEnvVar(
|
suspend fun updateEnvVar(
|
||||||
@Body vars: Map<String, String>,
|
@Body body: EnvVarUpdate,
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("api/env/reveal")
|
@POST("api/env/reveal")
|
||||||
suspend fun revealEnvVars(): Response<Map<String, String>>
|
suspend fun revealEnvVar(
|
||||||
|
@Body request: EnvVarRevealRequest,
|
||||||
|
): Response<EnvVarRevealResponse>
|
||||||
|
|
||||||
@POST("api/ops/backup")
|
@POST("api/ops/backup")
|
||||||
suspend fun triggerBackup(): Response<Unit>
|
suspend fun triggerBackup(): Response<Unit>
|
||||||
|
|
||||||
@GET("api/ops/doctor")
|
@POST("api/ops/doctor")
|
||||||
suspend fun runDoctor(): Response<DoctorResponse>
|
suspend fun runDoctor(): Response<DoctorResponse>
|
||||||
|
|
||||||
@GET("api/plugins/kanban/boards")
|
@GET("api/plugins/kanban/boards")
|
||||||
suspend fun getKanbanBoards(): Response<List<KanbanBoard>>
|
suspend fun getKanbanBoards(): Response<KanbanBoardsResponse>
|
||||||
|
|
||||||
@GET("api/plugins/kanban/boards/{id}/tasks")
|
@GET("api/plugins/kanban/board")
|
||||||
suspend fun getKanbanTasks(
|
suspend fun getKanbanBoard(): Response<KanbanBoardResponse>
|
||||||
@Path("id") boardId: String,
|
|
||||||
): Response<List<KanbanTask>>
|
|
||||||
|
|
||||||
@POST("api/plugins/kanban/tasks/{id}/move")
|
@POST("api/plugins/kanban/boards/{slug}/switch")
|
||||||
suspend fun moveKanbanTask(
|
suspend fun switchKanbanBoard(
|
||||||
|
@Path("slug") slug: String,
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
@PATCH("api/plugins/kanban/tasks/{id}")
|
||||||
|
suspend fun updateKanbanTask(
|
||||||
@Path("id") taskId: String,
|
@Path("id") taskId: String,
|
||||||
@Body body: MoveTaskRequest,
|
@Body body: Map<String, String?>,
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("api/plugins/kanban/tasks")
|
@POST("api/plugins/kanban/tasks")
|
||||||
suspend fun createKanbanTask(
|
suspend fun createKanbanTask(
|
||||||
@Body task: CreateTaskRequest,
|
@Query("board") board: String?,
|
||||||
|
@Body task: CreateTaskBody,
|
||||||
): Response<KanbanTask>
|
): Response<KanbanTask>
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -41,6 +42,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -135,8 +137,20 @@ fun ChannelsScreen(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Button(onClick = { showConfigureForm = !showConfigureForm }) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(if (showConfigureForm) "Cancel" else "Configure")
|
Switch(
|
||||||
|
checked = platform.enabled,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
viewModel.configurePlatform(
|
||||||
|
platform.id,
|
||||||
|
MessagingPlatformUpdate(enabled = isChecked),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Button(onClick = { showConfigureForm = !showConfigureForm }) {
|
||||||
|
Text(if (showConfigureForm) "Cancel" else "Configure")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,20 +161,19 @@ fun ChannelsScreen(
|
|||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
|
|
||||||
val fields =
|
val envFields = platform.envVars.orEmpty()
|
||||||
if (platform.name.lowercase() == "telegram") {
|
|
||||||
listOf("bot_token", "chat_id")
|
|
||||||
} else {
|
|
||||||
listOf("webhook_url", "channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
val inputValues = remember { mutableStateMapOf<String, String>() }
|
val inputValues = remember { mutableStateMapOf<String, String>() }
|
||||||
|
|
||||||
fields.forEach { field ->
|
envFields.forEach { field ->
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = inputValues[field].orEmpty(),
|
value = inputValues[field.key].orEmpty(),
|
||||||
onValueChange = { inputValues[field] = it },
|
onValueChange = { inputValues[field.key] = it },
|
||||||
label = { Text(field.replace("_", " ").uppercase()) },
|
label = { Text(field.prompt ?: field.key) },
|
||||||
|
placeholder = {
|
||||||
|
if (field.isSet) {
|
||||||
|
Text("******** (Already Configured)")
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -168,7 +181,7 @@ fun ChannelsScreen(
|
|||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.configurePlatform(platform.name, inputValues.toMap())
|
viewModel.configurePlatform(platform.id, inputValues.toMap())
|
||||||
showConfigureForm = false
|
showConfigureForm = false
|
||||||
},
|
},
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.align(Alignment.End),
|
||||||
@@ -176,16 +189,25 @@ fun ChannelsScreen(
|
|||||||
Text("Save Settings")
|
Text("Save Settings")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
platform.details?.let { details ->
|
val envFields = platform.envVars.orEmpty()
|
||||||
if (details.isNotEmpty()) {
|
if (envFields.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(text = "Details:", style = MaterialTheme.typography.titleSmall)
|
Text(text = "Configured Keys:", style = MaterialTheme.typography.titleSmall)
|
||||||
details.forEach { (key, value) ->
|
envFields.forEach { field ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "$key: $value",
|
text = field.key,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
text = if (field.isSet) "Configured" else "Not set",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (field.isSet) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.m57.hermescontrol.ui.channels
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -48,27 +49,34 @@ class ChannelsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun configurePlatform(
|
fun configurePlatform(
|
||||||
name: String,
|
platformId: String,
|
||||||
config: Map<String, String>,
|
update: MessagingPlatformUpdate,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val response =
|
val response =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.configurePlatform(name, config)
|
ApiClient.hermesApi.configurePlatform(platformId, update)
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_uiState.update { it.copy(toastMessage = "$name configured successfully") }
|
_uiState.update { it.copy(toastMessage = "$platformId configured successfully") }
|
||||||
loadPlatforms()
|
loadPlatforms()
|
||||||
} else {
|
} else {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to configure $name: HTTP ${response.code()}") }
|
_uiState.update { it.copy(toastMessage = "Failed to configure platform: HTTP ${response.code()}") }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to configure $name: ${e.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to configure platform: ${e.message}") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun configurePlatform(
|
||||||
|
platformId: String,
|
||||||
|
config: Map<String, String>,
|
||||||
|
) {
|
||||||
|
configurePlatform(platformId, MessagingPlatformUpdate(env = config))
|
||||||
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package com.m57.hermescontrol.ui.kanban
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.m57.hermescontrol.data.model.CreateTaskRequest
|
|
||||||
import com.m57.hermescontrol.data.model.KanbanBoard
|
import com.m57.hermescontrol.data.model.KanbanBoard
|
||||||
import com.m57.hermescontrol.data.model.KanbanTask
|
import com.m57.hermescontrol.data.model.KanbanTask
|
||||||
import com.m57.hermescontrol.data.model.MoveTaskRequest
|
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -37,10 +35,12 @@ class KanbanViewModel : ViewModel() {
|
|||||||
ApiClient.hermesApi.getKanbanBoards()
|
ApiClient.hermesApi.getKanbanBoards()
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val boards = response.body().orEmpty()
|
val boards = response.body()?.boards.orEmpty()
|
||||||
_uiState.update { it.copy(isLoading = false, boards = boards) }
|
_uiState.update { it.copy(isLoading = false, boards = boards) }
|
||||||
if (boards.isNotEmpty()) {
|
val currentSlug = response.body()?.current
|
||||||
selectBoard(boards.first())
|
val currentBoard = boards.find { it.id == currentSlug } ?: boards.firstOrNull()
|
||||||
|
if (currentBoard != null) {
|
||||||
|
selectBoard(currentBoard)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@@ -65,12 +65,23 @@ class KanbanViewModel : ViewModel() {
|
|||||||
_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 {
|
try {
|
||||||
|
// Switch current board slug
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ApiClient.hermesApi.switchKanbanBoard(board.id)
|
||||||
|
}
|
||||||
|
|
||||||
val response =
|
val response =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.getKanbanTasks(board.id)
|
ApiClient.hermesApi.getKanbanBoard()
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_uiState.update { it.copy(isLoading = false, tasks = response.body().orEmpty()) }
|
val allTasks =
|
||||||
|
response
|
||||||
|
.body()
|
||||||
|
?.columns
|
||||||
|
?.flatMap { it.tasks }
|
||||||
|
.orEmpty()
|
||||||
|
_uiState.update { it.copy(isLoading = false, tasks = allTasks) }
|
||||||
} else {
|
} else {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -101,12 +112,12 @@ class KanbanViewModel : ViewModel() {
|
|||||||
val response =
|
val response =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.createKanbanTask(
|
ApiClient.hermesApi.createKanbanTask(
|
||||||
CreateTaskRequest(
|
board = board.id,
|
||||||
boardId = board.id,
|
task =
|
||||||
title = title,
|
com.m57.hermescontrol.data.model.CreateTaskBody(
|
||||||
description = description,
|
title = title,
|
||||||
status = status,
|
body = description,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
@@ -142,7 +153,7 @@ class KanbanViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
val response =
|
val response =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.moveKanbanTask(task.id, MoveTaskRequest(newStatus))
|
ApiClient.hermesApi.updateKanbanTask(task.id, mapOf("status" to newStatus))
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
revertTaskMove(task.id, originalStatus, "Failed to move task: HTTP ${response.code()}")
|
revertTaskMove(task.id, originalStatus, "Failed to move task: HTTP ${response.code()}")
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -52,7 +56,7 @@ fun KeysScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadKeys(reveal = false)
|
viewModel.loadKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
LaunchedEffect(state.toastMessage) {
|
||||||
@@ -77,8 +81,11 @@ fun KeysScreen(
|
|||||||
},
|
},
|
||||||
title = { Text("Keys & Credentials") },
|
title = { Text("Keys & Credentials") },
|
||||||
actions = {
|
actions = {
|
||||||
Button(onClick = { viewModel.loadKeys(reveal = !state.isRevealed) }) {
|
IconButton(onClick = { viewModel.loadKeys() }) {
|
||||||
Text(if (state.isRevealed) "Hide Values" else "Reveal Values")
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Refresh,
|
||||||
|
contentDescription = "Refresh",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -108,9 +115,20 @@ fun KeysScreen(
|
|||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
items(state.envVars.toList()) { (key, value) ->
|
items(state.envVars.toList()) { (key, config) ->
|
||||||
var isEditing by remember { mutableStateOf(false) }
|
var isEditing by remember { mutableStateOf(false) }
|
||||||
var editedValue by remember { mutableStateOf(value) }
|
val revealedVal = state.revealedValues[key]
|
||||||
|
val isRevealed = revealedVal != null
|
||||||
|
var editedValue by remember { mutableStateOf(revealedVal ?: "") }
|
||||||
|
|
||||||
|
val displayValue =
|
||||||
|
if (isRevealed) {
|
||||||
|
revealedVal.orEmpty()
|
||||||
|
} else if (config.isSet) {
|
||||||
|
config.redactedValue ?: "********"
|
||||||
|
} else {
|
||||||
|
"Not configured"
|
||||||
|
}
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
@@ -119,6 +137,14 @@ fun KeysScreen(
|
|||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
)
|
)
|
||||||
|
if (!config.description.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = config.description,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -141,7 +167,6 @@ fun KeysScreen(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
isEditing = false
|
isEditing = false
|
||||||
editedValue = value
|
|
||||||
}) {
|
}) {
|
||||||
Text("Cancel")
|
Text("Cancel")
|
||||||
}
|
}
|
||||||
@@ -153,13 +178,36 @@ fun KeysScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = value,
|
text = displayValue,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
IconButton(onClick = { isEditing = true }) {
|
Row {
|
||||||
Text("Edit", style = MaterialTheme.typography.bodySmall)
|
if (config.isSet) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (isRevealed) {
|
||||||
|
viewModel.hideKey(key)
|
||||||
|
} else {
|
||||||
|
viewModel.revealKey(key)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isRevealed) Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||||
|
contentDescription = "Toggle Visibility",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(onClick = {
|
||||||
|
editedValue = if (isRevealed) revealedVal.orEmpty() else ""
|
||||||
|
isEditing = true
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Edit,
|
||||||
|
contentDescription = "Edit",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package com.m57.hermescontrol.ui.keys
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.m57.hermescontrol.data.model.EnvVarConfig
|
||||||
|
import com.m57.hermescontrol.data.model.EnvVarRevealRequest
|
||||||
|
import com.m57.hermescontrol.data.model.EnvVarUpdate
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -13,8 +16,8 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
data class KeysUiState(
|
data class KeysUiState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val envVars: Map<String, String> = emptyMap(),
|
val envVars: Map<String, EnvVarConfig> = emptyMap(),
|
||||||
val isRevealed: Boolean = false,
|
val revealedValues: Map<String, String> = emptyMap(),
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
@@ -29,18 +32,13 @@ class KeysViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
val response =
|
val response =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (reveal) {
|
ApiClient.hermesApi.getEnvVars()
|
||||||
ApiClient.hermesApi.revealEnvVars()
|
|
||||||
} else {
|
|
||||||
ApiClient.hermesApi.getEnvVars()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
envVars = response.body().orEmpty(),
|
envVars = response.body().orEmpty(),
|
||||||
isRevealed = reveal,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -57,23 +55,50 @@ class KeysViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun revealKey(key: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ApiClient.hermesApi.revealEnvVar(EnvVarRevealRequest(key))
|
||||||
|
}
|
||||||
|
if (response.isSuccessful && response.body() != null) {
|
||||||
|
val body = response.body()!!
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
revealedValues = state.revealedValues + (body.key to body.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_uiState.update { it.copy(toastMessage = "Failed to reveal key: HTTP ${response.code()}") }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.update { it.copy(toastMessage = "Failed to reveal key: ${e.message}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideKey(key: String) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
revealedValues = state.revealedValues.toMutableMap().apply { remove(key) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateKey(
|
fun updateKey(
|
||||||
key: String,
|
key: String,
|
||||||
value: String,
|
value: String,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val updatedVars =
|
|
||||||
_uiState.value.envVars.toMutableMap().apply {
|
|
||||||
put(key, value)
|
|
||||||
}
|
|
||||||
val response =
|
val response =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.updateEnvVar(updatedVars)
|
ApiClient.hermesApi.updateEnvVar(EnvVarUpdate(key, value))
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_uiState.update { it.copy(toastMessage = "Key updated successfully") }
|
_uiState.update { it.copy(toastMessage = "Key updated successfully") }
|
||||||
loadKeys(_uiState.value.isRevealed)
|
loadKeys()
|
||||||
} else {
|
} else {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update key: HTTP ${response.code()}") }
|
_uiState.update { it.copy(toastMessage = "Failed to update key: HTTP ${response.code()}") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,11 @@ class PluginsViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
val response =
|
val response =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.togglePlugin(plugin.name, TogglePluginRequest(targetEnabled))
|
if (targetEnabled) {
|
||||||
|
ApiClient.hermesApi.enablePlugin(plugin.name)
|
||||||
|
} else {
|
||||||
|
ApiClient.hermesApi.disablePlugin(plugin.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
revertPluginToggle(plugin.name, originalEnabled, "Failed to toggle plugin: HTTP ${response.code()}")
|
revertPluginToggle(plugin.name, originalEnabled, "Failed to toggle plugin: HTTP ${response.code()}")
|
||||||
@@ -82,7 +86,10 @@ class PluginsViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
val response =
|
val response =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
ApiClient.hermesApi.installPlugin(name)
|
ApiClient.hermesApi.installPlugin(
|
||||||
|
com.m57.hermescontrol.data.model
|
||||||
|
.AgentPluginInstallBody(name),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
_uiState.update { it.copy(toastMessage = "Plugin installed successfully") }
|
_uiState.update { it.copy(toastMessage = "Plugin installed successfully") }
|
||||||
|
|||||||
@@ -149,50 +149,37 @@ fun SystemScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doctor reports
|
// Doctor status
|
||||||
state.doctorReport?.let { report ->
|
state.doctorReport?.let { report ->
|
||||||
item {
|
item {
|
||||||
Text(
|
|
||||||
text = "Doctor Diagnostic Reports (Status: ${report.status})",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items(report.reports) { diagnosis ->
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = diagnosis.category.uppercase(),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(text = diagnosis.message, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = diagnosis.status.uppercase(),
|
text = "Doctor Diagnostics",
|
||||||
color =
|
style = MaterialTheme.typography.titleLarge,
|
||||||
if (diagnosis.status.lowercase() == "ok" ||
|
|
||||||
diagnosis.status.lowercase() == "success"
|
|
||||||
) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.error
|
|
||||||
},
|
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Diagnostics Status: ${if (report.ok) "Running" else "Failed"}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = if (report.ok) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
report.pid?.let { pid ->
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Process PID: $pid",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
report.name?.let { name ->
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Process Name: $name",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,21 @@ package com.m57.hermescontrol
|
|||||||
import androidx.navigation3.runtime.NavBackStack
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.model.CreateTaskRequest
|
import com.m57.hermescontrol.data.model.CreateTaskBody
|
||||||
import com.m57.hermescontrol.data.model.CronJob
|
import com.m57.hermescontrol.data.model.CronJob
|
||||||
import com.m57.hermescontrol.data.model.DoctorReport
|
|
||||||
import com.m57.hermescontrol.data.model.DoctorResponse
|
import com.m57.hermescontrol.data.model.DoctorResponse
|
||||||
|
import com.m57.hermescontrol.data.model.EnvVarConfig
|
||||||
import com.m57.hermescontrol.data.model.KanbanBoard
|
import com.m57.hermescontrol.data.model.KanbanBoard
|
||||||
|
import com.m57.hermescontrol.data.model.KanbanBoardResponse
|
||||||
|
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
||||||
|
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.model.LogResponse
|
import com.m57.hermescontrol.data.model.LogResponse
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatform
|
import com.m57.hermescontrol.data.model.MessagingPlatform
|
||||||
import com.m57.hermescontrol.data.model.MoveTaskRequest
|
|
||||||
import com.m57.hermescontrol.data.model.PluginInfo
|
import com.m57.hermescontrol.data.model.PluginInfo
|
||||||
import com.m57.hermescontrol.data.model.Skill
|
import com.m57.hermescontrol.data.model.Skill
|
||||||
import com.m57.hermescontrol.data.model.StatusResponse
|
import com.m57.hermescontrol.data.model.StatusResponse
|
||||||
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
||||||
import com.m57.hermescontrol.data.model.TogglePluginRequest
|
|
||||||
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.HermesApiService
|
import com.m57.hermescontrol.data.remote.HermesApiService
|
||||||
@@ -795,8 +796,7 @@ class E2eIntegrationTest {
|
|||||||
runTest {
|
runTest {
|
||||||
val plugin = PluginInfo("plugin-1", "Desc", "1.0", false, true)
|
val plugin = PluginInfo("plugin-1", "Desc", "1.0", false, true)
|
||||||
coEvery { mockApiService.getPlugins() } returns Response.success(listOf(plugin))
|
coEvery { mockApiService.getPlugins() } returns Response.success(listOf(plugin))
|
||||||
coEvery { mockApiService.togglePlugin("plugin-1", TogglePluginRequest(true)) } returns
|
coEvery { mockApiService.enablePlugin("plugin-1") } returns Response.success(Unit)
|
||||||
Response.success(Unit)
|
|
||||||
|
|
||||||
val viewModel = PluginsViewModel()
|
val viewModel = PluginsViewModel()
|
||||||
viewModel.loadPlugins()
|
viewModel.loadPlugins()
|
||||||
@@ -825,7 +825,7 @@ class E2eIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testChannelsConfiguration_success() =
|
fun testChannelsConfiguration_success() =
|
||||||
runTest {
|
runTest {
|
||||||
val platform = MessagingPlatform("telegram", false, false, null)
|
val platform = MessagingPlatform("telegram", "Telegram", false, false, emptyList())
|
||||||
coEvery { mockApiService.getMessagingPlatforms() } returns Response.success(listOf(platform))
|
coEvery { mockApiService.getMessagingPlatforms() } returns Response.success(listOf(platform))
|
||||||
coEvery { mockApiService.configurePlatform("telegram", any()) } returns Response.success(Unit)
|
coEvery { mockApiService.configurePlatform("telegram", any()) } returns Response.success(Unit)
|
||||||
|
|
||||||
@@ -844,14 +844,19 @@ class E2eIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testKeysManagement_success() =
|
fun testKeysManagement_success() =
|
||||||
runTest {
|
runTest {
|
||||||
coEvery { mockApiService.getEnvVars() } returns Response.success(mapOf("KEY1" to "value1"))
|
coEvery { mockApiService.getEnvVars() } returns
|
||||||
|
Response.success(mapOf("KEY1" to EnvVarConfig(true, "value1", "desc", null, null, false)))
|
||||||
coEvery { mockApiService.updateEnvVar(any()) } returns Response.success(Unit)
|
coEvery { mockApiService.updateEnvVar(any()) } returns Response.success(Unit)
|
||||||
|
|
||||||
val viewModel = KeysViewModel()
|
val viewModel = KeysViewModel()
|
||||||
viewModel.loadKeys(reveal = false)
|
viewModel.loadKeys(reveal = false)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
assertEquals("value1", viewModel.uiState.value.envVars["KEY1"])
|
assertEquals(
|
||||||
|
"value1",
|
||||||
|
viewModel.uiState.value.envVars["KEY1"]
|
||||||
|
?.redactedValue,
|
||||||
|
)
|
||||||
|
|
||||||
viewModel.updateKey("KEY1", "newValue")
|
viewModel.updateKey("KEY1", "newValue")
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
@@ -863,7 +868,7 @@ class E2eIntegrationTest {
|
|||||||
fun testSystemDiagnostics_success() =
|
fun testSystemDiagnostics_success() =
|
||||||
runTest {
|
runTest {
|
||||||
val stats = SystemStatsResponse(cpu = mapOf("percent" to 50.0), memory = mapOf("percent" to 60.0))
|
val stats = SystemStatsResponse(cpu = mapOf("percent" to 50.0), memory = mapOf("percent" to 60.0))
|
||||||
val doctor = DoctorResponse("ok", listOf(DoctorReport("networking", "ok", "all good")))
|
val doctor = DoctorResponse(true, 24856, "doctor")
|
||||||
coEvery { mockApiService.getSystemStats() } returns Response.success(stats)
|
coEvery { mockApiService.getSystemStats() } returns Response.success(stats)
|
||||||
coEvery { mockApiService.runDoctor() } returns Response.success(doctor)
|
coEvery { mockApiService.runDoctor() } returns Response.success(doctor)
|
||||||
coEvery { mockApiService.triggerBackup() } returns Response.success(Unit)
|
coEvery { mockApiService.triggerBackup() } returns Response.success(Unit)
|
||||||
@@ -878,9 +883,9 @@ class E2eIntegrationTest {
|
|||||||
?.cpuPercent,
|
?.cpuPercent,
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"ok",
|
true,
|
||||||
viewModel.uiState.value.doctorReport
|
viewModel.uiState.value.doctorReport
|
||||||
?.status,
|
?.ok,
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.triggerBackup()
|
viewModel.triggerBackup()
|
||||||
@@ -893,10 +898,13 @@ class E2eIntegrationTest {
|
|||||||
fun testKanbanBoardAndTasks_success() =
|
fun testKanbanBoardAndTasks_success() =
|
||||||
runTest {
|
runTest {
|
||||||
val board = KanbanBoard("board-1", "Backlog", null)
|
val board = KanbanBoard("board-1", "Backlog", null)
|
||||||
val task = KanbanTask("task-1", "board-1", "Do laundry", null, "todo", null)
|
val task = KanbanTask("task-1", "Do laundry", null, "todo", null)
|
||||||
coEvery { mockApiService.getKanbanBoards() } returns Response.success(listOf(board))
|
coEvery { mockApiService.getKanbanBoards() } returns
|
||||||
coEvery { mockApiService.getKanbanTasks("board-1") } returns Response.success(listOf(task))
|
Response.success(KanbanBoardsResponse(listOf(board), "board-1"))
|
||||||
coEvery { mockApiService.moveKanbanTask("task-1", MoveTaskRequest("in_progress")) } returns
|
coEvery { mockApiService.switchKanbanBoard("board-1") } returns Response.success(Unit)
|
||||||
|
coEvery { mockApiService.getKanbanBoard() } returns
|
||||||
|
Response.success(KanbanBoardResponse(listOf(KanbanColumn("todo", listOf(task))), null, null))
|
||||||
|
coEvery { mockApiService.updateKanbanTask("task-1", mapOf("status" to "in_progress")) } returns
|
||||||
Response.success(Unit)
|
Response.success(Unit)
|
||||||
|
|
||||||
val viewModel = KanbanViewModel()
|
val viewModel = KanbanViewModel()
|
||||||
|
|||||||
Reference in new issue
Block a user