dashboard: split api_state into per-section builders
drops the #[allow(clippy::too_many_lines)] on api_state by extracting four pure helpers: - build_container_views — live containers + any_stale flag - build_transient_views — agents in pre-creation Spawning state only - build_approval_views — pending approvals with diff html - build_tombstone_views — destroyed-but-kept state dirs api_state itself is now ~30 lines of orchestration. zero behavior change. each helper is independently readable + testable.
This commit is contained in:
parent
7b4adea325
commit
c27111ac32
1 changed files with 79 additions and 49 deletions
|
|
@ -161,7 +161,6 @@ struct ApprovalView {
|
|||
diff_html: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::Json<StateSnapshot> {
|
||||
let host = headers
|
||||
.get("host")
|
||||
|
|
@ -172,14 +171,48 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
let raw_containers = lifecycle::list().await.unwrap_or_default();
|
||||
let current_rev = crate::auto_update::current_flake_rev(&state.coord.hyperhive_flake);
|
||||
let transient_snapshot = state.coord.transient_snapshot();
|
||||
let approvals = gc_orphans(
|
||||
let pending_approvals = gc_orphans(
|
||||
&state.coord,
|
||||
state.coord.approvals.pending().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let mut containers = Vec::new();
|
||||
let (containers, any_stale) =
|
||||
build_container_views(&raw_containers, current_rev.as_deref(), &transient_snapshot).await;
|
||||
let transients = build_transient_views(&raw_containers, &transient_snapshot);
|
||||
let approvals = build_approval_views(pending_approvals).await;
|
||||
let tombstones = build_tombstone_views(&state.coord, &containers, &transient_snapshot);
|
||||
|
||||
let operator_inbox = state
|
||||
.coord
|
||||
.broker
|
||||
.recent_for(hive_sh4re::OPERATOR_RECIPIENT, 50)
|
||||
.unwrap_or_default();
|
||||
let questions = state.coord.questions.pending().unwrap_or_default();
|
||||
|
||||
axum::Json(StateSnapshot {
|
||||
hostname,
|
||||
manager_port: MANAGER_PORT,
|
||||
any_stale,
|
||||
containers,
|
||||
transients,
|
||||
approvals,
|
||||
operator_inbox,
|
||||
questions,
|
||||
tombstones,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build `ContainerView`s for every live nixos-container. Returns the
|
||||
/// list and whether any container is stale (drives the "↻ UPD4TE 4LL"
|
||||
/// banner).
|
||||
async fn build_container_views(
|
||||
raw_containers: &[String],
|
||||
current_rev: Option<&str>,
|
||||
transient_snapshot: &std::collections::HashMap<String, crate::coordinator::TransientState>,
|
||||
) -> (Vec<ContainerView>, bool) {
|
||||
let mut out = Vec::new();
|
||||
let mut any_stale = false;
|
||||
for c in &raw_containers {
|
||||
for c in raw_containers {
|
||||
let (logical, is_manager) = if c == MANAGER_NAME {
|
||||
(MANAGER_NAME.to_owned(), true)
|
||||
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
||||
|
|
@ -187,21 +220,16 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
} else {
|
||||
continue;
|
||||
};
|
||||
let needs_update = current_rev
|
||||
.as_deref()
|
||||
.is_some_and(|rev| crate::auto_update::agent_needs_update(&logical, rev));
|
||||
let needs_update =
|
||||
current_rev.is_some_and(|rev| crate::auto_update::agent_needs_update(&logical, rev));
|
||||
if needs_update {
|
||||
any_stale = true;
|
||||
}
|
||||
let needs_login = if is_manager {
|
||||
false
|
||||
} else {
|
||||
!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
||||
};
|
||||
let needs_login = !is_manager && !claude_has_session(&Coordinator::agent_claude_dir(&logical));
|
||||
let pending = transient_snapshot
|
||||
.get(&logical)
|
||||
.map(|st| transient_label(st.kind));
|
||||
containers.push(ContainerView {
|
||||
out.push(ContainerView {
|
||||
port: lifecycle::agent_web_port(&logical),
|
||||
running: lifecycle::is_running(&logical).await,
|
||||
container: c.clone(),
|
||||
|
|
@ -212,24 +240,37 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
pending,
|
||||
});
|
||||
}
|
||||
(out, any_stale)
|
||||
}
|
||||
|
||||
let transients = transient_snapshot
|
||||
.into_iter()
|
||||
/// Transient state for agents whose container does NOT yet exist
|
||||
/// (`Spawning`). Lifecycle ops on existing containers surface as
|
||||
/// `ContainerView.pending` inline; this list only catches pre-creation.
|
||||
fn build_transient_views(
|
||||
raw_containers: &[String],
|
||||
transient_snapshot: &std::collections::HashMap<String, crate::coordinator::TransientState>,
|
||||
) -> Vec<TransientView> {
|
||||
transient_snapshot
|
||||
.iter()
|
||||
.filter(|(name, _)| {
|
||||
!raw_containers
|
||||
.iter()
|
||||
.any(|c| c == &format!("{AGENT_PREFIX}{name}") || c == name)
|
||||
.any(|c| c == &format!("{AGENT_PREFIX}{name}") || c == *name)
|
||||
})
|
||||
.map(|(name, st)| TransientView {
|
||||
name,
|
||||
name: name.clone(),
|
||||
kind: transient_label(st.kind),
|
||||
secs: st.since.elapsed().as_secs(),
|
||||
})
|
||||
.collect();
|
||||
.collect()
|
||||
}
|
||||
|
||||
let mut approval_views = Vec::with_capacity(approvals.len());
|
||||
/// Render each pending approval into its dashboard view (short sha +
|
||||
/// unified diff for `ApplyCommit`, just the name for `Spawn`).
|
||||
async fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
||||
let mut out = Vec::with_capacity(approvals.len());
|
||||
for a in approvals {
|
||||
let view = match a.kind {
|
||||
out.push(match a.kind {
|
||||
hive_sh4re::ApprovalKind::ApplyCommit => {
|
||||
let sha = a.commit_ref[..a.commit_ref.len().min(12)].to_owned();
|
||||
let diff = approval_diff(&a.agent, &a.commit_ref).await;
|
||||
|
|
@ -248,27 +289,28 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
sha_short: None,
|
||||
diff_html: None,
|
||||
},
|
||||
};
|
||||
approval_views.push(view);
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
let operator_inbox = state
|
||||
.coord
|
||||
.broker
|
||||
.recent_for(hive_sh4re::OPERATOR_RECIPIENT, 50)
|
||||
.unwrap_or_default();
|
||||
let questions = state.coord.questions.pending().unwrap_or_default();
|
||||
|
||||
// Tombstones: state-dir names that don't appear in the live container
|
||||
// list (and aren't the manager). Operator can re-spawn or PURG3.
|
||||
let live: std::collections::HashSet<String> = containers
|
||||
/// State-dir names that don't appear in the live container list (and
|
||||
/// aren't the manager). Each one surfaces in the dashboard as a row
|
||||
/// with R3V1V3 + PURG3 actions.
|
||||
fn build_tombstone_views(
|
||||
coord: &Coordinator,
|
||||
containers: &[ContainerView],
|
||||
transient_snapshot: &std::collections::HashMap<String, crate::coordinator::TransientState>,
|
||||
) -> Vec<TombstoneView> {
|
||||
let _ = coord; // kept_state_names is a free fn but takes &self by future plan
|
||||
let live: std::collections::HashSet<&str> = containers
|
||||
.iter()
|
||||
.map(|c| c.name.clone())
|
||||
.chain(state.coord.transient_snapshot().into_keys())
|
||||
.map(|c| c.name.as_str())
|
||||
.chain(transient_snapshot.keys().map(String::as_str))
|
||||
.collect();
|
||||
let tombstones: Vec<TombstoneView> = Coordinator::kept_state_names()
|
||||
Coordinator::kept_state_names()
|
||||
.into_iter()
|
||||
.filter(|name| name != MANAGER_NAME && !live.contains(name))
|
||||
.filter(|name| name != MANAGER_NAME && !live.contains(name.as_str()))
|
||||
.map(|name| {
|
||||
let root = Coordinator::agent_state_root(&name);
|
||||
let state_bytes = dir_size_bytes(&root);
|
||||
|
|
@ -286,19 +328,7 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
has_creds,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
axum::Json(StateSnapshot {
|
||||
hostname,
|
||||
manager_port: MANAGER_PORT,
|
||||
any_stale,
|
||||
containers,
|
||||
transients,
|
||||
approvals: approval_views,
|
||||
operator_inbox,
|
||||
questions,
|
||||
tombstones,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sum the byte size of every regular file under `root`. Cheap to compute
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue