From a31cd8bb15cd75c7acbd4c1459fdbe63ccca9321 Mon Sep 17 00:00:00 2001 From: damocles Date: Tue, 26 May 2026 01:15:45 +0200 Subject: [PATCH] dashboard: operator-direct schedule submit + cancel endpoints (#444 step 3) --- hive-c0re/src/dashboard.rs | 80 +++++++++++++++++++++++++++++++++ hive-c0re/src/manager_server.rs | 7 +++ 2 files changed, 87 insertions(+) diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index b51f8750..f4e322a6 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -74,6 +74,8 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { .route("/request-spawn", post(post_request_spawn)) .route("/op-send", post(post_op_send)) .route("/meta-update", post(post_meta_update)) + .route("/api/schedules", get(api_schedules).post(post_schedule_new)) + .route("/api/schedules/{id}/cancel", post(post_schedule_cancel)) .route("/dashboard/stream", get(dashboard_stream)) .route("/dashboard/history", get(dashboard_history)) // Anything not matched by the dynamic routes above falls @@ -1389,6 +1391,84 @@ async fn api_reminders(State(state): State) -> Response { } } +/// `GET /api/schedules` — snapshot of every schedule for the +/// scheduled-prompts tab (#444). Returns the wire shape directly +/// so the frontend can render without an extra translation layer. +async fn api_schedules(State(state): State) -> Response { + match state.coord.scheduled_prompts.list() { + Ok(rows) => axum::Json( + rows.into_iter() + .map(crate::manager_server::schedule_to_wire_public) + .collect::>(), + ) + .into_response(), + Err(e) => error_response(&format!("scheduled_prompts list: {e:#}")), + } +} + +/// `POST /api/schedules` — operator-direct schedule creation +/// (mara: "user can add them manually"). Accepts the same +/// `SchedulePromptPayload` shape as the manager request flow but +/// skips the approval gate — the operator click *is* the +/// approval. The schedule lands directly with +/// `source = Operator` and the worker picks it up at fire time. +async fn post_schedule_new( + State(state): State, + axum::Json(payload): axum::Json, +) -> Response { + if payload.targets.is_empty() { + return error_response("schedule must have at least one target"); + } + if payload.body.trim().is_empty() { + return error_response("schedule body must be non-empty"); + } + if let Some(0) = payload.interval_seconds { + return error_response("interval_seconds must be > 0 (use None for one-shot)"); + } + let new = crate::scheduled_prompts::NewSchedule { + owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), + targets: payload.targets, + body: payload.body, + first_fire_at_unix: payload.first_fire_at_unix, + interval_seconds: payload.interval_seconds, + description: payload.description, + source: crate::scheduled_prompts::ScheduleSource::Operator, + }; + match state.coord.scheduled_prompts.submit(new) { + Ok(id) => axum::Json(serde_json::json!({"id": id})).into_response(), + Err(e) => error_response(&format!("schedule submit: {e:#}")), + } +} + +#[derive(serde::Deserialize, Default)] +struct CancelScheduleForm { + /// `None` / absent / empty array → cancel whole schedule. + #[serde(default)] + targets: Option>, +} + +/// `POST /api/schedules/{id}/cancel` — operator-side cancel +/// (whole schedule when no `targets` field, partial when one is +/// provided). Operator bypasses the topology check; the manager +/// surface enforces it for agent callers. +async fn post_schedule_cancel( + State(state): State, + AxumPath(id): AxumPath, + body: Option>, +) -> Response { + let targets = body + .and_then(|axum::Json(b)| b.targets) + .filter(|t| !t.is_empty()); + let result = match targets.as_deref() { + Some(list) => state.coord.scheduled_prompts.cancel_targets(id, list), + None => state.coord.scheduled_prompts.cancel_all(id), + }; + match result { + Ok(()) => (StatusCode::OK, "ok").into_response(), + Err(e) => error_response(&format!("cancel schedule {id}: {e:#}")), + } +} + /// Same-origin proxy that fetches the named agent's /// `GET /api/state` and forwards only the `links` field to the /// dashboard JS (issue #262). Lets the agent backend stay the diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index 2da1f79c..38331d50 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -841,6 +841,13 @@ fn cancel_authorized(requester: &str, owner: &str) -> bool { /// Map a `scheduled_prompts::Schedule` to its public wire shape. /// Field-by-field copy — the two types are intentionally identical; /// the separation keeps hive-sh4re free of hive-c0re-internal types. +/// Public alias `schedule_to_wire_public` re-exports for +/// `dashboard.rs::api_schedules` without crossing the module +/// boundary into the manager-server file. +pub fn schedule_to_wire_public(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSchedule { + schedule_to_wire(s) +} + fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSchedule { hive_sh4re::WireSchedule { id: s.id,