#!/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 la sesion tmux "lab". 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" SESSION="lab" 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() { # 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 if tmux send-keys -t "$SESSION" "claude" Enter 2>>"$LOG_FILE"; then log "TMUX: 'claude' lanzado dentro de la sesion recien creada" else log "TMUX: ERROR al lanzar 'claude' dentro de la sesion recien creada" fi } check_tmux_session() { 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 else log "TMUX: ERROR al recrear la sesion '${SESSION}'" fi } log "=== healthcheck inicio (intervalo=${INTERVAL_SEC}s) ===" while true; do check_network check_tmux_session sleep "$INTERVAL_SEC" done