dashboard: spinners on in-flight lifecycle actions + cleaner row layout
backend: - TransientKind grows Starting / Stopping / Restarting / Rebuilding / Destroying alongside the existing Spawning. each dashboard handler (start/restart/kill/rebuild/destroy) wraps the lifecycle call with set_transient + clear_transient so the dashboard knows what's in flight. transient kind is surfaced inline on ContainerView.pending (existing-container actions) — only Spawning (pre-creation) lands in the separate transients list. frontend: - container row is now two lines: identity + meta on top, action buttons below. less cluttered, leaves room for the pending state pill. pending rows dim their actions and surface a pulsing '◐ spawning… / starting… / stopping… / restarting… / rebuilding… / destroying…' indicator next to the name. - 'needs login' / 'needs update' chips moved into a unified .badge styling for consistency. - auto-refresh kicks in not only on transient spawn but on any container with a pending action.
This commit is contained in:
parent
300be8afa9
commit
c337cc06f8
5 changed files with 157 additions and 38 deletions
|
|
@ -120,6 +120,11 @@ struct ContainerView {
|
|||
running: bool,
|
||||
needs_update: bool,
|
||||
needs_login: bool,
|
||||
/// When a lifecycle action is in flight on this container, the kind
|
||||
/// (`starting`, `stopping`, etc.) so the JS can render a spinner +
|
||||
/// disable other buttons.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pending: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -176,6 +181,9 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
} else {
|
||||
!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
||||
};
|
||||
let pending = transient_snapshot
|
||||
.get(&logical)
|
||||
.map(|st| transient_label(st.kind));
|
||||
containers.push(ContainerView {
|
||||
port: lifecycle::agent_web_port(&logical),
|
||||
running: lifecycle::is_running(&logical).await,
|
||||
|
|
@ -184,6 +192,7 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
is_manager,
|
||||
needs_update,
|
||||
needs_login,
|
||||
pending,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -196,9 +205,7 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
})
|
||||
.map(|(name, st)| TransientView {
|
||||
name,
|
||||
kind: match st.kind {
|
||||
crate::coordinator::TransientKind::Spawning => "spawning",
|
||||
},
|
||||
kind: transient_label(st.kind),
|
||||
secs: st.since.elapsed().as_secs(),
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -337,10 +344,15 @@ async fn post_rebuild(State(state): State<AppState>, AxumPath(name): AxumPath<St
|
|||
"rebuild: hyperhive_flake has no canonical path; manual rebuild only via `hive-c0re rebuild`",
|
||||
);
|
||||
};
|
||||
let result = crate::auto_update::rebuild_agent(&state.coord, &name, ¤t_rev).await;
|
||||
let logical = strip_container_prefix(&name);
|
||||
state
|
||||
.coord
|
||||
.set_transient(&logical, crate::coordinator::TransientKind::Rebuilding);
|
||||
let result = crate::auto_update::rebuild_agent(&state.coord, &logical, ¤t_rev).await;
|
||||
state.coord.clear_transient(&logical);
|
||||
match result {
|
||||
Ok(()) => Redirect::to("/").into_response(),
|
||||
Err(e) => error_response(&format!("rebuild {name} failed: {e:#}")),
|
||||
Err(e) => error_response(&format!("rebuild {logical} failed: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +361,12 @@ async fn post_kill(State(state): State<AppState>, AxumPath(name): AxumPath<Strin
|
|||
if logical == lifecycle::MANAGER_NAME {
|
||||
return error_response("kill: refusing to stop the manager");
|
||||
}
|
||||
match lifecycle::kill(&logical).await {
|
||||
state
|
||||
.coord
|
||||
.set_transient(&logical, crate::coordinator::TransientKind::Stopping);
|
||||
let result = lifecycle::kill(&logical).await;
|
||||
state.coord.clear_transient(&logical);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
state.coord.unregister_agent(&logical);
|
||||
state
|
||||
|
|
@ -363,20 +380,27 @@ async fn post_kill(State(state): State<AppState>, AxumPath(name): AxumPath<Strin
|
|||
}
|
||||
}
|
||||
|
||||
async fn post_restart(
|
||||
State(_state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
) -> Response {
|
||||
async fn post_restart(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||
let logical = strip_container_prefix(&name);
|
||||
match lifecycle::restart(&logical).await {
|
||||
state
|
||||
.coord
|
||||
.set_transient(&logical, crate::coordinator::TransientKind::Restarting);
|
||||
let result = lifecycle::restart(&logical).await;
|
||||
state.coord.clear_transient(&logical);
|
||||
match result {
|
||||
Ok(()) => Redirect::to("/").into_response(),
|
||||
Err(e) => error_response(&format!("restart {logical} failed: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_start(State(_state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||
async fn post_start(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||
let logical = strip_container_prefix(&name);
|
||||
match lifecycle::start(&logical).await {
|
||||
state
|
||||
.coord
|
||||
.set_transient(&logical, crate::coordinator::TransientKind::Starting);
|
||||
let result = lifecycle::start(&logical).await;
|
||||
state.coord.clear_transient(&logical);
|
||||
match result {
|
||||
Ok(()) => Redirect::to("/").into_response(),
|
||||
Err(e) => error_response(&format!("start {logical} failed: {e:#}")),
|
||||
}
|
||||
|
|
@ -416,6 +440,20 @@ async fn post_update_all(State(state): State<AppState>) -> Response {
|
|||
}
|
||||
}
|
||||
|
||||
fn transient_label(k: crate::coordinator::TransientKind) -> &'static str {
|
||||
use crate::coordinator::TransientKind::{
|
||||
Destroying, Rebuilding, Restarting, Spawning, Starting, Stopping,
|
||||
};
|
||||
match k {
|
||||
Spawning => "spawning",
|
||||
Starting => "starting",
|
||||
Stopping => "stopping",
|
||||
Restarting => "restarting",
|
||||
Rebuilding => "rebuilding",
|
||||
Destroying => "destroying",
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert either a logical name or a container name back to the logical
|
||||
/// name. Sub-agents are `h-foo` → `foo`; manager stays `hm1nd`.
|
||||
fn strip_container_prefix(name: &str) -> String {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue