dashboard: add GET /api/build-logs/{agent} + /id/{id} endpoints (#726 phase 2)
Wires the Phase 1 sqlite store into the dashboard HTTP layer so the
frontend can surface full build logs without hitting journald.
Two new read endpoints:
- GET /api/build-logs/{agent}?limit=N — Vec<BuildLogHeader> JSON,
newest first, default limit 10, server-side cap at 50.
- GET /api/build-logs/id/{id} — BuildLogFull JSON (header +
stdout + stderr), HTTP 404 on vacuum-reaped / unknown ids.
Agent-name validated ([a-z0-9_-], 1-63 chars) before the sqlite call.
Both handlers reach coord.build_logs (Arc<BuildLogs>) introduced in
the Phase 1 commit. Docs updated in docs/web-ui.md.
This commit is contained in:
parent
cbe8d25e25
commit
f6b3145349
2 changed files with 57 additions and 0 deletions
|
|
@ -836,6 +836,16 @@ that's a browser-level decision, not ours.
|
|||
`Sent` event re-renders both the message-flow terminal and
|
||||
the operator inbox without a snapshot refetch. Used by the
|
||||
compose textbox under MESS4GE FL0W.
|
||||
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log
|
||||
headers for one agent, newest first. Returns
|
||||
`Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`,
|
||||
`started_at`, `finished_at`, `status` (`"ok"` / `"fail"` /
|
||||
`null` while in-progress). `limit` defaults to 10, server-side
|
||||
cap at 50. Agent name validated (`[a-z0-9_-]`, 1-63 chars).
|
||||
- `GET /api/build-logs/id/{id}` — full build log by id. Returns
|
||||
`BuildLogFull` (JSON): all header fields plus `stdout` and
|
||||
`stderr` as plain text (newline-terminated lines, utf-8). HTTP
|
||||
404 when the row is missing (vacuum-reaped or stale id).
|
||||
- `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for
|
||||
a managed container; rendered in the side panel.
|
||||
- `GET /api/approval-diff/{id}?base=applied|approved|previous` —
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/api/approval-diff/{id}", get(get_approval_diff))
|
||||
.route("/api/state-file", get(get_state_file))
|
||||
.route("/api/reminders", get(api_reminders))
|
||||
.route("/api/build-logs/{agent}", get(get_build_logs_agent))
|
||||
.route("/api/build-logs/id/{id}", get(get_build_log_full))
|
||||
.route("/api/agent/{name}/links", get(get_agent_links))
|
||||
.route("/api/agent/{name}/mark-all-read", post(post_mark_all_read))
|
||||
.route("/cancel-reminder/{id}", post(post_cancel_reminder))
|
||||
|
|
@ -1627,6 +1629,51 @@ async fn api_reminders(State(state): State<AppState>) -> Response {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BuildLogsQuery {
|
||||
/// Maximum number of rows to return. Capped server-side at 50
|
||||
/// (see `build_logs::list_recent_for_agent`). Default 10.
|
||||
#[serde(default)]
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
/// `GET /api/build-logs/{agent}?limit=N` — most-recent build log
|
||||
/// headers for one agent, newest first. Returns
|
||||
/// `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.
|
||||
async fn get_build_logs_agent(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
axum::extract::Query(q): axum::extract::Query<BuildLogsQuery>,
|
||||
) -> Response {
|
||||
if let Some(reason) = validate_agent_name(&name) {
|
||||
return (StatusCode::BAD_REQUEST, format!("bad agent name: {reason}")).into_response();
|
||||
}
|
||||
let limit = q.limit.unwrap_or(10);
|
||||
match state.coord.build_logs.list_recent_for_agent(&name, limit) {
|
||||
Ok(rows) => axum::Json(rows).into_response(),
|
||||
Err(e) => error_response(&format!("build-logs {name}: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET /api/build-logs/id/{id}` — full build log row (stdout +
|
||||
/// 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).
|
||||
async fn get_build_log_full(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(id): AxumPath<i64>,
|
||||
) -> Response {
|
||||
match state.coord.build_logs.get_full(id) {
|
||||
Ok(Some(log)) => axum::Json(log).into_response(),
|
||||
Ok(None) => {
|
||||
(StatusCode::NOT_FOUND, format!("build log #{id} not found")).into_response()
|
||||
}
|
||||
Err(e) => error_response(&format!("build-log {id}: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue