#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

STEP="STEP_050M07R13A_REBUILD_HMN_DOWNLOAD_ALL_AWG_SELECTOR"
PASS_RESULT="PASS_${STEP}"
STOP_RESULT="STOP_${STEP}"
PHASE="bootstrap"
FAILURE_REASON=""
RESULT="$STOP_RESULT"

TOKEN="e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4"
TUNNEL_BASE="https://receiver-rejected-average-combat.trycloudflare.com"
PUBLIC_ROOT="/opt/router-ops/public/r/$TOKEN"
TS="$(date -u +%Y%m%d-%H%M%S)"
SLUG="step050m07r13a_rebuild_hmn_download_all_awg_selector"
WORK_DIR="/opt/router-ops/work/${TS}_${SLUG}"
REPORT_DIR="$PUBLIC_ROOT/${TS}_${SLUG}"
REPORT_URL="$TUNNEL_BASE/r/$TOKEN/${TS}_${SLUG}/"

LOCAL_PLAN_DIR="$PUBLIC_ROOT/${TS}_local_architecture_plan_vm101_autonomous_hmn_recovery"
NEW_LOCAL_PLAN_URL="$TUNNEL_BASE/r/$TOKEN/${TS}_local_architecture_plan_vm101_autonomous_hmn_recovery/"
MODEL_DIR="$PUBLIC_ROOT/${TS}_vm101_model_hmn_download_selector_r13a"
NEW_MODEL_FILE_URL="$TUNNEL_BASE/r/$TOKEN/${TS}_vm101_model_hmn_download_selector_r13a/vm101-model.txt"
METHODS_DIR="$PUBLIC_ROOT/${TS}_vm101_methods_hmn_download_selector_r13a"
NEW_METHODS_URL="$TUNNEL_BASE/r/$TOKEN/${TS}_vm101_methods_hmn_download_selector_r13a/"

LOCAL_PLAN_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260711-181158_local_architecture_plan_vm101_autonomous_hmn_recovery/"
MODEL_FILE_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260713-180132_vm101_machine_git_v2_migration_final/vm101-model.txt"
METHODS_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260713-180132_vm101_machine_git_v2_migration_final/methods.txt"

GLOBAL_PROJECT_PLAN_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260711-123348_global_project_plan_wg_paid/"
ACCESS_MAP_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260711-120734_xs_map_audit_repair_publish/"
XS_MAP_URL="$ACCESS_MAP_URL"
VM100_PUBLIC_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260713-182653_vm100_git_source_097755408853/"
VM101_PUBLIC_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260713-182656_vm101_git_source_0cdefcb9bd39/"
VM121_PUBLIC_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260713-182700_vm121_git_source_93e0e5a31f09/"

TARGET="/root/hmn/hmn-download-all-awg.sh"
RUNTIME_LIB="/usr/local/lib/router-egress-vm101-runtime.sh"
EXPECTED_OLD_SHA="c20d8ff7feead493f2524c6da3c921bbf62cbdfb503a11a8ea75e4d15144e18f"
EXPECTED_RUNTIME_SHA="1fd8d8b9258f7ca1d711323b7652f0b46991e1d17346b59e1d8ff5d71ba471d9"
EXPECTED_NEW_SHA="f695a17f9ca5ac9414c11d6a3d008d2dfa1efe3219d11341a58dd63ff7d5e340"
EXPECTED_BODY_SHA="40c52441d31cad02155102bf86fd6583eff605d10d6ae63568e8b67d7911dfa4"
BASELINE_VM101_COMMIT="0cdefcb9bd39e750d200c00f56a3d55c1d18b14a"
VM101_ARCHIVE_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4/20260713-182656_vm101_git_source_0cdefcb9bd39/wg-paid-vm101-0cdefcb9bd39.tar.gz"
EXPECTED_ARCHIVE_SHA="d99cee7fdb679bb1086fd92dc39bc019462dcd773390845dc7daeb41edd5c155"

PVE_ALIAS="pve-mgts"
VM101_IP="10.71.100.2"
PVE_TO_OPENWRT_KEY="/root/.ssh/pve_to_openwrt_mgts_ed25519"
REMOTE_TMP="/root/hmn/.hmn-download-all-awg.sh.r13a.${TS}.tmp"
REMOTE_HARNESS="/tmp/hmn-download-selector-r13a.${TS}.sh"
BACKUP_PATH="/root/hmn/backups/hmn-download-all-awg.sh.before-r13a-${TS}"

CANDIDATE="$WORK_DIR/hmn-download-all-awg.candidate.sh"
HARNESS="$WORK_DIR/hmn-download-all-awg.selector-harness.sh"
PROGRESS_LOG="$REPORT_DIR/progress.log"
PREFLIGHT_LOG="$REPORT_DIR/preflight.log"
PREFLIGHT_ERR="$REPORT_DIR/preflight.stderr"
HARNESS_LOG="$REPORT_DIR/selector-harness.log"
HARNESS_ERR="$REPORT_DIR/selector-harness.stderr"
UPLOAD_LOG="$REPORT_DIR/upload.log"
UPLOAD_ERR="$REPORT_DIR/upload.stderr"
INSTALL_LOG="$REPORT_DIR/install.log"
INSTALL_ERR="$REPORT_DIR/install.stderr"
POSTCHECK_BEFORE_LOG="$REPORT_DIR/postcheck-before-machine-close.log"
POSTCHECK_BEFORE_ERR="$REPORT_DIR/postcheck-before-machine-close.stderr"
MACHINE_CLOSE_LOG="$REPORT_DIR/machine-close.log"
POSTCHECK_AFTER_LOG="$REPORT_DIR/postcheck-after-machine-close.log"
POSTCHECK_AFTER_ERR="$REPORT_DIR/postcheck-after-machine-close.stderr"
VERIFY_FILE="$REPORT_DIR/verify.txt"
ROLLBACK_FILE="$REPORT_DIR/rollback.sh"
METHODS_FILE="$REPORT_DIR/methods-and-limitations.txt"

BOOTSTRAP_IFACE=""
OLD_SHA_ACTUAL=""
NEW_SHA_ACTUAL=""
VM101_COMMIT=""
VM101_TREE=""
VM101_FILE_COUNT=""
CHANGED_FILES=""
REMOTE_HEAD=""
MACHINE_CLOSE_RAN="false"
REFRESH_RAN="false"
REBALANCE_RAN="false"
SERVICES_RESTARTED="false"
RUNTIME_IMPACT="false"
EXTERNAL_VERIFY="false"

mkdir -p "$WORK_DIR" "$REPORT_DIR" "$LOCAL_PLAN_DIR" "$MODEL_DIR" "$METHODS_DIR"
cp "$0" "$REPORT_DIR/step-code.sh"
chmod 600 "$REPORT_DIR/step-code.sh"
exec > >(tee -a "$PROGRESS_LOG") 2>&1

log() {
  printf '>>> [%s] %s\n' "$PHASE" "$*"
}

make_index() {
  local dir="$1"
  local title="$2"
  shift 2
  {
    printf '<!doctype html><html><head><meta charset="utf-8"><title>%s</title></head><body>\n' "$title"
    printf '<h1>%s</h1><ul>\n' "$title"
    while [ "$#" -gt 0 ]; do
      printf '<li><a href="%s">%s</a></li>\n' "$1" "$1"
      shift
    done
    printf '</ul></body></html>\n'
  } > "$dir/index.html"
  chmod 644 "$dir/index.html"
}

write_facts() {
  export STEP RESULT PHASE FAILURE_REASON TS REPORT_URL LOCAL_PLAN_URL GLOBAL_PROJECT_PLAN_URL
  export ACCESS_MAP_URL XS_MAP_URL VM100_PUBLIC_URL VM101_PUBLIC_URL VM121_PUBLIC_URL
  export MODEL_FILE_URL METHODS_URL EXPECTED_OLD_SHA OLD_SHA_ACTUAL EXPECTED_NEW_SHA NEW_SHA_ACTUAL
  export EXPECTED_RUNTIME_SHA EXPECTED_BODY_SHA BASELINE_VM101_COMMIT BOOTSTRAP_IFACE BACKUP_PATH
  export VM101_COMMIT VM101_TREE VM101_FILE_COUNT CHANGED_FILES REMOTE_HEAD MACHINE_CLOSE_RAN
  export REFRESH_RAN REBALANCE_RAN SERVICES_RESTARTED RUNTIME_IMPACT EXTERNAL_VERIFY
  python3 - "$REPORT_DIR/facts.json" <<'PY'
import json, os, sys
p=sys.argv[1]
def b(name): return os.environ.get(name,'').lower() == 'true'
data={
  "schema":"router-step-facts-v1",
  "step":os.environ.get("STEP"),
  "timestamp_utc":os.environ.get("TS"),
  "assessment":{
    "decision":os.environ.get("RESULT"),
    "phase":os.environ.get("PHASE"),
    "failure_reason":os.environ.get("FAILURE_REASON") or None,
  },
  "change":{
    "machine":"vm101",
    "target":"/root/hmn/hmn-download-all-awg.sh",
    "baseline_commit":os.environ.get("BASELINE_VM101_COMMIT"),
    "before_sha256_expected":os.environ.get("EXPECTED_OLD_SHA"),
    "before_sha256_actual":os.environ.get("OLD_SHA_ACTUAL") or None,
    "after_sha256_expected":os.environ.get("EXPECTED_NEW_SHA"),
    "after_sha256_actual":os.environ.get("NEW_SHA_ACTUAL") or None,
    "unchanged_body_sha256":os.environ.get("EXPECTED_BODY_SHA"),
    "runtime_library_sha256":os.environ.get("EXPECTED_RUNTIME_SHA"),
    "bootstrap_iface":os.environ.get("BOOTSTRAP_IFACE") or None,
    "backup_path":os.environ.get("BACKUP_PATH"),
  },
  "machine_git":{
    "machine_close_ran":b("MACHINE_CLOSE_RAN"),
    "commit":os.environ.get("VM101_COMMIT") or None,
    "tree":os.environ.get("VM101_TREE") or None,
    "remote_head":os.environ.get("REMOTE_HEAD") or None,
    "tracked_file_count":int(os.environ["VM101_FILE_COUNT"]) if os.environ.get("VM101_FILE_COUNT","").isdigit() else None,
    "changed_files":[x for x in os.environ.get("CHANGED_FILES","").splitlines() if x],
  },
  "safety":{
    "refresh_ran":b("REFRESH_RAN"),
    "rebalance_ran":b("REBALANCE_RAN"),
    "services_restarted":b("SERVICES_RESTARTED"),
    "runtime_impact":b("RUNTIME_IMPACT"),
    "external_http_verify":b("EXTERNAL_VERIFY"),
  },
  "links":{
    "step_report":os.environ.get("REPORT_URL"),
    "local_m07_plan":os.environ.get("LOCAL_PLAN_URL"),
    "global_project_plan":os.environ.get("GLOBAL_PROJECT_PLAN_URL"),
    "access_map":os.environ.get("ACCESS_MAP_URL"),
    "xs_map":os.environ.get("XS_MAP_URL"),
    "vm100":os.environ.get("VM100_PUBLIC_URL"),
    "vm101":os.environ.get("VM101_PUBLIC_URL") or None,
    "vm121":os.environ.get("VM121_PUBLIC_URL"),
    "vm101_model":os.environ.get("MODEL_FILE_URL"),
    "vm101_methods":os.environ.get("METHODS_URL"),
  }
}
with open(p,'w',encoding='utf-8') as f:
  json.dump(data,f,ensure_ascii=False,indent=2)
  f.write('\n')
PY
  chmod 644 "$REPORT_DIR/facts.json"
}

write_report() {
  cat > "$REPORT_DIR/report.txt" <<EOF_REPORT
RESULT=$RESULT
STEP=$STEP
TIMESTAMP_UTC=$TS
PHASE=$PHASE
FAILURE_REASON=${FAILURE_REASON:-NONE}

EXPECTED_STATE:
- Only VM101 /root/hmn/hmn-download-all-awg.sh changes.
- Caller HMN_REQUEST_IFACE overrides hmn.env.
- auto uses vm101_healthy_bootstrap_iface from the existing runtime library.
- Explicit vpn1..vpn5 is checked by vm101_strict_iface.
- Legacy direct table-200 selector is absent.
- Downloader body from TS= onward is byte-for-byte unchanged.
- Full refresh, rebalance, service restart and VPN recreation do not run.

ACTUAL_STATE:
- before_sha256_expected=$EXPECTED_OLD_SHA
- before_sha256_actual=${OLD_SHA_ACTUAL:-UNKNOWN}
- after_sha256_expected=$EXPECTED_NEW_SHA
- after_sha256_actual=${NEW_SHA_ACTUAL:-UNKNOWN}
- unchanged_body_sha256=$EXPECTED_BODY_SHA
- runtime_library_sha256=$EXPECTED_RUNTIME_SHA
- selected_bootstrap_iface=${BOOTSTRAP_IFACE:-UNKNOWN}
- backup_path=$BACKUP_PATH
- machine_git_commit=${VM101_COMMIT:-UNKNOWN}
- machine_git_tree=${VM101_TREE:-UNKNOWN}
- machine_git_remote_head=${REMOTE_HEAD:-UNKNOWN}
- machine_git_file_count=${VM101_FILE_COUNT:-UNKNOWN}
- changed_files=${CHANGED_FILES:-UNKNOWN}

INDEPENDENT_CHECKS:
- candidate contract: $REPORT_URL/candidate-contract.json
- selector harness: $REPORT_URL/selector-harness.log
- pre-close post-check: $REPORT_URL/postcheck-before-machine-close.log
- post-close independent check: $REPORT_URL/postcheck-after-machine-close.log
- external HTTP verification: $EXTERNAL_VERIFY

RUNTIME:
- RUNTIME_IMPACT=$RUNTIME_IMPACT
- REFRESH_RAN=$REFRESH_RAN
- REBALANCE_RAN=$REBALANCE_RAN
- SERVICES_RESTARTED=$SERVICES_RESTARTED

ROLLBACK:
- backup=$BACKUP_PATH
- script=${REPORT_URL}rollback.sh

STEP_REPORT_URL=$REPORT_URL
LOCAL_M07_PLAN_URL=$LOCAL_PLAN_URL
GLOBAL_PROJECT_PLAN_URL=$GLOBAL_PROJECT_PLAN_URL
ACCESS_MAP_URL=$ACCESS_MAP_URL
XS_MAP_URL=$XS_MAP_URL
VM100_PUBLIC_URL=$VM100_PUBLIC_URL
VM101_PUBLIC_URL=${VM101_PUBLIC_URL:-UNAVAILABLE}
VM121_PUBLIC_URL=$VM121_PUBLIC_URL
VM101_MODEL_URL=$MODEL_FILE_URL
VM101_METHODS_URL=$METHODS_URL
FACTS_URL=${REPORT_URL}facts.json
VERIFY_URL=${REPORT_URL}verify.txt
ROLLBACK_URL=${REPORT_URL}rollback.sh
ARCHITECTURE_PLAN_URL=$LOCAL_PLAN_URL
EOF_REPORT
  chmod 644 "$REPORT_DIR/report.txt"
}

