bash-tasks page: live interval poll + spawn_blocking the dir scan (review)
Two fixes from review: - Liveness: /api/state isn't polled while online (only during login), so hooking refreshBashTasks to it only populated on cold load. Bash tasks start + finish asynchronously between turns, so add a light ~4s interval to keep the tasks pill live; the /api/state-time call now just does the first-paint populate. Doc note corrected to match. - Move the blocking dir scan + per-file reads in /api/bash-tasks off the async executor via tokio::task::spawn_blocking (damocles nit).
This commit is contained in:
parent
bf98aa69aa
commit
cd7846b348
3 changed files with 37 additions and 19 deletions
|
|
@ -63,9 +63,11 @@ through. Three flex columns:
|
||||||
click opens the loose-ends flyout.
|
click opens the loose-ends flyout.
|
||||||
- **Tasks pill** (`⚙ tasks · N`): hidden when empty; click opens the
|
- **Tasks pill** (`⚙ tasks · N`): hidden when empty; click opens the
|
||||||
running-bash-tasks flyout (`GET /api/bash-tasks` — the in-flight
|
running-bash-tasks flyout (`GET /api/bash-tasks` — the in-flight
|
||||||
Pending/Running tasks from the in-container `bash-tasks/` dir). Polled
|
Pending/Running tasks from the in-container `bash-tasks/` dir). Unlike
|
||||||
on the same `/api/state` cycle as loose-ends, since tasks complete
|
loose-ends (refreshed on turn_end), tasks start + finish asynchronously
|
||||||
asynchronously between turns. Snapshot only (no SSE push yet).
|
between turns and `/api/state` isn't polled while online, so the pill
|
||||||
|
polls the endpoint on a light interval (≈4s). Snapshot only (no SSE push
|
||||||
|
yet).
|
||||||
- **Overflow button** (`⋯`): always visible. Opens a frosted popover
|
- **Overflow button** (`⋯`): always visible. Opens a frosted popover
|
||||||
(`#overflow-menu`, positioned outside the header to escape any
|
(`#overflow-menu`, positioned outside the header to escape any
|
||||||
stacking context) with four management rows followed by a model
|
stacking context) with four management rows followed by a model
|
||||||
|
|
|
||||||
|
|
@ -1354,9 +1354,11 @@ window.marked = marked;
|
||||||
// db, fetched via the per-agent socket). Cold-load fetches
|
// db, fetched via the per-agent socket). Cold-load fetches
|
||||||
// it here; turn_end refreshes it via the renderer below.
|
// it here; turn_end refreshes it via the renderer below.
|
||||||
refreshLooseEnds();
|
refreshLooseEnds();
|
||||||
// Running bash tasks live in the in-container bash-tasks dir (not
|
// Cold-load populate of the running-bash-tasks pill. Tasks complete
|
||||||
// /api/state); same poll cadence as loose-ends. They complete async
|
// asynchronously between turns (independent of turn_end SSE) and
|
||||||
// between turns, so polling on each state cycle keeps the count live.
|
// /api/state isn't polled while online, so a dedicated interval (set
|
||||||
|
// up next to the initial refreshState() below) keeps the count live;
|
||||||
|
// this call just fills it immediately on first paint.
|
||||||
refreshBashTasks();
|
refreshBashTasks();
|
||||||
// Skip the re-render if nothing structurally changed. The most
|
// Skip the re-render if nothing structurally changed. The most
|
||||||
// common case is `online` polling itself — without this guard, the
|
// common case is `online` polling itself — without this guard, the
|
||||||
|
|
@ -1387,6 +1389,12 @@ window.marked = marked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshState();
|
refreshState();
|
||||||
|
// Keep the running-bash-tasks pill live. Unlike loose-ends (refreshed on
|
||||||
|
// turn_end SSE), bash tasks start + finish asynchronously between turns and
|
||||||
|
// /api/state isn't polled while online — so poll the cheap snapshot endpoint
|
||||||
|
// on a light interval. Fails silently (renders zero) when offline. v1 is
|
||||||
|
// polling; an SSE push for task state could replace this later.
|
||||||
|
setInterval(refreshBashTasks, 4000);
|
||||||
|
|
||||||
// ─── live event stream ──────────────────────────────────────────────────
|
// ─── live event stream ──────────────────────────────────────────────────
|
||||||
// Scrolling, pill, backfill + SSE plumbing live in hive-fr0nt::TERMINAL_JS
|
// Scrolling, pill, backfill + SSE plumbing live in hive-fr0nt::TERMINAL_JS
|
||||||
|
|
|
||||||
|
|
@ -539,8 +539,13 @@ async fn api_loose_ends(State(state): State<AppState>) -> Response {
|
||||||
/// fail the whole list.
|
/// fail the whole list.
|
||||||
async fn api_bash_tasks() -> Response {
|
async fn api_bash_tasks() -> Response {
|
||||||
let dir = crate::paths::harness_dir().join("bash-tasks");
|
let dir = crate::paths::harness_dir().join("bash-tasks");
|
||||||
let mut tasks: Vec<hive_sh4re::TaskFile> = Vec::new();
|
// The dir scan + per-file reads are blocking fs I/O; run them off the
|
||||||
if let Ok(rd) = std::fs::read_dir(&dir) {
|
// async executor so a slow or large tasks dir can't stall other requests.
|
||||||
|
let tasks = tokio::task::spawn_blocking(move || {
|
||||||
|
let mut tasks: Vec<hive_sh4re::TaskFile> = Vec::new();
|
||||||
|
let Ok(rd) = std::fs::read_dir(&dir) else {
|
||||||
|
return tasks;
|
||||||
|
};
|
||||||
for entry in rd.flatten() {
|
for entry in rd.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
||||||
|
|
@ -559,17 +564,20 @@ async fn api_bash_tasks() -> Response {
|
||||||
tasks.push(task);
|
tasks.push(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Running before Pending, then oldest-first so a long-runner sits on top.
|
||||||
// Running before Pending, then oldest-first so a long-runner sits on top.
|
tasks.sort_by(|a, b| {
|
||||||
tasks.sort_by(|a, b| {
|
let rank = |s: &hive_sh4re::TaskStatus| match s {
|
||||||
let rank = |s: &hive_sh4re::TaskStatus| match s {
|
hive_sh4re::TaskStatus::Running => 0,
|
||||||
hive_sh4re::TaskStatus::Running => 0,
|
_ => 1,
|
||||||
_ => 1,
|
};
|
||||||
};
|
rank(&a.status)
|
||||||
rank(&a.status)
|
.cmp(&rank(&b.status))
|
||||||
.cmp(&rank(&b.status))
|
.then(a.created_at.cmp(&b.created_at))
|
||||||
.then(a.created_at.cmp(&b.created_at))
|
});
|
||||||
});
|
tasks
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
axum::Json(serde_json::json!({ "tasks": tasks })).into_response()
|
axum::Json(serde_json::json!({ "tasks": tasks })).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue