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;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
struct LiveBody {
|
||||
status: &'static str,
|
||||
}
|
||||
|
||||
/// Liveness. Always `200`; no further checks.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
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"
|
||||
)]
|
||||
pub(super) async fn get_health_live() -> Response {
|
||||
(
|
||||
StatusCode::OK,
|
||||
axum::Json(serde_json::json!({ "status": "ok" })),
|
||||
)
|
||||
.into_response()
|
||||
(StatusCode::OK, axum::Json(LiveBody { status: "ok" })).into_response()
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
|
|
|
|||
|
|
@ -8,26 +8,41 @@ use axum::{
|
|||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use super::{AppState, Ident, error_response, scan_validated_paths};
|
||||
use crate::audit_log::AuditEntry;
|
||||
use crate::container_stats::ContainerResource;
|
||||
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.
|
||||
///
|
||||
/// Returns messages addressed to `"operator"` that haven't been
|
||||
/// acked yet (the operator clears them via the existing
|
||||
/// `POST /api/agent/operator/mark-all-read`). Newest-first; path-shaped
|
||||
/// tokens are validated so the client renders file links like the
|
||||
/// terminal does. Shape: `{ "messages": [{ id, from, body, at,
|
||||
/// in_reply_to, file_refs }] }`.
|
||||
/// terminal does.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/operator-inbox",
|
||||
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"),
|
||||
),
|
||||
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)
|
||||
{
|
||||
Ok(messages) => {
|
||||
let items: Vec<serde_json::Value> = messages
|
||||
let messages: Vec<OperatorInboxItem> = messages
|
||||
.into_iter()
|
||||
.filter_map(|m| {
|
||||
let crate::broker::MessageEvent::Sent {
|
||||
|
|
@ -55,17 +70,17 @@ pub(super) async fn api_operator_inbox(State(state): State<AppState>) -> Respons
|
|||
return None;
|
||||
};
|
||||
let file_refs = scan_validated_paths(&body);
|
||||
Some(serde_json::json!({
|
||||
"id": id,
|
||||
"from": from,
|
||||
"body": body,
|
||||
"at": hive_sh4re::wire_time::from_secs(at),
|
||||
"in_reply_to": in_reply_to,
|
||||
"file_refs": file_refs,
|
||||
}))
|
||||
Some(OperatorInboxItem {
|
||||
id,
|
||||
from,
|
||||
at: hive_sh4re::wire_time::from_secs(at),
|
||||
body,
|
||||
in_reply_to,
|
||||
file_refs,
|
||||
})
|
||||
})
|
||||
.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:#}")),
|
||||
}
|
||||
|
|
@ -114,18 +129,23 @@ pub(super) async fn api_container_resources() -> 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
|
||||
/// audit entries, newest first (server-clamped to 500).
|
||||
///
|
||||
/// Backs the operator dashboard's audit view. Returns
|
||||
/// `{ "entries": [AuditEntry…], "total": N }` so the UI can show
|
||||
/// Backs the operator dashboard's audit view. `total` lets the UI 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 = 200, description = "recent audit entries + total count", body = AuditLogBody),
|
||||
(status = 500, description = "sqlite read failed"),
|
||||
),
|
||||
tag = "misc_api"
|
||||
|
|
@ -140,7 +160,12 @@ pub(super) async fn api_audit_log(State(state): State<AppState>) -> Response {
|
|||
Ok(n) => n,
|
||||
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
|
||||
|
|
@ -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
|
||||
/// `delivered_at` for any still-pending rows so vacuum can collect
|
||||
/// them). Returns `{ "marked": N }` so the frontend can show "cleared
|
||||
/// N messages" feedback without an extra fetch.
|
||||
/// them). `marked` lets the frontend 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 = 200, description = "count of messages marked read", body = MarkAllReadBody),
|
||||
(status = 400, description = "bad agent name"),
|
||||
(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()) {
|
||||
Ok(n) => {
|
||||
tracing::info!(%name, marked = n, "operator marked all messages read");
|
||||
axum::Json(serde_json::json!({ "marked": n })).into_response()
|
||||
Ok(marked) => {
|
||||
tracing::info!(%name, marked, "operator marked all messages read");
|
||||
axum::Json(MarkAllReadBody { marked }).into_response()
|
||||
}
|
||||
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};
|
||||
|
||||
#[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
|
||||
/// scheduled-prompts tab.
|
||||
///
|
||||
|
|
@ -74,7 +84,7 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
|
|||
// `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 = 200, description = "created; body carries the new row id", body = NewScheduleBody),
|
||||
(status = 400, description = "no targets, empty body, or interval_seconds == 0"),
|
||||
(status = 500, description = "submit failed"),
|
||||
),
|
||||
|
|
@ -108,7 +118,7 @@ pub(super) async fn post_schedule_new(
|
|||
match state.coord.scheduled_prompts.submit(&new) {
|
||||
Ok(id) => {
|
||||
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:#}"))),
|
||||
}
|
||||
|
|
@ -177,7 +187,7 @@ pub(super) async fn post_schedule_fire_now(
|
|||
post,
|
||||
path = "/api/rebuild-queue/{id}/cancel",
|
||||
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"
|
||||
)]
|
||||
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
|
||||
// scheduler picks up on its next pass — nothing to fire from here.
|
||||
state.coord.emit_rebuild_queue_snapshot();
|
||||
axum::Json(serde_json::json!({"cancelled": true})).into_response()
|
||||
axum::Json(CancelResultBody { cancelled: true }).into_response()
|
||||
} 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())
|
||||
}
|
||||
|
||||
/// 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(
|
||||
get,
|
||||
path = "/api/dashboard/history",
|
||||
|
|
@ -798,7 +810,7 @@ pub(super) async fn dashboard_history(State(state): State<AppState>) -> Response
|
|||
}
|
||||
})
|
||||
.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:#}")),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use anyhow::{Context, Result};
|
|||
use chrono::{DateTime, Utc};
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Process-singleton handle, set once at coordinator startup. Mirrors
|
||||
/// `build_logs::GLOBAL` — lets recording sites write without threading an
|
||||
|
|
@ -80,7 +81,7 @@ impl AuditOutcome {
|
|||
}
|
||||
|
||||
/// One audit row as returned to the dashboard.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct AuditEntry {
|
||||
pub id: i64,
|
||||
pub ts_unix: DateTime<Utc>,
|
||||
|
|
|
|||
Loading…
Reference in a new issue