hive-c0re: replace json! with typed structs in dashboard handlers
This commit is contained in:
parent
fbffccbbb2
commit
351341e87c
5 changed files with 86 additions and 37 deletions
|
|
@ -28,19 +28,20 @@ use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::host_stats::ServerWarning;
|
use crate::host_stats::ServerWarning;
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
struct LiveBody {
|
||||||
|
status: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
/// Liveness. Always `200`; no further checks.
|
/// Liveness. Always `200`; no further checks.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/health/live",
|
path = "/health/live",
|
||||||
responses((status = 200, description = "process is up", body = serde_json::Value)),
|
responses((status = 200, description = "process is up", body = LiveBody)),
|
||||||
tag = "health"
|
tag = "health"
|
||||||
)]
|
)]
|
||||||
pub(super) async fn get_health_live() -> Response {
|
pub(super) async fn get_health_live() -> Response {
|
||||||
(
|
(StatusCode::OK, axum::Json(LiveBody { status: "ok" })).into_response()
|
||||||
StatusCode::OK,
|
|
||||||
axum::Json(serde_json::json!({ "status": "ok" })),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
|
|
|
||||||
|
|
@ -8,26 +8,41 @@ use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::{IntoParams, ToSchema};
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
use super::{AppState, Ident, error_response, scan_validated_paths};
|
use super::{AppState, Ident, error_response, scan_validated_paths};
|
||||||
|
use crate::audit_log::AuditEntry;
|
||||||
use crate::container_stats::ContainerResource;
|
use crate::container_stats::ContainerResource;
|
||||||
use crate::hive_stats::HiveStats;
|
use crate::hive_stats::HiveStats;
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub(super) struct OperatorInboxItem {
|
||||||
|
id: i64,
|
||||||
|
from: String,
|
||||||
|
body: String,
|
||||||
|
at: chrono::DateTime<chrono::Utc>,
|
||||||
|
in_reply_to: Option<i64>,
|
||||||
|
file_refs: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub(super) struct OperatorInboxBody {
|
||||||
|
messages: Vec<OperatorInboxItem>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Unread operator-directed messages for the dashboard's Y3R C4LL inbox.
|
/// Unread operator-directed messages for the dashboard's Y3R C4LL inbox.
|
||||||
///
|
///
|
||||||
/// Returns messages addressed to `"operator"` that haven't been
|
/// Returns messages addressed to `"operator"` that haven't been
|
||||||
/// acked yet (the operator clears them via the existing
|
/// acked yet (the operator clears them via the existing
|
||||||
/// `POST /api/agent/operator/mark-all-read`). Newest-first; path-shaped
|
/// `POST /api/agent/operator/mark-all-read`). Newest-first; path-shaped
|
||||||
/// tokens are validated so the client renders file links like the
|
/// tokens are validated so the client renders file links like the
|
||||||
/// terminal does. Shape: `{ "messages": [{ id, from, body, at,
|
/// terminal does.
|
||||||
/// in_reply_to, file_refs }] }`.
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/operator-inbox",
|
path = "/api/operator-inbox",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "unread operator-directed messages", body = serde_json::Value),
|
(status = 200, description = "unread operator-directed messages", body = OperatorInboxBody),
|
||||||
(status = 500, description = "broker read failed"),
|
(status = 500, description = "broker read failed"),
|
||||||
),
|
),
|
||||||
tag = "misc_api"
|
tag = "misc_api"
|
||||||
|
|
@ -40,7 +55,7 @@ pub(super) async fn api_operator_inbox(State(state): State<AppState>) -> Respons
|
||||||
.unread_for_recipient("operator", INBOX_LIMIT)
|
.unread_for_recipient("operator", INBOX_LIMIT)
|
||||||
{
|
{
|
||||||
Ok(messages) => {
|
Ok(messages) => {
|
||||||
let items: Vec<serde_json::Value> = messages
|
let messages: Vec<OperatorInboxItem> = messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|m| {
|
.filter_map(|m| {
|
||||||
let crate::broker::MessageEvent::Sent {
|
let crate::broker::MessageEvent::Sent {
|
||||||
|
|
@ -55,17 +70,17 @@ pub(super) async fn api_operator_inbox(State(state): State<AppState>) -> Respons
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let file_refs = scan_validated_paths(&body);
|
let file_refs = scan_validated_paths(&body);
|
||||||
Some(serde_json::json!({
|
Some(OperatorInboxItem {
|
||||||
"id": id,
|
id,
|
||||||
"from": from,
|
from,
|
||||||
"body": body,
|
at: hive_sh4re::wire_time::from_secs(at),
|
||||||
"at": hive_sh4re::wire_time::from_secs(at),
|
body,
|
||||||
"in_reply_to": in_reply_to,
|
in_reply_to,
|
||||||
"file_refs": file_refs,
|
file_refs,
|
||||||
}))
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
axum::Json(serde_json::json!({ "messages": items })).into_response()
|
axum::Json(OperatorInboxBody { messages }).into_response()
|
||||||
}
|
}
|
||||||
Err(e) => error_response(&format!("operator-inbox failed: {e:#}")),
|
Err(e) => error_response(&format!("operator-inbox failed: {e:#}")),
|
||||||
}
|
}
|
||||||
|
|
@ -114,18 +129,23 @@ pub(super) async fn api_container_resources() -> Response {
|
||||||
axum::Json(crate::container_stats::gather().await).into_response()
|
axum::Json(crate::container_stats::gather().await).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub(super) struct AuditLogBody {
|
||||||
|
entries: Vec<AuditEntry>,
|
||||||
|
total: i64,
|
||||||
|
}
|
||||||
|
|
||||||
/// Most-recent agent-initiated privileged-action
|
/// Most-recent agent-initiated privileged-action
|
||||||
/// audit entries, newest first (server-clamped to 500).
|
/// audit entries, newest first (server-clamped to 500).
|
||||||
///
|
///
|
||||||
/// Backs the operator dashboard's audit view. Returns
|
/// Backs the operator dashboard's audit view. `total` lets the UI show
|
||||||
/// `{ "entries": [AuditEntry…], "total": N }` so the UI can show
|
|
||||||
/// "latest 500 of N" rather than silently capping. `ts_unix` is in
|
/// "latest 500 of N" rather than silently capping. `ts_unix` is in
|
||||||
/// **seconds**.
|
/// **seconds**.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/audit-log",
|
path = "/api/audit-log",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "recent audit entries + total count", body = serde_json::Value),
|
(status = 200, description = "recent audit entries + total count", body = AuditLogBody),
|
||||||
(status = 500, description = "sqlite read failed"),
|
(status = 500, description = "sqlite read failed"),
|
||||||
),
|
),
|
||||||
tag = "misc_api"
|
tag = "misc_api"
|
||||||
|
|
@ -140,7 +160,12 @@ pub(super) async fn api_audit_log(State(state): State<AppState>) -> Response {
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(e) => return error_response(&format!("audit-log count: {e:#}")),
|
Err(e) => return error_response(&format!("audit-log count: {e:#}")),
|
||||||
};
|
};
|
||||||
axum::Json(serde_json::json!({ "entries": entries, "total": total })).into_response()
|
axum::Json(AuditLogBody { entries, total }).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub(super) struct MarkAllReadBody {
|
||||||
|
marked: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Operator-driven "clear this agent's inbox" — backs the side-panel
|
/// Operator-driven "clear this agent's inbox" — backs the side-panel
|
||||||
|
|
@ -148,14 +173,14 @@ pub(super) async fn api_audit_log(State(state): State<AppState>) -> Response {
|
||||||
///
|
///
|
||||||
/// Marks every message addressed to the agent as acked (backfilling
|
/// Marks every message addressed to the agent as acked (backfilling
|
||||||
/// `delivered_at` for any still-pending rows so vacuum can collect
|
/// `delivered_at` for any still-pending rows so vacuum can collect
|
||||||
/// them). Returns `{ "marked": N }` so the frontend can show "cleared
|
/// them). `marked` lets the frontend show "cleared N messages"
|
||||||
/// N messages" feedback without an extra fetch.
|
/// feedback without an extra fetch.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/api/agent/{name}/mark-all-read",
|
path = "/api/agent/{name}/mark-all-read",
|
||||||
params(("name" = String, Path, description = "agent name")),
|
params(("name" = String, Path, description = "agent name")),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "count of messages marked read", body = serde_json::Value),
|
(status = 200, description = "count of messages marked read", body = MarkAllReadBody),
|
||||||
(status = 400, description = "bad agent name"),
|
(status = 400, description = "bad agent name"),
|
||||||
(status = 500, description = "broker write failed"),
|
(status = 500, description = "broker write failed"),
|
||||||
),
|
),
|
||||||
|
|
@ -172,9 +197,9 @@ pub(super) async fn post_mark_all_read(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match state.coord.broker.mark_all_read(name.as_str()) {
|
match state.coord.broker.mark_all_read(name.as_str()) {
|
||||||
Ok(n) => {
|
Ok(marked) => {
|
||||||
tracing::info!(%name, marked = n, "operator marked all messages read");
|
tracing::info!(%name, marked, "operator marked all messages read");
|
||||||
axum::Json(serde_json::json!({ "marked": n })).into_response()
|
axum::Json(MarkAllReadBody { marked }).into_response()
|
||||||
}
|
}
|
||||||
Err(e) => error_response(&format!("mark-all-read {name} failed: {e:#}")),
|
Err(e) => error_response(&format!("mark-all-read {name} failed: {e:#}")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,16 @@ use crate::scheduled_prompts_worker::FireNowReport;
|
||||||
|
|
||||||
use super::{AppState, error_problem, error_response};
|
use super::{AppState, error_problem, error_response};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub(super) struct NewScheduleBody {
|
||||||
|
id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, utoipa::ToSchema)]
|
||||||
|
pub(super) struct CancelResultBody {
|
||||||
|
cancelled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapshot of every schedule for the
|
/// Snapshot of every schedule for the
|
||||||
/// scheduled-prompts tab.
|
/// scheduled-prompts tab.
|
||||||
///
|
///
|
||||||
|
|
@ -74,7 +84,7 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
|
||||||
// `api_schedules` above.
|
// `api_schedules` above.
|
||||||
request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"),
|
request_body(content = serde_json::Value, description = "SchedulePromptPayload wire shape"),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "created; body carries the new row id", body = serde_json::Value),
|
(status = 200, description = "created; body carries the new row id", body = NewScheduleBody),
|
||||||
(status = 400, description = "no targets, empty body, or interval_seconds == 0"),
|
(status = 400, description = "no targets, empty body, or interval_seconds == 0"),
|
||||||
(status = 500, description = "submit failed"),
|
(status = 500, description = "submit failed"),
|
||||||
),
|
),
|
||||||
|
|
@ -108,7 +118,7 @@ pub(super) async fn post_schedule_new(
|
||||||
match state.coord.scheduled_prompts.submit(&new) {
|
match state.coord.scheduled_prompts.submit(&new) {
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
state.coord.emit_schedules_snapshot();
|
state.coord.emit_schedules_snapshot();
|
||||||
Ok(axum::Json(serde_json::json!({"id": id})).into_response())
|
Ok(axum::Json(NewScheduleBody { id }).into_response())
|
||||||
}
|
}
|
||||||
Err(e) => Err(error_problem(&format!("schedule submit: {e:#}"))),
|
Err(e) => Err(error_problem(&format!("schedule submit: {e:#}"))),
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +187,7 @@ pub(super) async fn post_schedule_fire_now(
|
||||||
post,
|
post,
|
||||||
path = "/api/rebuild-queue/{id}/cancel",
|
path = "/api/rebuild-queue/{id}/cancel",
|
||||||
params(("id" = u64, Path, description = "job-queue node id (a DAG's root cancels the group)")),
|
params(("id" = u64, Path, description = "job-queue node id (a DAG's root cancels the group)")),
|
||||||
responses((status = 200, description = "whether the DAG was cancelled", body = serde_json::Value)),
|
responses((status = 200, description = "whether the DAG was cancelled", body = CancelResultBody)),
|
||||||
tag = "schedules"
|
tag = "schedules"
|
||||||
)]
|
)]
|
||||||
pub(super) async fn post_rebuild_queue_cancel(
|
pub(super) async fn post_rebuild_queue_cancel(
|
||||||
|
|
@ -188,9 +198,9 @@ pub(super) async fn post_rebuild_queue_cancel(
|
||||||
// Any terminal side effect is the DAG's own spared tail node, which the
|
// Any terminal side effect is the DAG's own spared tail node, which the
|
||||||
// scheduler picks up on its next pass — nothing to fire from here.
|
// scheduler picks up on its next pass — nothing to fire from here.
|
||||||
state.coord.emit_rebuild_queue_snapshot();
|
state.coord.emit_rebuild_queue_snapshot();
|
||||||
axum::Json(serde_json::json!({"cancelled": true})).into_response()
|
axum::Json(CancelResultBody { cancelled: true }).into_response()
|
||||||
} else {
|
} else {
|
||||||
axum::Json(serde_json::json!({"cancelled": false})).into_response()
|
axum::Json(CancelResultBody { cancelled: false }).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -726,6 +726,18 @@ pub(super) async fn jobq_rollup(
|
||||||
axum::Json(state.coord.job_queue.state_rollup())
|
axum::Json(state.coord.job_queue.state_rollup())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Response body for `/api/dashboard/history`. No `ToSchema` — its
|
||||||
|
/// `events` field wraps [`crate::dashboard_events::DashboardEvent`],
|
||||||
|
/// which doesn't derive `ToSchema` either (a large enum with many
|
||||||
|
/// variants; see that type's doc comment for why annotating it is
|
||||||
|
/// out of scope here). The `responses(...)` doc below spells out the
|
||||||
|
/// shape in prose instead of a `body = ...` reference.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DashboardHistoryBody {
|
||||||
|
seq: u64,
|
||||||
|
events: Vec<crate::dashboard_events::DashboardEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/dashboard/history",
|
path = "/api/dashboard/history",
|
||||||
|
|
@ -798,7 +810,7 @@ pub(super) async fn dashboard_history(State(state): State<AppState>) -> Response
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
axum::Json(serde_json::json!({ "seq": seq, "events": events })).into_response()
|
axum::Json(DashboardHistoryBody { seq, events }).into_response()
|
||||||
}
|
}
|
||||||
Err(e) => error_response(&format!("dashboard/history failed: {e:#}")),
|
Err(e) => error_response(&format!("dashboard/history failed: {e:#}")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ use anyhow::{Context, Result};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
/// Process-singleton handle, set once at coordinator startup. Mirrors
|
/// Process-singleton handle, set once at coordinator startup. Mirrors
|
||||||
/// `build_logs::GLOBAL` — lets recording sites write without threading an
|
/// `build_logs::GLOBAL` — lets recording sites write without threading an
|
||||||
|
|
@ -80,7 +81,7 @@ impl AuditOutcome {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One audit row as returned to the dashboard.
|
/// One audit row as returned to the dashboard.
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
pub struct AuditEntry {
|
pub struct AuditEntry {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub ts_unix: DateTime<Utc>,
|
pub ts_unix: DateTime<Utc>,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue