#!/usr/bin/env bash # ============================================================================= # COCONEWS - Diagnóstico del sistema # Verifica que todos los servicios estén OK y muestra estado + sugerencias # # Uso: # bash deploy/debian/check.sh # diagnóstico completo # bash deploy/debian/check.sh --quick # solo servicios arriba/abajo # ============================================================================= RSS2_HOME="/opt/rss2" API_PORT="8080" QUICK=false [[ "${1:-}" == "--quick" ]] && QUICK=true RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' CYAN='\033[0;36m' OK() { echo -e " ${GREEN}[✓]${NC} $*"; } FAIL() { echo -e " ${RED}[✗]${NC} $*"; } WARN() { echo -e " ${YELLOW}[!]${NC} $*"; } INFO() { echo -e " ${DIM} $*${NC}"; } HEAD() { echo -e "\n${BOLD}${CYAN}── $* ${NC}"; } FIX() { echo -e " ${YELLOW} → Fix:${NC} $*"; } ERRORS=0 WARNINGS=0 fail_with_fix() { FAIL "$1" shift for fix in "$@"; do FIX "$fix"; done (( ERRORS++ )) || true } warn_with_fix() { WARN "$1" shift for fix in "$@"; do FIX "$fix"; done (( WARNINGS++ )) || true } echo "" echo -e "${BOLD}╔═══════════════════════════════════════════╗${NC}" echo -e "${BOLD}║ COCONEWS · Diagnóstico del Sistema ║${NC}" echo -e "${BOLD}╚═══════════════════════════════════════════╝${NC}" echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S') · $(hostname)${NC}" # ============================================================================= # 1. SERVICIOS SYSTEMD # ============================================================================= HEAD "Servicios systemd" GO_SERVICES=( "rss2-backend:API REST Go" "rss2-ingestor:Ingestor RSS" "rss2-scraper:Scraper HTML" "rss2-discovery:Discovery de feeds" "rss2-wiki:Wiki worker" "rss2-topics:Topics matcher" "rss2-related:Related news" "rss2-qdrant-worker:Qdrant sync" ) PY_SERVICES=( "rss2-langdetect:Detección de idioma" "rss2-translation-scheduler:Scheduler traducción" "rss2-translator:Traductor NLLB-200" "rss2-embeddings:Embeddings ML" "rss2-ner:NER entidades" "rss2-cluster:Clustering eventos" "rss2-categorizer:Categorizador" ) INFRA_SERVICES=( "rss2-qdrant:Vector DB Qdrant" "postgresql:PostgreSQL" "redis-server:Redis" "nginx:Nginx" ) check_service() { local svc="${1%%:*}" local name="${1##*:}" if ! command -v systemctl &>/dev/null; then WARN "$name (systemctl no disponible)" return fi local state state=$(systemctl is-active "$svc" 2>/dev/null || echo "unknown") case "$state" in active) OK "$name ($svc)" ;; failed) fail_with_fix "$name ($svc) — FAILED" \ "journalctl -u $svc -n 30 --no-pager" \ "systemctl restart $svc" ;; inactive) warn_with_fix "$name ($svc) — inactivo" \ "systemctl start $svc" \ "systemctl enable $svc" ;; *) warn_with_fix "$name ($svc) — $state" \ "systemctl status $svc" ;; esac } echo -e "\n ${DIM}Infraestructura:${NC}" for svc in "${INFRA_SERVICES[@]}"; do check_service "$svc"; done echo -e "\n ${DIM}Workers Go:${NC}" for svc in "${GO_SERVICES[@]}"; do check_service "$svc"; done echo -e "\n ${DIM}Workers Python (ML):${NC}" for svc in "${PY_SERVICES[@]}"; do check_service "$svc"; done [[ "$QUICK" == "true" ]] && { echo "" [[ "$ERRORS" -eq 0 && "$WARNINGS" -eq 0 ]] && \ echo -e " ${GREEN}${BOLD}Todo OK${NC}" || \ echo -e " ${RED}Errores: $ERRORS ${YELLOW}Advertencias: $WARNINGS${NC}" exit 0 } # ============================================================================= # 2. CONECTIVIDAD # ============================================================================= HEAD "Conectividad" # PostgreSQL PG_ENV="$RSS2_HOME/.env" if [[ -f "$PG_ENV" ]]; then source "$PG_ENV" 2>/dev/null || true fi DB_NAME="${POSTGRES_DB:-rss}" DB_USER="${POSTGRES_USER:-rss}" if pg_isready -q 2>/dev/null; then OK "PostgreSQL acepta conexiones" # Probar conexión con usuario rss2 if sudo -u postgres psql -d "$DB_NAME" -c "SELECT 1" &>/dev/null 2>&1; then NEWS=$(sudo -u postgres psql -tq -d "$DB_NAME" \ -c "SELECT COUNT(*) FROM noticias;" 2>/dev/null | tr -d ' \n' || echo "?") TRANS=$(sudo -u postgres psql -tq -d "$DB_NAME" \ -c "SELECT COUNT(*) FROM traducciones WHERE status='done';" 2>/dev/null | tr -d ' \n' || echo "?") PEND=$(sudo -u postgres psql -tq -d "$DB_NAME" \ -c "SELECT COUNT(*) FROM traducciones WHERE status='pending';" 2>/dev/null | tr -d ' \n' || echo "?") INFO "Base de datos: $DB_NAME" INFO "Noticias: $NEWS | Traducciones hechas: $TRANS | Pendientes: $PEND" else warn_with_fix "No se puede conectar a la BD '$DB_NAME' con usuario '$DB_USER'" \ "sudo -u postgres psql -c \"\\du\" # listar usuarios" \ "Revisa DB_USER y DB_PASS en $RSS2_HOME/.env" fi else fail_with_fix "PostgreSQL no responde" \ "sudo systemctl start postgresql" \ "sudo journalctl -u postgresql -n 20 --no-pager" \ "sudo pg_ctlcluster 16 main status" fi # Redis REDIS_PASS="${REDIS_PASSWORD:-}" REDIS_AUTH="" [[ -n "$REDIS_PASS" ]] && REDIS_AUTH="-a $REDIS_PASS" if redis-cli $REDIS_AUTH ping 2>/dev/null | grep -q PONG; then REDIS_MEM=$(redis-cli $REDIS_AUTH info memory 2>/dev/null | grep used_memory_human | cut -d: -f2 | tr -d '\r' || echo "?") OK "Redis responde (memoria usada: ${REDIS_MEM})" else fail_with_fix "Redis no responde" \ "sudo systemctl start redis-server" \ "redis-cli ping # sin auth" \ "Verifica REDIS_PASSWORD en $RSS2_HOME/.env" fi # Qdrant if curl -sf "http://127.0.0.1:6333/healthz" &>/dev/null; then QDRANT_VER=$(curl -sf "http://127.0.0.1:6333/" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('version','?'))" 2>/dev/null || echo "?") OK "Qdrant responde (v${QDRANT_VER})" else warn_with_fix "Qdrant no responde en :6333" \ "systemctl start rss2-qdrant" \ "journalctl -u rss2-qdrant -n 20 --no-pager" \ "Búsqueda semántica no funcionará hasta que Qdrant esté activo" fi # API Backend Go if curl -sf "http://127.0.0.1:${API_PORT}/api/stats" &>/dev/null; then STATS=$(curl -sf "http://127.0.0.1:${API_PORT}/api/stats" 2>/dev/null | \ python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"noticias={d.get('total_news','?')} feeds={d.get('total_feeds','?')}\")" 2>/dev/null || echo "") OK "API backend responde (:${API_PORT}) — $STATS" else fail_with_fix "API backend no responde en :${API_PORT}" \ "systemctl restart rss2-backend" \ "journalctl -u rss2-backend -n 30 --no-pager" \ "curl http://127.0.0.1:${API_PORT}/api/stats # prueba manual" fi # Nginx if curl -sf "http://127.0.0.1:8001/health" &>/dev/null; then OK "Nginx responde (:8001)" else fail_with_fix "Nginx no responde en :8001" \ "nginx -t # verificar config" \ "systemctl restart nginx" \ "journalctl -u nginx -n 20 --no-pager" fi # ============================================================================= # 3. BINARIOS GO # ============================================================================= HEAD "Binarios compilados" BINS=(server ingestor scraper discovery wiki_worker topics related qdrant_worker) for bin in "${BINS[@]}"; do BIN_PATH="$RSS2_HOME/bin/$bin" if [[ -x "$BIN_PATH" ]]; then SIZE=$(du -sh "$BIN_PATH" 2>/dev/null | cut -f1) OK "$bin (${SIZE})" else fail_with_fix "$bin no encontrado o sin permisos de ejecución en $BIN_PATH" \ "sudo bash deploy/debian/build.sh # recompila todos los binarios" fi done # ============================================================================= # 4. MODELOS ML # ============================================================================= HEAD "Modelos ML" # NLLB-200 CTranslate2 if [[ -d "$RSS2_HOME/models/nllb-ct2" ]] && \ [[ -f "$RSS2_HOME/models/nllb-ct2/model.bin" ]]; then SIZE=$(du -sh "$RSS2_HOME/models/nllb-ct2" 2>/dev/null | cut -f1) OK "NLLB-200 CTranslate2 (${SIZE})" else warn_with_fix "Modelo NLLB-200 no encontrado en $RSS2_HOME/models/nllb-ct2" \ "El traductor no funcionará hasta que esté el modelo" \ "Convierte con: ver sección 3 de DEPLOY_DEBIAN.md" fi # spaCy if "$RSS2_HOME/venv/bin/python" -c "import spacy; spacy.load('es_core_news_lg')" &>/dev/null 2>&1; then OK "spaCy es_core_news_lg" else warn_with_fix "Modelo spaCy es_core_news_lg no disponible" \ "$RSS2_HOME/venv/bin/python -m spacy download es_core_news_lg" \ "El worker NER no funcionará hasta instalarlo" fi # sentence-transformers (embeddings) if "$RSS2_HOME/venv/bin/python" -c "from sentence_transformers import SentenceTransformer" &>/dev/null 2>&1; then OK "sentence-transformers disponible" else warn_with_fix "sentence-transformers no instalado" \ "$RSS2_HOME/venv/bin/pip install sentence-transformers==3.0.1" \ "Los embeddings y búsqueda semántica no funcionarán" fi # ctranslate2 if "$RSS2_HOME/venv/bin/python" -c "import ctranslate2" &>/dev/null 2>&1; then OK "ctranslate2 disponible" else warn_with_fix "ctranslate2 no instalado" \ "$RSS2_HOME/venv/bin/pip install ctranslate2>=4.0.0" \ "La traducción NLLB-200 no funcionará" fi # ============================================================================= # 5. DISCO Y PERMISOS # ============================================================================= HEAD "Disco y permisos" # Espacio en disco DISK_FREE=$(df -h "$RSS2_HOME" 2>/dev/null | tail -1 | awk '{print $4}') DISK_PCT=$(df "$RSS2_HOME" 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%') if [[ "${DISK_PCT:-0}" -gt 90 ]]; then fail_with_fix "Disco al ${DISK_PCT}% — quedan solo ${DISK_FREE}" \ "du -sh $RSS2_HOME/* | sort -rh | head -10 # ver qué ocupa más" \ "Los modelos ML necesitan ~5 GB libres" elif [[ "${DISK_PCT:-0}" -gt 75 ]]; then warn_with_fix "Disco al ${DISK_PCT}% (libre: ${DISK_FREE})" \ "Revisa el espacio antes de descargar modelos ML" else OK "Disco: ${DISK_PCT}% usado, ${DISK_FREE} libres" fi # Permisos de /opt/rss2 if [[ -d "$RSS2_HOME" ]]; then OWNER=$(stat -c '%U' "$RSS2_HOME" 2>/dev/null || echo "?") if [[ "$OWNER" == "rss2" ]]; then OK "Propietario de $RSS2_HOME: rss2" else warn_with_fix "Propietario de $RSS2_HOME es '$OWNER', debería ser 'rss2'" \ "sudo chown -R rss2:rss2 $RSS2_HOME" fi fi # .env if [[ -f "$RSS2_HOME/.env" ]]; then ENV_PERMS=$(stat -c '%a' "$RSS2_HOME/.env" 2>/dev/null || echo "?") if [[ "$ENV_PERMS" == "600" ]]; then OK ".env con permisos 600 (correcto)" else warn_with_fix ".env con permisos ${ENV_PERMS} (debería ser 600)" \ "chmod 600 $RSS2_HOME/.env" fi # Verificar que no quedan valores por defecto peligrosos if grep -q "CAMBIA_ESTO\|changeme\|change_this" "$RSS2_HOME/.env" 2>/dev/null; then warn_with_fix ".env tiene valores por defecto sin cambiar" \ "nano $RSS2_HOME/.env # cambia POSTGRES_PASSWORD, REDIS_PASSWORD, SECRET_KEY" fi else fail_with_fix ".env no encontrado en $RSS2_HOME/.env" \ "cp deploy/debian/env.example $RSS2_HOME/.env" \ "nano $RSS2_HOME/.env # edita contraseñas" fi # ============================================================================= # RESUMEN FINAL # ============================================================================= echo "" echo -e "${BOLD}══════════════════════════════════════════════${NC}" if [[ "$ERRORS" -eq 0 && "$WARNINGS" -eq 0 ]]; then echo -e " ${GREEN}${BOLD}Sistema OK — todo funcionando correctamente${NC}" elif [[ "$ERRORS" -eq 0 ]]; then echo -e " ${YELLOW}${BOLD}Sistema funcional con $WARNINGS advertencia(s)${NC}" echo -e " ${DIM}Las advertencias no bloquean el funcionamiento básico${NC}" else echo -e " ${RED}${BOLD}$ERRORS error(es) encontrado(s)${YELLOW} $WARNINGS advertencia(s)${NC}" echo -e " ${DIM}Sigue los fixes indicados arriba para resolverlos${NC}" fi echo -e "${BOLD}══════════════════════════════════════════════${NC}" echo "" echo -e " ${DIM}Logs de servicios: journalctl -u rss2-backend -f${NC}" echo -e " ${DIM}Diagnóstico rápido: bash deploy/debian/check.sh --quick${NC}" echo "" exit $(( ERRORS > 0 ? 1 : 0 ))