Tercera sesion en SESSIONS: el healthcheck la recrea con claude --remote-control --continue igual que lab y cloud-practice. Separada de lab a proposito (no mezclar contexto/memoria con ARI). Imagen verificada: tmux-host:2026-09-28-scm (Claude 2.1.283, sin cache). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018r19s4H8MBGegqToqw5guP
180 lines
8.3 KiB
Bash
Executable File
180 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Chequeo de salud de tmux-host, corre en background dentro del propio
|
|
# contenedor (ver entrypoint.sh). Cada HEALTHCHECK_INTERVAL_SEC (30 min
|
|
# por defecto):
|
|
#
|
|
# a) Conectividad de red real (curl a gitea.cruzcloud.net). Si falla,
|
|
# SOLO se registra en el log y se notifica por Telegram (si el canal
|
|
# del lab-agent ya esta configurado) -- nunca se intenta "arreglar"
|
|
# la red del host desde aca, eso puede ser un problema de
|
|
# hardware/BIOS fuera del alcance del contenedor.
|
|
# b) Existencia de cada sesion tmux en SESSIONS (ver mas abajo -- hoy
|
|
# "lab" y "cloud-practice"). Si no existe (crash, o el contenedor
|
|
# se acaba de recrear), SI se recrea automaticamente -- es solo
|
|
# recuperar el propio entorno de trabajo, no tocar infraestructura
|
|
# externa. Al recrearla tambien se lanza `claude` adentro (ver
|
|
# launch_claude_in_session) -- una sesion recreada sin esto es un
|
|
# shell pelado, no "el entorno de trabajo".
|
|
#
|
|
# Todo evento (chequeo ok, falla, accion tomada) queda en LOG_FILE, que
|
|
# vive bajo /DATA (persistente, sobrevive a recrear el contenedor) y no
|
|
# solo en el stdout del contenedor.
|
|
#
|
|
# set -u sin 'e': un check individual que falle no debe matar el loop
|
|
# completo -- el siguiente ciclo tiene que seguir corriendo igual.
|
|
set -uo pipefail
|
|
|
|
WORKDIR="/DATA/AppData/k3d-lab-v3/lab-cruzcloud"
|
|
# Cada entrada: "nombre-sesion:workdir". "cloud-practice" agregada
|
|
# 2026-09-20 (ver ADR 0012, practica multi-cloud) -- vive en un
|
|
# subdirectorio de WORKDIR (repo platform-multicloud ya clonado ahi),
|
|
# asi que el mismo wait_for_workdir() de abajo (que solo espera por el
|
|
# WORKDIR padre) ya cubre el caso de arranque en frio para las dos.
|
|
# "scm-portal" agregada 2026-09-28: sesion dedicada al Portal SCM (Apps
|
|
# Script) sobre el repo client-initiatives (ADR 0013), separada de "lab"
|
|
# a proposito para no mezclar contexto ni memoria con ARI Shopping.
|
|
SESSIONS=(
|
|
"lab:${WORKDIR}"
|
|
"cloud-practice:${WORKDIR}/platform-multicloud"
|
|
"scm-portal:${WORKDIR}/client-initiatives"
|
|
)
|
|
LOG_DIR="/DATA/AppData/k3d-lab-v3/tmux-host/logs"
|
|
LOG_FILE="${LOG_DIR}/health.log"
|
|
# Reusa el mismo canal de Telegram que ya configuro scripts/lab-agent/ --
|
|
# una sola fuente de verdad para el bot token / chat id, en vez de
|
|
# duplicar credenciales entre los dos agentes del lab.
|
|
LAB_AGENT_ENV="${WORKDIR}/scripts/lab-agent/lab-agent.env"
|
|
NETWORK_CHECK_URL="https://gitea.cruzcloud.net/"
|
|
INTERVAL_SEC="${HEALTHCHECK_INTERVAL_SEC:-1800}"
|
|
# Cuanto esperar, SOLO al arrancar, a que WORKDIR este disponible antes de
|
|
# tocarlo. Tras un `docker restart` normal el host y sus mounts ya estaban
|
|
# arriba; tras un reinicio COMPLETO del NAS (corte de luz), Docker puede
|
|
# arrancar este contenedor antes de que /media/HDD-Storage termine de
|
|
# montarse en el host -- y WORKDIR llega a el via el symlink absoluto
|
|
# /DATA/AppData -> /media/HDD-Storage/AppData (ver el gotcha del symlink en
|
|
# docs-portal/docs/arquitectura/sesion-tmux-persistente.md). Sin esta
|
|
# espera, `mkdir -p "$LOG_DIR"` de mas abajo fallaba en silencio (set -u sin
|
|
# -e) y ese arranque quedaba SIN NINGUN rastro en el log -- asi se paso por
|
|
# alto originalmente el gotcha de "no lanzo claude" reportado 2026-09-19.
|
|
STARTUP_MOUNT_WAIT_MAX_SEC="${STARTUP_MOUNT_WAIT_MAX_SEC:-120}"
|
|
STARTUP_MOUNT_WAIT_STEP_SEC=5
|
|
|
|
wait_for_workdir() {
|
|
local waited=0
|
|
while [[ ! -d "$WORKDIR" ]] && [[ "$waited" -lt "$STARTUP_MOUNT_WAIT_MAX_SEC" ]]; do
|
|
sleep "$STARTUP_MOUNT_WAIT_STEP_SEC"
|
|
waited=$((waited + STARTUP_MOUNT_WAIT_STEP_SEC))
|
|
done
|
|
echo "$waited"
|
|
}
|
|
|
|
STARTUP_WAITED_SEC="$(wait_for_workdir)"
|
|
|
|
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
|
|
|
log() {
|
|
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOG_FILE" 2>/dev/null || true
|
|
}
|
|
|
|
if [[ ! -d "$WORKDIR" ]]; then
|
|
log "ARRANQUE: ERROR -- ${WORKDIR} sigue sin existir tras esperar ${STARTUP_MOUNT_WAIT_MAX_SEC}s. El mount de /DATA o /media/HDD-Storage del host no esta listo; revisar fuera del contenedor."
|
|
elif [[ "$STARTUP_WAITED_SEC" -gt 0 ]]; then
|
|
log "ARRANQUE: ${WORKDIR} tardo ${STARTUP_WAITED_SEC}s en estar disponible (probable reinicio completo del host reordenando el montaje de /DATA)."
|
|
fi
|
|
|
|
notify_telegram() {
|
|
local title="$1" message="$2"
|
|
if [[ ! -f "$LAB_AGENT_ENV" ]]; then
|
|
log "NOTIFY omitida: no existe ${LAB_AGENT_ENV}"
|
|
return 0
|
|
fi
|
|
local token_file chat_id token resp
|
|
token_file="$(grep -E '^TELEGRAM_BOT_TOKEN_FILE=' "$LAB_AGENT_ENV" | cut -d= -f2-)"
|
|
chat_id="$(grep -E '^TELEGRAM_CHAT_ID=' "$LAB_AGENT_ENV" | cut -d= -f2-)"
|
|
if [[ -z "$token_file" || ! -r "$token_file" || -z "$chat_id" ]]; then
|
|
log "NOTIFY omitida: canal de Telegram del lab-agent no esta configurado todavia"
|
|
return 0
|
|
fi
|
|
token="$(<"$token_file")"
|
|
resp="$(curl -s -m 10 -X POST "https://api.telegram.org/bot${token}/sendMessage" \
|
|
--data-urlencode "chat_id=${chat_id}" \
|
|
--data-urlencode "text=🤖 tmux-host healthcheck
|
|
${title}
|
|
──────────────
|
|
${message}" 2>&1)"
|
|
token=""
|
|
[[ "$resp" == *'"ok":true'* ]] || log "WARN: fallo al notificar por Telegram: ${resp}"
|
|
}
|
|
|
|
check_network() {
|
|
local code
|
|
code="$(curl -sL -o /dev/null -w '%{http_code}' --max-time 10 "$NETWORK_CHECK_URL" 2>/dev/null || echo "000")"
|
|
if [[ "$code" == "000" ]]; then
|
|
log "RED: sin conectividad real (curl ${NETWORK_CHECK_URL} sin respuesta). No se intenta corregir desde el contenedor."
|
|
notify_telegram "Sin conectividad" "curl ${NETWORK_CHECK_URL} no respondio desde tmux-host (posible falla de red del host/NAS, ej. hardware/BIOS). El contenedor NO intenta corregir esto, solo lo registra."
|
|
else
|
|
log "RED: ok (curl ${NETWORK_CHECK_URL} -> HTTP ${code})"
|
|
fi
|
|
}
|
|
|
|
launch_claude_in_session() {
|
|
local session="$1"
|
|
# El pane recien creado tarda un instante en tener su shell listo para
|
|
# recibir input -- un send-keys inmediato a veces se pierde. 1s alcanza
|
|
# sobrado para un shell interactivo local (no depende de red).
|
|
sleep 1
|
|
# --remote-control: sin esto la sesion nunca queda visible como
|
|
# controlable remotamente en claude.ai (confirmado en vivo 2026-09-20 --
|
|
# sin la flag, "remoteEnabled" queda en false para siempre porque nadie
|
|
# la pide). El nombre de sesion remota usa el hostname del contenedor
|
|
# (ZimaOS) como prefijo por defecto -- con 2 sesiones corriendo
|
|
# "claude --remote-control" a la vez, cada una aparece como una sesion
|
|
# remota distinta en claude.ai (mismo prefijo de host, pero cada una
|
|
# con su propio ID de sesion real).
|
|
# --continue (-c): retoma la conversacion mas reciente en WORKDIR en vez
|
|
# de arrancar una sesion en blanco cada vez que se recrea el pane (reinicio
|
|
# del NAS, crash del contenedor, etc). Si no hay conversacion previa en el
|
|
# directorio, el propio CLI arranca una nueva sin error.
|
|
if tmux send-keys -t "$session" "claude --remote-control --continue" Enter 2>>"$LOG_FILE"; then
|
|
log "TMUX: 'claude --remote-control --continue' lanzado dentro de la sesion '${session}' recien creada"
|
|
else
|
|
log "TMUX: ERROR al lanzar 'claude --remote-control --continue' dentro de la sesion '${session}' recien creada"
|
|
fi
|
|
}
|
|
|
|
check_tmux_session() {
|
|
local session="$1" workdir="$2"
|
|
if tmux has-session -t "$session" 2>/dev/null; then
|
|
log "TMUX: sesion '${session}' ok"
|
|
return
|
|
fi
|
|
if [[ ! -d "$workdir" ]]; then
|
|
# No se crea sobre un workdir ausente (quedaria posicionada en /). Se
|
|
# reintenta en el proximo ciclo -- ver STARTUP_MOUNT_WAIT_MAX_SEC arriba
|
|
# para el caso especifico de arranque en frio del host.
|
|
log "TMUX: sesion '${session}' no existe y ${workdir} tampoco -- no se crea esta vuelta, se reintenta en el proximo ciclo (${INTERVAL_SEC}s)."
|
|
return
|
|
fi
|
|
log "TMUX: sesion '${session}' no existe -- recreando en ${workdir}"
|
|
if tmux new-session -d -s "$session" -c "$workdir" 2>>"$LOG_FILE"; then
|
|
log "TMUX: sesion '${session}' recreada exitosamente"
|
|
# Una sesion recreada sin esto es un shell vacio hasta que alguien
|
|
# entre a mano y escriba "claude" -- exactamente el gotcha reportado
|
|
# tras el reinicio completo del NAS del 2026-09-18/19: la sesion SI se
|
|
# recreaba sola (confirmado en el log de ese arranque), pero Claude
|
|
# Code nunca arrancaba adentro.
|
|
launch_claude_in_session "$session"
|
|
else
|
|
log "TMUX: ERROR al recrear la sesion '${session}'"
|
|
fi
|
|
}
|
|
|
|
log "=== healthcheck inicio (intervalo=${INTERVAL_SEC}s) ==="
|
|
while true; do
|
|
check_network
|
|
for entry in "${SESSIONS[@]}"; do
|
|
check_tmux_session "${entry%%:*}" "${entry#*:}"
|
|
done
|
|
sleep "$INTERVAL_SEC"
|
|
done
|