feat: record install-time state, show CircuitForge node info in quickinfo

- lib/cf-apps.functions: cf-provision-node/install-cf-app/
  provision-orchard-node now write to ~/.config/cf-node-bootstrap/state.conf
  (record-cf-node-state) - provisioning profile, each installed app
  (name:install_type:clone_dir), and orchard role/coordinator URL. The
  orchard role/coordinator is read back from circuitforge-orch's own
  generated ~/.config/circuitforge/cf-orch.env rather than tracked
  separately, since that's the actual source of truth for what its
  interactive installer decided.
- 11-quickinfo.bashrc: new "CircuitForge Node" section (show_cf_node_info,
  default true) displaying the provisioning profile, orchard role/
  coordinator if joined, and each installed app with a best-effort
  running/stopped status. Docker-compose apps are checked by compose
  project from their clone dir rather than by container name, since
  compose's generated container names don't match the app name directly.
  Section is entirely absent if the state file doesn't exist yet (i.e.
  the CF Apps menu has never been run on this node).
This commit is contained in:
pyr0ball 2026-07-17 21:16:07 -07:00
parent 2a325b55bd
commit e33d6bb1ad
2 changed files with 136 additions and 0 deletions

View file

@ -8,6 +8,23 @@
cf_apps_manifest_dir="${cf_apps_manifest_dir:-${rundir}/cf-apps}"
# State file recording what this installer has set up on this node, so
# lib/skel/.bashrc.d/11-quickinfo.bashrc can report on it. Plain
# sourceable key=value pairs (values %q-escaped), one write-or-replace
# per key - not a general config format, just enough for quickinfo to read.
cf_node_state_file="${cf_node_state_file:-${HOME}/.config/cf-node-bootstrap/state.conf}"
record-cf-node-state() {
local key="$1" value="$2"
mkdir -p "$(dirname "$cf_node_state_file")"
touch "$cf_node_state_file"
if [ -s "$cf_node_state_file" ]; then
grep -v "^${key}=" "$cf_node_state_file" > "${cf_node_state_file}.tmp"
mv "${cf_node_state_file}.tmp" "$cf_node_state_file"
fi
printf '%s=%q\n' "$key" "$value" >> "$cf_node_state_file"
}
# Resets the app_* manifest variables/functions so sourcing one manifest
# can't leak stale values into the next.
_cf-apps-reset-manifest-vars() {
@ -88,6 +105,7 @@ cf-provision-node() {
if [ -z "$cf_provision_profile" ]; then
select-provision-profile
fi
record-cf-node-state "cf_node_profile" "$cf_provision_profile"
if [[ "$cf_provision_profile" == "orchard" ]]; then
provision-orchard-node
@ -212,6 +230,8 @@ install-cf-app() {
fi
;;
esac
record-cf-node-state "cf_app_${app_name//-/_}" "${app_name}:${install_type}:${clone_dir}"
}
# Narrow, single-purpose flow for adding a GPU worker node to an existing
@ -251,4 +271,20 @@ provision-orchard-node() {
boxline "Handing off to circuitforge-orch's own install.sh..."
(cd "$clone_dir" && ./install.sh)
# circuitforge-orch's installer writes its own resulting config to
# ~/.config/circuitforge/cf-orch.env (CFORCH_MODE, CFORCH_COORDINATOR_URL);
# read that back rather than trying to track which topology the user
# picked inside its interactive prompts.
local orch_env_file="${HOME}/.config/circuitforge/cf-orch.env"
if [ -f "$orch_env_file" ]; then
local cforch_mode cforch_coordinator_url
cforch_mode=$(grep -m1 '^CFORCH_MODE=' "$orch_env_file" | cut -d= -f2-)
cforch_coordinator_url=$(grep -m1 '^CFORCH_COORDINATOR_URL=' "$orch_env_file" | cut -d= -f2-)
record-cf-node-state "cf_orchard_role" "${cforch_mode:-unknown}"
record-cf-node-state "cf_orchard_coordinator_url" "$cforch_coordinator_url"
record-cf-node-state "cf_orchard_dir" "$clone_dir"
else
warn "Couldn't find ${orch_env_file} after install - orchard role won't show in quickinfo until it exists"
fi
}

View file

@ -97,6 +97,7 @@ show_smart_status=false # Requires root privileges or sudo config
show_top_processes=true
show_power_info=true # Enable power monitoring (requires appropriate hardware/sensors)
show_power_details=false
show_cf_node_info=true # Show installed CircuitForge apps / orchard role (requires cf-node-bootstrap's install-time state file)
# Services to check (space separated list)
critical_services="sshd nginx apache2 mysqld mariadb docker containerd kubelet cron"
@ -578,6 +579,85 @@ get_container_info() {
# fi
}
################################
# Get CircuitForge Node Info
################################
# Reads the state file cf-node-bootstrap's installer writes (see
# lib/cf-apps.functions:record-cf-node-state) - not present unless that
# installer's CF Apps menu has been run on this node at least once.
# Best-effort running/stopped check for a single installed app. Docker
# compose-based apps are checked by compose project (from the clone dir),
# not by container name, since compose's generated container names don't
# match the app name directly. Single-container apps (no compose file
# present) fall back to a name match against `docker ps`.
_cf_node_app_status() {
local dir="$1" install_type="$2" app_name="$3"
if [[ "$install_type" != "docker" ]]; then
echo "n/a"
return
fi
command -v docker >/dev/null 2>&1 || { echo "n/a"; return; }
local compose_file=""
for f in compose.yml docker-compose.yml; do
if [ -f "${dir}/${f}" ]; then
compose_file="$f"
break
fi
done
if [ -n "$compose_file" ]; then
if (cd "$dir" && docker compose -f "$compose_file" ps -q 2>/dev/null | grep -q .); then
echo "running"
else
echo "stopped"
fi
elif docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$app_name"; then
echo "running"
else
echo "stopped"
fi
}
get_cf_node_info() {
cf_node_info_available=false
cf_node_profile_display="N/A"
cf_node_apps_display=()
cf_node_orchard_display=""
local state_file="${HOME}/.config/cf-node-bootstrap/state.conf"
[ -f "$state_file" ] || return
cf_node_info_available=true
# State file only defines cf_node_*/cf_app_*/cf_orchard_* keys (written by
# record-cf-node-state), so sourcing it here won't clobber other quickinfo
# variables.
source "$state_file"
cf_node_profile_display="${cf_node_profile:-unknown}"
local var app_value app_name install_type clone_dir status status_disp
for var in $(compgen -v cf_app_ 2>/dev/null); do
app_value="${!var}"
IFS=':' read -r app_name install_type clone_dir <<< "$app_value"
status=$(_cf_node_app_status "$clone_dir" "$install_type" "$app_name")
case "$status" in
running) status_disp="${grn}running${dfl}" ;;
stopped) status_disp="${lrd}stopped${dfl}" ;;
*) status_disp="${gry}${install_type}${dfl}" ;;
esac
cf_node_apps_display+=("${app_name} ${gry}(${install_type})${dfl}: ${status_disp}")
done
if [ -n "$cf_orchard_role" ]; then
cf_node_orchard_display="${bld}Role:${dfl} ${ylw}${cf_orchard_role}${dfl}"
if [ -n "$cf_orchard_coordinator_url" ]; then
cf_node_orchard_display="${cf_node_orchard_display} | ${bld}Coordinator:${dfl} ${cyn}${cf_orchard_coordinator_url}${dfl}"
fi
fi
}
# Get public IP address using multiple fallbacks
get_wan_ip() {
wan_ip="Unavailable"
@ -855,6 +935,10 @@ if [ "$show_container_info" = true ]; then
get_container_info
fi
if [ "$show_cf_node_info" = true ]; then
get_cf_node_info
fi
if [ "$show_service_status" = true ]; then
get_service_status
fi
@ -951,6 +1035,22 @@ if [ "$show_container_info" = true ] && [[ "$in_container" = true || "$have_cont
fi
fi
# CircuitForge node information
if [ "$show_cf_node_info" = true ] && [ "$cf_node_info_available" = true ]; then
boxline ""
boxline "${bld}${unl}CircuitForge Node:${dfl}"
boxline " ${bld}Profile:${dfl} ${mag}${cf_node_profile_display}${dfl}"
if [ -n "$cf_node_orchard_display" ]; then
boxline " ${bld}Orchard:${dfl} ${cf_node_orchard_display}"
fi
if [ "${#cf_node_apps_display[@]}" -gt 0 ]; then
boxline " ${bld}Installed apps:${dfl}"
for app_line in "${cf_node_apps_display[@]}"; do
boxline " ${app_line}"
done
fi
fi
# Network information
boxline ""
boxline "${bld}${unl}Network Information:${dfl}"