dashboard: operator-direct schedule submit + cancel endpoints (#444 step 3)
This commit is contained in:
parent
aa7d8d9c9a
commit
a31cd8bb15
2 changed files with 87 additions and 0 deletions
|
|
@ -74,6 +74,8 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> 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<AppState>) -> 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<AppState>) -> Response {
|
||||
match state.coord.scheduled_prompts.list() {
|
||||
Ok(rows) => axum::Json(
|
||||
rows.into_iter()
|
||||
.map(crate::manager_server::schedule_to_wire_public)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.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<AppState>,
|
||||
axum::Json(payload): axum::Json<hive_sh4re::SchedulePromptPayload>,
|
||||
) -> 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<Vec<String>>,
|
||||
}
|
||||
|
||||
/// `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<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
body: Option<axum::Json<CancelScheduleForm>>,
|
||||
) -> 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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue