#!/usr/bin/env bash # ============================================================================== # install.sh — Admin Edu-Space (UniCABA) # Instalador automatizado para Proxmox VE (LXC) / Debian 12 / Ubuntu # Despliegue unificado: Backend Flask (Gunicorn) + Frontend Node.js (BFF) + PostgreSQL # Uso: sudo bash install.sh [--keep-db | --fresh-db] # ============================================================================== set -euo pipefail # ─── Colores y Helpers ───────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' ok() { echo -e "${GREEN} ✔ $*${RESET}"; } info() { echo -e "${CYAN} ▸ $*${RESET}"; } warn() { echo -e "${YELLOW} ⚠ $*${RESET}"; } err() { echo -e "${RED} ✘ $*${RESET}" >&2; exit 1; } hdr() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}\n"; } # ─── Argumentos de Línea de Comandos ─────────────────────────────────────────── ARG_DB_ACTION="" for arg in "$@"; do case "$arg" in --keep-db) ARG_DB_ACTION="keep" ;; --fresh-db|--restore-db) ARG_DB_ACTION="fresh" ;; -h|--help) echo "Uso: sudo bash install.sh [OPCIONES]" echo "Opciones:" echo " --keep-db Mantiene la base de datos existente si ya contiene datos." echo " --fresh-db Restaura la base de datos desde cero (DROP y recarga snapshot)." echo " -h, --help Muestra esta ayuda." exit 0 ;; *) warn "Opción desconocida ignorada: $arg" ;; esac done # ─── Variables Principales ───────────────────────────────────────────────────── ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKEND_DIR="${ROOT_DIR}/backend" FRONTEND_DIR="${ROOT_DIR}/frontend" VENV_DIR="${BACKEND_DIR}/venv" BACKEND_ENV="${BACKEND_DIR}/.env" FRONTEND_ENV="${FRONTEND_DIR}/.env" SERVICE_BACKEND="admin-edu-space-backend" SERVICE_FRONTEND="admin-edu-space-frontend" SERVICE_META="admin-edu-space" APP_USER="eduspace" PYTHON_REQUIRED_MAJOR=3 PYTHON_REQUIRED_MINOR=10 LOG_DIR="/var/log/${SERVICE_META}" # Ubicación del snapshot de base de datos SQL_BACKUP_FILE="${BACKEND_DIR}/classrooms_db.sql" if [[ ! -f "$SQL_BACKUP_FILE" && -f "${ROOT_DIR}/classrooms_db.sql" ]]; then SQL_BACKUP_FILE="${ROOT_DIR}/classrooms_db.sql" fi # ─── Verificación de Permisos Root ───────────────────────────────────────────── [[ $EUID -ne 0 ]] && err "Ejecutá este script con permisos de root: sudo bash install.sh" # Si el directorio está dentro de /root, trasladarlo a /opt para evitar problemas de permisos con el usuario de servicio if [[ "${ROOT_DIR}" == /root/* ]]; then NEW_DIR="/opt/admin-edu-space" warn "Detectado directorio en ${ROOT_DIR}. Trasladando a ${NEW_DIR} para evitar bloqueos de permisos..." systemctl stop "${SERVICE_BACKEND}" 2>/dev/null || true systemctl stop "${SERVICE_FRONTEND}" 2>/dev/null || true systemctl stop "${SERVICE_META}" 2>/dev/null || true mkdir -p "${NEW_DIR}" cp -a "${ROOT_DIR}/." "${NEW_DIR}/" cd "${NEW_DIR}" exec bash install.sh "$@" fi # Validar estructura básica del repositorio [[ ! -f "${BACKEND_DIR}/wsgi.py" ]] && err "No se encontró wsgi.py en ${BACKEND_DIR}. Verificá la estructura del proyecto." [[ ! -f "${FRONTEND_DIR}/package.json" ]] && err "No se encontró package.json en ${FRONTEND_DIR}. Verificá la estructura del proyecto." # Verificar e instalar git si no está presente en el sistema if ! command -v git &>/dev/null; then info "Git no está instalado. Instalando paquete git..." export DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 || true apt-get install -y -qq git >/dev/null 2>&1 || apt-get install -y git ok "Git instalado correctamente" fi git config --global --add safe.directory '*' 2>/dev/null || true echo -e "\n${BOLD}${CYAN}" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ Admin Edu-Space (UniCABA) — Instalador ║" echo "║ Backend (Flask/Gunicorn) + Frontend (Node.js) + PostgreSQL ║" echo "╚══════════════════════════════════════════════════════════════╝" echo -e "${RESET}" OS_NAME=$(grep -oP '(?<=^PRETTY_NAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || uname -s) info "Sistema Operativo: ${OS_NAME}" info "Directorio Raíz: ${ROOT_DIR}" info "Directorio Backend: ${BACKEND_DIR}" info "Directorio Frontend: ${FRONTEND_DIR}" info "Usuario de Servicio: ${APP_USER}" # ─── 1. Paquetes Base del Sistema ────────────────────────────────────────────── hdr "1. Actualizando repositorios y paquetes base" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq \ sudo curl wget git ca-certificates gnupg lsb-release \ build-essential libpq-dev libffi-dev libssl-dev pkg-config \ locales \ > /dev/null 2>&1 ok "Paquetes base instalados" pg_cmd() { if command -v sudo >/dev/null 2>&1; then sudo -u postgres psql "$@" elif command -v runuser >/dev/null 2>&1; then runuser -u postgres -- psql "$@" else su - postgres -c "psql $(printf '%q ' "$@")" fi } # ─── 2. Configurar Locale UTF-8 ──────────────────────────────────────────────── hdr "2. Configurando soporte de codificación (Locales UTF-8)" if [ -f /etc/locale.gen ]; then sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen sed -i 's/# es_AR.UTF-8 UTF-8/es_AR.UTF-8 UTF-8/' /etc/locale.gen locale-gen en_US.UTF-8 es_AR.UTF-8 > /dev/null 2>&1 || true fi update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 > /dev/null 2>&1 || true export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 ok "Locales configurados: en_US.UTF-8 / es_AR.UTF-8" # ─── 3. Instalar Python 3.10+ y entorno virtual ──────────────────────────────── hdr "3. Verificando entorno Python" apt-get install -y -qq python3 python3-venv python3-pip python3-dev > /dev/null 2>&1 PY_VERSION=$(python3 --version 2>/dev/null | awk '{print $2}' || echo "0.0") PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1) PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2) if [[ "$PY_MAJOR" -ge "$PYTHON_REQUIRED_MAJOR" && "$PY_MINOR" -ge "$PYTHON_REQUIRED_MINOR" ]]; then ok "Python $PY_VERSION disponible" else info "Python $PY_VERSION es menor a 3.10 — instalando Python 3.11..." apt-get install -y -qq python3.11 python3.11-venv python3.11-dev > /dev/null 2>&1 update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 > /dev/null 2>&1 PY_VERSION=$(python3 --version | awk '{print $2}') ok "Python $PY_VERSION instalado" fi # ─── 4. Instalar Node.js y npm para el Frontend BFF ─────────────────────────── hdr "4. Verificando entorno Node.js para el Frontend BFF" NODE_INSTALLED=false if command -v node &>/dev/null; then NODE_VER_NUM=$(node -v | tr -d 'v' | cut -d. -f1 || echo "0") if [[ "$NODE_VER_NUM" -ge 18 ]]; then NODE_INSTALLED=true ok "Node.js $(node -v) y npm $(npm -v 2>/dev/null || echo '') ya disponibles" fi fi if [ "$NODE_INSTALLED" = false ]; then info "Instalando Node.js 20 LTS vía repositorio oficial NodeSource..." mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg --yes 2>/dev/null || true echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null apt-get update -qq >/dev/null 2>&1 || true apt-get install -y -qq nodejs >/dev/null 2>&1 || apt-get install -y nodejs ok "Node.js $(node -v) instalado exitosamente" fi # ─── 5. Instalar y Levantar Motor PostgreSQL ─────────────────────────────────── hdr "5. Instalando y configurando motor de base de datos PostgreSQL" apt-get install -y -qq postgresql postgresql-contrib postgresql-client > /dev/null 2>&1 PG_VER=$(psql --version 2>/dev/null | awk '{print $3}' || echo "instalado") PG_MAJOR=$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1 || echo "") [[ -z "$PG_MAJOR" ]] && PG_MAJOR=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -n1 || echo "") [[ -z "$PG_MAJOR" ]] && PG_MAJOR="15" ok "Motor PostgreSQL instalado: ${PG_VER}" mkdir -p /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql chown -R postgres:postgres /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql chmod 2775 /run/postgresql /var/run/postgresql if ! pg_lsclusters 2>/dev/null | grep -q 'main'; then info "Cluster PostgreSQL no detectado — inicializando cluster ${PG_MAJOR} main..." pg_createcluster "${PG_MAJOR}" main --locale en_US.UTF-8 --start >/dev/null 2>&1 \ || pg_createcluster "${PG_MAJOR}" main --locale C.UTF-8 --start >/dev/null 2>&1 \ || true fi info "Iniciando servicio de PostgreSQL..." pg_ctlcluster "${PG_MAJOR}" main start >/dev/null 2>&1 || true systemctl start "postgresql@${PG_MAJOR}-main" 2>/dev/null || true systemctl start postgresql 2>/dev/null || true service postgresql start 2>/dev/null || true PG_READY=false for i in {1..20}; do if pg_cmd -c '\q' >/dev/null 2>&1; then PG_READY=true break fi sleep 1 done if [ "$PG_READY" = true ]; then PG_HBA=$(find /etc/postgresql/ -name pg_hba.conf 2>/dev/null | sort -V | tail -n1) if [[ -n "$PG_HBA" && -f "$PG_HBA" ]]; then sed -i -E 's/^(local\s+all\s+all\s+)peer/\1trust/' "$PG_HBA" 2>/dev/null || true sed -i -E 's/^(host\s+all\s+all\s+127\.0\.0\.1\/32\s+)(scram-sha-256|md5)/\1trust/' "$PG_HBA" 2>/dev/null || true sed -i -E 's/^(host\s+all\s+all\s+::1\/128\s+)(scram-sha-256|md5)/\1trust/' "$PG_HBA" 2>/dev/null || true systemctl reload "postgresql@${PG_MAJOR}-main" 2>/dev/null || systemctl reload postgresql 2>/dev/null || true fi systemctl enable "postgresql@${PG_MAJOR}-main" > /dev/null 2>&1 || true systemctl enable postgresql > /dev/null 2>&1 || true ok "PostgreSQL activo y aceptando conexiones" else err "No se pudo conectar al motor PostgreSQL local. Verificá los registros del sistema." fi # ─── 6. Configurar Variables de Entorno (.env) ───────────────────────────────── hdr "6. Configurando variables de entorno (Backend y Frontend)" DB_NAME="classrooms_db" DB_USER="postgres" DB_PASSWORD="password" if [[ -f "$BACKEND_ENV" ]]; then ok "backend/.env ya existe — se preserva la configuración actual" set -a # shellcheck disable=SC1090 source <(grep -v '^#' "$BACKEND_ENV" | grep '=') set +a if [[ -n "${DATABASE_URL:-}" ]]; then URL_USER=$(echo "$DATABASE_URL" | sed -E 's|^.*://([^:@]+).*|\1|') URL_PASS=$(echo "$DATABASE_URL" | sed -E 's|^.*://[^:]+:([^@]+)@.*|\1|') URL_NAME=$(echo "$DATABASE_URL" | sed -E 's|^.*/([^?]+).*|\1|') [[ "$URL_USER" != "$DATABASE_URL" && -n "$URL_USER" ]] && DB_USER="$URL_USER" [[ "$URL_PASS" != "$DATABASE_URL" && -n "$URL_PASS" ]] && DB_PASSWORD="$URL_PASS" [[ "$URL_NAME" != "$DATABASE_URL" && -n "$URL_NAME" ]] && DB_NAME="$URL_NAME" fi DB_USER="${DB_USER:-${DB_USERNAME:-postgres}}" DB_PASSWORD="${DB_PASSWORD:-password}" DB_NAME="${DB_NAME:-classrooms_db}" else SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))') cat > "$BACKEND_ENV" < "$FRONTEND_ENV" </dev/null 2>&1 \ || pg_cmd -c "ALTER USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \ || true pg_cmd -c "ALTER USER \"${u}\" WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true fi done pg_cmd -c "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true pg_cmd -c "ALTER USER postgres WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true # Detectar existencia y tablas previas DB_EXISTS=false TABLE_COUNT=0 if pg_cmd -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw "${DB_NAME}"; then DB_EXISTS=true TABLE_COUNT=$(pg_cmd -d "${DB_NAME}" -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" 2>/dev/null || echo "0") fi DO_RESTORE_FRESH=false if [ "$DB_EXISTS" = true ] && [ "$TABLE_COUNT" -gt 0 ]; then info "Base de datos detectada: '${DB_NAME}' con ${TABLE_COUNT} tabla(s) existente(s)." if [[ "$ARG_DB_ACTION" == "keep" ]]; then info "Bandera --keep-db recibida: Se mantiene la base de datos existente intacta." DO_RESTORE_FRESH=false elif [[ "$ARG_DB_ACTION" == "fresh" ]]; then warn "Bandera --fresh-db recibida: Se recreará la base de datos desde cero." DO_RESTORE_FRESH=true elif [ -t 0 ]; then echo -e "\n${BOLD}${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo -e "${BOLD} ¿Qué deseas hacer con la base de datos PostgreSQL existente?${RESET}" echo -e " [1] ${GREEN}${BOLD}Mantener la base de datos actual${RESET} (Recomendado: preserva registros y sincroniza esquema)" echo -e " [2] ${RED}${BOLD}Restaurar desde cero${RESET} (DROP DATABASE y recargar snapshot ${SQL_BACKUP_FILE##*/})" echo -e "${BOLD}${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" DB_CHOICE="" read -t 30 -p " Ingresá tu opción [1/2] (por defecto 1 en 30s): " DB_CHOICE || DB_CHOICE="1" DB_CHOICE="${DB_CHOICE:-1}" if [[ "$DB_CHOICE" == "2" ]]; then warn "Opción [2] seleccionada: Restauración destructiva desde cero." DO_RESTORE_FRESH=true else ok "Opción [1] seleccionada: Mantener base de datos existente." DO_RESTORE_FRESH=false fi else info "Ejecución no interactiva: se mantiene la base de datos existente por defecto." DO_RESTORE_FRESH=false fi else info "La base de datos '${DB_NAME}' no existe o está vacía. Se inicializará limpia." DO_RESTORE_FRESH=true fi # Otorgar privilegios base pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" >/dev/null 2>&1 || true # ─── 8. Crear Entorno Virtual e Instalar Dependencias Backend ─────────────────── hdr "8. Configurando entorno virtual Python e instalando dependencias backend" if [[ -d "$VENV_DIR" ]]; then info "Recreando entorno virtual para asegurar dependencias limpias..." rm -rf "$VENV_DIR" fi python3 -m venv "$VENV_DIR" > /dev/null 2>&1 "${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel --quiet info "Instalando dependencias desde backend/requirements.txt..." if ! "${VENV_DIR}/bin/pip" install -r "${BACKEND_DIR}/requirements.txt" --quiet; then warn "Reintentando instalación con aislamiento deshabilitado..." "${VENV_DIR}/bin/pip" install -r "${BACKEND_DIR}/requirements.txt" --no-build-isolation || err "Falló la instalación de dependencias de Python." fi # Verificación explícita de módulos críticos requeridos por modelos y esquemas if ! "${VENV_DIR}/bin/python" -c "import flask, sqlalchemy, cryptography, pydantic; print('Módulos críticos OK')" >/dev/null 2>&1; then warn "Asegurando librerías críticas en el venv (cryptography, pydantic)..." "${VENV_DIR}/bin/pip" install "cryptography>=42.0.0" "pydantic>=2.0.0" --quiet fi ok "Dependencias de Python instaladas correctamente" # ─── 9. Ejecución de Despliegue de Datos (Restaurar o Sincronizar) ────────────── hdr "9. Aplicando esquema y datos en la base de datos" cd "$BACKEND_DIR" if [ "$DO_RESTORE_FRESH" = true ]; then if [[ -f "$SQL_BACKUP_FILE" ]]; then BACKUP_SIZE=$(du -h "$SQL_BACKUP_FILE" | awk '{print $1}') info "Restaurando snapshot '${SQL_BACKUP_FILE##*/}' (${BACKUP_SIZE})..." pg_cmd -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" > /dev/null 2>&1 || true pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" > /dev/null 2>&1 sed '/^\\restrict/d' "$SQL_BACKUP_FILE" | pg_cmd -d "${DB_NAME}" > /dev/null 2>&1 || true ok "Snapshot SQL restaurado exitosamente" else info "No se encontró classrooms_db.sql. Creando esquema limpio con init_db.py..." fi else ok "Conservando tablas existentes en '${DB_NAME}'" fi # Otorgar permisos completos al usuario de la aplicación for u in "${DB_USER}" "${DB_USERNAME:-}" postgres eduspace_user; do if [[ -n "$u" ]]; then pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" > /dev/null 2>&1 || true pg_cmd -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"${u}\";" > /dev/null 2>&1 || true pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"${u}\";" > /dev/null 2>&1 || true fi done # Sincronización idempotente con init_db.py (crea tablas o columnas faltantes sin borrar datos) info "Sincronizando modelos y esquemas institucionales con init_db.py..." LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 \ PYTHONPATH="${BACKEND_DIR}" FLASK_APP=wsgi.py "${VENV_DIR}/bin/python" -X utf8 init_db.py ok "Esquema, usuarios y roles RBAC sincronizados con éxito" cd "$ROOT_DIR" # ─── 10. Instalar Dependencias del Frontend Node.js ──────────────────────────── hdr "10. Instalando dependencias de Node.js en frontend/" cd "$FRONTEND_DIR" info "Ejecutando npm install en frontend..." if [ -f "package-lock.json" ]; then npm ci --omit=dev --silent 2>/dev/null || npm install --omit=dev --silent else npm install --omit=dev --silent fi ok "Dependencias de Node.js instaladas en frontend/" cd "$ROOT_DIR" # ─── 11. Usuario del Sistema y Permisos de Directorio ─────────────────────────── hdr "11. Configurando usuario del sistema y permisos de seguridad" if id "$APP_USER" &>/dev/null; then info "El usuario de sistema '${APP_USER}' ya existe." else useradd --system --no-create-home --shell /usr/sbin/nologin "$APP_USER" ok "Usuario de sistema '${APP_USER}' creado (sin shell interactiva)" fi mkdir -p "${BACKEND_DIR}/app/static/uploads" mkdir -p "$LOG_DIR" chown -R "${APP_USER}:${APP_USER}" "$ROOT_DIR" chown -R "${APP_USER}:${APP_USER}" "$LOG_DIR" chmod -R 755 "$ROOT_DIR" chmod -R 775 "${BACKEND_DIR}/app/static/uploads" chmod 640 "$BACKEND_ENV" "$FRONTEND_ENV" chmod 755 "$LOG_DIR" ok "Permisos aplicados en ${ROOT_DIR} y ${LOG_DIR}" # ─── 12. Servicios Systemd (Backend Flask y Frontend Node.js) ────────────────── hdr "12. Configurando servicios systemd de arranque automático" GUNICORN_BIN="${VENV_DIR}/bin/gunicorn" NODE_BIN="$(command -v node)" # 12.1 Servicio Backend cat > "/etc/systemd/system/${SERVICE_BACKEND}.service" < "/etc/systemd/system/${SERVICE_FRONTEND}.service" < "/etc/systemd/system/${SERVICE_META}.service" < /dev/null 2>&1 info "Iniciando servicio backend..." systemctl restart "${SERVICE_BACKEND}" sleep 3 info "Iniciando servicio frontend..." systemctl restart "${SERVICE_FRONTEND}" systemctl restart "${SERVICE_META}" sleep 3 # Verificar salud de los servicios BACKEND_OK=false if systemctl is-active --quiet "$SERVICE_BACKEND"; then HTTP_BACK=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5000/api/v1/openapi.json || echo "000") if [[ "$HTTP_BACK" == "200" ]]; then ok "Backend Flask respondiendo activamente en puerto 5000 (OpenAPI HTTP 200)" BACKEND_OK=true else warn "Backend activo pero devolvió HTTP ${HTTP_BACK} al consultar OpenAPI." fi else warn "El servicio backend tardó en iniciar. Logs: journalctl -u ${SERVICE_BACKEND} -n 20 --no-pager" fi FRONTEND_OK=false if systemctl is-active --quiet "$SERVICE_FRONTEND"; then HTTP_FRONT=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/ || echo "000") if [[ "$HTTP_FRONT" == "200" || "$HTTP_FRONT" == "302" ]]; then ok "Frontend Node.js respondiendo activamente en puerto 3000 (HTTP ${HTTP_FRONT})" FRONTEND_OK=true else warn "Frontend activo pero devolvió HTTP ${HTTP_FRONT} al consultar raíz." fi else warn "El servicio frontend tardó en iniciar. Logs: journalctl -u ${SERVICE_FRONTEND} -n 20 --no-pager" fi # ─── Resumen Final y Datos de Acceso ────────────────────────────────────────── hdr "¡Despliegue Completado con Éxito!" IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1") echo -e " ${BOLD}Acceso Web a la Plataforma (Frontend):${RESET}" echo -e " ${GREEN}${BOLD}http://${IP}:3000${RESET} o ${GREEN}${BOLD}http://localhost:3000${RESET}" echo "" echo -e " ${BOLD}Acceso a la API REST (Backend Flask):${RESET}" echo -e " ${CYAN}http://${IP}:5000/api/v1${RESET} (OpenAPI spec en /api/v1/openapi.json)" echo "" echo -e " ${BOLD}Credenciales de Administrador Iniciales:${RESET}" echo -e " Email: ${CYAN}admin@edu-space.com${RESET}" echo -e " Password: ${CYAN}admin123${RESET}" echo "" echo -e " ${BOLD}Base de Datos Desplegada:${RESET}" echo -e " Motor: PostgreSQL (${PG_VER})" echo -e " Base: ${DB_NAME}" echo -e " Usuario: ${DB_USER}" echo -e " Modo: $( [ "$DO_RESTORE_FRESH" = true ] && echo "Restaurada desde cero" || echo "Base existente mantenida y sincronizada" )" echo "" echo -e " ${BOLD}Comandos de Gestión del Servicio:${RESET}" echo -e " Estado General: ${CYAN}systemctl status ${SERVICE_META}${RESET}" echo -e " Reiniciar Todo: ${CYAN}systemctl restart ${SERVICE_META}${RESET}" echo -e " Logs Backend: ${CYAN}journalctl -u ${SERVICE_BACKEND} -f${RESET}" echo -e " Logs Frontend: ${CYAN}journalctl -u ${SERVICE_FRONTEND} -f${RESET}" echo "" ok "Despliegue finalizado. Todos los servicios han sido configurados." echo ""