Complete scratch iteration 004 setup and grouped route-action fixes

This commit is contained in:
master
2026-03-12 19:28:42 +02:00
parent d8d3133060
commit 317e55e623
26 changed files with 1124 additions and 304 deletions

View File

@@ -70,6 +70,179 @@ service_http_probe_url() {
printf 'http://%s:%s%s' "$host" "$host_port" "$path"
}
get_compose_service_records() {
local seen_names=""
local compose_file compose_path expected_services services_json line service name state health
for compose_file in "$@"; do
if [[ "${compose_file}" = /* ]]; then
compose_path="${compose_file}"
else
compose_path="${COMPOSE_DIR}/${compose_file}"
fi
[[ -f "${compose_path}" ]] || continue
expected_services="$(docker compose -f "${compose_path}" config --services 2>/dev/null || true)"
services_json="$(docker compose -f "${compose_path}" ps --format json 2>/dev/null || true)"
[[ -n "${services_json}" ]] || continue
while IFS= read -r line; do
[[ -z "${line}" ]] && continue
service=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Service',''))" 2>/dev/null || true)
name=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Name',''))" 2>/dev/null || true)
state=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('State',''))" 2>/dev/null || true)
health=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Health',''))" 2>/dev/null || true)
if [[ -n "${expected_services}" ]] && ! printf '%s\n' "${expected_services}" | grep -Fxq "${service}"; then
continue
fi
[[ -n "${name}" ]] || continue
if printf '%s\n' "${seen_names}" | grep -Fxq "${name}"; then
continue
fi
seen_names="${seen_names}
${name}"
printf '%s|%s|%s|%s|%s\n' "${compose_path}" "${service}" "${name}" "${state,,}" "${health,,}"
done <<< "${services_json}"
done
}
wait_for_compose_convergence() {
local success_message="$1"
local restart_stalled="${2:-false}"
local max_wait="${3:-180}"
local restart_after="${4:-45}"
shift 4
local compose_files=("$@")
local elapsed=0
local poll_seconds=5
local restarted_services=""
while (( elapsed < max_wait )); do
local records
records="$(get_compose_service_records "${compose_files[@]}")"
if [[ -n "${records}" ]]; then
local pending=""
local blocking=""
local record compose_file service name state health
while IFS= read -r record; do
[[ -z "${record}" ]] && continue
IFS='|' read -r compose_file service name state health <<< "${record}"
if [[ "${state}" != "running" ]]; then
blocking="${blocking}
${compose_file}|${service}|${name}|state=${state}"
continue
fi
if [[ -z "${health}" || "${health}" == "healthy" ]]; then
continue
fi
if [[ "${health}" == "starting" ]]; then
pending="${pending}
${compose_file}|${service}|${name}|health=starting"
continue
fi
blocking="${blocking}
${compose_file}|${service}|${name}|health=${health}"
done <<< "${records}"
if [[ -z "${blocking//$'\n'/}" && -z "${pending//$'\n'/}" && ${elapsed} -gt ${poll_seconds} ]]; then
ok "${success_message}"
return 0
fi
if [[ "${restart_stalled}" == "true" && ${elapsed} -ge ${restart_after} && -n "${blocking//$'\n'/}" ]]; then
local restart_targets=""
while IFS= read -r record; do
[[ -z "${record}" ]] && continue
IFS='|' read -r compose_file service _ <<< "${record}"
local restart_key="${compose_file}|${service}"
if printf '%s\n' "${restarted_services}" | grep -Fxq "${restart_key}"; then
continue
fi
restarted_services="${restarted_services}
${restart_key}"
restart_targets="${restart_targets}
${compose_file}|${service}"
done <<< "${blocking}"
local compose_to_restart unique_compose_files service_to_restart
local -a services_for_compose
unique_compose_files=$(printf '%s\n' "${restart_targets}" | awk -F'|' 'NF { print $1 }' | sort -u)
while IFS= read -r compose_to_restart; do
[[ -z "${compose_to_restart}" ]] && continue
services_for_compose=()
while IFS= read -r service_to_restart; do
[[ -z "${service_to_restart}" ]] && continue
services_for_compose+=("${service_to_restart}")
done < <(printf '%s\n' "${restart_targets}" | awk -F'|' -v cf="${compose_to_restart}" 'NF && $1 == cf { print $2 }' | sort -u)
if (( ${#services_for_compose[@]} == 0 )); then
continue
fi
warn "Restarting stalled services from ${compose_to_restart}: ${services_for_compose[*]}"
(
cd "${COMPOSE_DIR}" &&
docker compose -f "${compose_to_restart}" restart "${services_for_compose[@]}" >/dev/null
) && ok "Restarted stalled services: ${services_for_compose[*]}" || \
warn "Failed to restart stalled services: ${services_for_compose[*]}"
done <<< "${unique_compose_files}"
fi
fi
sleep "${poll_seconds}"
elapsed=$((elapsed + poll_seconds))
done
local final_records
final_records="$(get_compose_service_records "${compose_files[@]}")"
local final_blocking=""
local final_pending=""
local record compose_file service name state health
while IFS= read -r record; do
[[ -z "${record}" ]] && continue
IFS='|' read -r compose_file service name state health <<< "${record}"
if [[ "${state}" != "running" ]]; then
final_blocking="${final_blocking}
${name} (state=${state})"
continue
fi
if [[ -n "${health}" && "${health}" != "healthy" ]]; then
if [[ "${health}" == "starting" ]]; then
final_pending="${final_pending}
${name} (health=starting)"
else
final_blocking="${final_blocking}
${name} (health=${health})"
fi
fi
done <<< "${final_records}"
if [[ -n "${final_blocking//$'\n'/}" ]]; then
warn "Timed out waiting for compose convergence after ${max_wait}s. Blocking services: $(printf '%s\n' "${final_blocking}" | awk 'NF { print }' | paste -sd ', ' -)"
elif [[ -n "${final_pending//$'\n'/}" ]]; then
warn "Timed out waiting for compose convergence after ${max_wait}s. Still starting: $(printf '%s\n' "${final_pending}" | awk 'NF { print }' | paste -sd ', ' -)"
else
warn "Timed out waiting for compose convergence after ${max_wait}s."
fi
return 1
}
# ─── 1. Check prerequisites ────────────────────────────────────────────────
check_prerequisites() {
@@ -264,27 +437,7 @@ start_infra() {
docker compose -f docker-compose.dev.yml up -d
echo ' Waiting for containers to become healthy...'
local max_wait=120
local elapsed=0
while (( elapsed < max_wait )); do
local all_healthy=true
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local health; health=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Health',''))" 2>/dev/null || true)
if [[ -n "$health" && "$health" != "healthy" ]]; then
all_healthy=false
fi
done < <(docker compose -f docker-compose.dev.yml ps --format json 2>/dev/null)
if [[ "$all_healthy" == "true" && $elapsed -gt 5 ]]; then
ok 'All infrastructure containers healthy'
cd "$ROOT"
return
fi
sleep 5
elapsed=$((elapsed + 5))
done
warn "Timed out waiting for healthy status after ${max_wait}s."
wait_for_compose_convergence 'All infrastructure containers healthy' false 120 45 docker-compose.dev.yml || true
cd "$ROOT"
}
@@ -330,6 +483,7 @@ start_platform() {
docker compose -f docker-compose.stella-ops.yml up -d
ok 'Platform services started'
cd "$ROOT"
wait_for_compose_convergence 'Platform services converged from zero-state startup' true 180 45 docker-compose.stella-ops.yml || true
}
http_status() {