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>,
|
diff_html: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::Json<StateSnapshot> {
|
async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::Json<StateSnapshot> {
|
||||||
let host = headers
|
let host = headers
|
||||||
.get("host")
|
.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 raw_containers = lifecycle::list().await.unwrap_or_default();
|
||||||
let current_rev = crate::auto_update::current_flake_rev(&state.coord.hyperhive_flake);
|
let current_rev = crate::auto_update::current_flake_rev(&state.coord.hyperhive_flake);
|
||||||
let transient_snapshot = state.coord.transient_snapshot();
|
let transient_snapshot = state.coord.transient_snapshot();
|
||||||
let approvals = gc_orphans(
|
let pending_approvals = gc_orphans(
|
||||||
&state.coord,
|
&state.coord,
|
||||||
state.coord.approvals.pending().unwrap_or_default(),
|
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;
|
let mut any_stale = false;
|
||||||
for c in &raw_containers {
|
for c in raw_containers {
|
||||||
let (logical, is_manager) = if c == MANAGER_NAME {
|
let (logical, is_manager) = if c == MANAGER_NAME {
|
||||||
(MANAGER_NAME.to_owned(), true)
|
(MANAGER_NAME.to_owned(), true)
|
||||||
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
} 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 {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let needs_update = current_rev
|
let needs_update =
|
||||||
.as_deref()
|
current_rev.is_some_and(|rev| crate::auto_update::agent_needs_update(&logical, rev));
|
||||||
.is_some_and(|rev| crate::auto_update::agent_needs_update(&logical, rev));
|
|
||||||
if needs_update {
|
if needs_update {
|
||||||
any_stale = true;
|
any_stale = true;
|
||||||
}
|
}
|
||||||
let needs_login = if is_manager {
|
let needs_login = !is_manager && !claude_has_session(&Coordinator::agent_claude_dir(&logical));
|
||||||
false
|
|
||||||
} else {
|
|
||||||
!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
|
||||||
};
|
|
||||||
let pending = transient_snapshot
|
let pending = transient_snapshot
|
||||||
.get(&logical)
|
.get(&logical)
|
||||||
.map(|st| transient_label(st.kind));
|
.map(|st| transient_label(st.kind));
|
||||||
containers.push(ContainerView {
|
out.push(ContainerView {
|
||||||
port: lifecycle::agent_web_port(&logical),
|
port: lifecycle::agent_web_port(&logical),
|
||||||
running: lifecycle::is_running(&logical).await,
|
running: lifecycle::is_running(&logical).await,
|
||||||
container: c.clone(),
|
container: c.clone(),
|
||||||
|
|
@ -212,24 +240,37 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
||||||
pending,
|
pending,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
(out, any_stale)
|
||||||
|
}
|
||||||
|
|
||||||
let transients = transient_snapshot
|
/// Transient state for agents whose container does NOT yet exist
|
||||||
.into_iter()
|
/// (`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, _)| {
|
.filter(|(name, _)| {
|
||||||
!raw_containers
|
!raw_containers
|
||||||
.iter()
|
.iter()
|
||||||
.any(|c| c == &format!("{AGENT_PREFIX}{name}") || c == name)
|
.any(|c| c == &format!("{AGENT_PREFIX}{name}") || c == *name)
|
||||||
})
|
})
|
||||||
.map(|(name, st)| TransientView {
|
.map(|(name, st)| TransientView {
|
||||||
name,
|
name: name.clone(),
|
||||||
kind: transient_label(st.kind),
|
kind: transient_label(st.kind),
|
||||||
secs: st.since.elapsed().as_secs(),
|
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 {
|
for a in approvals {
|
||||||
let view = match a.kind {
|
out.push(match a.kind {
|
||||||
hive_sh4re::ApprovalKind::ApplyCommit => {
|
hive_sh4re::ApprovalKind::ApplyCommit => {
|
||||||
let sha = a.commit_ref[..a.commit_ref.len().min(12)].to_owned();
|
let sha = a.commit_ref[..a.commit_ref.len().min(12)].to_owned();
|
||||||
let diff = approval_diff(&a.agent, &a.commit_ref).await;
|
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,
|
sha_short: None,
|
||||||
diff_html: None,
|
diff_html: None,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
approval_views.push(view);
|
|
||||||
}
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
let operator_inbox = state
|
/// State-dir names that don't appear in the live container list (and
|
||||||
.coord
|
/// aren't the manager). Each one surfaces in the dashboard as a row
|
||||||
.broker
|
/// with R3V1V3 + PURG3 actions.
|
||||||
.recent_for(hive_sh4re::OPERATOR_RECIPIENT, 50)
|
fn build_tombstone_views(
|
||||||
.unwrap_or_default();
|
coord: &Coordinator,
|
||||||
let questions = state.coord.questions.pending().unwrap_or_default();
|
containers: &[ContainerView],
|
||||||
|
transient_snapshot: &std::collections::HashMap<String, crate::coordinator::TransientState>,
|
||||||
// Tombstones: state-dir names that don't appear in the live container
|
) -> Vec<TombstoneView> {
|
||||||
// list (and aren't the manager). Operator can re-spawn or PURG3.
|
let _ = coord; // kept_state_names is a free fn but takes &self by future plan
|
||||||
let live: std::collections::HashSet<String> = containers
|
let live: std::collections::HashSet<&str> = containers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.name.clone())
|
.map(|c| c.name.as_str())
|
||||||
.chain(state.coord.transient_snapshot().into_keys())
|
.chain(transient_snapshot.keys().map(String::as_str))
|
||||||
.collect();
|
.collect();
|
||||||
let tombstones: Vec<TombstoneView> = Coordinator::kept_state_names()
|
Coordinator::kept_state_names()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|name| name != MANAGER_NAME && !live.contains(name))
|
.filter(|name| name != MANAGER_NAME && !live.contains(name.as_str()))
|
||||||
.map(|name| {
|
.map(|name| {
|
||||||
let root = Coordinator::agent_state_root(&name);
|
let root = Coordinator::agent_state_root(&name);
|
||||||
let state_bytes = dir_size_bytes(&root);
|
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,
|
has_creds,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect()
|
||||||
|
|
||||||
axum::Json(StateSnapshot {
|
|
||||||
hostname,
|
|
||||||
manager_port: MANAGER_PORT,
|
|
||||||
any_stale,
|
|
||||||
containers,
|
|
||||||
transients,
|
|
||||||
approvals: approval_views,
|
|
||||||
operator_inbox,
|
|
||||||
questions,
|
|
||||||
tombstones,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sum the byte size of every regular file under `root`. Cheap to compute
|
/// Sum the byte size of every regular file under `root`. Cheap to compute
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue