auto-update: surface pending updates in dashboard + include manager
This commit is contained in:
parent
a4e1556f90
commit
e777576528
3 changed files with 149 additions and 51 deletions
|
|
@ -42,6 +42,7 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/approve/{id}", post(post_approve))
|
||||
.route("/deny/{id}", post(post_deny))
|
||||
.route("/destroy/{name}", post(post_destroy))
|
||||
.route("/rebuild/{name}", post(post_rebuild))
|
||||
.route("/request-spawn", post(post_request_spawn))
|
||||
.route("/send", post(post_send))
|
||||
.route("/messages/stream", get(messages_stream))
|
||||
|
|
@ -64,6 +65,7 @@ async fn index(headers: HeaderMap, State(state): State<AppState>) -> Html<String
|
|||
|
||||
let containers = lifecycle::list().await.unwrap_or_default();
|
||||
let transient = state.coord.transient_snapshot();
|
||||
let current_rev = crate::auto_update::current_flake_rev(&state.coord.hyperhive_flake);
|
||||
let approvals = gc_orphans(
|
||||
&state.coord,
|
||||
state.coord.approvals.pending().unwrap_or_default(),
|
||||
|
|
@ -82,7 +84,7 @@ async fn index(headers: HeaderMap, State(state): State<AppState>) -> Html<String
|
|||
|
||||
Html(format!(
|
||||
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>hyperhive // h1ve-c0re</title>\n{refresh}\n{STYLE}\n</head>\n<body>\n{BANNER}\n{containers}\n{talk}\n{approvals_html}\n{MSG_FLOW}\n{FOOTER}\n{MSG_FLOW_JS}\n</body>\n</html>\n",
|
||||
containers = render_containers(&containers, &transient, &hostname),
|
||||
containers = render_containers(&containers, &transient, current_rev.as_deref(), &hostname),
|
||||
talk = render_talk(&containers),
|
||||
))
|
||||
}
|
||||
|
|
@ -163,6 +165,24 @@ async fn post_request_spawn(
|
|||
}
|
||||
}
|
||||
|
||||
async fn post_rebuild(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||
let Some(current_rev) = crate::auto_update::current_flake_rev(&state.coord.hyperhive_flake)
|
||||
else {
|
||||
return error_response(
|
||||
"rebuild: hyperhive_flake has no canonical path; manual rebuild only via `hive-c0re rebuild`",
|
||||
);
|
||||
};
|
||||
let result = if name == lifecycle::MANAGER_NAME {
|
||||
crate::auto_update::rebuild_manager(¤t_rev).await
|
||||
} else {
|
||||
crate::auto_update::rebuild_agent(&state.coord, &name, ¤t_rev).await
|
||||
};
|
||||
match result {
|
||||
Ok(()) => Redirect::to("/").into_response(),
|
||||
Err(e) => error_response(&format!("rebuild {name} failed: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_destroy(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||
match actions::destroy(&state.coord, &name).await {
|
||||
Ok(()) => Redirect::to("/").into_response(),
|
||||
|
|
@ -184,6 +204,7 @@ fn error_response(message: &str) -> Response {
|
|||
fn render_containers(
|
||||
containers: &[String],
|
||||
transient: &std::collections::HashMap<String, crate::coordinator::TransientState>,
|
||||
current_rev: Option<&str>,
|
||||
hostname: &str,
|
||||
) -> String {
|
||||
let mut out = String::from(
|
||||
|
|
@ -217,9 +238,10 @@ fn render_containers(
|
|||
out.push_str("<ul>\n");
|
||||
for container in containers {
|
||||
if container == MANAGER_NAME {
|
||||
let update_badge = update_badge_for(MANAGER_NAME, current_rev);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"<li><span class=\"glyph\">▓█▓▒░</span> <a href=\"http://{hostname}:{MANAGER_PORT}/\">{container}</a> <span class=\"role role-m1nd\">m1nd</span> <span class=\"meta\">:{MANAGER_PORT}</span></li>",
|
||||
"<li><span class=\"glyph\">▓█▓▒░</span> <a href=\"http://{hostname}:{MANAGER_PORT}/\">{container}</a> <span class=\"role role-m1nd\">m1nd</span>{update_badge} <span class=\"meta\">:{MANAGER_PORT}</span></li>",
|
||||
);
|
||||
} else if let Some(name) = container.strip_prefix(AGENT_PREFIX) {
|
||||
let port = lifecycle::agent_web_port(name);
|
||||
|
|
@ -231,9 +253,10 @@ fn render_containers(
|
|||
" <a class=\"role role-pending\" href=\"http://{hostname}:{port}/\">needs login →</a>",
|
||||
)
|
||||
};
|
||||
let update_badge = update_badge_for(name, current_rev);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"<li><span class=\"glyph\">▒░▒░░</span> <a href=\"http://{hostname}:{port}/\">{name}</a> <span class=\"role role-ag3nt\">ag3nt</span>{login_badge} <span class=\"meta\">{container} :{port}</span>\n <form method=\"POST\" action=\"/destroy/{name}\" class=\"inline\" onsubmit=\"return confirm('destroy {name}? container is removed; state + creds kept.');\"><button class=\"btn btn-destroy\" type=\"submit\">DESTR0Y</button></form>\n</li>",
|
||||
"<li><span class=\"glyph\">▒░▒░░</span> <a href=\"http://{hostname}:{port}/\">{name}</a> <span class=\"role role-ag3nt\">ag3nt</span>{login_badge}{update_badge} <span class=\"meta\">{container} :{port}</span>\n <form method=\"POST\" action=\"/destroy/{name}\" class=\"inline\" onsubmit=\"return confirm('destroy {name}? container is removed; state + creds kept.');\"><button class=\"btn btn-destroy\" type=\"submit\">DESTR0Y</button></form>\n</li>",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -319,6 +342,20 @@ fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<Approval> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Returns either an empty string (agent is up-to-date / no rev known) or
|
||||
/// a clickable "needs update" badge whose form POSTs to /rebuild/<name>.
|
||||
fn update_badge_for(name: &str, current_rev: Option<&str>) -> String {
|
||||
let Some(rev) = current_rev else {
|
||||
return String::new();
|
||||
};
|
||||
if !crate::auto_update::agent_needs_update(name, rev) {
|
||||
return String::new();
|
||||
}
|
||||
format!(
|
||||
" <form method=\"POST\" action=\"/rebuild/{name}\" class=\"inline\" onsubmit=\"return confirm('rebuild {name}? hot-reloads the container.');\"><button class=\"role role-pending btn-inline\" type=\"submit\" title=\"agent's last build is older than current hyperhive rev\">needs update ↻</button></form>",
|
||||
)
|
||||
}
|
||||
|
||||
/// Host-side mirror of `hive_ag3nt::login::has_session`. Returns true if the
|
||||
/// agent's bound `~/.claude/` dir on disk contains any regular file. The
|
||||
/// dashboard reads this each render so logins driven from the agent web UI
|
||||
|
|
@ -550,6 +587,13 @@ const STYLE: &str = r#"
|
|||
.spawnform input::placeholder { color: var(--muted); }
|
||||
.spawnform input:focus { outline: 1px solid var(--purple); }
|
||||
.role-pending { color: var(--amber); border-color: var(--amber); }
|
||||
.btn-inline {
|
||||
font-family: inherit;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
margin-left: 0.4em;
|
||||
}
|
||||
.btn-inline:hover { background: rgba(255, 184, 77, 0.1); }
|
||||
.kind {
|
||||
display: inline-block;
|
||||
margin-left: 0.4em;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue