Железный Егорин — форк Орбиты: тёплый стиль (amber), логин с odyssey WebGL-молнией справа, энергомониторинг+панель

Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент.
Доступ агентам: репо публичный, push german-токеном.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
German
2026-07-07 23:57:16 +03:00
co-authored by Claude Opus 4.8
commit b1c52ab748
114 changed files with 14340 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# ingress-watchdog.sh — алерт обрыва связи ингриса с MQTT-брокером (Milestone B)
# stdlib/bash only. Аддитивно. НЕ трогает приёмники/буферизацию/маршруты.
#
# Проба: TCP-connect к брокеру vm-mts1 10.50.0.48:1883 (таймаут 5с).
# Дебаунс: DOWN только после непрерывной недоступности > DOWN_THRESHOLD (60с).
# Алерт ТОЛЬКО на переходе состояния (up->down = ОБРЫВ, down->up = ВОССТАНОВЛЕНО).
# Каналы: всегда лог + статус-файл; Telegram — если в .env есть TG_TOKEN и TG_CHAT_ID.
set -u
# --- Конфиг (можно переопределить через .env) ---
BROKER_HOST="10.50.0.48"
BROKER_PORT="1883"
DOWN_THRESHOLD=60 # сек непрерывной недоступности до алерта ОБРЫВ
PROBE_TIMEOUT=5 # сек на TCP-пробу
STATE_FILE="/var/run/ingress-watchdog.state"
STATUS_FILE="/run/ingress-watchdog.status"
LOG_FILE="/var/log/ingress-watchdog.log"
ENV_FILE="/opt/ingress-watchdog/.env"
NODE_NAME="$(hostname)"
SPOOL_FILE="" # путь к queue.jsonl (задаётся в .env на каждой ноде)
ALERT_WEBHOOK="https://kpgen.servaki.online/webhook/alert" # единый n8n-хаб (можно переопределить в .env)
# --- Загрузка .env (TG_TOKEN, TG_CHAT_ID, SPOOL_FILE, NODE_NAME override) ---
if [ -f "$ENV_FILE" ]; then
# shellcheck disable=SC1090
. "$ENV_FILE"
fi
now="$(date +%s)"
ts="$(date '+%Y-%m-%d %H:%M:%S %Z')"
log() {
# НЕ логируем токен. Пишем строку в лог.
printf '%s [%s] %s\n' "$ts" "$NODE_NAME" "$1" >> "$LOG_FILE" 2>/dev/null
}
spool_count() {
if [ -n "${SPOOL_FILE:-}" ] && [ -f "$SPOOL_FILE" ]; then
wc -l < "$SPOOL_FILE" 2>/dev/null | tr -d ' '
else
echo 0
fi
}
send_telegram() {
# $1 = текст. Отправляем только если заданы token+chat_id.
local text="$1"
if [ -n "${TG_TOKEN:-}" ] && [ -n "${TG_CHAT_ID:-}" ]; then
timeout 10 curl -s -o /dev/null \
"https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${TG_CHAT_ID}" \
--data-urlencode "text=${text}" >/dev/null 2>&1 \
&& log "telegram: sent" \
|| log "telegram: send FAILED"
else
log "telegram: skipped (no token/chat_id)"
fi
}
send_webhook() {
# $1=severity (critical|warning|info), $2=title, $3=text. Единый хаб алертов (n8n).
local sev="$1" title="$2" text="$3" iso body
[ -z "${ALERT_WEBHOOK:-}" ] && { log "webhook: skipped (no ALERT_WEBHOOK)"; return; }
iso="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
body="$(printf '{"source":"orbita-ingress","node":"%s","severity":"%s","title":"%s","text":"%s","ts":"%s"}' \
"$NODE_NAME" "$sev" "$title" "$text" "$iso")"
local code
code="$(timeout 12 curl -s -o /dev/null -m 10 --retry 2 -w '%{http_code}' \
-X POST "$ALERT_WEBHOOK" -H 'Content-Type: application/json' -d "$body" 2>/dev/null)"
if [ "$code" = "200" ]; then log "webhook: sent ($sev) HTTP 200"; else log "webhook: send FAILED (HTTP ${code:-000})"; fi
}
write_status() {
# $1=state up|down, $2=human message
{
printf 'node=%s\n' "$NODE_NAME"
printf 'state=%s\n' "$1"
printf 'updated=%s\n' "$ts"
printf 'updated_epoch=%s\n' "$now"
printf 'broker=%s:%s\n' "$BROKER_HOST" "$BROKER_PORT"
printf 'spool_lines=%s\n' "$(spool_count)"
printf 'last_message=%s\n' "$2"
} > "$STATUS_FILE" 2>/dev/null
}
# --- Читаем предыдущий стейт ---
last_ok=0
current_state="up"
down_since=0
if [ -f "$STATE_FILE" ]; then
# shellcheck disable=SC1090
. "$STATE_FILE" 2>/dev/null || true
fi
# --- TCP-проба ---
if timeout "$PROBE_TIMEOUT" bash -c "cat < /dev/null > /dev/tcp/${BROKER_HOST}/${BROKER_PORT}" 2>/dev/null; then
probe_ok=1
else
probe_ok=0
fi
new_state="$current_state"
if [ "$probe_ok" -eq 1 ]; then
last_ok="$now"
if [ "$current_state" = "down" ]; then
# ВОССТАНОВЛЕНО (переход down->up)
new_state="up"
down_since=0
spc="$(spool_count)"
msg="✅ ВОССТАНОВЛЕНО: связь ингриса ${NODE_NAME} с брокером ${BROKER_HOST}:${BROKER_PORT} восстановлена. В спуле сейчас: ${spc} сообщ. (дренаж до-дошлёт). Время: ${ts}"
log "STATE up (recovered). spool=${spc}"
write_status "up" "recovered"
send_webhook "info" "Связь восстановлена ${NODE_NAME}" "брокер ${BROKER_HOST}:${BROKER_PORT} снова доступен; в буфере ${spc} сообщ. (дренаж до-дошлёт)"
send_telegram "$msg"
else
# остаёмся up, без алерта
write_status "up" "ok"
fi
else
# недоступно
if [ "$down_since" -eq 0 ] 2>/dev/null || [ -z "${down_since:-}" ]; then
down_since="$now"
fi
down_for=$(( now - down_since ))
if [ "$current_state" = "up" ] && [ "$down_for" -gt "$DOWN_THRESHOLD" ]; then
# ОБРЫВ (переход up->down после дебаунса)
new_state="down"
spc="$(spool_count)"
msg="🔴 ОБРЫВ: ингрис ${NODE_NAME} потерял связь с брокером ${BROKER_HOST}:${BROKER_PORT} (недоступен ${down_for}с). В спуле накоплено: ${spc} сообщ. и буферизуется. Время: ${ts}"
log "STATE down (link lost ${down_for}s). spool=${spc}"
write_status "down" "link_lost"
send_webhook "critical" "Обрыв связи ${NODE_NAME}" "брокер ${BROKER_HOST}:${BROKER_PORT} недоступен ${down_for}с; в буфере ${spc} сообщ., буферизуется"
send_telegram "$msg"
elif [ "$current_state" = "down" ]; then
# уже в down — не спамим
write_status "down" "still_down"
else
# недоступно, но порог ещё не перевален — ждём
write_status "up" "probing_down ${down_for}s"
fi
fi
# --- Сохраняем стейт ---
{
printf 'last_ok=%s\n' "$last_ok"
printf 'current_state=%s\n' "$new_state"
printf 'down_since=%s\n' "$down_since"
} > "$STATE_FILE" 2>/dev/null
exit 0