dashboard: remove the flow page and the endpoints only it used
The flow page showed the raw broker message stream and had a compose box. This removes the page and its bundle, its link in the pages menu, and the styles only it used. The history endpoint that backfilled it and the operator-send endpoint behind the compose box had no other caller, so they go too, along with the broker query behind the history endpoint. The stream endpoint stays: the dashboard tabs still subscribe to it. The docs lose the FL0W section and the references to the two endpoints.
This commit is contained in:
parent
acf82beba3
commit
fa131b46a6
24 changed files with 62 additions and 1121 deletions
|
|
@ -1,8 +1,8 @@
|
|||
//! Remaining single-endpoint dashboard handlers: mark-all-read, operator
|
||||
//! compose (`op-send`), spawn-request and hive-wide turn stats.
|
||||
//! Remaining single-endpoint dashboard handlers: mark-all-read,
|
||||
//! spawn-request and hive-wide turn stats.
|
||||
|
||||
use axum::{
|
||||
extract::{Form, Path as AxumPath, State},
|
||||
extract::{Path as AxumPath, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
|
@ -82,67 +82,3 @@ pub(super) async fn post_mark_all_read(
|
|||
Err(e) => error_response(&format!("mark-all-read {name} failed: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Operator-side compose form on the dashboard terminal. Drops a
|
||||
/// message into the broker as `{from: "operator", to, body}`. Same
|
||||
/// shape that per-agent web UIs use via `OperatorMsg`, but here the
|
||||
/// operator picks the recipient explicitly with `@name`. No
|
||||
/// 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, ToSchema)]
|
||||
pub(super) struct OpSendForm {
|
||||
to: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
) -> Response {
|
||||
let to = form.to.trim().to_owned();
|
||||
let body = form.body.trim().to_owned();
|
||||
if to.is_empty() {
|
||||
return error_response("op-send: `to` required");
|
||||
}
|
||||
if body.is_empty() {
|
||||
return error_response("op-send: `body` required");
|
||||
}
|
||||
if to == "*" {
|
||||
let errors = state
|
||||
.coord
|
||||
.broadcast_send(hive_sh4re::manager::OPERATOR_RECIPIENT, &body);
|
||||
if !errors.is_empty() {
|
||||
return error_response(&format!(
|
||||
"op-send broadcast partial fail: {}",
|
||||
errors.join("; ")
|
||||
));
|
||||
}
|
||||
} else if let Err(e) = state.coord.broker.send(&hive_sh4re::inbox::Message {
|
||||
from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::OPERATOR_RECIPIENT),
|
||||
to: to.clone(),
|
||||
body,
|
||||
in_reply_to: None,
|
||||
}) {
|
||||
return error_response(&format!("op-send to {to} failed: {e:#}"));
|
||||
}
|
||||
// 200 instead of 303 → the client doesn't refetch /api/state. The
|
||||
// broker `send` already emitted a `MessageEvent` which the
|
||||
// dashboard channel forwarder mirrors as `DashboardEvent::Sent`,
|
||||
// and the page's terminal derives from that stream — so the
|
||||
// operator's send shows up the same way an agent's send does, with
|
||||
// no full-state refresh in between.
|
||||
(axum::http::StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ fn app(coord: Arc<Coordinator>) -> axum::Router {
|
|||
.routes(routes!(state_files::get_state_file))
|
||||
.routes(routes!(misc_api::api_stats_hive))
|
||||
.routes(routes!(misc_api::post_mark_all_read))
|
||||
.routes(routes!(misc_api::post_op_send))
|
||||
.routes(routes!(build_logs::get_build_logs_all))
|
||||
.routes(routes!(build_logs::get_build_log_for_node))
|
||||
.routes(routes!(build_logs::get_build_log_raw_for_node))
|
||||
|
|
@ -137,7 +136,6 @@ fn app(coord: Arc<Coordinator>) -> axum::Router {
|
|||
.routes(routes!(meta_inputs::post_meta_update))
|
||||
.routes(routes!(build_logs::get_build_log_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_history))
|
||||
.routes(routes!(state_snapshot::jobq_graph))
|
||||
.routes(routes!(state_snapshot::jobq_rollup))
|
||||
.split_for_parts();
|
||||
|
|
@ -371,7 +369,6 @@ mod router_build_probe {
|
|||
.routes(routes!(state_files::get_state_file))
|
||||
.routes(routes!(misc_api::api_stats_hive))
|
||||
.routes(routes!(misc_api::post_mark_all_read))
|
||||
.routes(routes!(misc_api::post_op_send))
|
||||
.routes(routes!(build_logs::get_build_logs_all))
|
||||
.routes(routes!(build_logs::get_build_log_for_node))
|
||||
.routes(routes!(build_logs::get_build_log_raw_for_node))
|
||||
|
|
@ -391,7 +388,6 @@ mod router_build_probe {
|
|||
.routes(routes!(meta_inputs::post_meta_update))
|
||||
.routes(routes!(build_logs::get_build_log_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_history))
|
||||
.routes(routes!(state_snapshot::jobq_graph))
|
||||
.routes(routes!(state_snapshot::jobq_rollup));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
//! `/api/state` cold-load snapshot plus the dashboard's live read side:
|
||||
//! the `StateSnapshot` shape and its view builders, the
|
||||
//! `/api/dashboard/stream` SSE channel, and the `/api/dashboard/history`
|
||||
//! backfill. SPA shape + SSE channels: docs/web-ui/shape.md.
|
||||
//! `/api/dashboard/stream` SSE channel. SPA shape + SSE channels:
|
||||
//! docs/web-ui/shape.md.
|
||||
|
||||
use std::convert::Infallible;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::HeaderMap,
|
||||
response::{
|
||||
IntoResponse, Response,
|
||||
sse::{Event, KeepAlive, Sse},
|
||||
},
|
||||
response::sse::{Event, KeepAlive, Sse},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio_stream::wrappers::BroadcastStream;
|
||||
|
|
@ -20,9 +17,9 @@ use utoipa::IntoParams;
|
|||
|
||||
use crate::container_view::ContainerView;
|
||||
|
||||
use super::AppState;
|
||||
use super::meta_inputs::{MetaInputView, read_meta_inputs};
|
||||
use super::tombstones::{TombstoneView, build_tombstone_views};
|
||||
use super::{AppState, error_response, scan_validated_paths};
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -351,105 +348,14 @@ 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",
|
||||
responses(
|
||||
(status = 200, description = "`{ seq, events }` — up to the last 200 \
|
||||
broker messages as `DashboardEvent::Sent`/`Delivered` JSON, plus \
|
||||
`seq`: the dashboard channel's high-water mark at fetch time \
|
||||
(used by clients to dedupe against buffered live SSE frames)"),
|
||||
),
|
||||
tag = "state_snapshot"
|
||||
)]
|
||||
pub(super) async fn dashboard_history(State(state): State<AppState>) -> Response {
|
||||
// Backfill source for the dashboard terminal. Returns up to ~200
|
||||
// historical broker messages (no other event kinds are persisted)
|
||||
// converted to `DashboardEvent::Sent` JSON so the client can replay
|
||||
// through the same dispatch path as live frames. Wrapped in
|
||||
// `{ seq, events }`: the seq is the dashboard channel's high-water
|
||||
// mark at fetch time. Clients use it to dedupe their buffered live
|
||||
// SSE traffic (drop anything with `seq <= history_seq`) so a frame
|
||||
// that lands between SSE-subscribe and history-fetch isn't shown
|
||||
// twice and isn't lost. Historical rows carry `seq = 0`; the
|
||||
// boundary seq is what closes the dedupe window.
|
||||
const HISTORY_LIMIT: u64 = 200;
|
||||
let seq = state.coord.current_seq();
|
||||
match state.coord.broker.recent_all(HISTORY_LIMIT) {
|
||||
Ok(mut messages) => {
|
||||
messages.reverse();
|
||||
let events: Vec<crate::dashboard_events::DashboardEvent> = messages
|
||||
.into_iter()
|
||||
.map(|m| match m {
|
||||
crate::broker::MessageEvent::Sent {
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
body,
|
||||
at,
|
||||
in_reply_to,
|
||||
} => {
|
||||
let file_refs = scan_validated_paths(&body);
|
||||
crate::dashboard_events::DashboardEvent::Sent {
|
||||
seq: 0,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
body,
|
||||
at: hive_sh4re::wire_time::from_secs(at),
|
||||
in_reply_to,
|
||||
file_refs,
|
||||
}
|
||||
}
|
||||
crate::broker::MessageEvent::Delivered {
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
body,
|
||||
at,
|
||||
in_reply_to,
|
||||
} => {
|
||||
let file_refs = scan_validated_paths(&body);
|
||||
crate::dashboard_events::DashboardEvent::Delivered {
|
||||
seq: 0,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
body,
|
||||
at: hive_sh4re::wire_time::from_secs(at),
|
||||
in_reply_to,
|
||||
file_refs,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
axum::Json(DashboardHistoryBody { seq, events }).into_response()
|
||||
}
|
||||
Err(e) => error_response(&format!("dashboard/history failed: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `/dashboard/stream` query string. Today's only field is `kinds`:
|
||||
/// a comma-separated allow-list of event-`kind` strings.
|
||||
/// Empty / absent ⇒ no filter (current behaviour, all variants
|
||||
/// forwarded). Set ⇒ only the named kinds reach the subscriber,
|
||||
/// non-matches are skipped before the JSON serialise cost.
|
||||
///
|
||||
/// Useful for narrow pages (e.g. `flow.js` only cares about `sent`
|
||||
/// / `delivered` / `container_state_changed` / `container_removed`)
|
||||
/// that want to drop the dispatch overhead on every unrelated mutation.
|
||||
/// Useful for narrow pages that only care about a few kinds and want to
|
||||
/// drop the dispatch overhead on every unrelated mutation.
|
||||
#[derive(Deserialize, Default, IntoParams)]
|
||||
pub(super) struct DashboardStreamQuery {
|
||||
/// Comma-separated event kinds to forward. Each token is
|
||||
|
|
@ -464,8 +370,7 @@ pub(super) struct DashboardStreamQuery {
|
|||
params(DashboardStreamQuery),
|
||||
responses(
|
||||
(status = 200, description = "server-sent event stream; each event's \
|
||||
`data` is a JSON-serialised `DashboardEvent` (seq-tagged; pair \
|
||||
with `/api/dashboard/history` to backfill + dedupe on connect)",
|
||||
`data` is a JSON-serialised `DashboardEvent` (seq-tagged)",
|
||||
body = String, content_type = "text/event-stream"),
|
||||
),
|
||||
tag = "state_snapshot"
|
||||
|
|
|
|||
|
|
@ -269,38 +269,6 @@ impl Broker {
|
|||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Latest `limit` messages across every recipient, newest-first.
|
||||
/// Backs the dashboard's message-flow backfill so a reload doesn't
|
||||
/// blank the operator's view of recent traffic. Returns each row as
|
||||
/// a [`MessageEvent::Sent`] so the dashboard's live renderer (which
|
||||
/// already speaks `MessageEvent`) can replay history through the
|
||||
/// same code path. We don't synthesise `Delivered` events here —
|
||||
/// the recv-side acks live in a different table column and would
|
||||
/// double-render on backfill; the live stream picks them up
|
||||
/// immediately on the first new `recv`.
|
||||
pub fn recent_all(&self, limit: u64) -> Result<Vec<MessageEvent>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let limit_i = i64::try_from(limit.min(i64::MAX as u64)).unwrap_or(i64::MAX);
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, sender, recipient, body, sent_at, in_reply_to
|
||||
FROM messages
|
||||
ORDER BY id DESC
|
||||
LIMIT ?1",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![limit_i], |row| {
|
||||
Ok(MessageEvent::Sent {
|
||||
id: row.get(0)?,
|
||||
from: row.get(1)?,
|
||||
to: row.get(2)?,
|
||||
body: row.get(3)?,
|
||||
at: row.get(4)?,
|
||||
in_reply_to: row.get(5)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect::<rusqlite::Result<Vec<_>>>()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Number of undelivered messages addressed to `recipient`. Non-mutating
|
||||
/// — used by the harness to surface "N unread" in tool-result status
|
||||
/// lines without popping the queue.
|
||||
|
|
|
|||
Loading…
Reference in a new issue