Notificaciones vía Telegram, pausa nocturna y unidades systemd

Reemplaza ntfy.sh por Telegram Bot API (preferencia del usuario, no
reduce el costo — el costo real es de la API de Claude/Sonnet, no del
canal). Texto plano sin parse_mode: el motivo que devuelve Claude es
texto libre y puede traer caracteres que rompen el parser Markdown de
Telegram. Token en TELEGRAM_BOT_TOKEN_FILE (dueño devops, no root-only
-- el servicio corre como devops por el login de suscripción, mismo
criterio ya aplicado a GITEA_TOKEN_FILE). Validado en vivo: entrega
confirmada con el mismo código de notify_send().

Agrega pausa nocturna (ACTIVE_HOURS_START/END, default 06:00-23:00
hora local) para no acumular costo de API sin necesidad: el script
chequea el horario ANTES de tocar kubectl/docker/curl o invocar
claude -p, no solo antes de notificar. Nota documentada: esta ventana
no se solapa con COMMERCE_BACKEND_WINDOW_* (00:00-06:00), por lo que
medusa-deploy queda efectivamente inalcanzable para auto-restart en la
práctica -- pendiente de decisión aparte.

Agrega unidades systemd de usuario (lab-agent/systemd/*.service,
*.timer) con límites de CPU/memoria: OnCalendar cada 15 min dentro del
horario activo, corre como devops (systemctl --user), sin habilitar
todavía -- requiere confirmación explícita tras ver el costo mensual
estimado (~US$60-65, documentado en agente-monitoreo.md).
This commit is contained in:
devops committed 2026-08-26 00:00:56 -05:00
1 parent 61a951f576
commit c273056771
5 files changed
+131 -22

No files matched your search

+49 -14
View File
@@ -51,7 +51,18 @@ source "$ENV_FILE"
: "${GITEA_TOKEN_FILE:=/DATA/AppData/k3d-lab/lab-agent/secrets/gitea_token}"
: "${CLAUDE_MODEL:=claude-sonnet-5}"
: "${CLAUDE_MAX_BUDGET_USD:=0.50}"
: "${NTFY_URL:=}"
# Notificaciones vía Telegram Bot API (reemplaza a ntfy.sh, 2026-08-26).
# TELEGRAM_BOT_TOKEN_FILE: secreto, NO versionado. TELEGRAM_CHAT_ID no es
# secreto (solo identifica a quién mandar, no autentica nada por sí solo).
: "${TELEGRAM_BOT_TOKEN_FILE:=}"
: "${TELEGRAM_CHAT_ID:=}"
# Horario activo (hora LOCAL del host): fuera de esta ventana el script
# aborta ANTES de tocar kubectl/docker/curl o invocar claude -p — pausa
# nocturna para no acumular costo de API sin necesidad. Ver
# docs/arquitectura/agente-monitoreo.md para el tradeoff con la ventana
# de medusa-deploy (00:00-06:00).
: "${ACTIVE_HOURS_START:=06:00}"
: "${ACTIVE_HOURS_END:=23:00}"
: "${DRY_RUN:=1}"
for arg in "$@"; do
@@ -77,6 +88,26 @@ trap 'log "ERROR inesperado en línea $LINENO (comando: $BASH_COMMAND)"' ERR
log "=== lab-agent inicio (DRY_RUN=${DRY_RUN}) ==="
in_time_window() { # start end (HH:MM, hora local) -> 0 si "ahora" cae adentro
local start="$1" end="$2" now_hm
now_hm="$(date +%H:%M)"
if [[ "$start" < "$end" ]]; then
[[ ( "$now_hm" > "$start" || "$now_hm" == "$start" ) && "$now_hm" < "$end" ]]
else
[[ "$now_hm" > "$start" || "$now_hm" == "$start" || "$now_hm" < "$end" ]]
fi
}
# Pausa nocturna: si estamos fuera del horario activo, salir ANTES de tocar
# kubectl/docker/curl o invocar claude -p — ni siquiera se gasta el costo
# de diagnóstico, no solo el de la llamada a claude. Sin notificación (sería
# spam cada 15 min durante toda la pausa) — solo queda en el log del run.
if ! in_time_window "$ACTIVE_HOURS_START" "$ACTIVE_HOURS_END"; then
log "fuera de horario activo (${ACTIVE_HOURS_START}-${ACTIVE_HOURS_END} hora local) — se omite esta corrida sin costo de API"
log "=== lab-agent fin (omitido por horario) ==="
exit 0
fi
for c in "$KUBECTL_BIN" docker curl jq flock "$CLAUDE_BIN" date; do
command -v "$c" >/dev/null 2>&1 || { log "FATAL: falta comando requerido '$c'"; exit 1; }
done
@@ -141,13 +172,25 @@ log_action() { # resource decision result reason
notify_send() { # title message
local title="$1" message="$2"
if [[ -z "$NTFY_URL" ]]; then
log "NOTIFY (sin canal configurado aún — Paso 3) [$title]"
if [[ ! -r "$TELEGRAM_BOT_TOKEN_FILE" || -z "$TELEGRAM_CHAT_ID" ]]; then
log "NOTIFY (Telegram no configurado — falta TELEGRAM_BOT_TOKEN_FILE/TELEGRAM_CHAT_ID) [$title]"
log "$message"
return 0
fi
if ! curl -s -m 10 -H "Title: ${title}" -d "$message" "$NTFY_URL" >/dev/null 2>&1; then
log "WARN: fallo al enviar notificación a NTFY_URL"
local token text resp
token="$(<"$TELEGRAM_BOT_TOKEN_FILE")"
# Texto plano, sin parse_mode: el "reason" de claude es texto libre y
# puede traer _, *, [, ] etc. que rompen el parser Markdown de Telegram
# (400 Bad Request) — más vale entrega confiable que formato.
text="[${title}]
${message}"
text="${text:0:4000}"
resp="$(curl -s -m 10 -X POST "https://api.telegram.org/bot${token}/sendMessage" \
--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "text=${text}" 2>&1)"
token=""
if [[ "$resp" != *'"ok":true'* ]]; then
log "WARN: fallo al enviar notificación a Telegram: ${resp}"
fi
}
@@ -161,15 +204,7 @@ DEPLOY_JSON="$("$KUBECTL_BIN" -n "$NAMESPACE" get deployment -o json 2>/dev/null
mapfile -t DETECTED_DEPLOYMENTS < <(jq -r '.items[].metadata.name' <<<"$DEPLOY_JSON")
in_commerce_backend_window() {
local now_hm start end
now_hm="$(date +%H:%M)"
start="$COMMERCE_BACKEND_WINDOW_START"
end="$COMMERCE_BACKEND_WINDOW_END"
if [[ "$start" < "$end" ]]; then
[[ ( "$now_hm" > "$start" || "$now_hm" == "$start" ) && "$now_hm" < "$end" ]]
else
[[ "$now_hm" > "$start" || "$now_hm" == "$start" || "$now_hm" < "$end" ]]
fi
in_time_window "$COMMERCE_BACKEND_WINDOW_START" "$COMMERCE_BACKEND_WINDOW_END"
}
# devuelve: "eligible" | "not_eligible:<motivo>"