feat(#1220): complete Kanban management operations parity (#1234)

This commit is contained in:
m57 authored and GitHub committed 2026-09-23 12:25:37 +03:00
1 parent 33d87a11ec
commit 1270a08810
23 files changed
+1777 -51

No files matched your search

@@ -22,8 +22,14 @@ data class KanbanDetailEvent(
@Serializable
data class KanbanRun(
val id: Long,
val taskId: String? = null,
val profile: String? = null,
val stepKey: String? = null,
val status: String,
val claimLock: String? = null,
val claimExpires: Long? = null,
val maxRuntimeSeconds: Int? = null,
val lastHeartbeatAt: Long? = null,
val outcome: String? = null,
val summary: String? = null,
val error: String? = null,
@@ -59,3 +65,71 @@ data class KanbanAttachment(
data class AttachmentUploadResponse(
val attachment: KanbanAttachment? = null,
)
@Serializable
data class AttachmentListResponse(
val attachments: List<KanbanAttachment> = emptyList(),
)
@Serializable
data class DeleteAttachmentResponse(
val ok: Boolean,
val id: Long,
)
@Serializable
data class KanbanRunResponse(
val run: KanbanRun,
)
@Serializable
data class KanbanRunInspection(
val runId: Long,
val alive: Boolean,
val pid: Int? = null,
val reason: String? = null,
val cpuPercent: Double? = null,
val memoryRssBytes: Long? = null,
val memoryVmsBytes: Long? = null,
val numThreads: Int? = null,
val numFds: Int? = null,
val status: String? = null,
val createTime: Double? = null,
val cmdline: List<String>? = null,
val error: String? = null,
)
@Serializable
data class TerminateRunBody(
val reason: String? = null,
)
@Serializable
data class TerminateRunResponse(
val ok: Boolean,
val runId: Long,
val taskId: String,
)
@Serializable
data class KanbanActiveWorker(
val runId: Long,
val taskId: String,
val taskTitle: String,
val taskStatus: String,
val taskAssignee: String? = null,
val profile: String? = null,
val workerPid: Int,
val startedAt: Long? = null,
val claimLock: String? = null,
val claimExpires: Long? = null,
val lastHeartbeatAt: Long? = null,
val maxRuntimeSeconds: Int? = null,
)
@Serializable
data class ActiveWorkersResponse(
val workers: List<KanbanActiveWorker> = emptyList(),
val count: Int = workers.size,
val checkedAt: Long,
)
@@ -48,6 +48,18 @@ data class TaskLinks(
val children: List<String> = emptyList(),
)
@Serializable
data class TaskLinkBody(
val parentId: String,
val childId: String,
)
@Serializable
data class TaskLinkResponse(
val ok: Boolean,
val gated: Boolean = false,
)
@Serializable
data class ChildTaskResult(
val id: String,
@@ -75,6 +87,9 @@ data class KanbanTask(
val startedAt: Long? = null,
val workerPid: Int? = null,
val lastHeartbeatAt: Long? = null,
val currentRunId: Long? = null,
val workflowTemplateId: String? = null,
val currentStepKey: String? = null,
) {
val description: String? get() = body
val assignedTo: String? get() = assignee
@@ -112,6 +127,10 @@ data class KanbanTaskFull(
val startedAt: Long? = null,
val workerPid: Int? = null,
val lastHeartbeatAt: Long? = null,
val currentRunId: Long? = null,
val workflowTemplateId: String? = null,
val currentStepKey: String? = null,
val maxRuntimeSeconds: Int? = null,
val result: String? = null,
val createdBy: String? = null,
val modelOverride: String? = null,
@@ -245,6 +264,19 @@ data class ReclaimTaskResponse(
val taskId: String,
)
@Serializable
data class SpecifyTaskBody(
val author: String? = null,
)
@Serializable
data class SpecifyTaskResponse(
val ok: Boolean,
val taskId: String,
val reason: String? = null,
val newTitle: String? = null,
)
@Serializable
data class TaskEstimate(
val ok: Boolean,
@@ -1,5 +1,7 @@
package com.m57.hermescontrol.data.remote
import com.m57.hermescontrol.data.model.ActiveWorkersResponse
import com.m57.hermescontrol.data.model.AttachmentListResponse
import com.m57.hermescontrol.data.model.AttachmentUploadResponse
import com.m57.hermescontrol.data.model.AutoDescribeResponse
import com.m57.hermescontrol.data.model.BoardExportResult
@@ -10,6 +12,7 @@ import com.m57.hermescontrol.data.model.CreateBoardBody
import com.m57.hermescontrol.data.model.CreateBoardResponse
import com.m57.hermescontrol.data.model.CreateTaskBody
import com.m57.hermescontrol.data.model.CreateTaskResponse
import com.m57.hermescontrol.data.model.DeleteAttachmentResponse
import com.m57.hermescontrol.data.model.DeleteBoardResponse
import com.m57.hermescontrol.data.model.DispatchResult
import com.m57.hermescontrol.data.model.ExportBoardBody
@@ -18,6 +21,8 @@ import com.m57.hermescontrol.data.model.KanbanBoardResponse
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
import com.m57.hermescontrol.data.model.KanbanProfilesResponse
import com.m57.hermescontrol.data.model.KanbanProjectsResponse
import com.m57.hermescontrol.data.model.KanbanRunInspection
import com.m57.hermescontrol.data.model.KanbanRunResponse
import com.m57.hermescontrol.data.model.KanbanTaskDetailResponse
import com.m57.hermescontrol.data.model.OrchestrationSettings
import com.m57.hermescontrol.data.model.OrchestrationSettingsUpdate
@@ -27,7 +32,13 @@ import com.m57.hermescontrol.data.model.ReclaimTaskBody
import com.m57.hermescontrol.data.model.ReclaimTaskResponse
import com.m57.hermescontrol.data.model.RenameBoardBody
import com.m57.hermescontrol.data.model.RenameBoardResponse
import com.m57.hermescontrol.data.model.SpecifyTaskBody
import com.m57.hermescontrol.data.model.SpecifyTaskResponse
import com.m57.hermescontrol.data.model.TaskEstimate
import com.m57.hermescontrol.data.model.TaskLinkBody
import com.m57.hermescontrol.data.model.TaskLinkResponse
import com.m57.hermescontrol.data.model.TerminateRunBody
import com.m57.hermescontrol.data.model.TerminateRunResponse
import com.m57.hermescontrol.data.model.UpdateTaskBody
import com.m57.hermescontrol.data.model.UpdateTaskResponse
import com.m57.hermescontrol.data.model.WorkerLog
@@ -57,6 +68,8 @@ interface KanbanApiService {
@Query("board") board: String? = null,
@Query("include_archived") includeArchived: Boolean = false,
@Query("tenant") tenant: String? = null,
@Query("workflow_template_id") workflowTemplateId: String? = null,
@Query("current_step_key") currentStepKey: String? = null,
): Response<KanbanBoardResponse>
@POST("api/plugins/kanban/boards")
@@ -96,6 +109,8 @@ interface KanbanApiService {
suspend fun getTask(
@Path("id") taskId: String,
@Query("board") board: String? = null,
@Query("run_state_type") runStateType: String? = null,
@Query("run_state_name") runStateName: String? = null,
): Response<KanbanTaskDetailResponse>
@POST("api/plugins/kanban/tasks")
@@ -170,6 +185,12 @@ interface KanbanApiService {
@Part file: MultipartBody.Part,
): Response<AttachmentUploadResponse>
@GET("api/plugins/kanban/tasks/{id}/attachments")
suspend fun listAttachments(
@Path("id") taskId: String,
@Query("board") board: String? = null,
): Response<AttachmentListResponse>
@Streaming
@GET("api/plugins/kanban/attachments/{id}")
suspend fun downloadAttachment(
@@ -177,6 +198,49 @@ interface KanbanApiService {
@Query("board") board: String? = null,
): Response<ResponseBody>
@DELETE("api/plugins/kanban/attachments/{id}")
suspend fun deleteAttachment(
@Path("id") attachmentId: Long,
@Query("board") board: String? = null,
): Response<DeleteAttachmentResponse>
@GET("api/plugins/kanban/workers/active")
suspend fun getActiveWorkers(
@Query("board") board: String? = null,
): Response<ActiveWorkersResponse>
@GET("api/plugins/kanban/runs/{id}")
suspend fun getRun(
@Path("id") runId: Long,
@Query("board") board: String? = null,
): Response<KanbanRunResponse>
@GET("api/plugins/kanban/runs/{id}/inspect")
suspend fun inspectRun(
@Path("id") runId: Long,
@Query("board") board: String? = null,
): Response<KanbanRunInspection>
@POST("api/plugins/kanban/runs/{id}/terminate")
suspend fun terminateRun(
@Path("id") runId: Long,
@Query("board") board: String? = null,
@Body body: TerminateRunBody = TerminateRunBody(),
): Response<TerminateRunResponse>
@POST("api/plugins/kanban/tasks/{id}/specify")
suspend fun specifyTask(
@Path("id") taskId: String,
@Query("board") board: String? = null,
@Body body: SpecifyTaskBody = SpecifyTaskBody(),
): Response<SpecifyTaskResponse>
@POST("api/plugins/kanban/links")
suspend fun createTaskLink(
@Query("board") board: String? = null,
@Body body: TaskLinkBody,
): Response<TaskLinkResponse>
@GET("api/plugins/kanban/profiles")
suspend fun getKanbanProfiles(): Response<KanbanProfilesResponse>
@@ -1,5 +1,7 @@
package com.m57.hermescontrol.data.repository
import com.m57.hermescontrol.data.model.ActiveWorkersResponse
import com.m57.hermescontrol.data.model.AttachmentListResponse
import com.m57.hermescontrol.data.model.AttachmentUploadResponse
import com.m57.hermescontrol.data.model.AutoDescribeResponse
import com.m57.hermescontrol.data.model.BoardExportResult
@@ -10,6 +12,7 @@ import com.m57.hermescontrol.data.model.CreateBoardBody
import com.m57.hermescontrol.data.model.CreateBoardResponse
import com.m57.hermescontrol.data.model.CreateTaskBody
import com.m57.hermescontrol.data.model.CreateTaskResponse
import com.m57.hermescontrol.data.model.DeleteAttachmentResponse
import com.m57.hermescontrol.data.model.DeleteBoardResponse
import com.m57.hermescontrol.data.model.DispatchResult
import com.m57.hermescontrol.data.model.ExportBoardBody
@@ -18,6 +21,8 @@ import com.m57.hermescontrol.data.model.KanbanBoardResponse
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
import com.m57.hermescontrol.data.model.KanbanProfilesResponse
import com.m57.hermescontrol.data.model.KanbanProjectsResponse
import com.m57.hermescontrol.data.model.KanbanRunInspection
import com.m57.hermescontrol.data.model.KanbanRunResponse
import com.m57.hermescontrol.data.model.KanbanTaskDetailResponse
import com.m57.hermescontrol.data.model.OrchestrationSettings
import com.m57.hermescontrol.data.model.OrchestrationSettingsUpdate
@@ -27,7 +32,13 @@ import com.m57.hermescontrol.data.model.ReclaimTaskBody
import com.m57.hermescontrol.data.model.ReclaimTaskResponse
import com.m57.hermescontrol.data.model.RenameBoardBody
import com.m57.hermescontrol.data.model.RenameBoardResponse
import com.m57.hermescontrol.data.model.SpecifyTaskBody
import com.m57.hermescontrol.data.model.SpecifyTaskResponse
import com.m57.hermescontrol.data.model.TaskEstimate
import com.m57.hermescontrol.data.model.TaskLinkBody
import com.m57.hermescontrol.data.model.TaskLinkResponse
import com.m57.hermescontrol.data.model.TerminateRunBody
import com.m57.hermescontrol.data.model.TerminateRunResponse
import com.m57.hermescontrol.data.model.UpdateTaskBody
import com.m57.hermescontrol.data.model.UpdateTaskResponse
import com.m57.hermescontrol.data.model.WorkerLog
@@ -48,6 +59,8 @@ interface KanbanRepository {
board: String,
includeArchived: Boolean = false,
tenant: String? = null,
workflowTemplateId: String? = null,
currentStepKey: String? = null,
): NetworkResult<KanbanBoardResponse>
suspend fun createBoard(body: CreateBoardBody): NetworkResult<CreateBoardResponse>
@@ -72,6 +85,8 @@ interface KanbanRepository {
suspend fun getTask(
taskId: String,
board: String?,
runStateType: String? = null,
runStateName: String? = null,
): NetworkResult<KanbanTaskDetailResponse>
suspend fun createTask(
@@ -136,11 +151,49 @@ interface KanbanRepository {
file: MultipartBody.Part,
): NetworkResult<AttachmentUploadResponse>
suspend fun listAttachments(
taskId: String,
board: String,
): NetworkResult<AttachmentListResponse>
suspend fun deleteAttachment(
attachmentId: Long,
board: String,
): NetworkResult<DeleteAttachmentResponse>
suspend fun downloadAttachment(
attachmentId: Long,
board: String?,
): NetworkResult<ResponseBody>
suspend fun getActiveWorkers(board: String): NetworkResult<ActiveWorkersResponse>
suspend fun getRun(
runId: Long,
board: String,
): NetworkResult<KanbanRunResponse>
suspend fun inspectRun(
runId: Long,
board: String,
): NetworkResult<KanbanRunInspection>
suspend fun terminateRun(
runId: Long,
board: String,
): NetworkResult<TerminateRunResponse>
suspend fun specifyTask(
taskId: String,
board: String,
): NetworkResult<SpecifyTaskResponse>
suspend fun createTaskLink(
board: String,
parentId: String,
childId: String,
): NetworkResult<TaskLinkResponse>
suspend fun getProfiles(): NetworkResult<KanbanProfilesResponse>
suspend fun updateProfileDescription(
@@ -173,9 +226,11 @@ class KanbanRepositoryImpl(
board: String,
includeArchived: Boolean,
tenant: String?,
workflowTemplateId: String?,
currentStepKey: String?,
): NetworkResult<KanbanBoardResponse> =
withContext(ioDispatcher) {
safeApiCall { apiProvider().getBoard(board = board, includeArchived = includeArchived, tenant = tenant) }
safeApiCall { apiProvider().getBoard(board, includeArchived, tenant, workflowTemplateId, currentStepKey) }
}
override suspend fun createBoard(body: CreateBoardBody): NetworkResult<CreateBoardResponse> =
@@ -215,9 +270,11 @@ class KanbanRepositoryImpl(
override suspend fun getTask(
taskId: String,
board: String?,
runStateType: String?,
runStateName: String?,
): NetworkResult<KanbanTaskDetailResponse> =
withContext(ioDispatcher) {
safeApiCall { apiProvider().getTask(taskId = taskId, board = board) }
safeApiCall { apiProvider().getTask(taskId, board, runStateType, runStateName) }
}
override suspend fun createTask(
@@ -364,6 +421,18 @@ class KanbanRepositoryImpl(
safeApiCall { apiProvider().uploadAttachment(taskId = taskId, board = board, file = file) }
}
override suspend fun listAttachments(
taskId: String,
board: String,
): NetworkResult<AttachmentListResponse> =
withContext(ioDispatcher) { safeApiCall { apiProvider().listAttachments(taskId, board) } }
override suspend fun deleteAttachment(
attachmentId: Long,
board: String,
): NetworkResult<DeleteAttachmentResponse> =
withContext(ioDispatcher) { safeApiCall { apiProvider().deleteAttachment(attachmentId, board) } }
override suspend fun downloadAttachment(
attachmentId: Long,
board: String?,
@@ -372,6 +441,48 @@ class KanbanRepositoryImpl(
safeApiCall { apiProvider().downloadAttachment(attachmentId = attachmentId, board = board) }
}
override suspend fun getActiveWorkers(board: String): NetworkResult<ActiveWorkersResponse> =
withContext(ioDispatcher) { safeApiCall { apiProvider().getActiveWorkers(board) } }
override suspend fun getRun(
runId: Long,
board: String,
): NetworkResult<KanbanRunResponse> =
withContext(ioDispatcher) { safeApiCall { apiProvider().getRun(runId, board) } }
override suspend fun inspectRun(
runId: Long,
board: String,
): NetworkResult<KanbanRunInspection> =
withContext(ioDispatcher) { safeApiCall { apiProvider().inspectRun(runId, board) } }
override suspend fun terminateRun(
runId: Long,
board: String,
): NetworkResult<TerminateRunResponse> =
withContext(ioDispatcher) {
val result = safeApiCall { apiProvider().terminateRun(runId, board, TerminateRunBody(reason = "mobile")) }
if (result is NetworkResult.Success && result.data.ok) nudger.scheduleNudge(board)
result
}
override suspend fun specifyTask(
taskId: String,
board: String,
): NetworkResult<SpecifyTaskResponse> =
withContext(ioDispatcher) {
safeApiCall { apiProvider().specifyTask(taskId, board, SpecifyTaskBody(author = "mobile")) }
}
override suspend fun createTaskLink(
board: String,
parentId: String,
childId: String,
): NetworkResult<TaskLinkResponse> =
withContext(ioDispatcher) {
safeApiCall { apiProvider().createTaskLink(board, TaskLinkBody(parentId, childId)) }
}
override suspend fun getProfiles(): NetworkResult<KanbanProfilesResponse> =
withContext(ioDispatcher) {
safeApiCall { apiProvider().getKanbanProfiles() }