feat(#448): schema-driven config UI with category tabs, search, typed inputs

Closes #448

Replaced the raw YAML editor with a full schema-driven config management UI
matching the dashboard's ConfigPage experience.

### New API endpoints (HermesApiService)
- GET /api/config — load structured config
- GET /api/config/schema — load schema with field types + categories  
- GET /api/config/defaults — load defaults for reset
- PUT /api/config — save config (deep-merged on backend)

### Data models
- ConfigSchemaResponse — wraps fields + category_order
- SchemaField — type, description, category, options
- ConfigUpdateRequest — config map + optional profile

### ConfigViewModel
- Loads config + schema + defaults + path on init
- Tracks pending changes as dot-path → JsonElement map
- Builds nested JSON changeset for the save API
- YAML mode toggle with raw YAML load/save
- Per-category reset to defaults

### ConfigScreen
- Category tabs with field-count badges
- Search filtering across key, label, description, category
- Typed inputs: boolean toggles, select dropdowns, number inputs
- Modified highlighting with primaryContainer tint
- YAML ↔ Form mode toggle
- Sticky save indicator with pending-change count
This commit is contained in:
M57 authored and GitHub committed 2026-07-03 08:39:35 +03:00
1 parent d875c8a040
commit efb8e528ef
5 files changed
+962 -89

No files matched your search

@@ -1,5 +1,7 @@
package com.m57.hermescontrol.data.model
import com.google.gson.JsonElement
data class RawConfigResponse(
val path: String?,
val yaml: String?,
@@ -9,3 +11,20 @@ data class UpdateRawConfigRequest(
val yaml_text: String,
val profile: String? = null,
)
data class ConfigSchemaResponse(
val fields: Map<String, SchemaField>,
val category_order: List<String>,
)
data class SchemaField(
val type: String,
val description: String? = null,
val category: String? = null,
val options: List<String>? = null,
)
data class ConfigUpdateRequest(
val config: Map<String, JsonElement>,
val profile: String? = null,
)
@@ -1,8 +1,11 @@
package com.m57.hermescontrol.data.remote
import com.google.gson.JsonElement
import com.m57.hermescontrol.data.model.AchievementsResponse
import com.m57.hermescontrol.data.model.ActiveProfileResponse
import com.m57.hermescontrol.data.model.AgentPluginInstallBody
import com.m57.hermescontrol.data.model.ConfigSchemaResponse
import com.m57.hermescontrol.data.model.ConfigUpdateRequest
import com.m57.hermescontrol.data.model.CreateCronJobRequest
import com.m57.hermescontrol.data.model.CreateTaskBody
import com.m57.hermescontrol.data.model.CreateWebhookRequest
@@ -222,6 +225,20 @@ interface HermesApiService {
@Body body: UpdateRawConfigRequest,
): Response<Unit>
@GET("api/config")
suspend fun getConfig(): Response<Map<String, JsonElement>>
@GET("api/config/schema")
suspend fun getConfigSchema(): Response<ConfigSchemaResponse>
@GET("api/config/defaults")
suspend fun getConfigDefaults(): Response<Map<String, JsonElement>>
@PUT("api/config")
suspend fun updateConfig(
@Body body: ConfigUpdateRequest,
): Response<Unit>
@GET("api/mcp/servers")
suspend fun getMcpServers(): Response<McpServersResponse>