scheduled prompts: edit existing schedule (closes #474)
This commit is contained in:
parent
0072946ea3
commit
9fee1a1e56
6 changed files with 428 additions and 0 deletions
|
|
@ -75,6 +75,10 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.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}",
|
||||
axum::routing::patch(patch_schedule),
|
||||
)
|
||||
.route("/api/schedules/{id}/cancel", post(post_schedule_cancel))
|
||||
.route("/api/schedules/{id}/fire-now", post(post_schedule_fire_now))
|
||||
.route("/api/rebuild-queue/{id}/cancel", post(post_rebuild_queue_cancel))
|
||||
|
|
@ -1489,6 +1493,69 @@ struct CancelScheduleForm {
|
|||
targets: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Default)]
|
||||
struct EditScheduleForm {
|
||||
#[serde(default)]
|
||||
body: Option<String>,
|
||||
/// Double-`Option` semantics on the wire: missing key = leave
|
||||
/// alone, explicit `null` = clear, value = set. serde's
|
||||
/// `deserialize_with` trick to distinguish missing from null:
|
||||
/// we wrap each editable field in its own helper. Simpler
|
||||
/// here — keep them plain `Option<Option<_>>` and document
|
||||
/// that the dashboard caller passes JSON `null` to clear.
|
||||
#[serde(default, deserialize_with = "deserialize_some")]
|
||||
description: Option<Option<String>>,
|
||||
#[serde(default, deserialize_with = "deserialize_some")]
|
||||
interval_seconds: Option<Option<u64>>,
|
||||
#[serde(default)]
|
||||
next_fire_at_unix: Option<i64>,
|
||||
}
|
||||
|
||||
/// serde adaptor: turns missing-key into `None`, explicit-null
|
||||
/// into `Some(None)`, value into `Some(Some(v))`. Standard trick
|
||||
/// for distinguishing "field absent" from "field set to null" in
|
||||
/// JSON PATCH bodies.
|
||||
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
T: serde::Deserialize<'de>,
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
T::deserialize(deserializer).map(Some)
|
||||
}
|
||||
|
||||
/// `PATCH /api/schedules/{id}` — partial update of an existing
|
||||
/// schedule (#474). Mutable fields: `body`, `description`,
|
||||
/// `interval_seconds`, `next_fire_at_unix`. Targets stay
|
||||
/// immutable (per-target last-result history is keyed on them;
|
||||
/// the "I want different targets" workaround is cancel + submit
|
||||
/// a new schedule). JSON body uses missing-key = "leave alone",
|
||||
/// explicit null = "clear" for `description` + `interval_seconds`.
|
||||
/// Cancelled schedules are refused — submit a new one instead.
|
||||
/// Returns the updated `WireSchedule` so the caller's post-edit
|
||||
/// refresh has the new state inline.
|
||||
async fn patch_schedule(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
axum::Json(form): axum::Json<EditScheduleForm>,
|
||||
) -> Response {
|
||||
let patch = crate::scheduled_prompts::UpdateSchedule {
|
||||
body: form.body,
|
||||
description: form.description,
|
||||
interval_seconds: form.interval_seconds,
|
||||
next_fire_at_unix: form.next_fire_at_unix,
|
||||
};
|
||||
if let Err(e) = state.coord.scheduled_prompts.update(id, patch) {
|
||||
return error_response(&format!("edit schedule {id}: {e:#}"));
|
||||
}
|
||||
match state.coord.scheduled_prompts.get(id) {
|
||||
Ok(Some(s)) => {
|
||||
axum::Json(crate::manager_server::schedule_to_wire_public(s)).into_response()
|
||||
}
|
||||
Ok(None) => error_response(&format!("edit schedule {id}: row vanished post-update")),
|
||||
Err(e) => error_response(&format!("re-read schedule {id}: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `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
|
||||
|
|
|
|||
Loading…
Reference in a new issue