Author SHA1 Message Date
devops 9cfc3a3bdd Merge branch 'fix/promote-script-sin-perl-2026-09-20' 2026-09-20 17:46:01 +00:00
devops b06c1f211e fix(promote-to-production): reemplazar perl por awk, no esta en el host ZimaOS 2026-09-20 17:44:52 +00:00
devops 2058f1ca2a Merge branch 'docs/promote-script-obsoleto-2026-09-20' 2026-09-20 17:32:31 +00:00
devops dfaf5c5a7d docs: marcar promote-to-production.sh como obsoleto (ahora automatico) 2026-09-20 17:31:19 +00:00
devopsandClaude Sonnet 5 f99c955751 Merge branch 'fix/tmux-host-remote-control-continue'
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 15:46:10 +00:00
devopsandClaude Sonnet 5 e64afedf31 fix(tmux-host): retomar la ultima conversacion al recrear la sesion
launch_claude_in_session() ahora lanza 'claude --remote-control --continue'
en vez de 'claude --remote-control' a secas. -c/--continue retoma la
conversacion mas reciente en WORKDIR; si no hay ninguna arranca una nueva
sin error. Evita que cada recreacion de la sesion tmux (reinicio del NAS,
crash del contenedor) empiece con una conversacion en blanco.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 15:45:51 +00:00
devopsandClaude Sonnet 5 0ffa1ff15c Merge branch 'fix/tmux-host-remote-control'
Lanza claude --remote-control al recrear la sesion tmux-host para que
quede visible en claude.ai sin intervencion manual.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-20 15:16:58 +00:00
devopsandClaude Sonnet 5 f48860fcd8 fix(tmux-host): lanzar claude con --remote-control al recrear la sesion
Sin la flag, la sesion de tmux-host nunca queda visible como
controlable remotamente en claude.ai: remoteEnabled quedaba en false
porque nadie la pedia. Confirmado en vivo que --remote-control si
empareja (aparecio como sesion activa "zimaos-quizzical") mientras el
proceso se mantiene vivo.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-20 15:16:00 +00:00
devopsandClaude Sonnet 5 955eec6e75 fix(tmux-host): lanzar claude al recrear la sesion + backoff de arranque
check_tmux_session() recreaba la sesion tmux "lab" tras un reinicio del
NAS pero nunca lanzaba `claude` adentro -- quedaba un shell vacio hasta
que alguien entraba a mano. Confirmado en vivo tras un corte de luz real
(2026-09-19): la sesion SI se recreaba sola, pero Claude Code no.

Se agrega wait_for_workdir() con backoff (hasta 120s por defecto) antes
de tocar el log o crear la sesion, para el caso de un reinicio completo
del host donde /media/HDD-Storage puede tardar en montarse mas de lo
que tarda Docker en arrancar este contenedor -- antes eso fallaba en
silencio y ese arranque no dejaba rastro en el log.

Probado en un contenedor descartable aparte (tmux-host-test): stop+start
desde cero recreo la sesion con claude corriendo, sin intervencion.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-19 20:15:37 +00:00
devopsandClaude Sonnet 5 9d04c73b20 feat: bumpear APP_VERSION al promover a producción
Complementa el cambio en apps-registry (footer lee process.env.APP_VERSION
para mostrar la versión real desplegada) -- sin esto, promover a
Producción actualizaría la imagen pero dejaría el footer mostrando la
versión vieja.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
2026-09-14 04:04:17 +00:00
devopsandClaude Sonnet 5 3640cd5754 feat: script de promoción manual de Staging a Producción
Contraparte del cambio en apps-registry/.gitea/workflows/build.yaml:
desde ahora el CI solo actualiza Staging automáticamente en cada push.
Este script lee la versión actual en
workloads/ecommerce-staging/frontend/patches/deployment.yaml y la
aplica a workloads/ecommerce/frontend/patches/deployment.yaml (con
confirmación interactiva), commiteando y pusheando a apps-registry.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
2026-09-14 02:46:06 +00:00
devopsandClaude Sonnet 5 78b97bdd59 feat(tmux-host): Dockerfile con WORKDIR fijo + healthcheck cada 30 min
Reemplaza el docker run inline (alpine sh -c "apk add ...") por una
imagen construida desde scripts/tmux-host/Dockerfile, con WORKDIR en la
raiz del lab para que docker exec y lab-session.sh ya arranquen
posicionados ahi sin cd manual.

