topology: operator-driven move-agent via set_parent (#486)
This commit is contained in:
parent
501a86b725
commit
5456377622
5 changed files with 245 additions and 0 deletions
|
|
@ -72,6 +72,7 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/cancel-reminder/{id}", post(post_cancel_reminder))
|
||||
.route("/retry-reminder/{id}", post(post_retry_reminder))
|
||||
.route("/request-spawn", post(post_request_spawn))
|
||||
.route("/api/topology/set-parent", post(post_set_parent))
|
||||
.route("/op-send", post(post_op_send))
|
||||
.route("/meta-update", post(post_meta_update))
|
||||
.route("/api/schedules", get(api_schedules).post(post_schedule_new))
|
||||
|
|
@ -849,6 +850,17 @@ struct RequestSpawnForm {
|
|||
name: String,
|
||||
}
|
||||
|
||||
/// `POST /api/topology/set-parent` body. `child` is required.
|
||||
/// `new_parent` may be:
|
||||
/// - omitted entirely (form field absent) → no-op error,
|
||||
/// - empty string → promote to root,
|
||||
/// - non-empty → new parent's logical name.
|
||||
#[derive(Deserialize)]
|
||||
struct SetParentForm {
|
||||
child: String,
|
||||
new_parent: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnswerForm {
|
||||
answer: String,
|
||||
|
|
@ -1846,6 +1858,45 @@ async fn post_request_spawn(
|
|||
}
|
||||
}
|
||||
|
||||
/// `POST /api/topology/set-parent` — operator-driven parent move
|
||||
/// (#486). Form fields: `child` (required, agent name), `new_parent`
|
||||
/// (optional — empty / absent string ⇒ promote to root). Refuses
|
||||
/// cycles, unknown agents, and reparenting the manager. On success
|
||||
/// re-emits container snapshots so the dashboard tree repaints
|
||||
/// without a refresh.
|
||||
async fn post_set_parent(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<SetParentForm>,
|
||||
) -> Response {
|
||||
let child = form.child.trim().to_owned();
|
||||
if child.is_empty() {
|
||||
return error_response("set-parent: `child` required");
|
||||
}
|
||||
// Empty / whitespace-only `new_parent` ⇒ promote to root. Web
|
||||
// forms submit the empty string for a "no value" radio button,
|
||||
// so this is the ergonomic encoding.
|
||||
let new_parent = form
|
||||
.new_parent
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_owned);
|
||||
match crate::topology::set_parent(&child, new_parent.as_deref()) {
|
||||
Ok(()) => {
|
||||
tracing::info!(
|
||||
child = %child,
|
||||
new_parent = ?new_parent,
|
||||
"operator: set-parent via dashboard"
|
||||
);
|
||||
// Topology drives ContainerView.parent; refresh the
|
||||
// snapshot so connected viewers see the new tree.
|
||||
state.coord.rescan_containers_and_emit().await;
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
Err(e) => error_response(&format!("set-parent {child} failed: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_rebuild(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||
let logical = strip_container_prefix(&name);
|
||||
state.coord.rebuild_queue.enqueue(
|
||||
|
|
|
|||
Loading…
Reference in a new issue