mission canvas: add / edit / delete toolbar controls

Top-right toolbar grows three CRUD controls per your request:

  - Plus (always visible) — opens MissionWizard, selects the new
    mission on create
  - Pencil (draft-only) — opens EditMissionModal for title +
    description; PATCHes /api/missions/{id}
  - Trash (always visible) — window.confirm then DELETEs; sidebar
    selection clears via new onDeleted callback

Backend:
  - cm-db::repo::missions::update_meta(id, ws, title?, description?)
    — COALESCE-based partial patch
  - cm-db::repo::missions::delete(id, ws) — hard delete, cascades
    via FKs on phases/tasks/artifacts/benchmark_snapshots
  - PATCH /api/missions/{id} (draft-only) + DELETE /api/missions/{id}

Frontend:
  - lib/api/missions — updateMission + deleteMission clients
  - MissionCanvas — three toolbar buttons, EditMissionModal
    (title + textarea for description), local wizard state
  - Dashboard — passes onSelect + onDeleted so sidebar reacts to
    create + delete without stale selection

Edit is draft-only (backend enforces + button hidden past draft) so
in-flight missions can't have their brief mutated out from under
running agents. Delete is unconditional — operator responsibility to
Cancel first if a run is live.
This commit is contained in:
Omar Sobh
2026-07-20 08:32:52 -07:00
parent 4c32799906
commit 1f0117e35a
6 changed files with 408 additions and 2 deletions
+57
View File
@@ -287,6 +287,63 @@ pub async fn set_description(
Ok(Json(mission))
}
#[derive(Debug, Deserialize)]
pub struct UpdateMissionRequest {
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub description: Option<String>,
}
/// PATCH /api/missions/{id} — edit title + description. Draft-only.
pub async fn update_meta(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
Json(body): Json<UpdateMissionRequest>,
) -> Result<Json<Mission>, ApiError> {
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
.await?
.ok_or(ApiError::NotFound)?;
if mission.status != "draft" {
return Err(ApiError::BadRequest);
}
let title = body
.title
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty());
let description = body.description.as_deref();
cm_db::repo::missions::update_meta(
&state.pool,
id,
user.workspace_id.as_uuid(),
title,
description,
)
.await?;
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
.await?
.ok_or(ApiError::NotFound)?;
Ok(Json(mission))
}
/// DELETE /api/missions/{id} — hard-delete. Allowed in any status;
/// the operator is expected to Cancel first if a run is in flight
/// (cascades will still fire either way).
pub async fn delete(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
let deleted =
cm_db::repo::missions::delete(&state.pool, id, user.workspace_id.as_uuid()).await?;
if deleted == 0 {
return Err(ApiError::NotFound);
}
Ok(Json(serde_json::json!({ "deleted": true })))
}
pub async fn set_status(
State(state): State<AppState>,
Authed(user): Authed,