write_final_materials() {
  write_facts
  write_report
  make_index "$REPORT_DIR" "$STEP" \
    report.txt facts.json step-code.sh progress.log candidate-contract.json \
    selector-change.txt hmn-download-all-awg.candidate.sh selector-harness.log \
    preflight.log install.log postcheck-before-machine-close.log machine-close.log \
    postcheck-after-machine-close.log verify.txt rollback.sh methods-and-limitations.txt
}

print_final() {
  printf '\nRESULT=%s\n' "$RESULT"
  printf 'STEP_REPORT_URL=%s\n' "$REPORT_URL"
  printf 'LOCAL_M07_PLAN_URL=%s\n' "$LOCAL_PLAN_URL"
  printf 'GLOBAL_PROJECT_PLAN_URL=%s\n' "$GLOBAL_PROJECT_PLAN_URL"
  printf 'ACCESS_MAP_URL=%s\n' "$ACCESS_MAP_URL"
  printf 'XS_MAP_URL=%s\n' "$XS_MAP_URL"
  printf 'VM100_PUBLIC_URL=%s\n' "$VM100_PUBLIC_URL"
  printf 'VM101_PUBLIC_URL=%s\n' "${VM101_PUBLIC_URL:-UNAVAILABLE}"
  printf 'VM121_PUBLIC_URL=%s\n' "$VM121_PUBLIC_URL"
  printf 'VM101_MODEL_URL=%s\n' "$MODEL_FILE_URL"
  printf 'VM101_METHODS_URL=%s\n' "$METHODS_URL"
  printf 'FACTS_URL=%sfacts.json\n' "$REPORT_URL"
  printf 'VERIFY_URL=%sverify.txt\n' "$REPORT_URL"
  printf 'ROLLBACK_URL=%srollback.sh\n' "$REPORT_URL"
  printf 'ARCHITECTURE_PLAN_URL=%s\n' "$LOCAL_PLAN_URL"
}

on_error() {
  local rc=$?
  trap - ERR
  set +e
  RESULT="$STOP_RESULT"
  FAILURE_REASON="${FAILURE_REASON:-COMMAND_FAILED_IN_${PHASE}_RC_${rc}}"
  printf '>>> [STOP] phase=%s rc=%s reason=%s\n' "$PHASE" "$rc" "$FAILURE_REASON"
  write_final_materials
  print_final
  exit "$rc"
}
trap on_error ERR

vm101_run_script() {
  local script_file="$1"
  local stdout_file="$2"
  local stderr_file="$3"
  ssh -T -o BatchMode=yes "$PVE_ALIAS" \
    "ssh -T -o BatchMode=yes -i $PVE_TO_OPENWRT_KEY root@$VM101_IP /bin/ash -s" \
    < "$script_file" > "$stdout_file" 2> "$stderr_file"
}

vm101_put_file() {
  local local_file="$1"
  local remote_file="$2"
  local stdout_file="$3"
  local stderr_file="$4"
  ssh -T -o BatchMode=yes "$PVE_ALIAS" \
    "ssh -T -o BatchMode=yes -i $PVE_TO_OPENWRT_KEY root@$VM101_IP 'umask 077; cat > $remote_file'" \
    < "$local_file" > "$stdout_file" 2> "$stderr_file"
}

PHASE="local-preflight"
log "checking router-ops environment"
[ "$(id -un)" = "ops" ] || { FAILURE_REASON="RUN_AS_OPS_ON_ROUTER_OPS"; false; }
for cmd in ssh sha256sum python3 curl tar router-machine-close; do
  command -v "$cmd" >/dev/null 2>&1 || { FAILURE_REASON="MISSING_COMMAND_${cmd}"; false; }
done
[ -d "$PUBLIC_ROOT" ] && [ -w "$PUBLIC_ROOT" ] || { FAILURE_REASON="PUBLIC_ROOT_NOT_WRITABLE"; false; }

PHASE="candidate-build"
log "building exact candidate from published VM101 baseline"
ARCHIVE="$WORK_DIR/wg-paid-vm101-0cdefcb9bd39.tar.gz"
EXTRACT_DIR="$WORK_DIR/published-vm101"
curl -fsS --retry 4 --retry-all-errors --connect-timeout 10 --max-time 120 \
  "$VM101_ARCHIVE_URL" -o "$ARCHIVE"
ARCHIVE_SHA_ACTUAL="$(sha256sum "$ARCHIVE" | awk '{print $1}')"
[ "$ARCHIVE_SHA_ACTUAL" = "$EXPECTED_ARCHIVE_SHA" ] || { FAILURE_REASON="PUBLISHED_ARCHIVE_SHA_MISMATCH"; false; }
mkdir -p "$EXTRACT_DIR"
tar -xzf "$ARCHIVE" -C "$EXTRACT_DIR"
SOURCE_FILE="$EXTRACT_DIR/wg-paid-vm101-0cdefcb9bd39/root/hmn/hmn-download-all-awg.sh"
[ -f "$SOURCE_FILE" ] || { FAILURE_REASON="PUBLISHED_SOURCE_FILE_MISSING"; false; }
SOURCE_SHA_ACTUAL="$(sha256sum "$SOURCE_FILE" | awk '{print $1}')"
[ "$SOURCE_SHA_ACTUAL" = "$EXPECTED_OLD_SHA" ] || { FAILURE_REASON="PUBLISHED_SOURCE_SHA_MISMATCH"; false; }