Agrega scripts/tmux-host/healthcheck.sh (via entrypoint.sh, background
del propio contenedor): cada 30 min chequea conectividad real
(curl a gitea.cruzcloud.net) y la existencia de la sesion tmux "lab".
La red solo se loguea+notifica por Telegram (nunca se intenta corregir
desde el contenedor); la sesion tmux si se recrea sola si falta. Log
persistente en /DATA/AppData/k3d-lab-v3/tmux-host/logs/health.log.
Reusa el bot/chat_id de Telegram ya configurado en scripts/lab-agent/.

Validado en un contenedor descartable (tmux-host-test, intervalo de 60s):
confirmado WORKDIR correcto via docker exec, creacion inicial de la
sesion y recreacion automatica tras un kill-session manual.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012xyPak6HWFmBCqXrvFsejL
2026-09-06 17:16:41 +00:00
devops 40cb058fd4 Merge fix/lab-session-tmux-persistente into main
Merge local directo (sin PR de Gitea) por acuerdo del usuario; ver
feedback_git_fix_branches en memoria del proyecto para el porque de
este flujo estandar.
2026-08-27 22:18:47 -05:00
devops 0bf571cb93 feat: agrega lab-session.sh para reconectar a la sesion tmux persistente
Punto de entrada unico para adjuntarse (o crear si no existe) a la
sesion "lab" dentro del contenedor tmux-host, donde corre Claude Code
de forma persistente ante cortes de SSH/cambios de dispositivo.
2026-08-27 22:12:46 -05:00
devops acf7521990 Merge fix/adr0006-meilisearch-secrets into main 2026-08-26 15:00:07 -05:00
devops 9c0a111f82 create-commerce-secrets.sh: agregar MEILI_MASTER_KEY/MEILISEARCH_API_KEY
Fase 1 de la Etapa 2 del ADR 0006 (apps-registry): el StatefulSet de
Meilisearch lee MEILI_MASTER_KEY del secret commerce-secrets, y el
plugin de Medusa leera MEILISEARCH_API_KEY con el mismo valor (Fase 2,
distinto nombre de env var por cada lado, mismo secret).

Nota operativa: este script regenera el secret completo en cada
corrida (JWT_SECRET/COOKIE_SECRET/REVALIDATE_SECRET siempre nuevos).
Para aplicar SOLO estas dos claves nuevas al secret ya en uso en el
cluster real, no correr el script completo -- rotaria credenciales de
postgres/redis/minio ya activas. Se aplico via patch puntual al Secret
en vivo, fuera de este script, para esta vez.
2026-08-26 14:59:52 -05:00
devops 1f25869c4f Merge fix/lab-agent-follow-redirects into main 2026-08-26 13:33:26 -05:00
devops cef3dd3a28 lab-agent: follow redirects on public domain health checks
commerce.cruzcloud.net's root path now 301s to /app (Medusa admin) via
a Traefik Middleware — without -L the check reported a false 404 on an
otherwise healthy service. Generic fix (curl -L) instead of
hardcoding a per-domain path, since routing now owns the redirect.
2026-08-26 13:29:34 -05:00
devops 25276ad5b1 Merge branch 'fix/lab-agent-telegram-horario' into main
Notificaciones vía Telegram (reemplaza ntfy.sh), pausa nocturna
(06:00-23:00 hora local, con doble verificación script+systemd),
timer de systemd activado (cada 60 min, bajado de 15 para reducir
costo), formato con íconos + notificación silenciosa para info +
respuestas en hilo al recuperarse + log adjunto. Incluye el fix de
Environment=HOME= para systemd --user (el manager de devops arranca
con HOME=/DATA, no con el override de shell interactiva). Todo
validado en vivo antes de activar.
2026-08-26 00:29:02 -05:00
7 changed files with 305 additions and 1 deletions

No files matched your search

