agent ui: answer questions inline from the per-agent page

loose-ends question rows get a textarea + send button; the operator
answers as operator by POSTing to the core dashboard's
/answer-question route, not the per-agent socket — keeps the
operator-authority path off the agent's own socket. cross-origin POST
needs a CORS shim on that route for now; drops out once the gateway
makes the page same-origin.

also splits deployment/ops/boundaries/gateway work into TODO-ops.md.
This commit is contained in:
müde 2026-05-20 10:01:12 +02:00
parent f8795dc029
commit 56e7eb6e73
5 changed files with 221 additions and 8 deletions

View file

@ -759,6 +759,20 @@ struct AnswerForm {
answer: String,
}
/// Attach a permissive CORS header so the per-agent web UI — served on
/// a different port — can POST an operator answer here and read the
/// result. The dashboard has no auth, so `*` exposes nothing a plain
/// cross-origin form-POST couldn't already reach. This shim disappears
/// once the unifying gateway makes the agent page same-origin; see
/// `TODO-ops.md`.
fn with_cors(mut resp: Response) -> Response {
resp.headers_mut().insert(
axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
axum::http::HeaderValue::from_static("*"),
);
resp
}
async fn post_answer_question(
State(state): State<AppState>,
AxumPath(id): AxumPath<i64>,
@ -766,9 +780,9 @@ async fn post_answer_question(
) -> Response {
let answer = form.answer.trim();
if answer.is_empty() {
return error_response("answer: required");
return with_cors(error_response("answer: required"));
}
match state
let resp = match state
.coord
.questions
.answer(id, answer, hive_sh4re::OPERATOR_RECIPIENT)
@ -794,7 +808,8 @@ async fn post_answer_question(
(StatusCode::OK, "ok").into_response()
}
Err(e) => error_response(&format!("answer {id} failed: {e:#}")),
}
};
with_cors(resp)
}
/// Resolve a pending operator question with a sentinel answer when