hive-c0re: annotate remaining dashboard routes with utoipa
This commit is contained in:
parent
8ebefeb0d5
commit
582ebe5eee
21 changed files with 738 additions and 45 deletions
|
|
@ -10,11 +10,23 @@ use axum::{
|
|||
};
|
||||
use hive_sh4re::Approval;
|
||||
use serde::Deserialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use super::{AppState, error_response};
|
||||
use crate::actions;
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
/// `POST /api/approve/{id}` — approve a pending approval row.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/approve/{id}",
|
||||
params(("id" = i64, Path, description = "approval row id")),
|
||||
responses(
|
||||
(status = 200, description = "approved", body = String),
|
||||
(status = 500, description = "approve failed"),
|
||||
),
|
||||
tag = "approvals"
|
||||
)]
|
||||
pub(super) async fn post_approve(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
@ -29,12 +41,25 @@ pub(super) async fn post_approve(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, ToSchema)]
|
||||
pub(super) struct DenyForm {
|
||||
#[serde(default)]
|
||||
note: Option<String>,
|
||||
}
|
||||
|
||||
/// `POST /api/deny/{id}` — deny a pending approval row, with an optional
|
||||
/// note (form field `note`).
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/deny/{id}",
|
||||
params(("id" = i64, Path, description = "approval row id")),
|
||||
request_body(content = DenyForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "denied", body = String),
|
||||
(status = 500, description = "deny failed"),
|
||||
),
|
||||
tag = "approvals"
|
||||
)]
|
||||
pub(super) async fn post_deny(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ use axum::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tokio_stream::Stream;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use super::{AppState, Ident, error_response};
|
||||
use crate::build_logs::{BuildLogFull, BuildLogHeader};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub(super) struct BuildLogsAllQuery {
|
||||
/// Max rows to return. Capped at 100. Default 30.
|
||||
#[serde(default)]
|
||||
|
|
@ -29,6 +31,16 @@ pub(super) struct BuildLogsAllQuery {
|
|||
|
||||
/// `GET /api/build-logs?limit=N` — most-recent build log headers across
|
||||
/// all agents, newest first. Same JSON shape as the per-agent endpoint.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/build-logs",
|
||||
params(BuildLogsAllQuery),
|
||||
responses(
|
||||
(status = 200, description = "recent build log headers, newest first", body = Vec<BuildLogHeader>),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "build_logs"
|
||||
)]
|
||||
pub(super) async fn get_build_logs_all(
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Query(q): axum::extract::Query<BuildLogsAllQuery>,
|
||||
|
|
@ -40,7 +52,7 @@ pub(super) async fn get_build_logs_all(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub(super) struct BuildLogsQuery {
|
||||
/// Maximum number of rows to return. Capped server-side at 50
|
||||
/// (see `build_logs::list_recent_for_agent`). Default 10.
|
||||
|
|
@ -53,6 +65,20 @@ pub(super) struct BuildLogsQuery {
|
|||
/// `Vec<BuildLogHeader>` (JSON). Limit defaults to 10, server-side
|
||||
/// cap at 50. Backs the per-agent log chip in the agent card and
|
||||
/// the side-panel header list.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/build-logs/{agent}",
|
||||
params(
|
||||
("agent" = String, Path, description = "agent name"),
|
||||
BuildLogsQuery,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "recent build log headers for the agent, newest first", body = Vec<BuildLogHeader>),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "build_logs"
|
||||
)]
|
||||
pub(super) async fn get_build_logs_agent(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -79,6 +105,17 @@ pub(super) async fn get_build_logs_agent(
|
|||
/// stderr concatenated) by id. Returns `BuildLogFull` (JSON), or
|
||||
/// HTTP 404 when the id doesn't exist (vacuum-reaped, or the
|
||||
/// operator passed a stale id from a refresh race).
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/build-logs/id/{id}",
|
||||
params(("id" = i64, Path, description = "build log row id")),
|
||||
responses(
|
||||
(status = 200, description = "full build log row", body = BuildLogFull),
|
||||
(status = 404, description = "no such build log row"),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "build_logs"
|
||||
)]
|
||||
pub(super) async fn get_build_log_full(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
@ -96,6 +133,17 @@ pub(super) async fn get_build_log_full(
|
|||
/// node has no linked log (the client gates the request on `NodeView.has_log`,
|
||||
/// but a vacuum race can still 404). This is the on-demand log fetch the
|
||||
/// raw-graph dashboard uses instead of an inline `build_log_id` on the wire.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/build-log/{node_id}",
|
||||
params(("node_id" = u64, Path, description = "job-queue node id")),
|
||||
responses(
|
||||
(status = 200, description = "full build log row for the node's linked log", body = BuildLogFull),
|
||||
(status = 404, description = "node has no linked build log, or the log row is gone"),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "build_logs"
|
||||
)]
|
||||
pub(super) async fn get_build_log_for_node(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(node_id): AxumPath<u64>,
|
||||
|
|
@ -112,6 +160,17 @@ pub(super) async fn get_build_log_for_node(
|
|||
|
||||
/// `GET /api/build-log/{node_id}/raw` — the node's build log as `text/plain`
|
||||
/// for download (delegates to `get_build_log_raw` after resolving the node id).
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/build-log/{node_id}/raw",
|
||||
params(("node_id" = u64, Path, description = "job-queue node id")),
|
||||
responses(
|
||||
(status = 200, description = "build log text for download", body = String, content_type = "text/plain"),
|
||||
(status = 404, description = "node has no linked build log, or the log row is gone"),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "build_logs"
|
||||
)]
|
||||
pub(super) async fn get_build_log_raw_for_node(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(node_id): AxumPath<u64>,
|
||||
|
|
@ -239,6 +298,17 @@ pub(super) async fn get_build_log_stream(
|
|||
/// separator (same layout the JS side-panel renders). The
|
||||
/// `Content-Disposition` header triggers a browser download with a
|
||||
/// descriptive filename so the operator can save and share the log.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/build-logs/id/{id}/raw",
|
||||
params(("id" = i64, Path, description = "build log row id")),
|
||||
responses(
|
||||
(status = 200, description = "build log text for download", body = String, content_type = "text/plain"),
|
||||
(status = 404, description = "no such build log row"),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "build_logs"
|
||||
)]
|
||||
pub(super) async fn get_build_log_raw(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use std::path::Path;
|
|||
use axum::extract::{Form, Query};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use super::{Ident, error_response};
|
||||
use crate::coordinator::Coordinator;
|
||||
|
|
@ -41,18 +42,18 @@ fn read_base_url(dir: &Path, label: &str) -> Option<String> {
|
|||
.map(|s| s.base_url)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct ExtraForgeAccount {
|
||||
label: String,
|
||||
base_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct ExtraForgesResponse {
|
||||
forges: Vec<ExtraForgeAccount>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub(super) struct ExtraForgesQuery {
|
||||
agent: String,
|
||||
}
|
||||
|
|
@ -62,6 +63,16 @@ pub(super) struct ExtraForgesQuery {
|
|||
/// token` file in its state dir (mirrors `matrix_accounts.rs`'s filename-scan
|
||||
/// listing). `base_url` is backfilled from the matching `forge-<label>.json`
|
||||
/// sidecar when present. Never returns a token.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/extra-forges",
|
||||
params(ExtraForgesQuery),
|
||||
responses(
|
||||
(status = 200, description = "provisioned extra forge accounts for the agent", body = ExtraForgesResponse),
|
||||
(status = 500, description = "invalid agent name, or a state-dir read failed"),
|
||||
),
|
||||
tag = "extra_forges"
|
||||
)]
|
||||
pub(super) async fn get_extra_forges(Query(q): Query<ExtraForgesQuery>) -> Response {
|
||||
let agent = q.agent.trim();
|
||||
let Ok(agent) = Ident::parse(agent) else {
|
||||
|
|
@ -108,7 +119,7 @@ pub(super) async fn get_extra_forges(Query(q): Query<ExtraForgesQuery>) -> Respo
|
|||
/// Form body for `POST /api/extra-forge-account` (urlencoded, the
|
||||
/// dashboard's mutation convention). `action` is `"add"` (needs `base_url` +
|
||||
/// `token`) or `"remove"`.
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct ExtraForgeAccountForm {
|
||||
agent: String,
|
||||
label: String,
|
||||
|
|
@ -117,7 +128,7 @@ pub(super) struct ExtraForgeAccountForm {
|
|||
token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct ExtraForgeAccountResult {
|
||||
ok: bool,
|
||||
}
|
||||
|
|
@ -127,6 +138,16 @@ struct ExtraForgeAccountResult {
|
|||
/// deletes both files. Purely local — no remote account creation or
|
||||
/// revocation, there is no admin access assumed on the external forge.
|
||||
/// Operator-authenticated (dashboard). Never echoes the token back.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/extra-forge-account",
|
||||
request_body(content = ExtraForgeAccountForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "provisioned or removed", body = ExtraForgeAccountResult),
|
||||
(status = 500, description = "invalid input, or the state-dir write/delete failed"),
|
||||
),
|
||||
tag = "extra_forges"
|
||||
)]
|
||||
pub(super) async fn post_extra_forge_account(Form(f): Form<ExtraForgeAccountForm>) -> Response {
|
||||
let agent = f.agent.trim();
|
||||
let label = f.label.trim();
|
||||
|
|
|
|||
|
|
@ -22,6 +22,19 @@ use super::{AppState, error_response};
|
|||
/// `restart_infra`) and streams as an `AuditEntryAdded` event, so
|
||||
/// operator-driven and agent-driven (`infra_admin`) infra actions show up
|
||||
/// in the same AUDIT view.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/infra-container/{name}/{action}",
|
||||
params(
|
||||
("name" = String, Path, description = "infra container name (hive-ci/hive-forge/hive-gateway/hive-matrix)"),
|
||||
("action" = String, Path, description = "start | stop | restart"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "action completed", body = String),
|
||||
(status = 500, description = "unknown container/action, or the systemd action failed"),
|
||||
),
|
||||
tag = "infra_containers"
|
||||
)]
|
||||
pub(super) async fn post_infra_container(
|
||||
State(state): State<AppState>,
|
||||
AxumPath((name, action)): AxumPath<(String, String)>,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
/// Query params for `post_kill` / `post_restart`. `?graceful=1` routes to
|
||||
/// the graceful-stop/-restart orchestration (quiesce the harness, flush
|
||||
/// `/state`, then container stop/restart) instead of an immediate hard
|
||||
/// action. Defaults false → today's hard kill/restart.
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub(super) struct GracefulParams {
|
||||
#[serde(default)]
|
||||
graceful: bool,
|
||||
|
|
@ -29,6 +30,18 @@ use super::{AppState, Ident, error_response, guard_agent_name, strip_container_p
|
|||
use crate::job_queue::{Source, submit};
|
||||
use crate::{actions, lifecycle};
|
||||
|
||||
/// `POST /api/rebuild/{name}` — queue a rebuild DAG for `name`.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/rebuild/{name}",
|
||||
params(("name" = String, Path, description = "agent name")),
|
||||
responses(
|
||||
(status = 200, description = "rebuild queued", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_rebuild(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -46,6 +59,22 @@ pub(super) async fn post_rebuild(
|
|||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// `POST /api/kill/{name}?graceful=1` — stop `name`, hard by default or
|
||||
/// gracefully (quiesce → drain → stop) when `graceful=1`.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/kill/{name}",
|
||||
params(
|
||||
("name" = String, Path, description = "agent name"),
|
||||
GracefulParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "stop queued/performed", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_kill(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -90,6 +119,22 @@ pub(super) async fn post_kill(
|
|||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// `POST /api/restart/{name}?graceful=1` — restart `name`, hard by default
|
||||
/// or gracefully (quiesce → drain → restart) when `graceful=1`.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/restart/{name}",
|
||||
params(
|
||||
("name" = String, Path, description = "agent name"),
|
||||
GracefulParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "restart queued/performed", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_restart(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -119,6 +164,18 @@ pub(super) async fn post_restart(
|
|||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// `POST /api/start/{name}` — start `name`.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/start/{name}",
|
||||
params(("name" = String, Path, description = "agent name")),
|
||||
responses(
|
||||
(status = 200, description = "start queued", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_start(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -145,6 +202,18 @@ pub(super) async fn post_start(
|
|||
/// when the container next boots). Triggers an immediate rescan so the
|
||||
/// `paused` badge flips on the dashboard without waiting for the next
|
||||
/// periodic sweep.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/pause/{name}",
|
||||
params(("name" = String, Path, description = "agent name")),
|
||||
responses(
|
||||
(status = 200, description = "pause marker written", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
(status = 500, description = "marker write failed"),
|
||||
),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_pause(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -168,6 +237,18 @@ pub(super) async fn post_pause(
|
|||
///
|
||||
/// The inverse of `post_pause`. Removing a non-existent marker is a no-op
|
||||
/// (idempotent). Triggers an immediate rescan so the paused badge clears.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/resume/{name}",
|
||||
params(("name" = String, Path, description = "agent name")),
|
||||
responses(
|
||||
(status = 200, description = "pause marker removed", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
(status = 500, description = "marker removal failed"),
|
||||
),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_resume(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -190,7 +271,7 @@ pub(super) async fn post_resume(
|
|||
/// Form fields for `post_resource_limits`. Both fields are optional strings;
|
||||
/// an empty value clears the per-agent override for that field, falling back
|
||||
/// to the hive-wide default.
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, ToSchema)]
|
||||
pub(super) struct ResourceLimitsForm {
|
||||
#[serde(default)]
|
||||
cpu_quota: String,
|
||||
|
|
@ -207,6 +288,20 @@ pub(super) struct ResourceLimitsForm {
|
|||
/// limits take effect on the next container start or restart. Triggers an
|
||||
/// immediate rescan so `ContainerView.cpu_quota`/`memory_max` update on
|
||||
/// the dashboard via SSE without waiting for the next periodic sweep.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/resource-limits/{name}",
|
||||
params(("name" = String, Path, description = "agent name")),
|
||||
request_body(content = ResourceLimitsForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "limits written", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
(status = 422, description = "invalid cpu_quota/memory_max value"),
|
||||
(status = 500, description = "commit or drop-in write failed"),
|
||||
),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_resource_limits(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -257,6 +352,14 @@ pub(super) async fn post_resource_limits(
|
|||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// `POST /api/update-all` — queue a rebuild DAG for every live agent
|
||||
/// container.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/update-all",
|
||||
responses((status = 200, description = "rebuilds queued", body = String)),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_update_all(State(state): State<AppState>) -> Response {
|
||||
let containers = lifecycle::list().await.unwrap_or_default();
|
||||
for container in containers {
|
||||
|
|
@ -276,12 +379,28 @@ pub(super) async fn post_update_all(State(state): State<AppState>) -> Response {
|
|||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, ToSchema)]
|
||||
pub(super) struct DestroyForm {
|
||||
#[serde(default)]
|
||||
purge: Option<String>,
|
||||
}
|
||||
|
||||
/// `POST /api/destroy/{name}` — destroy `name`'s container. Form field
|
||||
/// `purge` (any non-empty value, e.g. `"on"`) also wipes the retained
|
||||
/// state dir instead of leaving a tombstone.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/destroy/{name}",
|
||||
params(("name" = String, Path, description = "agent name")),
|
||||
request_body(content = DestroyForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "destroyed", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
(status = 500, description = "destroy failed"),
|
||||
),
|
||||
tag = "lifecycle_ops"
|
||||
)]
|
||||
pub(super) async fn post_destroy(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
|
|||
|
|
@ -22,16 +22,17 @@ use std::path::Path;
|
|||
use axum::extract::{Form, Query};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use super::{Ident, error_response};
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub(super) struct MatrixAccountsQuery {
|
||||
agent: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct MatrixAccount {
|
||||
name: String,
|
||||
/// Effective homeserver, backfilled from the daemon snapshot; `None` when
|
||||
|
|
@ -46,7 +47,7 @@ struct MatrixAccount {
|
|||
user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct MatrixAccountsResponse {
|
||||
accounts: Vec<MatrixAccount>,
|
||||
/// Unix mtime of the daemon's `matrix-accounts.json` snapshot (when the
|
||||
|
|
@ -102,6 +103,19 @@ fn account_name_from_filename(fname: &str) -> Option<String> {
|
|||
Some(suffix.to_owned())
|
||||
}
|
||||
|
||||
/// `GET /api/matrix-accounts?agent=<name>` — matrix accounts provisioned
|
||||
/// for `agent`, backfilled with homeserver/live/user_id from the daemon's
|
||||
/// snapshot.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/matrix-accounts",
|
||||
params(MatrixAccountsQuery),
|
||||
responses(
|
||||
(status = 200, description = "provisioned matrix accounts for the agent", body = MatrixAccountsResponse),
|
||||
(status = 500, description = "invalid agent name, or a state-dir read failed"),
|
||||
),
|
||||
tag = "matrix_accounts"
|
||||
)]
|
||||
pub(super) async fn get_matrix_accounts(Query(q): Query<MatrixAccountsQuery>) -> Response {
|
||||
let agent = q.agent.trim();
|
||||
// Validate through the single `Ident` type so a crafted `agent` can't
|
||||
|
|
@ -156,7 +170,7 @@ pub(super) async fn get_matrix_accounts(Query(q): Query<MatrixAccountsQuery>) ->
|
|||
/// mutation convention). `mode` is `"password"` (needs `user_id` +
|
||||
/// `password`) or `"token"` (needs `token`; `user_id` is recovered via
|
||||
/// whoami).
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct MatrixLoginForm {
|
||||
agent: String,
|
||||
account: String,
|
||||
|
|
@ -167,7 +181,7 @@ pub(super) struct MatrixLoginForm {
|
|||
token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct MatrixLoginResult {
|
||||
ok: bool,
|
||||
user_id: String,
|
||||
|
|
@ -178,6 +192,16 @@ struct MatrixLoginResult {
|
|||
/// On success writes the token to `matrix-token-<account>` via hive-priv and
|
||||
/// kicks the daemon. Operator-authenticated (dashboard). Never echoes the
|
||||
/// token back — only `{ ok, user_id }`.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/matrix-account-login",
|
||||
request_body(content = MatrixLoginForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "account provisioned", body = MatrixLoginResult),
|
||||
(status = 500, description = "invalid input, or the homeserver login/whoami failed"),
|
||||
),
|
||||
tag = "matrix_accounts"
|
||||
)]
|
||||
pub(super) async fn post_matrix_account_login(Form(f): Form<MatrixLoginForm>) -> Response {
|
||||
let agent = f.agent.trim();
|
||||
let account = f.account.trim();
|
||||
|
|
@ -257,13 +281,13 @@ pub(super) async fn post_matrix_account_login(Form(f): Form<MatrixLoginForm>) ->
|
|||
/// `github-token` file. The GitHub counterpart of the matrix login form, but
|
||||
/// far simpler: no account creation, no homeserver, no login modes — the
|
||||
/// operator pastes a PAT for an existing account.
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct GithubAccountForm {
|
||||
agent: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct GithubAccountResult {
|
||||
ok: bool,
|
||||
}
|
||||
|
|
@ -275,6 +299,16 @@ struct GithubAccountResult {
|
|||
/// helper read the file live, so the new token takes effect immediately.
|
||||
/// Operator-authenticated (dashboard). Never echoes the token back — only
|
||||
/// `{ ok: true }`.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/github-account",
|
||||
request_body(content = GithubAccountForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "PAT provisioned", body = GithubAccountResult),
|
||||
(status = 500, description = "invalid agent name, empty token, or the write failed"),
|
||||
),
|
||||
tag = "matrix_accounts"
|
||||
)]
|
||||
pub(super) async fn post_github_account(Form(f): Form<GithubAccountForm>) -> Response {
|
||||
let agent = f.agent.trim();
|
||||
let token = f.token.trim();
|
||||
|
|
@ -291,12 +325,12 @@ pub(super) async fn post_github_account(Form(f): Form<GithubAccountForm>) -> Res
|
|||
axum::Json(GithubAccountResult { ok: true }).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub(super) struct GithubAccountQuery {
|
||||
agent: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct GithubAccountStatus {
|
||||
/// A `github-token` file exists in the agent's state dir (a PAT has been
|
||||
/// provisioned). A static PAT has no live/heartbeat concept, so this is
|
||||
|
|
@ -308,6 +342,16 @@ struct GithubAccountStatus {
|
|||
/// PAT provisioned (its `github-token` file exists). Lets the credentials tab
|
||||
/// show "token stored" vs "not set" instead of a black-hole paste field.
|
||||
/// Never returns the token itself.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/github-account",
|
||||
params(GithubAccountQuery),
|
||||
responses(
|
||||
(status = 200, description = "whether a github PAT is provisioned", body = GithubAccountStatus),
|
||||
(status = 500, description = "invalid agent name"),
|
||||
),
|
||||
tag = "matrix_accounts"
|
||||
)]
|
||||
pub(super) async fn get_github_account(Query(q): Query<GithubAccountQuery>) -> Response {
|
||||
let agent = q.agent.trim();
|
||||
let Ok(agent) = Ident::parse(agent) else {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
|
|
@ -172,7 +173,7 @@ pub(crate) fn emit_meta_inputs_snapshot(coord: &Coordinator) {
|
|||
/// list under the `inputs` field — the JS submitter joins the
|
||||
/// checked boxes since axum's `Form` extractor doesn't natively
|
||||
/// decode repeated keys without a helper.
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct MetaUpdateForm {
|
||||
inputs: String,
|
||||
}
|
||||
|
|
@ -183,6 +184,16 @@ pub(super) struct MetaUpdateForm {
|
|||
/// no rebuild ripple). Returns immediately after queueing the work;
|
||||
/// dashboard polls for progress via container `pending` spinners +
|
||||
/// the meta-inputs row sha update.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/meta-update",
|
||||
request_body(content = MetaUpdateForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "update + rebuild ripple queued", body = String),
|
||||
(status = 500, description = "no inputs selected"),
|
||||
),
|
||||
tag = "meta_inputs"
|
||||
)]
|
||||
pub(super) async fn post_meta_update(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<MetaUpdateForm>,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use super::{AppState, Ident, error_response, scan_validated_paths};
|
||||
use crate::container_stats::ContainerResource;
|
||||
use crate::hive_stats::HiveStats;
|
||||
|
||||
/// Unread operator-directed messages for the dashboard's Y3R C4LL inbox.
|
||||
/// Returns messages addressed to `"operator"` that haven't been
|
||||
|
|
@ -19,6 +22,15 @@ use super::{AppState, Ident, error_response, scan_validated_paths};
|
|||
/// tokens are validated so the client renders file links like the
|
||||
/// terminal does. Shape: `{ "messages": [{ id, from, body, at,
|
||||
/// in_reply_to, file_refs }] }`.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/operator-inbox",
|
||||
responses(
|
||||
(status = 200, description = "unread operator-directed messages", body = serde_json::Value),
|
||||
(status = 500, description = "broker read failed"),
|
||||
),
|
||||
tag = "misc_api"
|
||||
)]
|
||||
pub(super) async fn api_operator_inbox(State(state): State<AppState>) -> Response {
|
||||
const INBOX_LIMIT: u64 = 100;
|
||||
match state
|
||||
|
|
@ -58,7 +70,7 @@ pub(super) async fn api_operator_inbox(State(state): State<AppState>) -> Respons
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub(super) struct StatsHiveQuery {
|
||||
window: Option<String>,
|
||||
}
|
||||
|
|
@ -66,6 +78,13 @@ pub(super) struct StatsHiveQuery {
|
|||
/// Hive-wide turn-stats rollup for the dashboard swarm-stats view.
|
||||
/// Aggregates every agent's `hyperhive-turn-stats.sqlite` read-only
|
||||
/// (skips missing/unreadable ones). Window defaults to `24h`.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/stats-hive",
|
||||
params(StatsHiveQuery),
|
||||
responses((status = 200, description = "hive-wide turn-stats rollup", body = HiveStats)),
|
||||
tag = "misc_api"
|
||||
)]
|
||||
pub(super) async fn api_stats_hive(
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Query(q): axum::extract::Query<StatsHiveQuery>,
|
||||
|
|
@ -80,6 +99,12 @@ pub(super) async fn api_stats_hive(
|
|||
|
||||
/// Live per-agent-container CPU + memory load from cgroup v2. Samples
|
||||
/// CPU over a short interval (~200 ms), so this call briefly awaits.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/container-resources",
|
||||
responses((status = 200, description = "live per-container CPU + memory load", body = Vec<ContainerResource>)),
|
||||
tag = "misc_api"
|
||||
)]
|
||||
pub(super) async fn api_container_resources() -> Response {
|
||||
axum::Json(crate::container_stats::gather().await).into_response()
|
||||
}
|
||||
|
|
@ -90,6 +115,15 @@ pub(super) async fn api_container_resources() -> Response {
|
|||
/// `{ "entries": [AuditEntry…], "total": N }` so the UI can show
|
||||
/// "latest 500 of N" rather than silently capping. `ts_unix` is in
|
||||
/// **seconds**.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/audit-log",
|
||||
responses(
|
||||
(status = 200, description = "recent audit entries + total count", body = serde_json::Value),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "misc_api"
|
||||
)]
|
||||
pub(super) async fn api_audit_log(State(state): State<AppState>) -> Response {
|
||||
const LIMIT: usize = 500;
|
||||
let entries = match state.coord.audit_log.list_recent(LIMIT) {
|
||||
|
|
@ -109,6 +143,17 @@ pub(super) async fn api_audit_log(State(state): State<AppState>) -> Response {
|
|||
/// rows so vacuum can collect them). Returns `{ "marked": N }` so the
|
||||
/// frontend can show "cleared N messages" feedback without an extra
|
||||
/// fetch.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/agent/{name}/mark-all-read",
|
||||
params(("name" = String, Path, description = "agent name")),
|
||||
responses(
|
||||
(status = 200, description = "count of messages marked read", body = serde_json::Value),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 500, description = "broker write failed"),
|
||||
),
|
||||
tag = "misc_api"
|
||||
)]
|
||||
pub(super) async fn post_mark_all_read(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -135,12 +180,24 @@ pub(super) async fn post_mark_all_read(
|
|||
/// validation that `to` resolves to a known agent — broker accepts
|
||||
/// arbitrary recipients (and the agent's inbox grows whether or not
|
||||
/// they exist, which is fine for spawn-then-greet flows).
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct OpSendForm {
|
||||
to: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
/// `POST /api/op-send` — operator compose: drop a message into the
|
||||
/// broker addressed to `to` (or `*` to broadcast).
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/op-send",
|
||||
request_body(content = OpSendForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "message sent", body = String),
|
||||
(status = 500, description = "missing to/body, or the broker send failed"),
|
||||
),
|
||||
tag = "misc_api"
|
||||
)]
|
||||
pub(super) async fn post_op_send(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<OpSendForm>,
|
||||
|
|
@ -180,11 +237,22 @@ pub(super) async fn post_op_send(
|
|||
(axum::http::StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct RequestSpawnForm {
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// `POST /api/request-spawn` — queue a spawn approval for `name`.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/request-spawn",
|
||||
request_body(content = RequestSpawnForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "spawn approval queued", body = String),
|
||||
(status = 500, description = "missing name, or the approval submit failed"),
|
||||
),
|
||||
tag = "misc_api"
|
||||
)]
|
||||
pub(super) async fn post_request_spawn(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<RequestSpawnForm>,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,22 @@ use crate::lifecycle;
|
|||
tags(
|
||||
(name = "health", description = "hive-wide liveness/readiness probes"),
|
||||
(name = "journal", description = "container + host journal reads"),
|
||||
(name = "approvals", description = "approve/deny pending approval rows"),
|
||||
(name = "build_logs", description = "build log headers, full rows, and raw text downloads"),
|
||||
(name = "extra_forges", description = "external (non-internal) forge account provisioning"),
|
||||
(name = "infra_containers", description = "start/stop/restart of hive infrastructure containers"),
|
||||
(name = "lifecycle_ops", description = "agent container lifecycle: rebuild/restart/start/stop/pause/limits"),
|
||||
(name = "matrix_accounts", description = "matrix + github account provisioning for agents"),
|
||||
(name = "meta_inputs", description = "bulk flake-input update for the meta flake"),
|
||||
(name = "misc_api", description = "operator inbox, compose, spawn-request, hive stats, audit log"),
|
||||
(name = "permissions", description = "tool-group + capability assignment for agents"),
|
||||
(name = "questions", description = "answer/cancel pending operator questions"),
|
||||
(name = "schedules", description = "scheduled-prompt + rebuild-queue CRUD"),
|
||||
(name = "state_files", description = "proxied reads of allow-listed per-agent state files"),
|
||||
(name = "state_snapshot", description = "cold-load dashboard snapshot"),
|
||||
(name = "tombstones", description = "purge of retained state for destroyed agents"),
|
||||
(name = "topology", description = "operator-driven agent reparenting"),
|
||||
(name = "webhook", description = "forgejo webhook receivers"),
|
||||
)
|
||||
)]
|
||||
struct ApiDoc;
|
||||
|
|
@ -423,3 +439,18 @@ mod tests {
|
|||
assert_eq!(fv["status"], 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod router_build_probe {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn probe_multi_path_routes_macro_does_not_panic() {
|
||||
let _ = OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi()).routes(routes!(
|
||||
health::get_health_live,
|
||||
health::get_health_ready,
|
||||
journal::get_journal,
|
||||
journal::get_journal_host,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use problem_details::ProblemDetails;
|
||||
|
||||
use super::{AppState, Ident, guard_agent_name, strip_container_prefix};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub(super) struct ToolGroupsSnapshot {
|
||||
/// Ordered list of all known tool-group names. Drives the column
|
||||
/// headers in the capabilities table — the UI does not hard-code them.
|
||||
|
|
@ -36,6 +37,14 @@ pub(super) struct ToolGroupsSnapshot {
|
|||
effective: std::collections::BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// `GET /api/tool-groups` — every known tool-group name + description,
|
||||
/// plus the per-agent explicit/effective assignment maps.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/tool-groups",
|
||||
responses((status = 200, description = "tool-group catalogue + assignments", body = ToolGroupsSnapshot)),
|
||||
tag = "permissions"
|
||||
)]
|
||||
pub(super) async fn get_tool_groups(
|
||||
State(state): State<AppState>,
|
||||
) -> axum::Json<ToolGroupsSnapshot> {
|
||||
|
|
@ -105,11 +114,25 @@ pub(crate) fn roster_and_effective(
|
|||
(agents, effective)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct SetToolGroupsBody {
|
||||
groups: Vec<String>,
|
||||
}
|
||||
|
||||
/// `POST /api/tool-groups/{agent}` — replace `agent`'s explicit tool-group
|
||||
/// assignment (JSON body `{"groups": [...]}`).
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/tool-groups/{agent}",
|
||||
params(("agent" = String, Path, description = "agent name")),
|
||||
request_body = SetToolGroupsBody,
|
||||
responses(
|
||||
(status = 200, description = "tool-groups queued for write", body = String),
|
||||
(status = 400, description = "unknown group name, or no such agent"),
|
||||
(status = 404, description = "no such agent"),
|
||||
),
|
||||
tag = "permissions"
|
||||
)]
|
||||
pub(super) async fn post_tool_groups(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -145,7 +168,7 @@ pub(super) async fn post_tool_groups(
|
|||
Ok((StatusCode::OK, "ok").into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub(super) struct CapabilitiesSnapshot {
|
||||
/// Ordered list of all known capability names. Drives the column
|
||||
/// headers in the capabilities table — the UI does not hard-code them.
|
||||
|
|
@ -164,6 +187,14 @@ pub(super) struct CapabilitiesSnapshot {
|
|||
effective: std::collections::BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// `GET /api/capabilities` — every known capability name + description,
|
||||
/// plus the per-agent explicit/effective grant maps.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/capabilities",
|
||||
responses((status = 200, description = "capability catalogue + assignments", body = CapabilitiesSnapshot)),
|
||||
tag = "permissions"
|
||||
)]
|
||||
pub(super) async fn get_capabilities(
|
||||
State(state): State<AppState>,
|
||||
) -> axum::Json<CapabilitiesSnapshot> {
|
||||
|
|
@ -191,11 +222,25 @@ pub(super) async fn get_capabilities(
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct SetCapabilitiesBody {
|
||||
caps: Vec<String>,
|
||||
}
|
||||
|
||||
/// `POST /api/capabilities/{agent}` — replace `agent`'s explicit capability
|
||||
/// grant set (JSON body `{"caps": [...]}`).
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/capabilities/{agent}",
|
||||
params(("agent" = String, Path, description = "agent name")),
|
||||
request_body = SetCapabilitiesBody,
|
||||
responses(
|
||||
(status = 200, description = "capabilities queued for write", body = String),
|
||||
(status = 400, description = "unknown capability name"),
|
||||
(status = 404, description = "no such agent"),
|
||||
),
|
||||
tag = "permissions"
|
||||
)]
|
||||
pub(super) async fn post_capabilities(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
@ -236,7 +281,7 @@ pub(super) async fn post_capabilities(
|
|||
/// field leaves that perm-type untouched, an empty array clears it, a
|
||||
/// populated array fully replaces it (same replace semantics as the
|
||||
/// per-agent endpoints).
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct PermChangeBody {
|
||||
agent: String,
|
||||
#[serde(default)]
|
||||
|
|
@ -245,7 +290,7 @@ pub(super) struct PermChangeBody {
|
|||
capabilities: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct BatchPermsBody {
|
||||
changes: Vec<PermChangeBody>,
|
||||
}
|
||||
|
|
@ -261,6 +306,17 @@ type StagedPerm = (String, Option<Vec<String>>, Option<Vec<String>>);
|
|||
/// groups both changed rebuilds once, not twice. The whole batch is
|
||||
/// atomic: every change is validated up front and on any validation
|
||||
/// error nothing is written or enqueued.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/permissions",
|
||||
request_body = BatchPermsBody,
|
||||
responses(
|
||||
(status = 200, description = "batch permission change queued", body = String),
|
||||
(status = 400, description = "unknown tool-group/capability name, or no such agent"),
|
||||
(status = 404, description = "no such agent"),
|
||||
),
|
||||
tag = "permissions"
|
||||
)]
|
||||
pub(super) async fn post_permissions(
|
||||
State(state): State<AppState>,
|
||||
axum::Json(body): axum::Json<BatchPermsBody>,
|
||||
|
|
@ -320,12 +376,20 @@ pub(super) async fn post_permissions(
|
|||
/// persisted). The client uses this to drive the "stale permission entries"
|
||||
/// sub-section in K3PT ST4T3 without having to fetch three separate
|
||||
/// endpoints and perform set arithmetic on the client side.
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub(super) struct StalePermsResponse {
|
||||
/// Ghost agent names, sorted. Empty list → no stale entries.
|
||||
stale: Vec<String>,
|
||||
}
|
||||
|
||||
/// `GET /api/permissions/stale` — agent names with explicit permission
|
||||
/// entries but no matching live container or kept-state dir.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/permissions/stale",
|
||||
responses((status = 200, description = "ghost agent names with stale permission entries", body = StalePermsResponse)),
|
||||
tag = "permissions"
|
||||
)]
|
||||
pub(super) async fn get_stale_permissions(
|
||||
State(state): State<AppState>,
|
||||
) -> axum::Json<StalePermsResponse> {
|
||||
|
|
@ -375,6 +439,17 @@ pub(super) async fn get_stale_permissions(
|
|||
/// the format check ([`Ident::parse`]) is applied. No rebuild is
|
||||
/// enqueued (the agent doesn't exist to rebuild); the SSE snapshots
|
||||
/// update the P3RM1SS10NS tab live.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/permissions/{agent}",
|
||||
params(("agent" = String, Path, description = "agent name (need not be live)")),
|
||||
responses(
|
||||
(status = 200, description = "stale permission entries cleared", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 500, description = "tool-groups/capabilities file write failed"),
|
||||
),
|
||||
tag = "permissions"
|
||||
)]
|
||||
pub(super) async fn delete_agent_permissions(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use problem_details::ProblemDetails;
|
||||
|
||||
use super::{AppState, error_response};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct AnswerForm {
|
||||
answer: String,
|
||||
}
|
||||
|
|
@ -37,6 +38,20 @@ fn with_cors(resp: impl IntoResponse) -> Response {
|
|||
resp
|
||||
}
|
||||
|
||||
/// `POST /answer-question/{id}` — record the operator's answer and
|
||||
/// notify the asker.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/answer-question/{id}",
|
||||
params(("id" = i64, Path, description = "question row id")),
|
||||
request_body(content = AnswerForm, content_type = "application/x-www-form-urlencoded"),
|
||||
responses(
|
||||
(status = 200, description = "answered", body = String),
|
||||
(status = 400, description = "empty answer"),
|
||||
(status = 500, description = "answer failed (already answered, unknown id, ...)"),
|
||||
),
|
||||
tag = "questions"
|
||||
)]
|
||||
pub(super) async fn post_answer_question(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
@ -85,6 +100,18 @@ pub(super) async fn post_answer_question(
|
|||
/// so it can fall back on whatever default it had. Same code path as
|
||||
/// a real answer — just lets the operator close the loop instead of
|
||||
/// letting the question dangle forever.
|
||||
/// `POST /cancel-question/{id}` — resolve a pending question with the
|
||||
/// `[cancelled]` sentinel answer.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/cancel-question/{id}",
|
||||
params(("id" = i64, Path, description = "question row id")),
|
||||
responses(
|
||||
(status = 200, description = "cancelled", body = String),
|
||||
(status = 500, description = "cancel failed (already answered, unknown id, ...)"),
|
||||
),
|
||||
tag = "questions"
|
||||
)]
|
||||
pub(super) async fn post_cancel_question(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,25 @@ use axum::{
|
|||
use problem_details::ProblemDetails;
|
||||
|
||||
use crate::scheduled_prompts::ScheduleNotFoundOrCancelled;
|
||||
use crate::scheduled_prompts_worker::FireNowReport;
|
||||
|
||||
use super::{AppState, error_problem, error_response};
|
||||
|
||||
/// `GET /api/schedules` — snapshot of every schedule for the
|
||||
/// scheduled-prompts tab. Returns the wire shape directly
|
||||
/// so the frontend can render without an extra translation layer.
|
||||
// `hive_sh4re::WireSchedule` (the actual body) has no `ToSchema` — adding
|
||||
// one would pull `utoipa` into the wire-types crate for a single dashboard
|
||||
// endpoint. `serde_json::Value` placeholder; see the batch report.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/schedules",
|
||||
responses(
|
||||
(status = 200, description = "every schedule, wire shape", body = Vec<serde_json::Value>),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "schedules"
|
||||
)]
|
||||
pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
|
||||
match state.coord.scheduled_prompts.list() {
|
||||
Ok(rows) => {
|
||||
|
|
@ -50,6 +63,20 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
|
|||
/// 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.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/schedules",
|
||||
// `hive_sh4re::SchedulePromptPayload` (the actual body) has no `ToSchema` —
|
||||
// same reasoning as the `Vec<serde_json::Value>` placeholder on
|
||||
// `api_schedules` above.
|
||||
request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"),
|
||||
responses(
|
||||
(status = 200, description = "created; body carries the new row id", body = serde_json::Value),
|
||||
(status = 400, description = "no targets, empty body, or interval_seconds == 0"),
|
||||
(status = 500, description = "submit failed"),
|
||||
),
|
||||
tag = "schedules"
|
||||
)]
|
||||
pub(super) async fn post_schedule_new(
|
||||
State(state): State<AppState>,
|
||||
axum::Json(payload): axum::Json<hive_sh4re::SchedulePromptPayload>,
|
||||
|
|
@ -86,7 +113,7 @@ pub(super) async fn post_schedule_new(
|
|||
|
||||
/// Optional JSON body for `fire-now`. Absent / empty body ⇒
|
||||
/// `reset_timer = false` (back-compat: cadence stays intact).
|
||||
#[derive(serde::Deserialize, Default)]
|
||||
#[derive(serde::Deserialize, Default, utoipa::ToSchema)]
|
||||
pub(super) struct FireNowBody {
|
||||
#[serde(default)]
|
||||
reset_timer: bool,
|
||||
|
|
@ -100,6 +127,17 @@ pub(super) struct FireNowBody {
|
|||
/// wrong." For recurring schedules the cadence stays intact unless
|
||||
/// the body carries `{"reset_timer": true}`, in which case the
|
||||
/// countdown is re-armed from now (`next_fire_at = now + interval`).
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/schedules/{id}/fire-now",
|
||||
params(("id" = i64, Path, description = "schedule row id")),
|
||||
request_body(content = FireNowBody, description = "optional; absent body means reset_timer = false"),
|
||||
responses(
|
||||
(status = 200, description = "fired; per-target outcome counts", body = FireNowReport),
|
||||
(status = 500, description = "schedule missing, cancelled, or fully drained"),
|
||||
),
|
||||
tag = "schedules"
|
||||
)]
|
||||
pub(super) async fn post_schedule_fire_now(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
@ -123,6 +161,13 @@ pub(super) async fn post_schedule_fire_now(
|
|||
/// flip to Cancelled, `{"cancelled": false}` when the DAG was
|
||||
/// Running / terminal / gone. On success a fresh `RebuildQueueChanged`
|
||||
/// snapshot fires so the state flip surfaces live.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/rebuild-queue/{id}/cancel",
|
||||
params(("id" = u64, Path, description = "job-queue DAG id")),
|
||||
responses((status = 200, description = "whether the DAG was cancelled", body = serde_json::Value)),
|
||||
tag = "schedules"
|
||||
)]
|
||||
pub(super) async fn post_rebuild_queue_cancel(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<u64>,
|
||||
|
|
@ -137,14 +182,14 @@ pub(super) async fn post_rebuild_queue_cancel(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Default)]
|
||||
#[derive(serde::Deserialize, Default, utoipa::ToSchema)]
|
||||
pub(super) struct CancelScheduleForm {
|
||||
/// `None` / absent / empty array → cancel whole schedule.
|
||||
#[serde(default)]
|
||||
targets: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Default)]
|
||||
#[derive(serde::Deserialize, Default, utoipa::ToSchema)]
|
||||
#[allow(
|
||||
clippy::option_option,
|
||||
reason = "double-Option carries three-state PATCH semantics on the wire \
|
||||
|
|
@ -203,6 +248,16 @@ where
|
|||
/// `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.
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/api/schedules/{id}",
|
||||
params(("id" = i64, Path, description = "schedule row id")),
|
||||
responses(
|
||||
(status = 200, description = "updated, wire shape", body = serde_json::Value),
|
||||
(status = 500, description = "update failed (cancelled, not found, ...)"),
|
||||
),
|
||||
tag = "schedules"
|
||||
)]
|
||||
pub(super) async fn patch_schedule(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
@ -233,6 +288,17 @@ pub(super) async fn patch_schedule(
|
|||
/// `POST /api/schedules/{id}/pause` — pause a schedule so the worker
|
||||
/// skips it until explicitly resumed. Idempotent; no-op on an already-
|
||||
/// paused row. Returns 404 when the schedule is cancelled or not found.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/schedules/{id}/pause",
|
||||
params(("id" = i64, Path, description = "schedule row id")),
|
||||
responses(
|
||||
(status = 200, description = "paused", body = String),
|
||||
(status = 404, description = "schedule cancelled or not found"),
|
||||
(status = 500, description = "pause failed"),
|
||||
),
|
||||
tag = "schedules"
|
||||
)]
|
||||
pub(super) async fn post_schedule_pause(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
@ -255,6 +321,17 @@ pub(super) async fn post_schedule_pause(
|
|||
/// `POST /api/schedules/{id}/resume` — resume a paused schedule.
|
||||
/// Idempotent; no-op on an already-active row. Returns 404 when the
|
||||
/// schedule is cancelled or not found.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/schedules/{id}/resume",
|
||||
params(("id" = i64, Path, description = "schedule row id")),
|
||||
responses(
|
||||
(status = 200, description = "resumed", body = String),
|
||||
(status = 404, description = "schedule cancelled or not found"),
|
||||
(status = 500, description = "resume failed"),
|
||||
),
|
||||
tag = "schedules"
|
||||
)]
|
||||
pub(super) async fn post_schedule_resume(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
@ -278,6 +355,16 @@ pub(super) async fn post_schedule_resume(
|
|||
/// (whole schedule when no `targets` field, partial when one is
|
||||
/// provided). Operator bypasses the topology check; the manager
|
||||
/// surface enforces it for agent callers.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/schedules/{id}/cancel",
|
||||
params(("id" = i64, Path, description = "schedule row id")),
|
||||
responses(
|
||||
(status = 200, description = "cancelled (whole or partial)", body = String),
|
||||
(status = 500, description = "cancel failed"),
|
||||
),
|
||||
tag = "schedules"
|
||||
)]
|
||||
pub(super) async fn post_schedule_cancel(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ use std::path::Path;
|
|||
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use super::error_response;
|
||||
use crate::paths::{AGENTS_ROOT, SHARED_ROOT};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub(super) struct StateFileQuery {
|
||||
path: String,
|
||||
}
|
||||
|
|
@ -163,6 +164,20 @@ pub fn scan_validated_paths(body: &str) -> Vec<String> {
|
|||
out
|
||||
}
|
||||
|
||||
/// `GET /api/state-file?path=…` — serve an allow-listed per-agent
|
||||
/// `state/` or `shared/` file. Raster images get their real
|
||||
/// content-type; everything else is served as (possibly truncated)
|
||||
/// text.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/state-file",
|
||||
params(StateFileQuery),
|
||||
responses(
|
||||
(status = 200, description = "file contents (text, truncated at 1 MiB) or image bytes"),
|
||||
(status = 500, description = "path outside the allow-list, not a regular file, or read failed"),
|
||||
),
|
||||
tag = "state_files"
|
||||
)]
|
||||
pub(super) async fn get_state_file(
|
||||
axum::extract::Query(q): axum::extract::Query<StateFileQuery>,
|
||||
) -> Response {
|
||||
|
|
|
|||
|
|
@ -291,6 +291,21 @@ where
|
|||
/// minutes.
|
||||
const CRASH_WARNING_WINDOW: std::time::Duration = std::time::Duration::from_mins(10);
|
||||
|
||||
/// `GET /api/state` — cold-load snapshot of the whole dashboard: roster,
|
||||
/// approvals (+ history), questions (+ history), tombstones, job queue,
|
||||
/// meta inputs, and more. Live clients then follow `/api/dashboard/stream`
|
||||
/// (SSE) for incremental updates keyed off `seq`.
|
||||
// `StateSnapshot` is a large tree of nested view types (`ContainerView`,
|
||||
// `ApprovalView`, `QuestionView`, ...) with no `ToSchema` anywhere in that
|
||||
// graph; wiring it up is a schema-modelling project of its own, well past
|
||||
// "annotate what's reachable". `serde_json::Value` placeholder for now —
|
||||
// see the batch report.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/state",
|
||||
responses((status = 200, description = "full dashboard snapshot", body = serde_json::Value)),
|
||||
tag = "state_snapshot"
|
||||
)]
|
||||
pub(super) async fn api_state(
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
|
|
|
|||
|
|
@ -104,6 +104,19 @@ pub(crate) async fn emit_tombstones_snapshot(coord: &Arc<Coordinator>) {
|
|||
});
|
||||
}
|
||||
|
||||
/// `POST /api/purge-tombstone/{name}` — wipe a tombstoned agent's
|
||||
/// retained state dir + applied config dir entirely.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/purge-tombstone/{name}",
|
||||
params(("name" = String, Path, description = "agent name")),
|
||||
responses(
|
||||
(status = 200, description = "purged", body = String),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(status = 500, description = "a live container still exists, or a dir removal failed"),
|
||||
),
|
||||
tag = "tombstones"
|
||||
)]
|
||||
pub(super) async fn post_purge_tombstone(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use problem_details::ProblemDetails;
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ use crate::job_queue::{Source, submit};
|
|||
/// `--root` flag for safety; the HTTP surface is permissive
|
||||
/// because the dashboard form encodes "no value" as the empty
|
||||
/// string for the optional radio-group input.)
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct SetParentForm {
|
||||
child: String,
|
||||
new_parent: Option<String>,
|
||||
|
|
@ -39,7 +40,7 @@ pub(super) struct SetParentForm {
|
|||
|
||||
/// One entry in a `POST /api/topology/set-parent-bulk` JSON array.
|
||||
/// `new_parent`: absent/null/empty-string all mean "promote to root".
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub(super) struct SetParentBulkEntry {
|
||||
child: String,
|
||||
#[serde(default)]
|
||||
|
|
@ -56,6 +57,15 @@ pub(super) struct SetParentBulkEntry {
|
|||
/// re-emits the queue snapshot immediately so the dashboard shows the
|
||||
/// queued move without a refresh; the tree itself repaints once the
|
||||
/// commit lands.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/topology/set-parent",
|
||||
responses(
|
||||
(status = 200, description = "reparent queued", body = String),
|
||||
(status = 400, description = "missing/invalid child or new_parent identifier"),
|
||||
),
|
||||
tag = "topology"
|
||||
)]
|
||||
pub(super) async fn post_set_parent(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<SetParentForm>,
|
||||
|
|
@ -98,6 +108,15 @@ pub(super) async fn post_set_parent(
|
|||
/// First identifier that fails to parse aborts the whole batch before
|
||||
/// anything is submitted — a partially-invalid bulk move never reaches
|
||||
/// the queue.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/topology/set-parent-bulk",
|
||||
responses(
|
||||
(status = 200, description = "reparents queued", body = String),
|
||||
(status = 400, description = "an invalid child identifier in the batch"),
|
||||
),
|
||||
tag = "topology"
|
||||
)]
|
||||
pub(super) async fn post_set_parent_bulk(
|
||||
State(state): State<AppState>,
|
||||
axum::Json(body): axum::Json<Vec<SetParentBulkEntry>>,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,24 @@ pub(super) struct PushWebhookRepo {
|
|||
///
|
||||
/// The gateway routes `/webhook/` → hive-c0re; the HMAC secret protects
|
||||
/// the endpoint from unauthenticated callers.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/webhook/knowledge",
|
||||
request_body(
|
||||
content = String,
|
||||
content_type = "application/json",
|
||||
description = "Forgejo push-webhook payload, taken as raw bytes \
|
||||
(not a typed extractor) so HMAC verification runs \
|
||||
over the exact wire bytes before any JSON parsing"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "processed (pull triggered or ignored)", body = String),
|
||||
(status = 400, description = "invalid JSON payload"),
|
||||
(status = 401, description = "bad or missing HMAC signature"),
|
||||
(status = 503, description = "HMAC secret unavailable at startup"),
|
||||
),
|
||||
tag = "webhook"
|
||||
)]
|
||||
pub(super) async fn post_webhook_knowledge(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
|
|
@ -177,6 +195,25 @@ struct PrWebhookRepo {
|
|||
///
|
||||
/// hive-c0re registers this hook automatically at startup via
|
||||
/// [`crate::forge::ensure_config_pr_webhook`].
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/webhook/config-pr",
|
||||
request_body(
|
||||
content = String,
|
||||
content_type = "application/json",
|
||||
description = "Forgejo pull_request-webhook payload, taken as raw \
|
||||
bytes (not a typed extractor) so HMAC verification \
|
||||
runs over the exact wire bytes before any JSON \
|
||||
parsing"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "processed (approval queued or ignored)", body = String),
|
||||
(status = 400, description = "invalid JSON payload"),
|
||||
(status = 401, description = "bad or missing HMAC signature"),
|
||||
(status = 503, description = "HMAC secret unavailable at startup"),
|
||||
),
|
||||
tag = "webhook"
|
||||
)]
|
||||
pub(super) async fn post_webhook_config_pr(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ use std::time::Duration;
|
|||
|
||||
use serde::Serialize;
|
||||
use tokio::time::sleep;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ const CPU_SAMPLE: Duration = Duration::from_millis(200);
|
|||
|
||||
const MACHINE_SLICE: &str = "/sys/fs/cgroup/machine.slice";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ContainerResource {
|
||||
/// Agent name (without the `h-` machine prefix).
|
||||
pub name: String,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use std::time::Duration;
|
|||
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
use hive_sh4re::wire_time::now_unix;
|
||||
|
|
@ -168,13 +169,13 @@ fn resolve_prices(model: &str, table: &PriceTable) -> Prices {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct KeyCount {
|
||||
pub key: String,
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct AgentRollup {
|
||||
pub name: String,
|
||||
pub turns: u64,
|
||||
|
|
@ -186,7 +187,7 @@ pub struct AgentRollup {
|
|||
pub est_cost_usd: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct HiveStats {
|
||||
pub window: &'static str,
|
||||
pub from: i64,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use hive_sh4re::wire_time::now_unix;
|
|||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::broadcast;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Process-singleton handle, set once at coordinator startup. Lets
|
||||
/// the `lifecycle` module's `run` / `prebuild_toplevel` access the
|
||||
|
|
@ -87,7 +88,7 @@ impl BuildStatus {
|
|||
/// Header-only row returned by `list_recent_for_agent`. Carries the
|
||||
/// metadata the dashboard's agent-card chip needs (status + age +
|
||||
/// id-to-open) without the multi-MB stdout/stderr payload.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct BuildLogHeader {
|
||||
pub id: i64,
|
||||
pub agent: String,
|
||||
|
|
@ -103,7 +104,7 @@ pub struct BuildLogHeader {
|
|||
|
||||
/// Full row with stdout/stderr text inlined. Returned by `get_full`,
|
||||
/// backs the side-panel viewer's payload.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct BuildLogFull {
|
||||
#[serde(flatten)]
|
||||
pub header: BuildLogHeader,
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ fn notify_operator_missing_target(coord: &Coordinator, schedule: &Schedule, targ
|
|||
/// Per-target outcome counts for one `fire_now` invocation.
|
||||
/// Returned to the operator so the dashboard can render
|
||||
/// "fired to N (M failed, K missing)" without a follow-up GET.
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
|
||||
pub struct FireNowReport {
|
||||
/// Targets the broker accepted the message for.
|
||||
pub ok: u32,
|
||||
|
|
|
|||
Loading…
Reference in a new issue