Telegram: notificación silenciosa para info y respuestas en hilo
log() pasa a escribir en stderr (transparente, sigue yendo al mismo RUN_LOG vía el "exec > tee 2>&1"): necesario para que notify_send pueda devolver el message_id por stdout sin que un log() interno se cuele en la captura -- mismo patrón de bug que ya se había corregido en execute_corrective_action. notify_send ahora acepta un reply_to_message_id opcional y manda disable_notification=true para nivel "info" (ruido de rutina no debe sonar; fallas/acciones/alertas de seguridad sí). Cuando ejecuta una acción correctiva, el message_id enviado se guarda por recurso en state.json (attach_telegram_message_id); si ese recurso se recupera solo en una corrida posterior, compute_eligibility manda un aviso silencioso como respuesta al mensaje original antes de limpiar el estado -- agrupa visualmente todo un incidente en un hilo. Validado en vivo con el canario desechable: acción real -> message_id capturado en state.json -> parcheado a imagen sana -> reply en hilo confirmado al recuperarse, silencioso.
This commit is contained in:
1 parent
c273056771
commit
cd163e6b7c
1 file changed
+126
-28
+126
-28
@@ -82,7 +82,11 @@ ACTIONS_LOG="${LOG_DIR}/actions.jsonl"
|
|||||||
exec > >(tee -a "$RUN_LOG") 2>&1
|
exec > >(tee -a "$RUN_LOG") 2>&1
|
||||||
|
|
||||||
iso_now() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
iso_now() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||||
log() { echo "[$(iso_now)] $*"; }
|
log() { echo "[$(iso_now)] $*" >&2; } # stderr a propósito: el stdout de una función
|
||||||
|
# queda limpio para $(...) (ej. capturar un
|
||||||
|
# message_id de Telegram) sin que un log()
|
||||||
|
# interno se cuele en la captura. Va al mismo
|
||||||
|
# RUN_LOG igual, por el "exec > tee 2>&1" de arriba.
|
||||||
|
|
||||||
trap 'log "ERROR inesperado en línea $LINENO (comando: $BASH_COMMAND)"' ERR
|
trap 'log "ERROR inesperado en línea $LINENO (comando: $BASH_COMMAND)"' ERR
|
||||||
|
|
||||||
@@ -158,6 +162,14 @@ mark_action_taken() { # resource -> preserva first_unhealthy_at, marca last_acti
|
|||||||
state_write "$resource" "$new_state"
|
state_write "$resource" "$new_state"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attach_telegram_message_id() { # resource message_id -> guarda el id para poder responder en hilo cuando se recupere
|
||||||
|
local resource="$1" msg_id="$2" prev new_state
|
||||||
|
[[ -z "$msg_id" ]] && return 0
|
||||||
|
prev="$(state_read "$resource")"
|
||||||
|
new_state="$(jq -nc --argjson prev "$prev" --arg mid "$msg_id" '$prev + {telegram_message_id: ($mid | tonumber)}')"
|
||||||
|
state_write "$resource" "$new_state"
|
||||||
|
}
|
||||||
|
|
||||||
log_action() { # resource decision result reason
|
log_action() { # resource decision result reason
|
||||||
local resource="$1" decision="$2" result="$3" reason="$4"
|
local resource="$1" decision="$2" result="$3" reason="$4"
|
||||||
jq -nc --arg ts "$(iso_now)" --arg resource "$resource" --arg decision "$decision" \
|
jq -nc --arg ts "$(iso_now)" --arg resource "$resource" --arg decision "$decision" \
|
||||||
@@ -170,27 +182,74 @@ log_action() { # resource decision result reason
|
|||||||
# Notificación (Paso 3 — no-op documentado hasta que NTFY_URL esté configurado)
|
# Notificación (Paso 3 — no-op documentado hasta que NTFY_URL esté configurado)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
notify_send() { # title message
|
notify_level_icon() { # level -> emoji
|
||||||
local title="$1" message="$2"
|
case "$1" in
|
||||||
|
error) echo "❌" ;;
|
||||||
|
security) echo "🚨" ;;
|
||||||
|
action_ok) echo "✅" ;;
|
||||||
|
action_fail) echo "🛑" ;;
|
||||||
|
info) echo "👀" ;;
|
||||||
|
*) echo "🤖" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_send() { # level title message [reply_to_message_id] -> imprime el message_id enviado (stdout), o nada si falló/no configurado
|
||||||
|
local level="$1" title="$2" message="$3" reply_to="${4:-}" icon
|
||||||
|
icon="$(notify_level_icon "$level")"
|
||||||
if [[ ! -r "$TELEGRAM_BOT_TOKEN_FILE" || -z "$TELEGRAM_CHAT_ID" ]]; then
|
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 "NOTIFY (Telegram no configurado — falta TELEGRAM_BOT_TOKEN_FILE/TELEGRAM_CHAT_ID) [${icon} ${title}]"
|
||||||
log "$message"
|
log "$message"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
local token text resp
|
local token text resp silent="false"
|
||||||
|
# Silenciosa (sin sonido/vibración) solo para "info" — ruido de rutina
|
||||||
|
# (nada elegible, todo sano) no debería interrumpir; fallas/acciones/
|
||||||
|
# alertas de seguridad sí hacen sonido.
|
||||||
|
[[ "$level" == "info" ]] && silent="true"
|
||||||
token="$(<"$TELEGRAM_BOT_TOKEN_FILE")"
|
token="$(<"$TELEGRAM_BOT_TOKEN_FILE")"
|
||||||
# Texto plano, sin parse_mode: el "reason" de claude es texto libre y
|
# Texto plano, sin parse_mode: el "reason" de claude es texto libre y
|
||||||
# puede traer _, *, [, ] etc. que rompen el parser Markdown de Telegram
|
# puede traer _, *, [, ] etc. que rompen el parser Markdown de Telegram
|
||||||
# (400 Bad Request) — más vale entrega confiable que formato.
|
# (400 Bad Request) — más vale entrega confiable que formato. Los
|
||||||
text="[${title}]
|
# emojis andan bien en texto plano, no dependen de parse_mode.
|
||||||
|
text="🤖 lab-agent
|
||||||
|
${icon} ${title}
|
||||||
|
──────────────
|
||||||
${message}"
|
${message}"
|
||||||
text="${text:0:4000}"
|
text="${text:0:4000}"
|
||||||
resp="$(curl -s -m 10 -X POST "https://api.telegram.org/bot${token}/sendMessage" \
|
local curl_args=(-s -m 10 -X POST "https://api.telegram.org/bot${token}/sendMessage"
|
||||||
--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
|
--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}"
|
||||||
--data-urlencode "text=${text}" 2>&1)"
|
--data-urlencode "text=${text}"
|
||||||
|
--data-urlencode "disable_notification=${silent}")
|
||||||
|
[[ -n "$reply_to" ]] && curl_args+=(--data-urlencode "reply_to_message_id=${reply_to}")
|
||||||
|
resp="$(curl "${curl_args[@]}" 2>&1)"
|
||||||
|
token=""
|
||||||
|
if [[ "$resp" == *'"ok":true'* ]]; then
|
||||||
|
jq -r '.result.message_id // empty' <<<"$resp" 2>/dev/null
|
||||||
|
else
|
||||||
|
log "WARN: fallo al enviar notificación a Telegram: ${resp}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_attach_log() { # caption -> adjunta RUN_LOG como documento (best-effort)
|
||||||
|
local caption="$1"
|
||||||
|
[[ -r "$TELEGRAM_BOT_TOKEN_FILE" && -n "$TELEGRAM_CHAT_ID" ]] || return 0
|
||||||
|
if [[ ! -r "$RUN_LOG" ]]; then
|
||||||
|
log "WARN: no se pudo adjuntar el log a Telegram, no es legible: ${RUN_LOG}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local token resp
|
||||||
|
token="$(<"$TELEGRAM_BOT_TOKEN_FILE")"
|
||||||
|
# sendDocument, no sendMessage: el log completo va como archivo, no como
|
||||||
|
# texto. Nota: al momento de subirlo el archivo todavía se está
|
||||||
|
# escribiendo (exec > tee), así que le faltan sus últimas 2-3 líneas
|
||||||
|
# (el cierre "=== lab-agent fin ===") — cosmético, no afecta el resto.
|
||||||
|
resp="$(curl -s -m 30 -X POST "https://api.telegram.org/bot${token}/sendDocument" \
|
||||||
|
-F "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||||
|
-F "caption=${caption:0:1000}" \
|
||||||
|
-F "document=@${RUN_LOG}" 2>&1)"
|
||||||
token=""
|
token=""
|
||||||
if [[ "$resp" != *'"ok":true'* ]]; then
|
if [[ "$resp" != *'"ok":true'* ]]; then
|
||||||
log "WARN: fallo al enviar notificación a Telegram: ${resp}"
|
log "WARN: fallo al adjuntar el log a Telegram: ${resp}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +277,20 @@ compute_eligibility() { # resource_key is_unhealthy window_ok
|
|||||||
blocked="$(jq -r '.blocked // false' <<<"$state")"
|
blocked="$(jq -r '.blocked // false' <<<"$state")"
|
||||||
|
|
||||||
if [[ "$is_unhealthy" != "1" ]]; then
|
if [[ "$is_unhealthy" != "1" ]]; then
|
||||||
[[ -n "$first_unhealthy_at" ]] && state_clear "$resource_key"
|
if [[ -n "$first_unhealthy_at" ]]; then
|
||||||
|
local prev_msg_id
|
||||||
|
prev_msg_id="$(jq -r '.telegram_message_id // empty' <<<"$state")"
|
||||||
|
# Si hubo una acción notificada sobre este recurso y ahora se
|
||||||
|
# recuperó solo, avisar como respuesta al mensaje original (hilo).
|
||||||
|
# >/dev/null a propósito: no necesitamos el message_id de ESTE aviso,
|
||||||
|
# y notify_send también se usa en otros lados vía $(...) -- no
|
||||||
|
# mezclar su stdout con el "not_eligible:healthy" que devuelve esta
|
||||||
|
# función.
|
||||||
|
if [[ -n "$prev_msg_id" ]]; then
|
||||||
|
notify_send "info" "Recuperado" "✅ ${resource_key} volvió a estar sano por su cuenta." "$prev_msg_id" >/dev/null
|
||||||
|
fi
|
||||||
|
state_clear "$resource_key"
|
||||||
|
fi
|
||||||
echo "not_eligible:healthy"
|
echo "not_eligible:healthy"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -478,7 +550,7 @@ CLAUDE_AUTH_ENV=()
|
|||||||
if [[ -n "$ANTHROPIC_API_KEY_FILE" ]]; then
|
if [[ -n "$ANTHROPIC_API_KEY_FILE" ]]; then
|
||||||
if [[ ! -r "$ANTHROPIC_API_KEY_FILE" ]]; then
|
if [[ ! -r "$ANTHROPIC_API_KEY_FILE" ]]; then
|
||||||
log "FATAL: ANTHROPIC_API_KEY_FILE está configurado (${ANTHROPIC_API_KEY_FILE}) pero no se puede leer"
|
log "FATAL: ANTHROPIC_API_KEY_FILE está configurado (${ANTHROPIC_API_KEY_FILE}) pero no se puede leer"
|
||||||
notify_send "lab-agent: error de configuración" "Falta o no es legible ${ANTHROPIC_API_KEY_FILE}."
|
notify_send error "Error de configuración" "Falta o no es legible ${ANTHROPIC_API_KEY_FILE}."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
CLAUDE_AUTH_ENV=("ANTHROPIC_API_KEY=$(<"$ANTHROPIC_API_KEY_FILE")")
|
CLAUDE_AUTH_ENV=("ANTHROPIC_API_KEY=$(<"$ANTHROPIC_API_KEY_FILE")")
|
||||||
@@ -500,7 +572,8 @@ CLAUDE_JSON="$(
|
|||||||
2>>"$RUN_LOG"
|
2>>"$RUN_LOG"
|
||||||
)" || {
|
)" || {
|
||||||
log "FATAL: la invocación a claude -p falló"
|
log "FATAL: la invocación a claude -p falló"
|
||||||
notify_send "lab-agent: error" "claude -p terminó con error, ver ${RUN_LOG}"
|
notify_send error "claude -p falló" "La invocación terminó con error, ver log adjunto"
|
||||||
|
notify_attach_log "❌ claude -p falló"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,19 +584,21 @@ IS_ERROR="$(jq -r '.is_error // false' <<<"$CLAUDE_JSON")"
|
|||||||
PERMISSION_DENIALS="$(jq -c '.permission_denials // []' <<<"$CLAUDE_JSON")"
|
PERMISSION_DENIALS="$(jq -c '.permission_denials // []' <<<"$CLAUDE_JSON")"
|
||||||
if [[ "$PERMISSION_DENIALS" != "[]" ]]; then
|
if [[ "$PERMISSION_DENIALS" != "[]" ]]; then
|
||||||
log "ALERTA: claude intentó usar herramientas pese a --allowedTools \"\": ${PERMISSION_DENIALS}"
|
log "ALERTA: claude intentó usar herramientas pese a --allowedTools \"\": ${PERMISSION_DENIALS}"
|
||||||
notify_send "lab-agent: ALERTA de seguridad" "El modelo intentó invocar herramientas denegadas: ${PERMISSION_DENIALS}"
|
notify_send security "ALERTA de seguridad" "El modelo intentó invocar herramientas denegadas pese a --allowedTools \"\": ${PERMISSION_DENIALS}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$IS_ERROR" == "true" ]]; then
|
if [[ "$IS_ERROR" == "true" ]]; then
|
||||||
log "FATAL: claude -p devolvió is_error=true"
|
log "FATAL: claude -p devolvió is_error=true"
|
||||||
notify_send "lab-agent: error" "claude -p devolvió un error, ver ${RUN_LOG}"
|
notify_send error "claude -p devolvió error" "is_error=true, ver log adjunto"
|
||||||
|
notify_attach_log "❌ claude -p devolvió error"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DECISION_JSON="$(jq -r '.structured_output // empty' <<<"$CLAUDE_JSON")"
|
DECISION_JSON="$(jq -r '.structured_output // empty' <<<"$CLAUDE_JSON")"
|
||||||
if [[ -z "$DECISION_JSON" ]]; then
|
if [[ -z "$DECISION_JSON" ]]; then
|
||||||
log "FATAL: no se recibió structured_output válido"
|
log "FATAL: no se recibió structured_output válido"
|
||||||
notify_send "lab-agent: error" "Respuesta sin structured_output, ver ${RUN_LOG}"
|
notify_send error "Respuesta sin structured_output" "claude -p no devolvió salida estructurada válida, ver log adjunto"
|
||||||
|
notify_attach_log "❌ Respuesta sin structured_output"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -587,6 +662,8 @@ execute_corrective_action() { # resource -> exit 0=ok/1=fail, deja LAST_ACTION_*
|
|||||||
|
|
||||||
NOTIFY_LINES=()
|
NOTIFY_LINES=()
|
||||||
ACTIONS_TAKEN=0
|
ACTIONS_TAKEN=0
|
||||||
|
ACTIONS_FAILED=0
|
||||||
|
ACTED_RESOURCES=() # recursos con decision=="restart" este ciclo -> para hilo de Telegram
|
||||||
|
|
||||||
while IFS= read -r action_json; do
|
while IFS= read -r action_json; do
|
||||||
[[ -z "$action_json" ]] && continue
|
[[ -z "$action_json" ]] && continue
|
||||||
@@ -599,13 +676,15 @@ while IFS= read -r action_json; do
|
|||||||
if [[ "$decision" == "restart" && "$eligibility" != "eligible" ]]; then
|
if [[ "$decision" == "restart" && "$eligibility" != "eligible" ]]; then
|
||||||
log "ANOMALÍA: claude propuso restart en '${resource}' pero el wrapper lo marca '${eligibility}' — RECHAZADO, no se ejecuta"
|
log "ANOMALÍA: claude propuso restart en '${resource}' pero el wrapper lo marca '${eligibility}' — RECHAZADO, no se ejecuta"
|
||||||
log_action "$resource" "restart_rejected" "rejected_not_eligible" "claude_reason=${claude_reason} | wrapper_eligibility=${eligibility}"
|
log_action "$resource" "restart_rejected" "rejected_not_eligible" "claude_reason=${claude_reason} | wrapper_eligibility=${eligibility}"
|
||||||
NOTIFY_LINES+=("[RECHAZADO] ${resource}: claude propuso restart pero no es elegible (${eligibility}). Motivo de claude: ${claude_reason}")
|
ACTIONS_FAILED=1
|
||||||
|
NOTIFY_LINES+=("🛑 Rechazado — ${resource}: claude propuso restart pero no es elegible (${eligibility}). Motivo de claude: ${claude_reason}")
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$decision" == "restart" ]]; then
|
if [[ "$decision" == "restart" ]]; then
|
||||||
if execute_corrective_action "$resource"; then result="ok"; else result="fail"; fi
|
if execute_corrective_action "$resource"; then result="ok"; else result="fail"; fi
|
||||||
ACTIONS_TAKEN=1
|
ACTIONS_TAKEN=1
|
||||||
|
ACTED_RESOURCES+=("$resource")
|
||||||
|
|
||||||
# Tras cualquier intento REAL (éxito o fallo), marcar blocked=true: un
|
# Tras cualquier intento REAL (éxito o fallo), marcar blocked=true: un
|
||||||
# solo intento automático por incidente, sin reintentos silenciosos.
|
# solo intento automático por incidente, sin reintentos silenciosos.
|
||||||
@@ -618,17 +697,20 @@ while IFS= read -r action_json; do
|
|||||||
if [[ "$result" == "ok" ]]; then
|
if [[ "$result" == "ok" ]]; then
|
||||||
log "ACCIÓN EJECUTADA: ${LAST_ACTION_CMD} -> OK"
|
log "ACCIÓN EJECUTADA: ${LAST_ACTION_CMD} -> OK"
|
||||||
log_action "$resource" "restart" "success" "$claude_reason"
|
log_action "$resource" "restart" "success" "$claude_reason"
|
||||||
NOTIFY_LINES+=("[ACCIÓN OK] ${resource}: ${LAST_ACTION_CMD}. Motivo: ${claude_reason}")
|
NOTIFY_LINES+=("✅ Acción ejecutada — ${resource}: ${LAST_ACTION_CMD}
|
||||||
|
Motivo: ${claude_reason}")
|
||||||
else
|
else
|
||||||
log "ACCIÓN FALLIDA: ${LAST_ACTION_CMD:-N/A} -> ${LAST_ACTION_STDERR}"
|
log "ACCIÓN FALLIDA: ${LAST_ACTION_CMD:-N/A} -> ${LAST_ACTION_STDERR}"
|
||||||
log_action "$resource" "restart" "failure" "$claude_reason | stderr=${LAST_ACTION_STDERR}"
|
log_action "$resource" "restart" "failure" "$claude_reason | stderr=${LAST_ACTION_STDERR}"
|
||||||
NOTIFY_LINES+=("[ACCIÓN FALLÓ] ${resource}: ${LAST_ACTION_STDERR}. Queda bloqueado hasta que un humano lo revise (no se reintentará solo).")
|
ACTIONS_FAILED=1
|
||||||
|
NOTIFY_LINES+=("🛑 Acción falló — ${resource}: ${LAST_ACTION_STDERR}
|
||||||
|
Queda bloqueado hasta revisión humana, no se reintenta solo.")
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
log "notify_only: ${resource} — ${claude_reason}"
|
log "notify_only: ${resource} — ${claude_reason}"
|
||||||
log_action "$resource" "notify_only" "n/a" "$claude_reason"
|
log_action "$resource" "notify_only" "n/a" "$claude_reason"
|
||||||
if [[ "$eligibility" != "not_eligible:healthy" ]]; then
|
if [[ "$eligibility" != "not_eligible:healthy" ]]; then
|
||||||
NOTIFY_LINES+=("[SOLO DIAGNÓSTICO] ${resource} (${eligibility}): ${claude_reason}")
|
NOTIFY_LINES+=("👀 Diagnóstico — ${resource} (${eligibility}): ${claude_reason}")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done < <(jq -c '.actions[]?' <<<"$DECISION_JSON")
|
done < <(jq -c '.actions[]?' <<<"$DECISION_JSON")
|
||||||
@@ -638,13 +720,29 @@ done < <(jq -c '.actions[]?' <<<"$DECISION_JSON")
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
if [[ "${#NOTIFY_LINES[@]}" -gt 0 || "$ACTIONS_TAKEN" == "1" ]]; then
|
if [[ "${#NOTIFY_LINES[@]}" -gt 0 || "$ACTIONS_TAKEN" == "1" ]]; then
|
||||||
MSG="Resumen: ${CLAUDE_SUMMARY}"$'\n\n'
|
MSG="📋 Resumen: ${CLAUDE_SUMMARY}"$'\n\n'
|
||||||
for l in "${NOTIFY_LINES[@]}"; do MSG="${MSG}${l}"$'\n'; done
|
for l in "${NOTIFY_LINES[@]}"; do MSG="${MSG}${l}"$'\n\n'; done
|
||||||
MSG="${MSG}"$'\n'"log completo: ${RUN_LOG}"
|
MSG="${MSG}📄 log completo adjunto abajo"
|
||||||
title="lab-agent"
|
|
||||||
[[ "$ACTIONS_TAKEN" == "1" ]] && title="lab-agent: acción correctiva ejecutada"
|
if [[ "$ACTIONS_FAILED" == "1" ]]; then
|
||||||
[[ "$DRY_RUN" == "1" ]] && title="[DRY_RUN] ${title}"
|
level="action_fail"; title="Acción falló o fue rechazada"
|
||||||
notify_send "$title" "$MSG"
|
elif [[ "$ACTIONS_TAKEN" == "1" ]]; then
|
||||||
|
level="action_ok"; title="Acción correctiva ejecutada"
|
||||||
|
else
|
||||||
|
level="info"; title="Novedades del ciclo"
|
||||||
|
fi
|
||||||
|
[[ "$DRY_RUN" == "1" ]] && title="🧪 [DRY_RUN] ${title}"
|
||||||
|
SENT_MSG_ID="$(notify_send "$level" "$title" "$MSG")"
|
||||||
|
notify_attach_log "$(notify_level_icon "$level") ${title}"
|
||||||
|
# Guardar el message_id contra cada recurso accionado este ciclo, para
|
||||||
|
# poder responder en hilo el día que se recupere solo (ver
|
||||||
|
# compute_eligibility). No aplica en DRY_RUN, ya que no se toca el
|
||||||
|
# estado real en modo simulación.
|
||||||
|
if [[ -n "$SENT_MSG_ID" && "$DRY_RUN" != "1" ]]; then
|
||||||
|
for r in "${ACTED_RESOURCES[@]}"; do
|
||||||
|
attach_telegram_message_id "$r" "$SENT_MSG_ID"
|
||||||
|
done
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
log "Sin anomalías ni acciones este ciclo — no se notifica."
|
log "Sin anomalías ni acciones este ciclo — no se notifica."
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in new issue
Block a user