python3 - "$SOURCE_FILE" "$CANDIDATE" <<'PY_BUILD'
from pathlib import Path
import sys
src=Path(sys.argv[1])
out=Path(sys.argv[2])
s=src.read_text()
old='''BASE="/root/hmn"
ENV_FILE="$BASE/hmn.env"

if [ ! -f "$ENV_FILE" ]; then
  echo "ERROR: нет $ENV_FILE"
  echo "Сначала запусти:"
  echo "  /root/hmn/hmn-set-code.sh"
  exit 1
fi

. "$ENV_FILE"

AWG_PARAM="${HMN_AWG_PARAM:-1}"
SERVERLIST_URL="${HMN_SERVERLIST_URL:-https://hide-my-name.net/api/serverlist.php?out=js&wg}"
CONFIG_URL="${HMN_CONFIG_URL:-https://hide-my-name.net/api/vpn_get_config_wg.php}"

if [ -z "${HMN_ACCESS_CODE:-}" ]; then
  echo "ERROR: HMN_ACCESS_CODE пустой в $ENV_FILE"
  exit 1
fi

if ! command -v curl >/dev/null 2>&1; then
  echo "ERROR: нужен curl"
  exit 1
fi

if [ "${HMN_REQUEST_IFACE:-auto}" = "auto" ]; then
  ACTIVE="$(ip route show table 200 | awk '/^default dev /{print $3; exit}')"
else
  ACTIVE="$HMN_REQUEST_IFACE"
fi

if [ -z "${ACTIVE:-}" ]; then
  echo "ERROR: не нашёл active VPN interface."
  echo "Проверь:"
  echo "  ip route show table 200"
  exit 1
fi
'''
new='''BASE="/root/hmn"
ENV_FILE="$BASE/hmn.env"

if [ ! -f "$ENV_FILE" ]; then
  echo "ERROR: нет $ENV_FILE"
  echo "Сначала запусти:"
  echo "  /root/hmn/hmn-set-code.sh"
  exit 1
fi

HMN_REQUEST_IFACE_CALLER_SET="${HMN_REQUEST_IFACE+x}"
HMN_REQUEST_IFACE_CALLER_VALUE="${HMN_REQUEST_IFACE-}"

. "$ENV_FILE"

AWG_PARAM="${HMN_AWG_PARAM:-1}"
SERVERLIST_URL="${HMN_SERVERLIST_URL:-https://hide-my-name.net/api/serverlist.php?out=js&wg}"
CONFIG_URL="${HMN_CONFIG_URL:-https://hide-my-name.net/api/vpn_get_config_wg.php}"

if [ -z "${HMN_ACCESS_CODE:-}" ]; then
  echo "ERROR: HMN_ACCESS_CODE пустой в $ENV_FILE"
  exit 1
fi

if ! command -v curl >/dev/null 2>&1; then
  echo "ERROR: нужен curl"
  exit 1
fi

RUNTIME_LIB="/usr/local/lib/router-egress-vm101-runtime.sh"

if [ ! -r "$RUNTIME_LIB" ]; then
  echo "ERROR: нет читаемой runtime library: $RUNTIME_LIB"
  exit 1
fi

. "$RUNTIME_LIB"

if [ "$HMN_REQUEST_IFACE_CALLER_SET" = "x" ]; then
  REQUEST_IFACE="$HMN_REQUEST_IFACE_CALLER_VALUE"
else
  REQUEST_IFACE="${HMN_REQUEST_IFACE:-auto}"
fi

case "$REQUEST_IFACE" in
  auto)
    ACTIVE="$(vm101_healthy_bootstrap_iface || true)"
    ;;

  vpn1|vpn2|vpn3|vpn4|vpn5)
    if ! vm101_strict_iface "$REQUEST_IFACE" 1 0; then
      echo "ERROR: requested VPN interface is invalid or unhealthy: $REQUEST_IFACE"
      exit 1
    fi

    ACTIVE="$REQUEST_IFACE"
    ;;

  *)
    echo "ERROR: requested VPN interface is invalid or unhealthy: $REQUEST_IFACE"
    exit 1
    ;;
esac

if [ -z "${ACTIVE:-}" ]; then
  echo "ERROR: не найден ни один здоровый VPN interface vpn1-vpn5."
  echo "Optional table 200 routes:"
  ip route show table 200 2>/dev/null || true
  exit 1
fi
'''
if s.count(old)!=1:
    raise SystemExit(f'legacy selector block count={s.count(old)}')
out.write_text(s.replace(old,new,1))
PY_BUILD
chmod 700 "$CANDIDATE"

NEW_SHA_ACTUAL="$(sha256sum "$CANDIDATE" | awk '{print $1}')"
[ "$NEW_SHA_ACTUAL" = "$EXPECTED_NEW_SHA" ] || { FAILURE_REASON="CANDIDATE_SHA_MISMATCH"; false; }
bash -n "$CANDIDATE"

python3 - "$CANDIDATE" "$REPORT_DIR/candidate-contract.json" "$EXPECTED_BODY_SHA" <<'PY'
from pathlib import Path
import hashlib, json, sys
p=Path(sys.argv[1]); out=Path(sys.argv[2]); expected_body=sys.argv[3]
s=p.read_text()
marker='TS="$(date +%Y%m%d-%H%M%S)"'
assert s.count(marker)==1
body=s[s.index(marker):]
checks={
 "runtime_library_sourced":'. "$RUNTIME_LIB"' in s,
 "caller_set_captured_before_env":s.index('HMN_REQUEST_IFACE_CALLER_SET=') < s.index('. "$ENV_FILE"'),
 "caller_value_captured_before_env":s.index('HMN_REQUEST_IFACE_CALLER_VALUE=') < s.index('. "$ENV_FILE"'),
 "caller_override_preferred":'if [ "$HMN_REQUEST_IFACE_CALLER_SET" = "x" ]' in s,
 "explicit_iface_whitelist":'vpn1|vpn2|vpn3|vpn4|vpn5)' in s,
 "explicit_iface_strict_check":'vm101_strict_iface "$REQUEST_IFACE" 1 0' in s,
 "healthy_bootstrap_used":'vm101_healthy_bootstrap_iface || true' in s,
 "legacy_direct_selector_absent":"ACTIVE=\"$(ip route show table 200 | awk" not in s,
 "no_healthy_is_fatal":'не найден ни один здоровый VPN interface vpn1-vpn5' in s,
 "body_unchanged_hash":hashlib.sha256(body.encode()).hexdigest()==expected_body,
}
failed=[k for k,v in checks.items() if not v]
data={"schema":"hmn-download-selector-candidate-v1","candidate_sha256":hashlib.sha256(s.encode()).hexdigest(),"body_sha256":hashlib.sha256(body.encode()).hexdigest(),"checks":checks,"failures":failed}
out.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n')
if failed: raise SystemExit('failed checks: '+','.join(failed))
PY
chmod 644 "$REPORT_DIR/candidate-contract.json"
cp "$CANDIDATE" "$REPORT_DIR/hmn-download-all-awg.candidate.sh"
chmod 644 "$REPORT_DIR/hmn-download-all-awg.candidate.sh"

cat > "$REPORT_DIR/selector-change.txt" <<'EOF_CHANGE'
Changed selector contract only:
- preserve caller HMN_REQUEST_IFACE before sourcing hmn.env;
- source /usr/local/lib/router-egress-vm101-runtime.sh;
- auto -> vm101_healthy_bootstrap_iface;
- explicit vpn1..vpn5 -> vm101_strict_iface;
- invalid/unhealthy/no healthy VPN -> fatal;
- legacy direct table-200 selector removed;
- all code from TS="$(date +%Y%m%d-%H%M%S)" to EOF remains unchanged.
EOF_CHANGE
chmod 644 "$REPORT_DIR/selector-change.txt"

python3 - "$CANDIDATE" "$HARNESS" <<'PY'
from pathlib import Path
import sys
src=Path(sys.argv[1]).read_text()
marker='TS="$(date +%Y%m%d-%H%M%S)"'
replacement='printf "SELECTED_INTERFACE=%s\\n" "$ACTIVE"\nexit 0\n\n'+marker
if src.count(marker)!=1: raise SystemExit('marker count != 1')
Path(sys.argv[2]).write_text(src.replace(marker,replacement,1))
PY
chmod 700 "$HARNESS"

PHASE="remote-preflight"
log "checking exact VM101 baseline, runtime library, free space and Machine Git cleanliness"
REMOTE_PREFLIGHT="$WORK_DIR/remote-preflight.sh"
cat > "$REMOTE_PREFLIGHT" <<EOF_REMOTE
#!/bin/ash
set -eu
TARGET='$TARGET'
RUNTIME_LIB='$RUNTIME_LIB'
EXPECTED_OLD='$EXPECTED_OLD_SHA'
EXPECTED_RUNTIME='$EXPECTED_RUNTIME_SHA'
[ -f "\$TARGET" ]
[ -r "\$RUNTIME_LIB" ]
OLD_SHA="\$(sha256sum "\$TARGET" | awk '{print \$1}')"
RUNTIME_SHA="\$(sha256sum "\$RUNTIME_LIB" | awk '{print \$1}')"
[ "\$OLD_SHA" = "\$EXPECTED_OLD" ]
[ "\$RUNTIME_SHA" = "\$EXPECTED_RUNTIME" ]
FREE_KB="\$(df -Pk /root | awk 'NR==2 {print \$4}')"
[ "\$FREE_KB" -ge 2048 ]
. "\$RUNTIME_LIB"
BOOTSTRAP_IFACE="\$(vm101_healthy_bootstrap_iface || true)"
case "\$BOOTSTRAP_IFACE" in vpn1|vpn2|vpn3|vpn4|vpn5) ;; *) exit 21 ;; esac
vm101_strict_iface "\$BOOTSTRAP_IFACE" 1 0
echo "OLD_SHA=\$OLD_SHA"
echo "RUNTIME_SHA=\$RUNTIME_SHA"
echo "FREE_KB=\$FREE_KB"
echo "BOOTSTRAP_IFACE=\$BOOTSTRAP_IFACE"
echo "TARGET_META=\$(ls -ldn "\$TARGET")"
echo "MACHINE_GIT_CHECK_BEGIN"
/usr/local/sbin/router-vm101-git-publish.sh --check
echo "MACHINE_GIT_CHECK_END"
EOF_REMOTE
chmod 700 "$REMOTE_PREFLIGHT"
vm101_run_script "$REMOTE_PREFLIGHT" "$PREFLIGHT_LOG" "$PREFLIGHT_ERR"
cat "$PREFLIGHT_LOG"
[ ! -s "$PREFLIGHT_ERR" ] || cat "$PREFLIGHT_ERR"
OLD_SHA_ACTUAL="$(awk -F= '/^OLD_SHA=/{print $2; exit}' "$PREFLIGHT_LOG")"
BOOTSTRAP_IFACE="$(awk -F= '/^BOOTSTRAP_IFACE=/{print $2; exit}' "$PREFLIGHT_LOG")"
[ "$OLD_SHA_ACTUAL" = "$EXPECTED_OLD_SHA" ] || { FAILURE_REASON="REMOTE_BASELINE_HASH_MISMATCH"; false; }
case "$BOOTSTRAP_IFACE" in vpn1|vpn2|vpn3|vpn4|vpn5) ;; *) FAILURE_REASON="NO_HEALTHY_BOOTSTRAP_IFACE"; false ;; esac

PHASE="selector-harness"
log "proving auto, explicit caller override and invalid interface rejection without starting downloader"
vm101_put_file "$HARNESS" "$REMOTE_HARNESS" "$UPLOAD_LOG" "$UPLOAD_ERR"
REMOTE_HARNESS_TEST="$WORK_DIR/remote-harness-test.sh"
cat > "$REMOTE_HARNESS_TEST" <<EOF_REMOTE
#!/bin/ash
set -eu
HARNESS='$REMOTE_HARNESS'
BOOTSTRAP='$BOOTSTRAP_IFACE'
chmod 700 "\$HARNESS"
AUTO_OUT="\$(HMN_REQUEST_IFACE=auto "\$HARNESS")"
EXPLICIT_OUT="\$(HMN_REQUEST_IFACE="\$BOOTSTRAP" "\$HARNESS")"
set +e
INVALID_OUT="\$(HMN_REQUEST_IFACE=eth0 "\$HARNESS" 2>&1)"
INVALID_RC=\$?
set -e
rm -f "\$HARNESS"
[ "\$AUTO_OUT" = "SELECTED_INTERFACE=\$BOOTSTRAP" ]
[ "\$EXPLICIT_OUT" = "SELECTED_INTERFACE=\$BOOTSTRAP" ]
[ "\$INVALID_RC" -ne 0 ]
printf 'AUTO_OUT=%s\n' "\$AUTO_OUT"
printf 'EXPLICIT_OUT=%s\n' "\$EXPLICIT_OUT"
printf 'INVALID_RC=%s\n' "\$INVALID_RC"
printf 'INVALID_OUT=%s\n' "\$INVALID_OUT"
echo 'REFRESH_RAN=false'
echo 'REBALANCE_RAN=false'
EOF_REMOTE
chmod 700 "$REMOTE_HARNESS_TEST"
vm101_run_script "$REMOTE_HARNESS_TEST" "$HARNESS_LOG" "$HARNESS_ERR"
cat "$HARNESS_LOG"
[ ! -s "$HARNESS_ERR" ] || cat "$HARNESS_ERR"

PHASE="candidate-upload"
log "uploading candidate by raw stdin to same-directory temporary file"
: > "$UPLOAD_LOG"
: > "$UPLOAD_ERR"
vm101_put_file "$CANDIDATE" "$REMOTE_TMP" "$UPLOAD_LOG" "$UPLOAD_ERR"
[ ! -s "$UPLOAD_ERR" ] || cat "$UPLOAD_ERR"

PHASE="atomic-install"
log "installing candidate atomically with backup and automatic local rollback on verification failure"
REMOTE_INSTALL="$WORK_DIR/remote-install.sh"
cat > "$REMOTE_INSTALL" <<EOF_REMOTE
#!/bin/ash
set -eu
TARGET='$TARGET'
TMP='$REMOTE_TMP'
BACKUP='$BACKUP_PATH'
EXPECTED_OLD='$EXPECTED_OLD_SHA'
EXPECTED_NEW='$EXPECTED_NEW_SHA'
CURRENT="\$(sha256sum "\$TARGET" | awk '{print \$1}')"
CANDIDATE="\$(sha256sum "\$TMP" | awk '{print \$1}')"
[ "\$CURRENT" = "\$EXPECTED_OLD" ]
[ "\$CANDIDATE" = "\$EXPECTED_NEW" ]
/bin/ash -n "\$TMP"
mkdir -p /root/hmn/backups
[ ! -e "\$BACKUP" ]
cp -p "\$TARGET" "\$BACKUP"
chown 0:0 "\$BACKUP"
chmod 700 "\$BACKUP"
chown 0:0 "\$TMP"
chmod 700 "\$TMP"
mv -f "\$TMP" "\$TARGET"
sync
if ! /bin/ash -n "\$TARGET"; then
  cp -p "\$BACKUP" "\$TARGET"
  sync
  echo 'ROLLBACK_PERFORMED=true'
  exit 31
fi
INSTALLED="\$(sha256sum "\$TARGET" | awk '{print \$1}')"
if [ "\$INSTALLED" != "\$EXPECTED_NEW" ]; then
  cp -p "\$BACKUP" "\$TARGET"
  sync
  echo 'ROLLBACK_PERFORMED=true'
  exit 32
fi
echo "BEFORE_SHA=\$CURRENT"
echo "CANDIDATE_SHA=\$CANDIDATE"
echo "INSTALLED_SHA=\$INSTALLED"
echo "BACKUP_PATH=\$BACKUP"
echo "TARGET_META=\$(ls -ldn "\$TARGET")"
echo 'ROLLBACK_PERFORMED=false'
EOF_REMOTE
chmod 700 "$REMOTE_INSTALL"
vm101_run_script "$REMOTE_INSTALL" "$INSTALL_LOG" "$INSTALL_ERR"
cat "$INSTALL_LOG"
[ ! -s "$INSTALL_ERR" ] || cat "$INSTALL_ERR"
NEW_SHA_ACTUAL="$(awk -F= '/^INSTALLED_SHA=/{print $2; exit}' "$INSTALL_LOG")"
[ "$NEW_SHA_ACTUAL" = "$EXPECTED_NEW_SHA" ] || { FAILURE_REASON="INSTALLED_HASH_MISMATCH"; false; }

PHASE="postcheck-before-machine-close"
log "independent installed-file check; no refresh or rebalance"
REMOTE_POSTCHECK_BEFORE="$WORK_DIR/remote-postcheck-before.sh"
cat > "$REMOTE_POSTCHECK_BEFORE" <<EOF_REMOTE
#!/bin/ash
set -eu
TARGET='$TARGET'
EXPECTED_NEW='$EXPECTED_NEW_SHA'
EXPECTED_BODY='$EXPECTED_BODY_SHA'
RUNTIME_LIB='$RUNTIME_LIB'
/bin/ash -n "\$TARGET"
INSTALLED="\$(sha256sum "\$TARGET" | awk '{print \$1}')"
[ "\$INSTALLED" = "\$EXPECTED_NEW" ]
BODY="\$(awk 'BEGIN{f=0} /^TS=/{f=1} f{print}' "\$TARGET" | sha256sum | awk '{print \$1}')"
[ "\$BODY" = "\$EXPECTED_BODY" ]
grep -Fq 'HMN_REQUEST_IFACE_CALLER_SET="\${HMN_REQUEST_IFACE+x}"' "\$TARGET"
grep -Fq '. "\$RUNTIME_LIB"' "\$TARGET"
grep -Fq 'ACTIVE="\$(vm101_healthy_bootstrap_iface || true)"' "\$TARGET"
grep -Fq 'vm101_strict_iface "\$REQUEST_IFACE" 1 0' "\$TARGET"
if grep -Fq 'ACTIVE="\$(ip route show table 200 | awk' "\$TARGET"; then exit 41; fi
. "\$RUNTIME_LIB"
ACTIVE="\$(vm101_healthy_bootstrap_iface || true)"
case "\$ACTIVE" in vpn1|vpn2|vpn3|vpn4|vpn5) ;; *) exit 42 ;; esac
vm101_strict_iface "\$ACTIVE" 1 0
echo "INSTALLED_SHA=\$INSTALLED"
echo "BODY_SHA=\$BODY"
echo "BOOTSTRAP_IFACE=\$ACTIVE"
echo 'LEGACY_DIRECT_SELECTOR_ABSENT=true'
echo 'RUNTIME_LIBRARY_SOURCED=true'
echo 'CALLER_OVERRIDE_PRESERVED=true'
echo 'REFRESH_RAN=false'
echo 'REBALANCE_RAN=false'
echo 'SERVICES_RESTARTED=false'
echo 'MACHINE_GIT_DIRTY_EXPECTED_BEFORE_CLOSE=true'
/usr/local/sbin/router-vm101-git-publish.sh --check
EOF_REMOTE
chmod 700 "$REMOTE_POSTCHECK_BEFORE"
vm101_run_script "$REMOTE_POSTCHECK_BEFORE" "$POSTCHECK_BEFORE_LOG" "$POSTCHECK_BEFORE_ERR"
cat "$POSTCHECK_BEFORE_LOG"
[ ! -s "$POSTCHECK_BEFORE_ERR" ] || cat "$POSTCHECK_BEFORE_ERR"

PHASE="machine-close"
log "committing, pushing, cloning exact commit on VM130, archiving and publishing VM101"
router-machine-close vm101 "$STEP: replace legacy downloader selector with existing healthy bootstrap contract" \
  2>&1 | tee "$MACHINE_CLOSE_LOG"
MACHINE_CLOSE_RAN="true"
VM101_PUBLIC_URL="$(awk -F= '/^VM101_PUBLIC_URL=/{v=$2} END{print v}' "$MACHINE_CLOSE_LOG")"
[ -n "$VM101_PUBLIC_URL" ] || { FAILURE_REASON="MACHINE_CLOSE_DID_NOT_RETURN_VM101_PUBLIC_URL"; false; }

PHASE="postcheck-after-machine-close"
log "independently verifying commit, remote head, clean tree and one-file change"
REMOTE_POSTCHECK_AFTER="$WORK_DIR/remote-postcheck-after.sh"
cat > "$REMOTE_POSTCHECK_AFTER" <<'EOF_REMOTE'
#!/bin/ash
set -eu
GIT_DIR=/root/.vm101-source.git
WORK_TREE=/
TARGET=/root/hmn/hmn-download-all-awg.sh
HEAD="$(git --git-dir="$GIT_DIR" rev-parse HEAD)"
TREE="$(git --git-dir="$GIT_DIR" rev-parse HEAD^{tree})"
REMOTE_HEAD="$(git --git-dir="$GIT_DIR" ls-remote origin refs/heads/main | awk '{print $1}')"
STATUS="$(git -C "$WORK_TREE" --git-dir="$GIT_DIR" --work-tree="$WORK_TREE" status --short)"
CHANGED="$(git --git-dir="$GIT_DIR" diff-tree --no-commit-id --name-only -r "$HEAD")"
COUNT="$(git --git-dir="$GIT_DIR" ls-tree -r --name-only "$HEAD" | wc -l | tr -d ' ')"
TARGET_SHA="$(sha256sum "$TARGET" | awk '{print $1}')"
[ "$HEAD" = "$REMOTE_HEAD" ]
[ -z "$STATUS" ]
[ "$CHANGED" = 'root/hmn/hmn-download-all-awg.sh' ]
echo "VM101_COMMIT=$HEAD"
echo "VM101_TREE=$TREE"
echo "REMOTE_HEAD=$REMOTE_HEAD"
echo "VM101_FILE_COUNT=$COUNT"
echo "TARGET_SHA=$TARGET_SHA"
echo "CHANGED_FILES_BEGIN"
printf '%s\n' "$CHANGED"
echo "CHANGED_FILES_END"
echo 'WORK_TREE_CLEAN=true'
echo 'LOCAL_REMOTE_MATCH=true'
EOF_REMOTE
chmod 700 "$REMOTE_POSTCHECK_AFTER"
vm101_run_script "$REMOTE_POSTCHECK_AFTER" "$POSTCHECK_AFTER_LOG" "$POSTCHECK_AFTER_ERR"
cat "$POSTCHECK_AFTER_LOG"
[ ! -s "$POSTCHECK_AFTER_ERR" ] || cat "$POSTCHECK_AFTER_ERR"
VM101_COMMIT="$(awk -F= '/^VM101_COMMIT=/{print $2; exit}' "$POSTCHECK_AFTER_LOG")"
VM101_TREE="$(awk -F= '/^VM101_TREE=/{print $2; exit}' "$POSTCHECK_AFTER_LOG")"
REMOTE_HEAD="$(awk -F= '/^REMOTE_HEAD=/{print $2; exit}' "$POSTCHECK_AFTER_LOG")"
VM101_FILE_COUNT="$(awk -F= '/^VM101_FILE_COUNT=/{print $2; exit}' "$POSTCHECK_AFTER_LOG")"
CHANGED_FILES="$(awk '/^CHANGED_FILES_BEGIN$/{f=1;next}/^CHANGED_FILES_END$/{f=0}f' "$POSTCHECK_AFTER_LOG")"
[ "$VM101_COMMIT" = "$REMOTE_HEAD" ] || { FAILURE_REASON="POSTCHECK_LOCAL_REMOTE_MISMATCH"; false; }
[ "$CHANGED_FILES" = "root/hmn/hmn-download-all-awg.sh" ] || { FAILURE_REASON="POSTCHECK_UNEXPECTED_CHANGED_FILES"; false; }

PHASE="publish-model-methods-plan"
log "publishing fresh VM101 model, methods and updated local M07 plan"
cat > "$MODEL_DIR/vm101-model.txt" <<EOF_MODEL
VM101 current source model
Generated UTC: $TS

Git:
- repository: git@github.com:DChuvelev/wg-paid-vm101.git
- branch: main
- commit: $VM101_COMMIT
- tree: $VM101_TREE
- remote head: $REMOTE_HEAD
- tracked files: $VM101_FILE_COUNT
- local equals GitHub: true
- tracked working tree clean: true

Changed in $STEP:
- /root/hmn/hmn-download-all-awg.sh
- before sha256: $EXPECTED_OLD_SHA
- after sha256: $EXPECTED_NEW_SHA
- runtime library sha256: $EXPECTED_RUNTIME_SHA
- selected healthy bootstrap interface during verification: $BOOTSTRAP_IFACE

Selector contract:
- caller HMN_REQUEST_IFACE has priority over hmn.env
- auto uses vm101_healthy_bootstrap_iface
- explicit vpn1..vpn5 uses vm101_strict_iface
- legacy direct table-200 selector removed
- no healthy VPN is fatal

Runtime:
- refresh ran: false
- rebalance ran: false
- services restarted: false
- runtime impact: false
EOF_MODEL
chmod 644 "$MODEL_DIR/vm101-model.txt"
cat > "$MODEL_DIR/facts.json" <<EOF_MODEL_FACTS
{
  "schema": "vm101-current-model-v1",
  "generated_at_utc": "$TS",
  "commit": "$VM101_COMMIT",
  "tree": "$VM101_TREE",
  "tracked_files": $VM101_FILE_COUNT,
  "target_sha256": "$EXPECTED_NEW_SHA",
  "runtime_library_sha256": "$EXPECTED_RUNTIME_SHA",
  "runtime_impact": false
}
EOF_MODEL_FACTS
chmod 644 "$MODEL_DIR/facts.json"
make_index "$MODEL_DIR" "VM101 model $TS" vm101-model.txt facts.json

cat > "$METHODS_DIR/methods.txt" <<EOF_METHODS
VM101 HMN downloader selector method
Generated UTC: $TS

Source of code:
- Published Machine Git baseline commit $BASELINE_VM101_COMMIT.
- No broad live filesystem inventory was performed.

Access:
- router-ops -> ssh pve-mgts -> VM101 10.71.100.2.
- Existing key on Proxmox: $PVE_TO_OPENWRT_KEY.

Deployment:
- raw stdin transport;
- same-directory temporary file;
- exact SHA-256 verification;
- BusyBox ash syntax check;
- root:root and mode 0700;
- atomic mv;
- timestamped rollback copy.

Reused implementation:
- $RUNTIME_LIB
- vm101_healthy_bootstrap_iface
- vm101_strict_iface
- existing Machine Git v2 publisher
- existing router-machine-close workflow

Verification:
- selector-only harness exits before downloader creates run/config directories;
- auto selection checked;
- explicit caller override checked;
- invalid eth0 request rejected;
- downloader body from TS= onward retains SHA-256 $EXPECTED_BODY_SHA;
- full refresh and rebalance were not invoked.

Limitations:
- This STEP does not prove a full HideMyName pool download.
- This STEP does not modify hmn-validate-current-pool.sh.
- A controlled full refresh remains a later M07 action after validator alignment.
EOF_METHODS
chmod 644 "$METHODS_DIR/methods.txt"
cp "$REPORT_DIR/candidate-contract.json" "$METHODS_DIR/candidate-contract.json"
cp "$REPORT_DIR/selector-change.txt" "$METHODS_DIR/selector-change.txt"
make_index "$METHODS_DIR" "VM101 methods $TS" methods.txt candidate-contract.json selector-change.txt

cat > "$LOCAL_PLAN_DIR/plan.md" <<EOF_PLAN
# Канонический локальный план VM101

Актуальность: $TS UTC  
Выполнено: 6/15  
Текущий этап: M07 — В РАБОТЕ

## Последний STEP

- $STEP
- RESULT=$PASS_RESULT
- Исправлен только selector в /root/hmn/hmn-download-all-awg.sh.
- auto теперь использует существующий vm101_healthy_bootstrap_iface.
- Явный vpn1..vpn5 проверяется vm101_strict_iface.
- Полный refresh и rebalance не запускались.
- Следующая узкая цель: hmn-validate-current-pool.sh и оставшаяся legacy-зависимость от table 200.

## Этапы