+8
View File
@@ -32,6 +32,12 @@ JWT_SECRET="$(random_secret)"
COOKIE_SECRET="$(random_secret)"
REVALIDATE_SECRET="$(random_secret)"
# Mismo valor para las dos claves: MEILI_MASTER_KEY la lee el propio
# contenedor getmeili/meilisearch (servidor), MEILISEARCH_API_KEY la lee
# el plugin @rokmohar/medusa-plugin-meilisearch en Medusa (cliente) --
# cada lado usa su propia convencion de nombre de env var.
MEILI_MASTER_KEY="$(random_secret)"
DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres-svc:5432/${POSTGRES_DB}?sslmode=disable"
REDIS_URL="redis://:${REDIS_PASSWORD}@redis-svc:6379"
@@ -49,6 +55,8 @@ kubectl -n "${NAMESPACE}" create secret generic commerce-secrets \
--from-literal=JWT_SECRET="${JWT_SECRET}" \
--from-literal=COOKIE_SECRET="${COOKIE_SECRET}" \
--from-literal=REVALIDATE_SECRET="${REVALIDATE_SECRET}" \
--from-literal=MEILI_MASTER_KEY="${MEILI_MASTER_KEY}" \
--from-literal=MEILISEARCH_API_KEY="${MEILI_MASTER_KEY}" \
--dry-run=client \
-o yaml |
kubectl apply -f -
+5 -1
View File
@@ -449,7 +449,11 @@ CLUSTER_WIDE_SNAPSHOT="$("$KUBECTL_BIN" get pods -A -o custom-columns='NS:.metad
PUBLIC_CHECKS=""
for d in $PUBLIC_DOMAINS; do
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "https://${d}/" 2>/dev/null || echo "000")"
# -L: sigue redirects (ej. commerce.cruzcloud.net/ -> /app vía Middleware
# de Traefik) para reportar el estado final real, sin tener que
# hardcodear una ruta distinta por dominio en este script — el routing
# decide el path, no este check.
code="$(curl -sL -o /dev/null -w '%{http_code}' --max-time 10 "https://${d}/" 2>/dev/null || echo "000")"
PUBLIC_CHECKS="${PUBLIC_CHECKS}${d} -> HTTP ${code}\n"
log "curl https://${d}/ -> HTTP ${code}"
done
Executable
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Reconecta a la sesion tmux persistente "lab" dentro del contenedor
# tmux-host. Si la sesion no existe (p.ej. el contenedor se acaba de
# recrear), la crea. El comando es siempre el mismo sin importar el
# estado previo.
#
# Ver docs-portal: arquitectura/sesion-tmux-persistente.md
CONTAINER="tmux-host"
SESSION="lab"
# Repo raiz del lab (CLAUDE.md, apps-registry/, platform-infra/, scripts/).
# Mismo path que el WORKDIR de scripts/tmux-host/Dockerfile -- pasado
# explicito acá (-c) para no depender de que docker exec herede el WORKDIR
# de la imagen.
WORKDIR="/DATA/AppData/k3d-lab-v3/lab-cruzcloud"
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
echo "El contenedor '$CONTAINER' no esta corriendo. Verifica con 'docker ps -a'." >&2
exit 1
fi
docker exec -it "$CONTAINER" tmux attach -t "$SESSION" || \
docker exec -it "$CONTAINER" tmux new -s "$SESSION" -c "$WORKDIR"
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# OBSOLETO como camino normal desde 2026-09-20: la promoción de Staging a
# Producción ahora corre sola dentro de .gitea/workflows/build.yaml (step
# "Promover a Producción (CD)"), en el mismo job que ya despliega a
# Staging -- decisión del usuario de eliminar el gate humano entre los
# dos ambientes. En uso normal este script ya no hace nada (Producción
# va a estar al día apenas termine el pipeline).
#
# Se conserva solo como herramienta de excepción/rollback manual --
# ej. forzar Producción a una versión de Staging vieja sin pasar por un
# build nuevo. Mismo sed/perl que el step automático del pipeline, con
# la diferencia de que este sí pide confirmación interactiva.
#
# Asume que este repo (scripts) y apps-registry están clonados como
# hermanos bajo el mismo directorio del lab (mismo layout que
# lab-cruzcloud/{apps-registry,platform-infra,scripts} en esta NAS).
APPS_REGISTRY_DIR="${APPS_REGISTRY_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../apps-registry" && pwd)}"
STAGING_MANIFEST="${APPS_REGISTRY_DIR}/workloads/ecommerce-staging/frontend/patches/deployment.yaml"
PROD_MANIFEST="${APPS_REGISTRY_DIR}/workloads/ecommerce/frontend/patches/deployment.yaml"
IMAGE_NAME="gitea.cruzcloud.net/devops/ecommerce-frontend"
test -d "${APPS_REGISTRY_DIR}/.git" || {
echo "ERROR: no se encontró un checkout de apps-registry en ${APPS_REGISTRY_DIR}."
echo "Definí APPS_REGISTRY_DIR=/ruta/al/checkout si no está junto a este repo."
exit 1
}
STAGING_VERSION="$(grep -oE "${IMAGE_NAME}:v[0-9.]+" "${STAGING_MANIFEST}" | head -1 | cut -d: -f2)"
test -n "${STAGING_VERSION}" || {
echo "ERROR: no se pudo leer la versión actual de Staging desde ${STAGING_MANIFEST}."
exit 1
}
CURRENT_PROD_VERSION="$(grep -oE "${IMAGE_NAME}:v[0-9.]+" "${PROD_MANIFEST}" | head -1 | cut -d: -f2)"
echo "Staging: ${STAGING_VERSION}"
echo "Producción: ${CURRENT_PROD_VERSION}"
if [[ "${STAGING_VERSION}" == "${CURRENT_PROD_VERSION}" ]]; then
echo "Producción ya está en ${STAGING_VERSION}. Nada que promover."
exit 0
fi
read -rp "¿Promover ${STAGING_VERSION} a Producción? [s/N] " CONFIRM
[[ "${CONFIRM}" =~ ^[sS]$ ]] || { echo "Cancelado."; exit 1; }
cd "${APPS_REGISTRY_DIR}"
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(image: ${IMAGE_NAME}:).*|\1${STAGING_VERSION}|g" "${PROD_MANIFEST}"
# Mismo bump que hace build.yaml en Staging -- el footer lee APP_VERSION
# para mostrar la version real desplegada. awk en vez de perl (usado en
# build.yaml, donde sí está disponible porque corre en un runner
# ubuntu-latest): el host real de ZimaOS donde se corre este script a
# mano no trae perl instalado (confirmado en vivo 2026-09-20), a
# diferencia de sed/awk que sí son parte del sistema base.
awk -v ver="${STAGING_VERSION}" '
/- name: APP_VERSION/ { print; getline; sub(/value:.*/, "value: " ver); print; next }
{ print }
' "${PROD_MANIFEST}" > "${PROD_MANIFEST}.tmp" && mv "${PROD_MANIFEST}.tmp" "${PROD_MANIFEST}"
git add "${PROD_MANIFEST}"
if git diff --cached --quiet; then
echo "No hay cambios en el manifiesto de Producción."
exit 0
fi
git commit -m "chore: promote ${STAGING_VERSION} to production"
git push origin HEAD:main
echo "Promovido. Argo CD sincroniza ecommerce-app en unos segundos."
+23
View File
@@ -0,0 +1,23 @@
# Imagen de tmux-host: sesion tmux persistente para Claude Code sobre el
# lab GitOps de cruzcloud. Reemplaza el `docker run ... alpine sh -c "apk
# add ... && npm install ..."` inline documentado antes en
# docs/arquitectura/sesion-tmux-persistente.md — mismo contenido, pero
# construido una vez como imagen en vez de reinstalar todo en cada arranque
# del contenedor.
FROM alpine:3.24
RUN apk add --no-cache tmux docker-cli bash curl git kubectl nodejs npm \
&& npm install -g @anthropic-ai/claude-code \
&& git config --global --add safe.directory '*'
# Repo raiz del lab (CLAUDE.md, apps-registry/, platform-infra/, scripts/).
# docker exec hereda este WORKDIR como cwd por defecto, y ademas
# lab-session.sh y healthcheck.sh lo pasan explicito via `tmux ... -c`
# para no depender de ese comportamiento implicito.
WORKDIR /DATA/AppData/k3d-lab-v3/lab-cruzcloud
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/healthcheck.sh /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# Proceso principal (PID 1) del contenedor tmux-host: lanza el chequeo de
# salud en background y mantiene el contenedor vivo, igual que el
# `tail -f /dev/null` del `docker run` inline original.
set -eu
/usr/local/bin/healthcheck.sh &
exec tail -f /dev/null
+159
View File
@@ -0,0 +1,159 @@
#!/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
# --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.
# --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 recien creada"
else
log "TMUX: ERROR al lanzar 'claude --remote-control --continue' 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