Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cab121b35 | ||
|
|
59041d4f03 |
4 changed files with 81 additions and 1 deletions
|
|
@ -81,6 +81,12 @@ code {
|
||||||
color: var(--purple); border-color: var(--purple);
|
color: var(--purple); border-color: var(--purple);
|
||||||
text-shadow: 0 0 6px color-mix(in srgb, var(--purple) 40%, transparent);
|
text-shadow: 0 0 6px color-mix(in srgb, var(--purple) 40%, transparent);
|
||||||
}
|
}
|
||||||
|
/* Paused agent: turn loop parked, container still up. Clickable (btn-inline):
|
||||||
|
clicking sends POST /api/resume/{name} so the badge doubles as a resume button. */
|
||||||
|
.badge-paused {
|
||||||
|
color: var(--yellow); border-color: var(--yellow);
|
||||||
|
background: color-mix(in srgb, var(--yellow) 10%, transparent);
|
||||||
|
}
|
||||||
/* Active Claude model badge on dashboard container rows. */
|
/* Active Claude model badge on dashboard container rows. */
|
||||||
.badge-model {
|
.badge-model {
|
||||||
color: var(--blue); border-color: var(--blue);
|
color: var(--blue); border-color: var(--blue);
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,19 @@ function buildAgentMenu(c, forgeBase) {
|
||||||
menuItem('▶ ST4RT', { action: '/api/start/', confirm: `start ${c.name}?` }),
|
menuItem('▶ ST4RT', { action: '/api/start/', confirm: `start ${c.name}?` }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Pause/resume is orthogonal to running: a paused stopped agent boots
|
||||||
|
// paused; a paused running agent keeps its container but drives no turns.
|
||||||
|
if (c.paused) {
|
||||||
|
dropdown.append(
|
||||||
|
menuItem('▶ R3SUM3', { action: '/api/resume/', confirm: `resume ${c.name}? the turn loop restarts and drains queued messages.` }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dropdown.append(
|
||||||
|
menuItem('⏸ P4US3', { action: '/api/pause/', confirm: `pause ${c.name}? parks the turn loop — inbox messages queue unacked.` }),
|
||||||
|
);
|
||||||
|
}
|
||||||
dropdown.append(
|
dropdown.append(
|
||||||
|
menuSep(),
|
||||||
menuItem('↻ R3BU1LD', { action: '/api/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
|
menuItem('↻ R3BU1LD', { action: '/api/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
|
||||||
menuSep(),
|
menuSep(),
|
||||||
// Deep-link to the AGENT log tab pre-filtered to this container.
|
// Deep-link to the AGENT log tab pre-filtered to this container.
|
||||||
|
|
@ -457,6 +469,7 @@ function containerRowFingerprint(c, node, pending, opRunning, selected,
|
||||||
askerCount, targetCount, gatewayLinks, hostname) {
|
askerCount, targetCount, gatewayLinks, hostname) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
running: c.running,
|
running: c.running,
|
||||||
|
paused: c.paused,
|
||||||
needs_login: c.needs_login,
|
needs_login: c.needs_login,
|
||||||
needs_update: c.needs_update,
|
needs_update: c.needs_update,
|
||||||
active_model: c.active_model,
|
active_model: c.active_model,
|
||||||
|
|
@ -657,6 +670,15 @@ function buildContainerLi(c, node, opts) {
|
||||||
{ class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
|
{ class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
|
||||||
'needs login →'));
|
'needs login →'));
|
||||||
}
|
}
|
||||||
|
if (c.paused) {
|
||||||
|
// Paused badge is also a resume button: clicking POSTs /api/resume/{name}
|
||||||
|
// which removes the marker and flips the badge off via the SSE rescan.
|
||||||
|
head.append(form(
|
||||||
|
'/api/resume/' + c.name, 'badge badge-paused btn-inline', '⏸ paused',
|
||||||
|
`resume ${c.name}? the turn loop restarts and drains queued messages.`,
|
||||||
|
{}, { noRefresh: true },
|
||||||
|
));
|
||||||
|
}
|
||||||
if (c.needs_update) {
|
if (c.needs_update) {
|
||||||
head.append(form(
|
head.append(form(
|
||||||
'/api/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',
|
'/api/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ pub(super) struct GracefulParams {
|
||||||
graceful: bool,
|
graceful: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
use super::{AppState, error_response, guard_agent_name, strip_container_prefix};
|
use super::{AppState, Ident, error_response, guard_agent_name, strip_container_prefix};
|
||||||
use crate::job_queue::{Source, submit};
|
use crate::job_queue::{Source, submit};
|
||||||
use crate::{actions, lifecycle};
|
use crate::{actions, lifecycle};
|
||||||
|
|
||||||
|
|
@ -137,6 +137,56 @@ pub(super) async fn post_start(
|
||||||
(StatusCode::OK, "ok").into_response()
|
(StatusCode::OK, "ok").into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `POST /api/pause/{name}` — write the pause marker for `name`.
|
||||||
|
///
|
||||||
|
/// Unlike the lifecycle ops above this is not a DAG: it writes a single
|
||||||
|
/// marker file, which the harness stats at the top of its serve loop.
|
||||||
|
/// Works on stopped containers too (the marker is sticky and takes effect
|
||||||
|
/// when the container next boots). Triggers an immediate rescan so the
|
||||||
|
/// `paused` badge flips on the dashboard without waiting for the next
|
||||||
|
/// periodic sweep.
|
||||||
|
pub(super) async fn post_pause(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AxumPath(name): AxumPath<String>,
|
||||||
|
) -> Response {
|
||||||
|
let logical = strip_container_prefix(&name);
|
||||||
|
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||||
|
return reject;
|
||||||
|
}
|
||||||
|
let ident = match Ident::parse(&logical) {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(e) => return (StatusCode::BAD_REQUEST, format!("bad agent name: {e}")).into_response(),
|
||||||
|
};
|
||||||
|
if let Err(e) = crate::coordinator::Coordinator::set_paused(&ident, true) {
|
||||||
|
return error_response(&format!("pause {logical}: {e}"));
|
||||||
|
}
|
||||||
|
state.coord.rescan_containers_and_emit().await;
|
||||||
|
(StatusCode::OK, "ok").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `POST /api/resume/{name}` — remove the pause marker for `name`.
|
||||||
|
///
|
||||||
|
/// The inverse of `post_pause`. Removing a non-existent marker is a no-op
|
||||||
|
/// (idempotent). Triggers an immediate rescan so the paused badge clears.
|
||||||
|
pub(super) async fn post_resume(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AxumPath(name): AxumPath<String>,
|
||||||
|
) -> Response {
|
||||||
|
let logical = strip_container_prefix(&name);
|
||||||
|
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||||
|
return reject;
|
||||||
|
}
|
||||||
|
let ident = match Ident::parse(&logical) {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(e) => return (StatusCode::BAD_REQUEST, format!("bad agent name: {e}")).into_response(),
|
||||||
|
};
|
||||||
|
if let Err(e) = crate::coordinator::Coordinator::set_paused(&ident, false) {
|
||||||
|
return error_response(&format!("resume {logical}: {e}"));
|
||||||
|
}
|
||||||
|
state.coord.rescan_containers_and_emit().await;
|
||||||
|
(StatusCode::OK, "ok").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn post_update_all(State(state): State<AppState>) -> Response {
|
pub(super) async fn post_update_all(State(state): State<AppState>) -> Response {
|
||||||
let containers = lifecycle::list().await.unwrap_or_default();
|
let containers = lifecycle::list().await.unwrap_or_default();
|
||||||
for container in containers {
|
for container in containers {
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,8 @@ pub async fn serve(
|
||||||
.route("/api/restart/{name}", post(lifecycle_ops::post_restart))
|
.route("/api/restart/{name}", post(lifecycle_ops::post_restart))
|
||||||
.route("/api/start/{name}", post(lifecycle_ops::post_start))
|
.route("/api/start/{name}", post(lifecycle_ops::post_start))
|
||||||
.route("/api/rebuild/{name}", post(lifecycle_ops::post_rebuild))
|
.route("/api/rebuild/{name}", post(lifecycle_ops::post_rebuild))
|
||||||
|
.route("/api/pause/{name}", post(lifecycle_ops::post_pause))
|
||||||
|
.route("/api/resume/{name}", post(lifecycle_ops::post_resume))
|
||||||
.route("/api/update-all", post(lifecycle_ops::post_update_all))
|
.route("/api/update-all", post(lifecycle_ops::post_update_all))
|
||||||
.route(
|
.route(
|
||||||
"/api/infra-container/{name}/{action}",
|
"/api/infra-container/{name}/{action}",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue