From f406659e8e052625c892970adf8fb6e6d2338a68 Mon Sep 17 00:00:00 2001
From: Griffen Fargo <3642037+gfargo@users.noreply.github.com>
Date: Thu, 27 Aug 2026 12:30:05 -0400
Subject: [PATCH] refactor(status-all): expose target provenance
---
lib/cmd_dashboard.sh | 27 +++++--
lib/cmd_status_all.sh | 160 ++++++++++++++++++++++++++++++++++++------
2 files changed, 159 insertions(+), 28 deletions(-)
diff --git a/lib/cmd_dashboard.sh b/lib/cmd_dashboard.sh
index f7f2359..3bb0052 100644
--- a/lib/cmd_dashboard.sh
+++ b/lib/cmd_dashboard.sh
@@ -309,17 +309,34 @@ _dashboard_render_html() {
echo "
Stacks
"
if command -v jq >/dev/null 2>&1 && printf '%s' "$stacks_json" | jq -e . >/dev/null 2>&1; then
- echo "| Stack | Health | Last Deploy | Backup Age |
"
- printf '%s' "$stacks_json" | jq -r '.stacks[]? | [.name, (.health//"-"), (.last_deploy//"-"), (.backup_age//"-")] | @tsv' |
- while IFS=$'\t' read -r name health last_deploy backup_age; do
+ echo "| Stack | Target | Health | Last Deploy | Backup Age |
"
+ printf '%s' "$stacks_json" | jq -r '
+ def target:
+ if (.target_scope // "") == "remote" then
+ if (.target_host_alias // "") != "" and (.target_host // "") != "" then
+ .target_host_alias + " (" + .target_host + ")"
+ elif (.target_host_alias // "") != "" then .target_host_alias
+ elif (.target_host // "") != "" then .target_host
+ else "remote" end
+ elif (.target_scope // "") == "local" then "local"
+ else "-" end;
+ def evidence($value; $source):
+ if $value == "-" or $source == "" then $value
+ else $value + " [" + ($source | gsub("_"; " ")) + "]" end;
+ .stacks[]? |
+ [.name, target, (.health // "-"),
+ evidence((.last_deploy // "-"); (.last_deploy_source // "")),
+ evidence((.backup_age // "-"); (.backup_source // ""))] | @tsv' |
+ while IFS=$'\t' read -r name target health last_deploy backup_age; do
local glyph=""
case "$health" in
healthy) glyph="ok" ;;
degraded) glyph="warn" ;;
down) glyph="err" ;;
esac
- printf '| %s | %s | %s | %s |
\n' \
- "$(_dashboard_html_escape "$name")" "$glyph" "$(_dashboard_html_escape "$health")" \
+ printf '| %s | %s | %s | %s | %s |
\n' \
+ "$(_dashboard_html_escape "$name")" "$(_dashboard_html_escape "$target")" \
+ "$glyph" "$(_dashboard_html_escape "$health")" \
"$(_dashboard_html_escape "$last_deploy")" "$(_dashboard_html_escape "$backup_age")"
done
echo "
"
diff --git a/lib/cmd_status_all.sh b/lib/cmd_status_all.sh
index a511030..860a18b 100644
--- a/lib/cmd_status_all.sh
+++ b/lib/cmd_status_all.sh
@@ -71,33 +71,81 @@ _status_backup_age() {
_status_newest_mtime "$dir" "*"
}
-# _status_health — emits one of: healthy | degraded | down | unknown
-#
-# Fast check — runs `docker compose ps` and aggregates container State.
-# Dispatches to the VPS over SSH for stacks whose env file sets VPS_HOST
-# (see _status_health_remote). Returns "unknown" if compose isn't available
-# or the stack has no docker-compose.yml.
-_status_health() {
+# _status_resolve_env_file
+# Uses the entrypoint's stack-aware resolver when available. The fallback keeps
+# this module standalone for tests and direct library consumers.
+_status_resolve_env_file() {
local stack="$1"
local env_name="${2:-}"
local stack_dir="$CLI_ROOT/stacks/$stack"
- [ -f "$stack_dir/docker-compose.yml" ] || { echo "unknown"; return; }
+ if declare -F resolve_env_file >/dev/null 2>&1; then
+ resolve_env_file "$stack" "$env_name"
+ return
+ fi
- local env_file
if [ -n "$env_name" ]; then
- env_file="$CLI_ROOT/.$env_name.env"
+ if [ -f "$stack_dir/.$env_name.enc.env" ]; then
+ echo "$stack_dir/.$env_name.enc.env"
+ elif [ -f "$stack_dir/.$env_name.env" ]; then
+ echo "$stack_dir/.$env_name.env"
+ elif [ -f "$CLI_ROOT/.$env_name.enc.env" ]; then
+ echo "$CLI_ROOT/.$env_name.enc.env"
+ else
+ echo "$CLI_ROOT/.$env_name.env"
+ fi
+ elif [ -f "$stack_dir/.env" ]; then
+ echo "$stack_dir/.env"
else
- env_file="$CLI_ROOT/.env"
+ echo "$CLI_ROOT/.env"
fi
+}
+
+# _status_prepare_stack_context
+# Resolves health intent exactly like normal stack dispatch: stack-aware base
+# env first, then topology defaults and the tracked host layer. Callers should
+# run this in a subshell per stack so arbitrary env-layer values cannot leak to
+# the next row.
+_status_prepare_stack_context() {
+ local stack="$1"
+ local env_name="${2:-}"
+ local stack_dir="$CLI_ROOT/stacks/$stack"
- # Each stack's env file is optional and independent — clear connection vars
- # from the previous loop iteration so a stack with no VPS_HOST doesn't
- # inherit the prior stack's remote target.
- unset VPS_HOST VPS_USER VPS_PORT VPS_SSH_KEY VPS_DEPLOY_DIR 2>/dev/null || true
- [ -f "$env_file" ] && safe_load_env "$env_file" 2>/dev/null || true
+ unset VPS_HOST VPS_USER VPS_PORT VPS_SSH_KEY VPS_DEPLOY_DIR _TOPO_ACTIVE_HOST_ALIAS 2>/dev/null || true
+ _STATUS_ENV_FILE=$(_status_resolve_env_file "$stack" "$env_name")
+ [ -f "$_STATUS_ENV_FILE" ] && safe_load_env "$_STATUS_ENV_FILE" 2>/dev/null || true
+
+ if declare -F topology_apply_to_env >/dev/null 2>&1; then
+ topology_apply_to_env "$stack" "$stack_dir"
+ fi
+
+ _STATUS_TARGET_SCOPE="local"
if should_dispatch_remote; then
+ _STATUS_TARGET_SCOPE="remote"
+ fi
+ _STATUS_TARGET_ALIAS="${_TOPO_ACTIVE_HOST_ALIAS:-}"
+ _STATUS_TARGET_HOST="${VPS_HOST:-}"
+ if [ -n "$env_name" ]; then
+ _STATUS_HEALTH_ENV="$env_name"
+ elif [ "$_STATUS_TARGET_SCOPE" = "remote" ]; then
+ # run_remote_strut preserves the historical no-flag remote default.
+ _STATUS_HEALTH_ENV="prod"
+ else
+ _STATUS_HEALTH_ENV="default"
+ fi
+}
+
+# _status_health_resolved
+# Probes health after _status_prepare_stack_context has resolved the target.
+_status_health_resolved() {
+ local stack="$1"
+ local env_name="${2:-}"
+ local stack_dir="$CLI_ROOT/stacks/$stack"
+
+ [ -f "$stack_dir/docker-compose.yml" ] || { echo "unknown"; return; }
+
+ if [ "$_STATUS_TARGET_SCOPE" = "remote" ]; then
_status_health_remote "$stack" "$env_name"
return
fi
@@ -105,7 +153,7 @@ _status_health() {
command -v docker >/dev/null 2>&1 || { echo "unknown"; return; }
local compose_cmd
- if ! compose_cmd=$(resolve_compose_cmd "$stack" "$env_file" "" 2>/dev/null); then
+ if ! compose_cmd=$(resolve_compose_cmd "$stack" "$_STATUS_ENV_FILE" "" 2>/dev/null); then
echo "unknown"
return
fi
@@ -139,6 +187,31 @@ _status_health() {
fi
}
+# _status_health — compatibility wrapper used by tests and
+# direct callers. Main aggregation uses _status_health_context so target
+# provenance is returned with the result.
+_status_health() {
+ local stack="$1"
+ local env_name="${2:-}"
+ _status_prepare_stack_context "$stack" "$env_name"
+ _status_health_resolved "$stack" "$env_name"
+}
+
+# _status_health_context
+# Emits unit-separator fields: health, scope, alias, host, effective env.
+_status_health_context() {
+ local stack="$1"
+ local env_name="${2:-}"
+ local health
+ _status_prepare_stack_context "$stack" "$env_name"
+ # Call the public wrapper so existing integrations that override
+ # _status_health keep working; command substitution isolates its context.
+ health=$(_status_health "$stack" "$env_name")
+ printf '%s\x1f%s\x1f%s\x1f%s\x1f%s\n' \
+ "$health" "$_STATUS_TARGET_SCOPE" "$_STATUS_TARGET_ALIAS" \
+ "$_STATUS_TARGET_HOST" "$_STATUS_HEALTH_ENV"
+}
+
# _status_health_remote — health for a VPS-deployed stack
#
# Dispatches `health --json` over SSH via run_remote_strut and aggregates the
@@ -185,6 +258,32 @@ _status_health_glyph() {
esac
}
+_status_target_label() {
+ local scope="$1"
+ local alias="${2:-}"
+ local host="${3:-}"
+
+ if [ "$scope" != "remote" ]; then
+ echo "local"
+ elif [ -n "$alias" ] && [ -n "$host" ]; then
+ echo "$alias ($host)"
+ elif [ -n "$alias" ]; then
+ echo "$alias"
+ else
+ echo "${host:-remote}"
+ fi
+}
+
+_status_evidence_age() {
+ local age="$1"
+ local source="$2"
+ if [ "$age" = "-" ]; then
+ echo "-"
+ else
+ echo "$age [$source]"
+ fi
+}
+
# ── Dashboard command ────────────────────────────────────────────────────────
# cmd_status_all [--env ] [--json]
@@ -229,7 +328,7 @@ EOF
[ "${#stacks[@]}" -eq 0 ] && {
if [ "$json_mode" = "true" ]; then
- echo '{"timestamp":"'"$(date -u +%FT%TZ)"'","stacks":[],"summary":{"total":0,"healthy":0,"degraded":0,"down":0}}'
+ echo '{"timestamp":"'"$(date -u +%FT%TZ)"'","stacks":[],"summary":{"total":0,"healthy":0,"degraded":0,"down":0,"unknown":0}}'
return 0
fi
warn "No stacks found — run 'strut scaffold ' to create one"
@@ -242,11 +341,14 @@ EOF
local total=0 healthy=0 degraded=0 down=0 unknown=0
local -a rows_stack rows_health rows_deploy rows_backup
+ local -a rows_target_scope rows_target_alias rows_target_host rows_health_env
for name in "${stacks[@]}"; do
total=$((total + 1))
- local health deploy_ts backup_ts
- health=$(_status_health "$name" "$env_name")
+ local health_context health target_scope target_alias target_host health_env
+ local deploy_ts backup_ts
+ health_context=$(_status_health_context "$name" "$env_name")
+ IFS=$'\x1f' read -r health target_scope target_alias target_host health_env <<< "$health_context"
deploy_ts=$(_status_last_deploy "$name")
backup_ts=$(_status_backup_age "$name")
@@ -265,6 +367,10 @@ EOF
rows_health+=("$health")
rows_deploy+=("$deploy_age")
rows_backup+=("$backup_age")
+ rows_target_scope+=("$target_scope")
+ rows_target_alias+=("$target_alias")
+ rows_target_host+=("$target_host")
+ rows_health_env+=("$health_env")
done
if [ "$json_mode" = "true" ]; then
@@ -280,6 +386,13 @@ EOF
out_json_field "health" "${rows_health[$i]}"
out_json_field "last_deploy" "${rows_deploy[$i]}"
out_json_field "backup_age" "${rows_backup[$i]}"
+ out_json_field "target_scope" "${rows_target_scope[$i]}"
+ out_json_field "target_host_alias" "${rows_target_alias[$i]}"
+ out_json_field "target_host" "${rows_target_host[$i]}"
+ out_json_field "health_source" "${rows_target_scope[$i]}"
+ out_json_field "health_env" "${rows_health_env[$i]}"
+ out_json_field "last_deploy_source" "local_rollback"
+ out_json_field "backup_source" "local_backup"
out_json_close_object
done
out_json_close_array
@@ -292,14 +405,15 @@ EOF
[ -n "$env_name" ] && title="$title ($env_name)"
echo -e "${BLUE}${title}${NC}"
echo ""
- out_table_header "Stack" "Health" "Last Deploy" "Backup Age"
+ out_table_header "Stack" "Target" "Health" "Last Deploy" "Backup Age"
local i
for i in "${!rows_stack[@]}"; do
out_table_row \
"${rows_stack[$i]}" \
+ "$(_status_target_label "${rows_target_scope[$i]}" "${rows_target_alias[$i]}" "${rows_target_host[$i]}")" \
"$(_status_health_glyph "${rows_health[$i]}")" \
- "${rows_deploy[$i]}" \
- "${rows_backup[$i]}"
+ "$(_status_evidence_age "${rows_deploy[$i]}" "local rollback")" \
+ "$(_status_evidence_age "${rows_backup[$i]}" "local backup")"
done
out_table_render
echo ""