From e33d6bb1ad35c64a7114cb79d318fa770281a8db Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Fri, 17 Jul 2026 21:16:07 -0700 Subject: [PATCH] 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). --- lib/cf-apps.functions | 36 +++++++++ lib/skel/.bashrc.d/11-quickinfo.bashrc | 100 +++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/lib/cf-apps.functions b/lib/cf-apps.functions index d374ada..150d2b1 100644 --- a/lib/cf-apps.functions +++ b/lib/cf-apps.functions @@ -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 } diff --git a/lib/skel/.bashrc.d/11-quickinfo.bashrc b/lib/skel/.bashrc.d/11-quickinfo.bashrc index 54ed441..6074331 100755 --- a/lib/skel/.bashrc.d/11-quickinfo.bashrc +++ b/lib/skel/.bashrc.d/11-quickinfo.bashrc @@ -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}"