feat: cron job editor dialog (closes #150)
Implements a full-screen cron job editor dialog following the skills editor pattern: - Create/edit/delete cron jobs with all backend fields - CronJobRepeat data class matching backend object shape - Fixed Gson crash on cron tab load
This commit is contained in:
6 files changed
+608
-10
No files matched your search
@@ -1,5 +1,10 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
data class CronJobRepeat(
|
||||
val times: Int? = null,
|
||||
val completed: Int = 0,
|
||||
)
|
||||
|
||||
data class CronJob(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@@ -10,6 +15,20 @@ data class CronJob(
|
||||
val schedule_display: String? = null,
|
||||
val last_status: String? = null,
|
||||
val next_run_at: String? = null,
|
||||
// Full editor fields — all optional with defaults for backward compat
|
||||
val enabled: Boolean? = null,
|
||||
val prompt: String? = null,
|
||||
val deliver: String? = null,
|
||||
val skills: List<String>? = null,
|
||||
val model: String? = null,
|
||||
val provider: String? = null,
|
||||
val base_url: String? = null,
|
||||
val script: String? = null,
|
||||
val context_from: List<String>? = null,
|
||||
val enabled_toolsets: List<String>? = null,
|
||||
val workdir: String? = null,
|
||||
val no_agent: Boolean? = null,
|
||||
val repeat: CronJobRepeat? = null,
|
||||
) {
|
||||
val scheduleText: String
|
||||
get() =
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
data class CreateCronJobRequest(
|
||||
val name: String = "",
|
||||
val schedule: String,
|
||||
val prompt: String = "",
|
||||
val deliver: String = "local",
|
||||
val skills: List<String>? = null,
|
||||
val model: String? = null,
|
||||
val provider: String? = null,
|
||||
val base_url: String? = null,
|
||||
val script: String? = null,
|
||||
val context_from: List<String>? = null,
|
||||
val enabled_toolsets: List<String>? = null,
|
||||
val workdir: String? = null,
|
||||
val no_agent: Boolean = false,
|
||||
val repeat: Int? = null,
|
||||
)
|
||||
|
||||
data class UpdateCronJobRequest(
|
||||
val updates: Map<String, Any?>,
|
||||
)
|
||||
@@ -3,6 +3,7 @@ package com.m57.hermescontrol.data.remote
|
||||
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.CreateCronJobRequest
|
||||
import com.m57.hermescontrol.data.model.CreateTaskBody
|
||||
import com.m57.hermescontrol.data.model.CronJob
|
||||
import com.m57.hermescontrol.data.model.DoctorResponse
|
||||
@@ -38,6 +39,7 @@ import com.m57.hermescontrol.data.model.SystemStatsResponse
|
||||
import com.m57.hermescontrol.data.model.ToggleSkillRequest
|
||||
import com.m57.hermescontrol.data.model.Toolset
|
||||
import com.m57.hermescontrol.data.model.ToolsetToggleRequest
|
||||
import com.m57.hermescontrol.data.model.UpdateCronJobRequest
|
||||
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
|
||||
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
|
||||
import com.m57.hermescontrol.data.model.UpdateRawConfigRequest
|
||||
@@ -103,6 +105,22 @@ interface HermesApiService {
|
||||
@Path("id") id: String,
|
||||
): Response<Unit>
|
||||
|
||||
@GET("api/cron/jobs/{id}")
|
||||
suspend fun getCronJob(
|
||||
@Path("id") id: String,
|
||||
): Response<CronJob>
|
||||
|
||||
@POST("api/cron/jobs")
|
||||
suspend fun createCronJob(
|
||||
@Body body: CreateCronJobRequest,
|
||||
): Response<CronJob>
|
||||
|
||||
@PUT("api/cron/jobs/{id}")
|
||||
suspend fun updateCronJob(
|
||||
@Path("id") id: String,
|
||||
@Body body: UpdateCronJobRequest,
|
||||
): Response<CronJob>
|
||||
|
||||
@POST("api/cron/jobs/{id}/trigger")
|
||||
suspend fun triggerCronJob(
|
||||
@Path("id") id: String,
|
||||
|
||||
@@ -4,29 +4,48 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.R
|
||||
@@ -63,6 +82,14 @@ fun CronJobsScreen(
|
||||
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
|
||||
isRefreshing = state.isLoading,
|
||||
onRefresh = { viewModel.loadCronJobs() },
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.openNewJobDialog() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = stringResource(R.string.cron_action_add),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
when {
|
||||
state.isLoading && state.jobs.isEmpty() -> {
|
||||
@@ -102,7 +129,7 @@ fun CronJobsScreen(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = job.name,
|
||||
@@ -113,9 +140,7 @@ fun CronJobsScreen(
|
||||
StatusBadge(
|
||||
text =
|
||||
if (job.state == "active") {
|
||||
stringResource(
|
||||
R.string.cron_status_active,
|
||||
)
|
||||
stringResource(R.string.cron_status_active)
|
||||
} else {
|
||||
stringResource(R.string.cron_status_paused)
|
||||
},
|
||||
@@ -144,6 +169,15 @@ fun CronJobsScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { viewModel.openEditJobDialog(job.id) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Edit,
|
||||
contentDescription = stringResource(R.string.cron_action_edit),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (job.state == "active") {
|
||||
IconButton(
|
||||
onClick = { viewModel.pauseCronJob(job.id) },
|
||||
@@ -191,4 +225,261 @@ fun CronJobsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Editor Dialog ──
|
||||
if (state.editorState.isOpen) {
|
||||
CronJobEditorDialog(
|
||||
state = state.editorState,
|
||||
onFieldChange = { name, value -> viewModel.updateEditorField(name, value) },
|
||||
onToggleNoAgent = { viewModel.toggleNoAgent() },
|
||||
onSave = { viewModel.saveEditor() },
|
||||
onDismiss = { viewModel.closeEditor() },
|
||||
onClearToast = { viewModel.clearEditorToast() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CronJobEditorDialog(
|
||||
state: CronJobEditorState,
|
||||
onFieldChange: (String, String) -> Unit,
|
||||
onToggleNoAgent: () -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onClearToast: () -> Unit,
|
||||
) {
|
||||
var showDiscardConfirm by remember { mutableStateOf(false) }
|
||||
val hasChanges =
|
||||
state.name.isNotEmpty() || state.schedule.isNotEmpty() ||
|
||||
state.prompt.isNotEmpty() || state.skills.isNotEmpty()
|
||||
|
||||
ToastEffect(toastMessage = state.toastMessage, onClearToast = onClearToast)
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
if (hasChanges && !state.isNew) {
|
||||
showDiscardConfirm = true
|
||||
} else {
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
) {
|
||||
HermesScaffold(
|
||||
title = {
|
||||
Text(
|
||||
if (state.isNew) {
|
||||
stringResource(R.string.cron_edit_title_new)
|
||||
} else {
|
||||
stringResource(R.string.cron_edit_title)
|
||||
},
|
||||
)
|
||||
},
|
||||
navigationIcon =
|
||||
NavIcon.Back(
|
||||
onBack = {
|
||||
if (hasChanges && !state.isNew) {
|
||||
showDiscardConfirm = true
|
||||
} else {
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
),
|
||||
actions = {
|
||||
if (!state.isLoading) {
|
||||
IconButton(
|
||||
onClick = onSave,
|
||||
enabled = !state.isSaving,
|
||||
) {
|
||||
if (state.isSaving) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.fillMaxSize(0.6f),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.cron_edit_action_save),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
if (state.isLoading) {
|
||||
LoadingState()
|
||||
} else {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
// Name
|
||||
OutlinedTextField(
|
||||
value = state.name,
|
||||
onValueChange = { onFieldChange("name", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_name)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Schedule (required)
|
||||
OutlinedTextField(
|
||||
value = state.schedule,
|
||||
onValueChange = { onFieldChange("schedule", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_schedule)) },
|
||||
placeholder = { Text(stringResource(R.string.cron_edit_hint_schedule)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Prompt
|
||||
OutlinedTextField(
|
||||
value = state.prompt,
|
||||
onValueChange = { onFieldChange("prompt", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_prompt)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
textStyle =
|
||||
MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
),
|
||||
)
|
||||
|
||||
// Delivery
|
||||
OutlinedTextField(
|
||||
value = state.deliver,
|
||||
onValueChange = { onFieldChange("deliver", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_deliver)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
supportingText = {
|
||||
Text(
|
||||
stringResource(R.string.cron_edit_hint_deliver),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// Skills
|
||||
OutlinedTextField(
|
||||
value = state.skills,
|
||||
onValueChange = { onFieldChange("skills", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_skills)) },
|
||||
placeholder = { Text(stringResource(R.string.cron_edit_hint_skills)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
textStyle = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
|
||||
// Model
|
||||
OutlinedTextField(
|
||||
value = state.model,
|
||||
onValueChange = { onFieldChange("model", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_model)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Provider
|
||||
OutlinedTextField(
|
||||
value = state.provider,
|
||||
onValueChange = { onFieldChange("provider", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_provider)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Base URL
|
||||
OutlinedTextField(
|
||||
value = state.base_url,
|
||||
onValueChange = { onFieldChange("base_url", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_base_url)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Script path
|
||||
OutlinedTextField(
|
||||
value = state.script,
|
||||
onValueChange = { onFieldChange("script", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_script)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Work directory
|
||||
OutlinedTextField(
|
||||
value = state.workdir,
|
||||
onValueChange = { onFieldChange("workdir", it) },
|
||||
label = { Text(stringResource(R.string.cron_edit_field_workdir)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// No Agent toggle
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
checked = state.no_agent,
|
||||
onCheckedChange = { onToggleNoAgent() },
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.cron_edit_field_no_agent),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDiscardConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDiscardConfirm = false },
|
||||
title = { Text(stringResource(R.string.cron_discard_title)) },
|
||||
text = { Text(stringResource(R.string.cron_discard_message)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showDiscardConfirm = false
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.action_discard))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDiscardConfirm = false }) {
|
||||
Text(stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package com.m57.hermescontrol.ui.cron
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.model.CreateCronJobRequest
|
||||
import com.m57.hermescontrol.data.model.CronJob
|
||||
import com.m57.hermescontrol.data.model.UpdateCronJobRequest
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
@@ -22,6 +24,30 @@ data class CronJobsUiState(
|
||||
val jobs: List<CronJob> = emptyList(),
|
||||
val errorMessage: String? = null,
|
||||
val toastMessage: String? = null,
|
||||
// Editor state
|
||||
val editorState: CronJobEditorState = CronJobEditorState(),
|
||||
)
|
||||
|
||||
data class CronJobEditorState(
|
||||
val isOpen: Boolean = false,
|
||||
val isNew: Boolean = false,
|
||||
val jobId: String? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val isSaving: Boolean = false,
|
||||
val toastMessage: String? = null,
|
||||
// Form fields
|
||||
val name: String = "",
|
||||
val schedule: String = "",
|
||||
val prompt: String = "",
|
||||
val deliver: String = "local",
|
||||
val skills: String = "",
|
||||
val model: String = "",
|
||||
val provider: String = "",
|
||||
val base_url: String = "",
|
||||
val script: String = "",
|
||||
val workdir: String = "",
|
||||
val enabled: Boolean = true,
|
||||
val no_agent: Boolean = false,
|
||||
)
|
||||
|
||||
class CronJobsViewModel :
|
||||
@@ -54,7 +80,6 @@ class CronJobsViewModel :
|
||||
|
||||
fun pauseCronJob(id: String) {
|
||||
val originalJobs = _uiState.value.jobs
|
||||
// Optimistic update
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
jobs =
|
||||
@@ -76,7 +101,6 @@ class CronJobsViewModel :
|
||||
|
||||
fun resumeCronJob(id: String) {
|
||||
val originalJobs = _uiState.value.jobs
|
||||
// Optimistic update
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
jobs =
|
||||
@@ -106,7 +130,6 @@ class CronJobsViewModel :
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update { it.copy(toastMessage = "Job triggered successfully") }
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update { it.copy(toastMessage = "Failed to trigger cron job: ${result.error.message}") }
|
||||
}
|
||||
@@ -116,7 +139,6 @@ class CronJobsViewModel :
|
||||
|
||||
fun deleteCronJob(id: String) {
|
||||
val originalJobs = _uiState.value.jobs
|
||||
// Optimistic update
|
||||
_uiState.update { state ->
|
||||
state.copy(jobs = state.jobs.filter { it.id != id })
|
||||
}
|
||||
@@ -131,6 +153,176 @@ class CronJobsViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
// ── Editor ──
|
||||
|
||||
fun openNewJobDialog() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
editorState =
|
||||
CronJobEditorState(
|
||||
isOpen = true,
|
||||
isNew = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openEditJobDialog(id: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
editorState =
|
||||
CronJobEditorState(
|
||||
isOpen = true,
|
||||
isNew = false,
|
||||
jobId = id,
|
||||
isLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getCronJob(id) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
val job = result.data ?: return@launch
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
editorState =
|
||||
CronJobEditorState(
|
||||
isOpen = true,
|
||||
isNew = false,
|
||||
jobId = id,
|
||||
name = job.name,
|
||||
schedule = extractScheduleString(job),
|
||||
prompt = job.prompt.orEmpty(),
|
||||
deliver = job.deliver ?: "local",
|
||||
skills = (job.skills ?: emptyList()).joinToString("\n"),
|
||||
model = job.model.orEmpty(),
|
||||
provider = job.provider.orEmpty(),
|
||||
base_url = job.base_url.orEmpty(),
|
||||
script = job.script.orEmpty(),
|
||||
workdir = job.workdir.orEmpty(),
|
||||
enabled = job.enabled ?: true,
|
||||
no_agent = job.no_agent ?: false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
editorState =
|
||||
CronJobEditorState(
|
||||
isOpen = true,
|
||||
isNew = false,
|
||||
jobId = id,
|
||||
toastMessage = "Failed to load job: ${result.error.message}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeEditor() {
|
||||
_uiState.update { it.copy(editorState = CronJobEditorState()) }
|
||||
}
|
||||
|
||||
fun updateEditorField(
|
||||
name: String,
|
||||
value: String,
|
||||
) {
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
editorState =
|
||||
state.editorState.applyFieldChange(name, value).copy(
|
||||
toastMessage = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveEditor() {
|
||||
val editor = _uiState.value.editorState
|
||||
if (editor.schedule.isBlank()) {
|
||||
_uiState.update { it.copy(editorState = editor.copy(toastMessage = "Schedule is required")) }
|
||||
return
|
||||
}
|
||||
_uiState.update { it.copy(editorState = editor.copy(isSaving = true)) }
|
||||
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (editor.isNew) {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.createCronJob(
|
||||
CreateCronJobRequest(
|
||||
name = editor.name,
|
||||
schedule = editor.schedule,
|
||||
prompt = editor.prompt,
|
||||
deliver = editor.deliver,
|
||||
skills = parseLines(editor.skills),
|
||||
model = editor.model.ifBlank { null },
|
||||
provider = editor.provider.ifBlank { null },
|
||||
base_url = editor.base_url.ifBlank { null },
|
||||
script = editor.script.ifBlank { null },
|
||||
workdir = editor.workdir.ifBlank { null },
|
||||
no_agent = editor.no_agent,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val updates = mutableMapOf<String, Any?>()
|
||||
if (editor.name.isNotEmpty()) updates["name"] = editor.name
|
||||
updates["schedule"] = editor.schedule
|
||||
updates["prompt"] = editor.prompt
|
||||
updates["deliver"] = editor.deliver
|
||||
updates["skills"] = parseLines(editor.skills)
|
||||
updates["model"] = editor.model.ifBlank { null }
|
||||
updates["provider"] = editor.provider.ifBlank { null }
|
||||
updates["base_url"] = editor.base_url.ifBlank { null }
|
||||
updates["script"] = editor.script.ifBlank { null }
|
||||
updates["workdir"] = editor.workdir.ifBlank { null }
|
||||
updates["no_agent"] = editor.no_agent
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.updateCronJob(
|
||||
editor.jobId ?: "",
|
||||
UpdateCronJobRequest(updates = updates),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
closeEditor()
|
||||
loadCronJobs()
|
||||
}
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
editorState =
|
||||
it.editorState.copy(
|
||||
isSaving = false,
|
||||
toastMessage = "Failed to save: ${result.error.message}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearEditorToast() {
|
||||
_uiState.update { it.copy(editorState = it.editorState.copy(toastMessage = null)) }
|
||||
}
|
||||
|
||||
override fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
|
||||
private fun revertJobs(
|
||||
originalJobs: List<CronJob>,
|
||||
errorMsg: String,
|
||||
@@ -138,7 +330,41 @@ class CronJobsViewModel :
|
||||
_uiState.update { it.copy(jobs = originalJobs, toastMessage = errorMsg) }
|
||||
}
|
||||
|
||||
override fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
private fun extractScheduleString(job: CronJob): String {
|
||||
val s = job.schedule
|
||||
return when (s) {
|
||||
is String -> s
|
||||
is Map<*, *> -> (s["value"] as? String) ?: job.scheduleText
|
||||
else -> job.scheduleText
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLines(skills: String): List<String> =
|
||||
skills.lines()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
private fun CronJobEditorState.applyFieldChange(
|
||||
name: String,
|
||||
value: String,
|
||||
): CronJobEditorState =
|
||||
when (name) {
|
||||
"name" -> copy(name = value)
|
||||
"schedule" -> copy(schedule = value)
|
||||
"prompt" -> copy(prompt = value)
|
||||
"deliver" -> copy(deliver = value)
|
||||
"skills" -> copy(skills = value)
|
||||
"model" -> copy(model = value)
|
||||
"provider" -> copy(provider = value)
|
||||
"base_url" -> copy(base_url = value)
|
||||
"script" -> copy(script = value)
|
||||
"workdir" -> copy(workdir = value)
|
||||
else -> this
|
||||
}
|
||||
|
||||
fun toggleNoAgent() {
|
||||
_uiState.update {
|
||||
it.copy(editorState = it.editorState.copy(no_agent = !it.editorState.no_agent))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user