| ID | Этап | Статус |
|---|---|---|
| M01 | Health monitor обнаруживает отказ VPN-слота | ВЫПОЛНЕНО |
| M02 | Локальная замена отказавшего VPN-туннеля | ВЫПОЛНЕНО |
| M03 | Quarantine отказавших endpoints | ВЫПОЛНЕНО |
| M04 | Глобальный repair counter после локальных замен | ВЫПОЛНЕНО |
| M05 | Порог full refresh и dry-run решения | ВЫПОЛНЕНО |
| M06 | Постоянно включить emergency commit на VM101 | ВЫПОЛНЕНО |
| M07 | Выполнить контролируемый реальный full HideMyName refresh | В РАБОТЕ |
| M08 | Реализовать DEGRADED_POOL при ошибке HideMyName refresh | ОЖИДАЕТ |
| M09 | Реализовать SLOT_EXHAUSTED и консолидацию пользователей | ОЖИДАЕТ |
| M10 | Реализовать DIRECT_EMERGENCY при нуле рабочих VPN | ОЖИДАЕТ |
| M11 | Реализовать cached-pool bootstrap через vpn1 | ОЖИДАЕТ |
| M12 | Реализовать абстрактный bootstrap strategy contract | ОЖИДАЕТ |
| M13 | Реализовать безопасный возврат Direct → VPN | ОЖИДАЕТ |
| M14 | Провести полную последовательную имитацию отказов на VM101 | ОЖИДАЕТ |
| M15 | Доказать перезагрузочную устойчивость state machine | ОЖИДАЕТ |

## Актуальные ссылки

- STEP report: $REPORT_URL
- VM101 Machine Git: $VM101_PUBLIC_URL
- VM101 model: $NEW_MODEL_FILE_URL
- VM101 methods: $NEW_METHODS_URL
- Global project plan: $GLOBAL_PROJECT_PLAN_URL
- Access / XS Map: $XS_MAP_URL
EOF_PLAN
chmod 644 "$LOCAL_PLAN_DIR/plan.md"
cat > "$LOCAL_PLAN_DIR/facts.json" <<EOF_PLAN_FACTS
{
  "schema": "vm101-local-plan-v1",
  "generated_at_utc": "$TS",
  "completed": 6,
  "total": 15,
  "current_milestone": "M07",
  "current_status": "IN_PROGRESS",
  "latest_step": "$STEP",
  "latest_result": "$PASS_RESULT",
  "next_target": "/root/hmn/hmn-validate-current-pool.sh"
}
EOF_PLAN_FACTS
chmod 644 "$LOCAL_PLAN_DIR/facts.json"
make_index "$LOCAL_PLAN_DIR" "Канонический локальный план VM101 $TS" plan.md facts.json

LOCAL_PLAN_URL="$NEW_LOCAL_PLAN_URL"
MODEL_FILE_URL="$NEW_MODEL_FILE_URL"
METHODS_URL="$NEW_METHODS_URL"

cat > "$METHODS_FILE" <<EOF_METHODS_REPORT
Methods used:
- published VM101 Machine Git baseline as source of target code;
- exact source hash gate on VM101;
- existing nested SSH route from XS Map;
- existing runtime helper library;
- raw stdin, same-directory temporary file and atomic mv;
- existing Machine Git v2 and router-machine-close.

Environment limitations:
- no full downloader execution;
- no refresh or rebalance;
- no VM100 or VM121 inspection/change;
- no service restart;
- no new SSH keys, aliases, libraries or recovery mechanisms.
EOF_METHODS_REPORT
chmod 644 "$METHODS_FILE"

cat > "$ROLLBACK_FILE" <<EOF_ROLLBACK
#!/usr/bin/env bash
set -Eeuo pipefail
BACKUP='$BACKUP_PATH'
EXPECTED_OLD='$EXPECTED_OLD_SHA'
TARGET='$TARGET'
ssh -T -o BatchMode=yes '$PVE_ALIAS' \
  "ssh -T -o BatchMode=yes -i $PVE_TO_OPENWRT_KEY root@$VM101_IP /bin/ash -s" <<'REMOTE_ROLLBACK'
set -eu
BACKUP='$BACKUP_PATH'
EXPECTED_OLD='$EXPECTED_OLD_SHA'
TARGET='$TARGET'
[ -f "\$BACKUP" ]
TMP="\${TARGET}.rollback.\$\$"
cp -p "\$BACKUP" "\$TMP"
chown 0:0 "\$TMP"
chmod 700 "\$TMP"
/bin/ash -n "\$TMP"
SHA="\$(sha256sum "\$TMP" | awk '{print \$1}')"
[ "\$SHA" = "\$EXPECTED_OLD" ]
mv -f "\$TMP" "\$TARGET"
sync
/bin/ash -n "\$TARGET"
FINAL="\$(sha256sum "\$TARGET" | awk '{print \$1}')"
[ "\$FINAL" = "\$EXPECTED_OLD" ]
echo "ROLLBACK_TARGET_SHA=\$FINAL"
REMOTE_ROLLBACK
router-machine-close vm101 "ROLLBACK_$STEP: restore hmn-download-all-awg.sh baseline selector"
EOF_ROLLBACK
chmod 700 "$ROLLBACK_FILE"

PHASE="final-verification"
log "writing formal report and performing external HTTP checks"
RESULT="$PASS_RESULT"
FAILURE_REASON=""
cat > "$VERIFY_FILE" <<EOF_VERIFY
candidate_sha256=$NEW_SHA_ACTUAL
installed_sha256=$NEW_SHA_ACTUAL
unchanged_body_sha256=$EXPECTED_BODY_SHA
bootstrap_iface=$BOOTSTRAP_IFACE
selector_harness_pass=true
legacy_direct_selector_absent=true
machine_close_ran=$MACHINE_CLOSE_RAN
vm101_commit=$VM101_COMMIT
vm101_tree=$VM101_TREE
remote_head=$REMOTE_HEAD
changed_files=$CHANGED_FILES
work_tree_clean=true
refresh_ran=$REFRESH_RAN
rebalance_ran=$REBALANCE_RAN
services_restarted=$SERVICES_RESTARTED
runtime_impact=$RUNTIME_IMPACT
external_http_verify=pending
EOF_VERIFY
chmod 644 "$VERIFY_FILE"
write_final_materials

HTTP_CHECK_LOG="$REPORT_DIR/http-verify.log"
: > "$HTTP_CHECK_LOG"
check_url() {
  local url="$1"
  printf 'CHECK %s\n' "$url" | tee -a "$HTTP_CHECK_LOG"
  curl -fsS --retry 4 --retry-all-errors --connect-timeout 10 --max-time 40 "$url" >/dev/null
  printf 'PASS %s\n' "$url" | tee -a "$HTTP_CHECK_LOG"
}
for url in \
  "$REPORT_URL" \
  "${REPORT_URL}report.txt" \
  "${REPORT_URL}facts.json" \
  "${REPORT_URL}step-code.sh" \
  "${REPORT_URL}verify.txt" \
  "${REPORT_URL}rollback.sh" \
  "$LOCAL_PLAN_URL" \
  "$GLOBAL_PROJECT_PLAN_URL" \
  "$ACCESS_MAP_URL" \
  "$VM100_PUBLIC_URL" \
  "$VM101_PUBLIC_URL" \
  "$VM121_PUBLIC_URL" \
  "$MODEL_FILE_URL" \
  "$METHODS_URL"
do
  check_url "$url"
done
EXTERNAL_VERIFY="true"
sed -i 's/external_http_verify=pending/external_http_verify=true/' "$VERIFY_FILE"
write_final_materials
check_url "${REPORT_URL}report.txt"
check_url "${REPORT_URL}facts.json"

PHASE="complete"
RESULT="$PASS_RESULT"
write_final_materials
log "STEP completed"
print_final
