Compare commits
No commits in common. "289c5a4ee5b2c781b9cc36ad385d81bb19efa52c" and "c4b4b9cd899fb0915cf798b26101f2ac4896ced7" have entirely different histories.
289c5a4ee5
...
c4b4b9cd89
353
projects/gamilit/.gitignore
vendored
353
projects/gamilit/.gitignore
vendored
@ -1,106 +1,148 @@
|
|||||||
# GAMILIT Monorepo - .gitignore
|
# GAMILIT Monorepo - .gitignore
|
||||||
# Generado: 2025-11-01 (RFC-0001)
|
# Generado: 2025-11-01 (RFC-0001)
|
||||||
|
# Actualizado: 2025-12-05
|
||||||
|
|
||||||
# === NODE.JS ===
|
# ============================================
|
||||||
node_modules/
|
# NODE.JS - DEPENDENCIAS (GLOBAL)
|
||||||
|
# ============================================
|
||||||
|
# Ignorar node_modules en CUALQUIER nivel de anidación
|
||||||
|
**/node_modules/
|
||||||
|
|
||||||
|
# Lock files de otros package managers (mantener solo package-lock.json)
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Logs de npm/yarn/pnpm
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# Dependency directories
|
# Directorios de dependencias alternativas
|
||||||
jspm_packages/
|
jspm_packages/
|
||||||
|
bower_components/
|
||||||
|
|
||||||
# Optional npm cache directory
|
# Cache de npm
|
||||||
.npm
|
.npm/
|
||||||
|
.npmrc.local
|
||||||
|
|
||||||
# Optional eslint cache
|
# Cache de eslint/stylelint
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
.stylelintcache
|
||||||
|
|
||||||
# === TYPESCRIPT ===
|
# ============================================
|
||||||
|
# TYPESCRIPT / BUILD ARTIFACTS (GLOBAL)
|
||||||
|
# ============================================
|
||||||
|
# Ignorar dist/build en CUALQUIER nivel
|
||||||
|
**/dist/
|
||||||
|
**/build/
|
||||||
|
**/out/
|
||||||
|
**/.next/
|
||||||
|
**/.nuxt/
|
||||||
|
**/.turbo/
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
dist/
|
**/*.tsbuildinfo
|
||||||
build/
|
|
||||||
*.js.map
|
*.js.map
|
||||||
|
|
||||||
# === ANGULAR / NX ===
|
# ============================================
|
||||||
|
# FRAMEWORKS ESPECÍFICOS
|
||||||
|
# ============================================
|
||||||
|
# Angular / NX
|
||||||
.angular/
|
.angular/
|
||||||
.nx/cache/
|
.nx/
|
||||||
.nx/workspace-data/
|
**/.nx/
|
||||||
|
|
||||||
# === NESTJS ===
|
# Vite
|
||||||
/apps/backend/dist/
|
**/.vite/
|
||||||
/apps/backend/build/
|
|
||||||
|
|
||||||
# === ENVIRONMENT FILES ===
|
# Webpack
|
||||||
# IMPORTANTE: Nunca commitear secrets reales
|
.webpack/
|
||||||
|
**/.webpack/
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ENVIRONMENT FILES - SECRETS
|
||||||
|
# ============================================
|
||||||
|
# CRÍTICO: Nunca commitear secrets
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
.env.production
|
.env.production
|
||||||
.env.development
|
.env.development
|
||||||
.env.test
|
.env.test
|
||||||
# Permitir archivos de ejemplo (sin secrets)
|
.env.staging
|
||||||
!.env.*.example
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Archivos de configuración con secrets
|
# Archivos con secrets
|
||||||
config/secrets.json
|
**/secrets.json
|
||||||
config/credentials.json
|
**/credentials.json
|
||||||
**/*secrets*.json
|
**/*secrets*.json
|
||||||
**/*credentials*.json
|
**/*credentials*.json
|
||||||
|
**/*.secret
|
||||||
|
**/*.secrets
|
||||||
|
|
||||||
# === DATABASES ===
|
# Configuración de base de datos con credenciales
|
||||||
# PostgreSQL
|
**/database.config.ts
|
||||||
|
!**/database.config.example.ts
|
||||||
|
**/ormconfig.json
|
||||||
|
!**/ormconfig.example.json
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# DATABASES
|
||||||
|
# ============================================
|
||||||
|
# Backups y dumps
|
||||||
*.sql.backup
|
*.sql.backup
|
||||||
*.dump
|
*.dump
|
||||||
*.pgdata
|
*.pgdata
|
||||||
|
*.sql.gz
|
||||||
|
|
||||||
# Local database files
|
# Bases de datos locales
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.db
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
# Database connection strings (excepción: ejemplos con .example)
|
# Redis
|
||||||
database.config.ts
|
dump.rdb
|
||||||
!database.config.example.ts
|
|
||||||
|
|
||||||
# === LOGS ===
|
# ============================================
|
||||||
logs/
|
# LOGS (GLOBAL)
|
||||||
*.log
|
# ============================================
|
||||||
npm-debug.log*
|
**/logs/
|
||||||
yarn-debug.log*
|
**/*.log
|
||||||
yarn-error.log*
|
**/pm2-logs/
|
||||||
pnpm-debug.log*
|
**/*.pm2.log
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# PM2 logs
|
# ============================================
|
||||||
pm2-logs/
|
# TESTING (GLOBAL)
|
||||||
*.pm2.log
|
# ============================================
|
||||||
|
**/coverage/
|
||||||
# === TESTING ===
|
**/.nyc_output/
|
||||||
coverage/
|
|
||||||
.nyc_output/
|
|
||||||
*.lcov
|
*.lcov
|
||||||
|
|
||||||
# Jest
|
# Jest
|
||||||
jest-cache/
|
**/jest-cache/
|
||||||
|
.jest/
|
||||||
|
|
||||||
# Cypress
|
# Cypress
|
||||||
cypress/screenshots/
|
**/cypress/screenshots/
|
||||||
cypress/videos/
|
**/cypress/videos/
|
||||||
cypress/downloads/
|
**/cypress/downloads/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
**/playwright-report/
|
||||||
|
**/playwright/.cache/
|
||||||
|
**/test-results/
|
||||||
|
|
||||||
# E2E reports
|
# E2E reports
|
||||||
e2e-reports/
|
**/e2e-reports/
|
||||||
test-results/
|
|
||||||
|
|
||||||
# === IDEs and EDITORS ===
|
# ============================================
|
||||||
# VSCode
|
# IDEs and EDITORS
|
||||||
|
# ============================================
|
||||||
|
# VSCode - mantener configuración compartida
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
@ -119,92 +161,117 @@ test-results/
|
|||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
*.sublime-project
|
*.sublime-project
|
||||||
|
|
||||||
# Vim
|
# Vim/Neovim
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
.netrwhist
|
||||||
|
Session.vim
|
||||||
|
|
||||||
# Emacs
|
# Emacs
|
||||||
*~
|
|
||||||
\#*\#
|
\#*\#
|
||||||
.\#*
|
.\#*
|
||||||
|
*.elc
|
||||||
|
auto-save-list/
|
||||||
|
|
||||||
# === OS FILES ===
|
# ============================================
|
||||||
|
# OS FILES
|
||||||
|
# ============================================
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
.LSOverride
|
.LSOverride
|
||||||
._*
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
|
||||||
# Linux
|
# Linux
|
||||||
*~
|
*~
|
||||||
.directory
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
$RECYCLE.BIN/
|
$RECYCLE.BIN/
|
||||||
|
*.lnk
|
||||||
|
|
||||||
# === DOCKER ===
|
# ============================================
|
||||||
# No ignorar Dockerfiles, solo archivos temporales
|
# DOCKER
|
||||||
|
# ============================================
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
docker-compose.local.yml
|
||||||
.dockerignore.local
|
.dockerignore.local
|
||||||
|
**/*.dockerignore.local
|
||||||
|
|
||||||
# === BUILD ARTIFACTS ===
|
# ============================================
|
||||||
/apps/*/dist/
|
# DEPLOYMENT & SECURITY
|
||||||
/apps/*/build/
|
# ============================================
|
||||||
/libs/*/dist/
|
|
||||||
|
|
||||||
# Webpack
|
|
||||||
.webpack/
|
|
||||||
|
|
||||||
# === DEPLOYMENT ===
|
|
||||||
# PM2
|
# PM2
|
||||||
ecosystem.config.js.local
|
ecosystem.config.js.local
|
||||||
pm2.config.js.local
|
pm2.config.js.local
|
||||||
|
|
||||||
# Deploy keys (excepción: .example files)
|
# Claves y certificados
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
!*.example.pem
|
!*.example.pem
|
||||||
!*.example.key
|
!*.example.key
|
||||||
|
|
||||||
# SSL certificates (excepción: self-signed para dev)
|
# SSL certificates
|
||||||
*.crt
|
*.crt
|
||||||
*.cer
|
*.cer
|
||||||
!dev-cert.crt
|
!dev-cert.crt
|
||||||
|
!*.example.crt
|
||||||
|
|
||||||
# === TEMP FILES ===
|
# SSH keys
|
||||||
tmp/
|
id_rsa*
|
||||||
temp/
|
id_ed25519*
|
||||||
*.tmp
|
*.pub
|
||||||
*.temp
|
!*.example.pub
|
||||||
*.cache
|
|
||||||
|
|
||||||
# === ARTIFACTS (parcial) ===
|
# ============================================
|
||||||
# Mantener reportes importantes, ignorar temporales
|
# TEMP FILES (GLOBAL)
|
||||||
|
# ============================================
|
||||||
|
**/tmp/
|
||||||
|
**/temp/
|
||||||
|
**/.tmp/
|
||||||
|
**/.temp/
|
||||||
|
**/*.tmp
|
||||||
|
**/*.temp
|
||||||
|
**/*.cache
|
||||||
|
**/.cache/
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ARTIFACTS
|
||||||
|
# ============================================
|
||||||
/artifacts/temp/
|
/artifacts/temp/
|
||||||
/artifacts/cache/
|
/artifacts/cache/
|
||||||
/artifacts/**/*.tmp
|
/artifacts/**/*.tmp
|
||||||
|
/artifacts/**/*.cache
|
||||||
|
|
||||||
# === CLAUDE CODE ===
|
# ============================================
|
||||||
# Excluir toda la carpeta .claude (configuración local de IA)
|
# CLAUDE CODE / AI
|
||||||
|
# ============================================
|
||||||
|
# Configuración local de Claude Code
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
# === ORCHESTRATION ===
|
# Pero NO ignorar orchestration (necesario para Claude Code cloud)
|
||||||
# IMPORTANTE: orchestration/ DEBE estar en el repo para Claude Code cloud
|
# Solo ignorar temporales dentro de orchestration
|
||||||
# Contiene: prompts, directivas, trazas, inventarios, templates
|
|
||||||
# Solo ignorar subcarpetas temporales específicas y archivos comprimidos
|
|
||||||
orchestration/.archive/
|
orchestration/.archive/
|
||||||
orchestration/.tmp/
|
orchestration/.tmp/
|
||||||
orchestration/**/*.tmp
|
orchestration/**/*.tmp
|
||||||
orchestration/**/*.cache
|
orchestration/**/*.cache
|
||||||
|
|
||||||
# === REFERENCE (Código de Referencia) ===
|
# ============================================
|
||||||
# IMPORTANTE: reference/ DEBE estar en el repo para Claude Code cloud
|
# REFERENCE (Código de Referencia)
|
||||||
# Contiene: proyectos de referencia para análisis y desarrollo
|
# ============================================
|
||||||
# Ignorar solo carpetas de build/dependencias dentro de reference/
|
# reference/ DEBE estar en el repo
|
||||||
|
# Solo ignorar build/dependencias dentro
|
||||||
reference/**/node_modules/
|
reference/**/node_modules/
|
||||||
reference/**/dist/
|
reference/**/dist/
|
||||||
reference/**/build/
|
reference/**/build/
|
||||||
@ -219,16 +286,9 @@ reference/**/*.tmp
|
|||||||
reference/**/*.cache
|
reference/**/*.cache
|
||||||
reference/**/.DS_Store
|
reference/**/.DS_Store
|
||||||
|
|
||||||
# === MIGRATION (temporal) ===
|
# ============================================
|
||||||
# Durante migración, mantener docs de análisis
|
# PACKAGE MANAGERS
|
||||||
# Descomentar después de migración completa:
|
# ============================================
|
||||||
# /docs-analysis/
|
|
||||||
|
|
||||||
# === ARCHIVOS ESPECÍFICOS GRANDES ===
|
|
||||||
# Si hay archivos markdown extremadamente grandes (>50MB), considerarlos para LFS
|
|
||||||
# *.large.md
|
|
||||||
|
|
||||||
# === PACKAGE MANAGERS ===
|
|
||||||
# Yarn
|
# Yarn
|
||||||
.yarn/*
|
.yarn/*
|
||||||
!.yarn/patches
|
!.yarn/patches
|
||||||
@ -241,33 +301,102 @@ reference/**/.DS_Store
|
|||||||
# PNPM
|
# PNPM
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
|
|
||||||
# === MONITORING ===
|
# ============================================
|
||||||
# Application monitoring
|
# MONITORING & OBSERVABILITY
|
||||||
|
# ============================================
|
||||||
newrelic_agent.log
|
newrelic_agent.log
|
||||||
.monitors/
|
.monitors/
|
||||||
|
**/.sentry/
|
||||||
|
**/sentry-debug.log
|
||||||
|
|
||||||
# === MISC ===
|
# ============================================
|
||||||
# Backups - Archivos
|
# BACKUPS (GLOBAL)
|
||||||
|
# ============================================
|
||||||
|
# Archivos
|
||||||
*.backup
|
*.backup
|
||||||
*.bak
|
*.bak
|
||||||
*.old
|
*.old
|
||||||
|
*.orig
|
||||||
|
|
||||||
# Backups - Carpetas
|
# Carpetas
|
||||||
*_old/
|
**/*_old/
|
||||||
*_bckp/
|
**/*_bckp/
|
||||||
*_bkp/
|
**/*_bkp/
|
||||||
*_backup/
|
**/*_backup/
|
||||||
*.old/
|
**/*.old/
|
||||||
*.bak/
|
**/*.bak/
|
||||||
*.backup/
|
**/*.backup/
|
||||||
|
|
||||||
# Backups específicos (carpetas identificadas en workspace)
|
# Específicos del proyecto
|
||||||
orchestration_old/
|
orchestration_old/
|
||||||
orchestration_bckp/
|
orchestration_bckp/
|
||||||
docs_bkp/
|
docs_bkp/
|
||||||
|
|
||||||
# Compressed files (si no son assets del proyecto)
|
# ============================================
|
||||||
|
# COMPRESSED FILES
|
||||||
|
# ============================================
|
||||||
*.zip
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
*.tar
|
||||||
*.rar
|
*.rar
|
||||||
|
*.7z
|
||||||
|
# Excepto assets
|
||||||
!assets/**/*.zip
|
!assets/**/*.zip
|
||||||
|
!public/**/*.zip
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# MISCELÁNEOS
|
||||||
|
# ============================================
|
||||||
|
# Archivos de debug
|
||||||
|
**/*.debug
|
||||||
|
**/debug.log
|
||||||
|
|
||||||
|
# Archivos de error
|
||||||
|
**/*.error
|
||||||
|
**/error.log
|
||||||
|
|
||||||
|
# Storybook
|
||||||
|
**/storybook-static/
|
||||||
|
|
||||||
|
# Parcel
|
||||||
|
.parcel-cache/
|
||||||
|
|
||||||
|
# Serverless
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Archivos generados por IDEs
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Archivos de licencia generados
|
||||||
|
LICENSE.txt.bak
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# APPS ESPECÍFICAS DEL MONOREPO
|
||||||
|
# ============================================
|
||||||
|
# Backend NestJS
|
||||||
|
apps/backend/dist/
|
||||||
|
apps/backend/build/
|
||||||
|
apps/backend/.nest/
|
||||||
|
|
||||||
|
# Frontend React/Vite
|
||||||
|
apps/frontend/dist/
|
||||||
|
apps/frontend/build/
|
||||||
|
apps/frontend/.vite/
|
||||||
|
|
||||||
|
# Database - mantener DDL, ignorar generados
|
||||||
|
apps/database/*.dump
|
||||||
|
apps/database/*.backup
|
||||||
|
apps/database/data/
|
||||||
|
|
||||||
|
# DevOps - ignorar logs y temp
|
||||||
|
apps/devops/logs/
|
||||||
|
apps/devops/tmp/
|
||||||
|
|||||||
@ -1,214 +0,0 @@
|
|||||||
# INSTRUCCIONES DE DEPLOYMENT - GAMILIT
|
|
||||||
|
|
||||||
**Version:** 1.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Este archivo contiene las instrucciones de deployment que el agente ejecuta DESPUES de hacer backup y pull.
|
|
||||||
|
|
||||||
**Prerequisitos:**
|
|
||||||
- Ya ejecutaste backup de BD y configuraciones
|
|
||||||
- Ya hiciste pull del repositorio
|
|
||||||
- Los servicios PM2 estan detenidos
|
|
||||||
- Tienes las variables DB_PASSWORD y DATABASE_URL configuradas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FASE 1: RESTAURAR CONFIGURACIONES
|
|
||||||
|
|
||||||
Restaurar los .env de produccion desde el backup:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit
|
|
||||||
BACKUP_DIR="/home/gamilit/backups/latest"
|
|
||||||
|
|
||||||
# Restaurar .env.production
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/backend/.env.production 2>/dev/null || echo "WARN: No habia .env.production de backend"
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/frontend/.env.production 2>/dev/null || echo "WARN: No habia .env.production de frontend"
|
|
||||||
|
|
||||||
# Crear symlinks .env -> .env.production
|
|
||||||
cd apps/backend && ln -sf .env.production .env && cd ../..
|
|
||||||
cd apps/frontend && ln -sf .env.production .env && cd ../..
|
|
||||||
|
|
||||||
# Verificar
|
|
||||||
echo "=== Configuraciones restauradas ==="
|
|
||||||
ls -la apps/backend/.env* 2>/dev/null
|
|
||||||
ls -la apps/frontend/.env* 2>/dev/null
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FASE 2: RECREAR BASE DE DATOS
|
|
||||||
|
|
||||||
La BD se recrea completamente (politica de CARGA LIMPIA):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/apps/database
|
|
||||||
|
|
||||||
# Verificar que el script existe
|
|
||||||
ls -la drop-and-recreate-database.sh
|
|
||||||
|
|
||||||
# Ejecutar recreacion completa
|
|
||||||
# Este script: DROP BD -> CREATE BD -> DDL (16 fases) -> Seeds (38+ archivos)
|
|
||||||
./drop-and-recreate-database.sh "$DATABASE_URL"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Si el script falla**, alternativas:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Opcion A: Si la BD existe pero esta vacia
|
|
||||||
./create-database.sh
|
|
||||||
|
|
||||||
# Opcion B: Si necesitas crear desde cero (usuario no existe)
|
|
||||||
cd scripts && ./init-database.sh --env prod --password "$DB_PASSWORD"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FASE 3: BUILD
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
echo "=== Building Backend ==="
|
|
||||||
cd apps/backend
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
echo "=== Building Frontend ==="
|
|
||||||
cd apps/frontend
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
echo "=== Build completado ==="
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FASE 4: INICIAR SERVICIOS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit
|
|
||||||
|
|
||||||
# Iniciar con PM2
|
|
||||||
pm2 start ecosystem.config.js --env production
|
|
||||||
|
|
||||||
# Guardar configuracion
|
|
||||||
pm2 save
|
|
||||||
|
|
||||||
# Mostrar estado
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FASE 5: VALIDAR DEPLOYMENT
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit
|
|
||||||
|
|
||||||
echo "=== 5.1 Estado de PM2 ==="
|
|
||||||
pm2 list
|
|
||||||
|
|
||||||
echo "=== 5.2 Health del Backend ==="
|
|
||||||
curl -s http://localhost:3006/api/health | head -20
|
|
||||||
|
|
||||||
echo "=== 5.3 Frontend accesible ==="
|
|
||||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3005)
|
|
||||||
echo "HTTP Status: $HTTP_CODE"
|
|
||||||
|
|
||||||
echo "=== 5.4 Validacion de Base de Datos ==="
|
|
||||||
psql "$DATABASE_URL" -c "SELECT 'tenants' as tabla, COUNT(*) as total FROM auth_management.tenants
|
|
||||||
UNION ALL SELECT 'users', COUNT(*) FROM auth.users
|
|
||||||
UNION ALL SELECT 'modules', COUNT(*) FROM educational_content.modules
|
|
||||||
UNION ALL SELECT 'maya_ranks', COUNT(*) FROM gamification_system.maya_ranks
|
|
||||||
UNION ALL SELECT 'feature_flags', COUNT(*) FROM system_configuration.feature_flags;"
|
|
||||||
|
|
||||||
echo "=== 5.5 Logs recientes ==="
|
|
||||||
pm2 logs --lines 10 --nostream
|
|
||||||
```
|
|
||||||
|
|
||||||
**Valores esperados:**
|
|
||||||
- tenants: 14+
|
|
||||||
- users: 20+
|
|
||||||
- modules: 5
|
|
||||||
- maya_ranks: 5
|
|
||||||
- feature_flags: 26+
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ROLLBACK (Si algo falla)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
BACKUP_DIR="/home/gamilit/backups/latest"
|
|
||||||
|
|
||||||
# Restaurar BD
|
|
||||||
echo "=== Restaurando Base de Datos ==="
|
|
||||||
gunzip -c "$BACKUP_DIR/database/gamilit_*.sql.gz" | psql "$DATABASE_URL"
|
|
||||||
|
|
||||||
# Restaurar configs
|
|
||||||
echo "=== Restaurando Configuraciones ==="
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/backend/ 2>/dev/null || true
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/frontend/ 2>/dev/null || true
|
|
||||||
|
|
||||||
# Reiniciar servicios
|
|
||||||
pm2 restart all
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## REPORTE DE ERRORES
|
|
||||||
|
|
||||||
Si algo falla, reporta:
|
|
||||||
1. Numero de FASE donde fallo (1-5)
|
|
||||||
2. Comando exacto que fallo
|
|
||||||
3. Mensaje de error completo
|
|
||||||
4. Output de: `pm2 list` y `pm2 logs --lines 50 --nostream`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## INFORMACION ADICIONAL
|
|
||||||
|
|
||||||
### Estructura del proyecto
|
|
||||||
|
|
||||||
```
|
|
||||||
/home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/
|
|
||||||
├── apps/
|
|
||||||
│ ├── backend/ # NestJS API (puerto 3006, 2 instancias PM2)
|
|
||||||
│ ├── frontend/ # React App (puerto 3005, 1 instancia PM2)
|
|
||||||
│ └── database/ # DDL, Seeds, Scripts
|
|
||||||
├── scripts/ # Scripts de produccion
|
|
||||||
├── ecosystem.config.js # Configuracion PM2
|
|
||||||
└── logs/ # Logs de aplicacion
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scripts disponibles
|
|
||||||
|
|
||||||
| Script | Ubicacion | Proposito |
|
|
||||||
|--------|-----------|-----------|
|
|
||||||
| drop-and-recreate-database.sh | apps/database/ | Recrear BD completa |
|
|
||||||
| create-database.sh | apps/database/ | Solo DDL + Seeds |
|
|
||||||
| init-database.sh | apps/database/scripts/ | Crear usuario + BD |
|
|
||||||
| build-production.sh | scripts/ | Solo build |
|
|
||||||
| deploy-production.sh | scripts/ | Solo deploy PM2 |
|
|
||||||
| pre-deploy-check.sh | scripts/ | Validacion pre-deploy |
|
|
||||||
| diagnose-production.sh | scripts/ | Diagnostico del sistema |
|
|
||||||
|
|
||||||
### Documentacion adicional
|
|
||||||
|
|
||||||
| Archivo | Proposito |
|
|
||||||
|---------|-----------|
|
|
||||||
| docs/95-guias-desarrollo/DIRECTIVA-DEPLOYMENT.md | Checklist |
|
|
||||||
| docs/95-guias-desarrollo/GUIA-DESPLIEGUE-PRODUCCION-COMPLETA.md | Guia detallada |
|
|
||||||
| docs/95-guias-desarrollo/GUIA-VALIDACION-PRODUCCION.md | Troubleshooting |
|
|
||||||
| apps/database/FLUJO-CARGA-LIMPIA.md | Recreacion de BD |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Ultima actualizacion: 2025-12-18 v1.0*
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
# ACTUALIZACION DE PRODUCCION - GAMILIT
|
|
||||||
|
|
||||||
**LEER ESTE ARCHIVO DESPUES DE HACER `git pull`**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## INSTRUCCIONES RAPIDAS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Configurar password de base de datos
|
|
||||||
export DB_PASSWORD="tu_password"
|
|
||||||
|
|
||||||
# 2. Hacer scripts ejecutables
|
|
||||||
chmod +x scripts/*.sh
|
|
||||||
|
|
||||||
# 3. Ejecutar actualizacion completa
|
|
||||||
./scripts/update-production.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QUE HACE EL SCRIPT
|
|
||||||
|
|
||||||
El script `update-production.sh` ejecuta automaticamente:
|
|
||||||
|
|
||||||
1. Detiene PM2
|
|
||||||
2. Respalda configuraciones (.env) a `/home/gamilit/backups/`
|
|
||||||
3. Respalda base de datos completa (pg_dump)
|
|
||||||
4. Restaura configuraciones despues del pull
|
|
||||||
5. Recrea base de datos limpia con todos los seeds
|
|
||||||
6. Instala dependencias (npm install)
|
|
||||||
7. Build de aplicaciones (npm run build)
|
|
||||||
8. Inicia servicios (pm2 start)
|
|
||||||
9. Valida el deployment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DOCUMENTACION COMPLETA
|
|
||||||
|
|
||||||
Si necesitas mas detalles o algo falla, lee estas guias:
|
|
||||||
|
|
||||||
| Guia | Path |
|
|
||||||
|------|------|
|
|
||||||
| Actualizacion paso a paso | `docs/95-guias-desarrollo/GUIA-ACTUALIZACION-PRODUCCION.md` |
|
|
||||||
| Validacion y troubleshooting | `docs/95-guias-desarrollo/GUIA-VALIDACION-PRODUCCION.md` |
|
|
||||||
| Despliegue completo | `docs/95-guias-desarrollo/GUIA-DESPLIEGUE-PRODUCCION-COMPLETA.md` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SI ALGO FALLA
|
|
||||||
|
|
||||||
### Diagnostico rapido
|
|
||||||
```bash
|
|
||||||
./scripts/diagnose-production.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reparar datos faltantes
|
|
||||||
```bash
|
|
||||||
./scripts/repair-missing-data.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rollback
|
|
||||||
Los backups estan en `/home/gamilit/backups/YYYYMMDD_HHMMSS/`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver backups disponibles
|
|
||||||
ls -la /home/gamilit/backups/
|
|
||||||
|
|
||||||
# Restaurar base de datos
|
|
||||||
gunzip -c /home/gamilit/backups/YYYYMMDD_HHMMSS/database/gamilit_*.sql.gz | psql "$DATABASE_URL"
|
|
||||||
|
|
||||||
# Restaurar configuraciones
|
|
||||||
cp /home/gamilit/backups/YYYYMMDD_HHMMSS/config/* apps/backend/
|
|
||||||
cp /home/gamilit/backups/YYYYMMDD_HHMMSS/config/* apps/frontend/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMANDOS PM2
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 list # Ver procesos
|
|
||||||
pm2 logs # Ver logs
|
|
||||||
pm2 restart all # Reiniciar todo
|
|
||||||
pm2 monit # Monitor en tiempo real
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Servidor:** 74.208.126.102
|
|
||||||
**Backend:** Puerto 3006
|
|
||||||
**Frontend:** Puerto 3005
|
|
||||||
**Base de datos:** PostgreSQL puerto 5432, database gamilit_platform
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
# PROMPT PARA AGENTE DE PRODUCCION - GAMILIT
|
|
||||||
|
|
||||||
**Version:** 4.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Servidor:** 74.208.126.102
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## INSTRUCCIONES DE USO
|
|
||||||
|
|
||||||
Este es el prompt INICIAL que se le da al agente de produccion.
|
|
||||||
**Copiar TODO el contenido entre `=== INICIO PROMPT ===` y `=== FIN PROMPT ===`**
|
|
||||||
|
|
||||||
El agente ejecutara:
|
|
||||||
1. Backup de BD y configuraciones
|
|
||||||
2. Pull del repositorio
|
|
||||||
3. Leera las instrucciones de deployment del repositorio
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
=== INICIO PROMPT ===
|
|
||||||
|
|
||||||
Eres el agente de deployment de GAMILIT en el servidor de produccion (74.208.126.102).
|
|
||||||
|
|
||||||
CONTEXTO:
|
|
||||||
- Workspace: /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit
|
|
||||||
- Base de datos: PostgreSQL gamilit_platform, usuario gamilit_user
|
|
||||||
|
|
||||||
Tu trabajo tiene 2 ETAPAS:
|
|
||||||
|
|
||||||
## ETAPA 1: BACKUP Y PULL (Ejecutar ahora)
|
|
||||||
|
|
||||||
### Paso 1.1: Configurar variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# IMPORTANTE: Reemplaza [PASSWORD] con el password real de la BD
|
|
||||||
export DB_PASSWORD="[PASSWORD]"
|
|
||||||
export DATABASE_URL="postgresql://gamilit_user:$DB_PASSWORD@localhost:5432/gamilit_platform"
|
|
||||||
|
|
||||||
# Verificar conexion
|
|
||||||
psql "$DATABASE_URL" -c "SELECT 1;" && echo "Conexion OK" || echo "ERROR: No se puede conectar"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 1.2: Backup de Base de Datos
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit
|
|
||||||
|
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
||||||
BACKUP_DIR="/home/gamilit/backups/$TIMESTAMP"
|
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"/{database,config,logs}
|
|
||||||
|
|
||||||
echo "=== Backup de Base de Datos ==="
|
|
||||||
pg_dump "$DATABASE_URL" | gzip > "$BACKUP_DIR/database/gamilit_$TIMESTAMP.sql.gz"
|
|
||||||
ls -lh "$BACKUP_DIR/database/"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 1.3: Backup de Configuraciones
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "=== Backup de Configuraciones ==="
|
|
||||||
cp apps/backend/.env.production "$BACKUP_DIR/config/" 2>/dev/null || true
|
|
||||||
cp apps/backend/.env "$BACKUP_DIR/config/backend.env" 2>/dev/null || true
|
|
||||||
cp apps/frontend/.env.production "$BACKUP_DIR/config/" 2>/dev/null || true
|
|
||||||
cp apps/frontend/.env "$BACKUP_DIR/config/frontend.env" 2>/dev/null || true
|
|
||||||
cp ecosystem.config.js "$BACKUP_DIR/config/" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "=== Backup de Logs ==="
|
|
||||||
cp logs/*.log "$BACKUP_DIR/logs/" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Crear symlink a ultimo backup
|
|
||||||
ln -sfn "$BACKUP_DIR" "/home/gamilit/backups/latest"
|
|
||||||
|
|
||||||
echo "=== BACKUP COMPLETADO: $BACKUP_DIR ==="
|
|
||||||
ls -la "$BACKUP_DIR/"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 1.4: Detener servicios
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 stop all
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
### Paso 1.5: Pull del repositorio
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit
|
|
||||||
|
|
||||||
git fetch origin
|
|
||||||
git status
|
|
||||||
git reset --hard origin/main
|
|
||||||
git log --oneline -5
|
|
||||||
|
|
||||||
echo "=== Repositorio actualizado ==="
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ETAPA 2: DEPLOYMENT (Leer del repositorio)
|
|
||||||
|
|
||||||
Ahora que tienes el repositorio actualizado, LEE el archivo:
|
|
||||||
|
|
||||||
**INSTRUCCIONES-DEPLOYMENT.md**
|
|
||||||
|
|
||||||
ubicado en la raiz del proyecto:
|
|
||||||
/home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/INSTRUCCIONES-DEPLOYMENT.md
|
|
||||||
|
|
||||||
Ese archivo contiene las instrucciones completas para:
|
|
||||||
- Restaurar configuraciones
|
|
||||||
- Recrear base de datos
|
|
||||||
- Build de backend y frontend
|
|
||||||
- Iniciar servicios PM2
|
|
||||||
- Validar deployment
|
|
||||||
- Rollback si algo falla
|
|
||||||
|
|
||||||
EJECUTA las instrucciones de ese archivo.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Si algo falla en la ETAPA 1, reporta:
|
|
||||||
1. El paso donde fallo (1.1, 1.2, etc.)
|
|
||||||
2. El comando exacto
|
|
||||||
3. El mensaje de error completo
|
|
||||||
|
|
||||||
=== FIN PROMPT ===
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Ultima actualizacion: 2025-12-18 v4.0*
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
# ============================================================================
|
|
||||||
# GAMILIT Backend - Production Environment Variables (EXAMPLE)
|
|
||||||
# ============================================================================
|
|
||||||
# INSTRUCCIONES:
|
|
||||||
# 1. Copiar este archivo a .env.production
|
|
||||||
# 2. Reemplazar todos los valores <...> con valores reales
|
|
||||||
# 3. Configurar como variables de entorno del sistema (recomendado)
|
|
||||||
# O usar archivo .env.production (asegurar permisos restrictivos)
|
|
||||||
|
|
||||||
# ==================== SERVER ====================
|
|
||||||
NODE_ENV=production
|
|
||||||
PORT=3006
|
|
||||||
API_PREFIX=api
|
|
||||||
|
|
||||||
# ==================== DATABASE ====================
|
|
||||||
# IMPORTANTE: Usar credenciales seguras en producción
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_NAME=gamilit_platform
|
|
||||||
DB_USER=gamilit_user
|
|
||||||
DB_PASSWORD=<PASSWORD_SEGURO_AQUI>
|
|
||||||
DB_SYNCHRONIZE=false
|
|
||||||
DB_LOGGING=false
|
|
||||||
|
|
||||||
# ==================== JWT ====================
|
|
||||||
# IMPORTANTE: NUNCA usar valores de ejemplo en producción
|
|
||||||
# Generar secret seguro con: openssl rand -base64 32
|
|
||||||
JWT_SECRET=<GENERAR_SECRET_SEGURO_AQUI>
|
|
||||||
JWT_EXPIRES_IN=15m
|
|
||||||
JWT_REFRESH_EXPIRES_IN=7d
|
|
||||||
|
|
||||||
# ==================== CORS ====================
|
|
||||||
# CRÍTICO: Configurar orígenes permitidos correctamente
|
|
||||||
#
|
|
||||||
# ============================================================================
|
|
||||||
# ADVERTENCIA: HEADERS CORS DUPLICADOS
|
|
||||||
# ============================================================================
|
|
||||||
# Si usas Nginx como proxy SSL, NO agregar headers CORS en Nginx.
|
|
||||||
# NestJS maneja CORS internamente en main.ts.
|
|
||||||
# Headers duplicados causan error:
|
|
||||||
# "The 'Access-Control-Allow-Origin' header contains multiple values"
|
|
||||||
#
|
|
||||||
# SOLUCION: Solo NestJS maneja CORS, Nginx solo hace proxy sin headers CORS.
|
|
||||||
# Ver: docs/95-guias-desarrollo/GUIA-CORS-PRODUCCION.md
|
|
||||||
# ============================================================================
|
|
||||||
#
|
|
||||||
# Servidor actual: 74.208.126.102
|
|
||||||
# Frontend puerto: 3005
|
|
||||||
#
|
|
||||||
# CONFIGURACION RECOMENDADA CON SSL:
|
|
||||||
# Incluye HTTPS y HTTP para compatibilidad durante transición
|
|
||||||
CORS_ORIGIN=https://74.208.126.102:3005,https://74.208.126.102,http://74.208.126.102:3005,http://74.208.126.102
|
|
||||||
ENABLE_CORS=true
|
|
||||||
ENABLE_SWAGGER=false
|
|
||||||
|
|
||||||
# ==================== LOGGING ====================
|
|
||||||
LOG_LEVEL=warn
|
|
||||||
LOG_TO_FILE=true
|
|
||||||
|
|
||||||
# ==================== RATE LIMITING ====================
|
|
||||||
RATE_LIMIT_TTL=60
|
|
||||||
RATE_LIMIT_MAX=100
|
|
||||||
|
|
||||||
# ==================== SESSION ====================
|
|
||||||
# Generar secret con: openssl rand -base64 32
|
|
||||||
SESSION_SECRET=<GENERAR_SECRET_SEGURO_AQUI>
|
|
||||||
SESSION_MAX_AGE=86400000
|
|
||||||
|
|
||||||
# ==================== EMAIL ====================
|
|
||||||
# Configurar si se usa envío de emails
|
|
||||||
EMAIL_FROM=noreply@gamilit.com
|
|
||||||
EMAIL_REPLY_TO=support@gamilit.com
|
|
||||||
|
|
||||||
# ==================== FRONTEND URL ====================
|
|
||||||
# URL del frontend para redirects, links en emails, etc.
|
|
||||||
# IMPORTANTE: Usar HTTPS si el servidor tiene SSL configurado
|
|
||||||
FRONTEND_URL=https://74.208.126.102:3005
|
|
||||||
|
|
||||||
# ==================== WEB PUSH NOTIFICATIONS (VAPID) ====================
|
|
||||||
# Web Push API nativo - No requiere servicios externos (Firebase, etc.)
|
|
||||||
# IMPORTANTE: Generar claves VAPID con el comando:
|
|
||||||
# npx web-push generate-vapid-keys
|
|
||||||
#
|
|
||||||
# VAPID (Voluntary Application Server Identification):
|
|
||||||
# - Public Key: Se comparte con el frontend para crear subscripciones
|
|
||||||
# - Private Key: Se mantiene secreta en backend para firmar notificaciones
|
|
||||||
# - Subject: Email de contacto o URL del sitio (mailto: o https:)
|
|
||||||
#
|
|
||||||
# Ejemplo de generación:
|
|
||||||
# $ npx web-push generate-vapid-keys
|
|
||||||
# Public Key: BN4GvZtEZiZuqaaObWga7lEP-S1WCv7L1c...
|
|
||||||
# Private Key: aB3cDefGh4IjKlM5nOpQr6StUvWxYz...
|
|
||||||
#
|
|
||||||
# Compatible con: Chrome, Firefox, Edge, Safari 16.4+
|
|
||||||
# NOTA: Si no se configuran, push notifications quedarán deshabilitadas (graceful degradation)
|
|
||||||
#
|
|
||||||
VAPID_PUBLIC_KEY=<GENERAR_CON_WEB_PUSH_GENERATE_VAPID_KEYS>
|
|
||||||
VAPID_PRIVATE_KEY=<GENERAR_CON_WEB_PUSH_GENERATE_VAPID_KEYS>
|
|
||||||
VAPID_SUBJECT=mailto:admin@gamilit.com
|
|
||||||
|
|
||||||
# ==================== OPCIONALES ====================
|
|
||||||
# Descomentar y configurar si se usan
|
|
||||||
|
|
||||||
# Uploads
|
|
||||||
# MAX_FILE_SIZE=5242880
|
|
||||||
# UPLOAD_DESTINATION=./uploads
|
|
||||||
# ALLOWED_MIME_TYPES=image/jpeg,image/png,image/gif,application/pdf
|
|
||||||
|
|
||||||
# Mantenimiento
|
|
||||||
# MAINTENANCE_MODE=false
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# NOTAS DE SEGURIDAD
|
|
||||||
# ============================================================================
|
|
||||||
#
|
|
||||||
# 1. JWT_SECRET: DEBE ser diferente al de desarrollo, usar 32+ caracteres aleatorios
|
|
||||||
# 2. DB_PASSWORD: NUNCA commitear credenciales reales al repositorio
|
|
||||||
# 3. SESSION_SECRET: Generar valor único y seguro
|
|
||||||
# 4. CORS_ORIGIN: Solo incluir orígenes confiables, NUNCA usar "*"
|
|
||||||
# 5. ENABLE_SWAGGER: DEBE estar en false en producción
|
|
||||||
# 6. Permisos archivo: chmod 600 .env.production (solo owner puede leer)
|
|
||||||
# 7. Logs: Verificar que LOG_TO_FILE no exponga datos sensibles
|
|
||||||
#
|
|
||||||
# ============================================================================
|
|
||||||
# VALIDACIÓN PRE-DEPLOY
|
|
||||||
# ============================================================================
|
|
||||||
#
|
|
||||||
# Antes de deploy, verificar:
|
|
||||||
# [ ] JWT_SECRET cambiado de valor de ejemplo
|
|
||||||
# [ ] DB_PASSWORD configurado correctamente
|
|
||||||
# [ ] CORS_ORIGIN incluye origen del frontend en producción
|
|
||||||
# [ ] ENABLE_SWAGGER=false
|
|
||||||
# [ ] FRONTEND_URL apunta a URL correcta
|
|
||||||
# [ ] Todas las variables <...> reemplazadas
|
|
||||||
# [ ] VAPID_* configuradas si se requieren push notifications (generar con npx web-push generate-vapid-keys)
|
|
||||||
#
|
|
||||||
# ============================================================================
|
|
||||||
6
projects/gamilit/apps/backend/.gitignore
vendored
6
projects/gamilit/apps/backend/.gitignore
vendored
@ -33,11 +33,13 @@ coverage/
|
|||||||
logs/
|
logs/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Uploads (keep directory structure but ignore files)
|
||||||
|
uploads/exercises/*
|
||||||
|
!uploads/exercises/.gitkeep
|
||||||
|
|
||||||
.env*
|
.env*
|
||||||
.flaskenv*
|
.flaskenv*
|
||||||
!.env.project
|
!.env.project
|
||||||
!.env.vault
|
!.env.vault
|
||||||
!.env.*.example
|
|
||||||
!.env.example
|
|
||||||
# Backups de scripts automáticos
|
# Backups de scripts automáticos
|
||||||
**/*.backup-*
|
**/*.backup-*
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, NotFoundException, BadRequestException, ConflictException, Logger } from '@nestjs/common';
|
import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, IsNull } from 'typeorm';
|
import { Repository, IsNull } from 'typeorm';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
@ -6,7 +6,6 @@ import { User, EmailVerificationToken } from '../entities';
|
|||||||
import {
|
import {
|
||||||
VerifyEmailDto,
|
VerifyEmailDto,
|
||||||
} from '../dto';
|
} from '../dto';
|
||||||
import { MailService } from '@/modules/mail/mail.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EmailVerificationService
|
* EmailVerificationService
|
||||||
@ -34,8 +33,6 @@ import { MailService } from '@/modules/mail/mail.service';
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailVerificationService {
|
export class EmailVerificationService {
|
||||||
private readonly logger = new Logger(EmailVerificationService.name);
|
|
||||||
|
|
||||||
private readonly TOKEN_LENGTH_BYTES = 32;
|
private readonly TOKEN_LENGTH_BYTES = 32;
|
||||||
|
|
||||||
private readonly TOKEN_EXPIRATION_HOURS = 24;
|
private readonly TOKEN_EXPIRATION_HOURS = 24;
|
||||||
@ -47,7 +44,8 @@ export class EmailVerificationService {
|
|||||||
@InjectRepository(EmailVerificationToken, 'auth')
|
@InjectRepository(EmailVerificationToken, 'auth')
|
||||||
private readonly tokenRepository: Repository<EmailVerificationToken>,
|
private readonly tokenRepository: Repository<EmailVerificationToken>,
|
||||||
|
|
||||||
private readonly mailService: MailService,
|
// TODO: Inject MailerService
|
||||||
|
// private readonly mailerService: MailerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,17 +89,9 @@ export class EmailVerificationService {
|
|||||||
await this.tokenRepository.save(verificationToken);
|
await this.tokenRepository.save(verificationToken);
|
||||||
|
|
||||||
// 8. Enviar email con token plaintext
|
// 8. Enviar email con token plaintext
|
||||||
try {
|
// TODO: Implementar envío de email
|
||||||
await this.mailService.sendVerificationEmail(email, plainToken);
|
// await this.mailerService.sendEmailVerification(email, plainToken);
|
||||||
this.logger.log(`Verification email sent to: ${email}`);
|
console.log(`[DEV] Email verification token for ${email}: ${plainToken}`);
|
||||||
} catch (error) {
|
|
||||||
// Log error pero no fallar (el token ya está creado)
|
|
||||||
this.logger.error(`Failed to send verification email to ${email}:`, error);
|
|
||||||
// En desarrollo, mostrar el token para testing
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
this.logger.debug(`[DEV] Verification token for ${email}: ${plainToken}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: 'Email de verificación enviado' };
|
return { message: 'Email de verificación enviado' };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common';
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, IsNull } from 'typeorm';
|
import { Repository, IsNull } from 'typeorm';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
@ -9,7 +9,6 @@ import {
|
|||||||
ResetPasswordDto,
|
ResetPasswordDto,
|
||||||
} from '../dto';
|
} from '../dto';
|
||||||
import { MailService } from '@/modules/mail/mail.service';
|
import { MailService } from '@/modules/mail/mail.service';
|
||||||
import { SessionManagementService } from './session-management.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PasswordRecoveryService
|
* PasswordRecoveryService
|
||||||
@ -34,8 +33,6 @@ import { SessionManagementService } from './session-management.service';
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PasswordRecoveryService {
|
export class PasswordRecoveryService {
|
||||||
private readonly logger = new Logger(PasswordRecoveryService.name);
|
|
||||||
|
|
||||||
private readonly TOKEN_LENGTH_BYTES = 32;
|
private readonly TOKEN_LENGTH_BYTES = 32;
|
||||||
|
|
||||||
private readonly TOKEN_EXPIRATION_HOURS = 1;
|
private readonly TOKEN_EXPIRATION_HOURS = 1;
|
||||||
@ -49,7 +46,8 @@ export class PasswordRecoveryService {
|
|||||||
|
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
|
|
||||||
private readonly sessionManagementService: SessionManagementService,
|
// TODO: Inject SessionManagementService for logout
|
||||||
|
// private readonly sessionService: SessionManagementService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,16 +90,14 @@ export class PasswordRecoveryService {
|
|||||||
// 7. Enviar email con token plaintext
|
// 7. Enviar email con token plaintext
|
||||||
try {
|
try {
|
||||||
await this.mailService.sendPasswordResetEmail(user.email, plainToken);
|
await this.mailService.sendPasswordResetEmail(user.email, plainToken);
|
||||||
this.logger.log(`Password reset email sent to: ${user.email}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log error pero NO fallar (por seguridad, no revelar errores)
|
// Log error pero NO fallar (por seguridad, no revelar errores)
|
||||||
this.logger.error(`Failed to send password reset email to ${user.email}:`, error);
|
console.error(`Failed to send password reset email to ${user.email}:`, error);
|
||||||
// En desarrollo, mostrar el token para testing
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
this.logger.debug(`[DEV] Password reset token for ${user.email}: ${plainToken}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback para desarrollo (si SMTP no configurado)
|
||||||
|
console.log(`[DEV] Password reset token for ${user.email}: ${plainToken}`);
|
||||||
|
|
||||||
return { message: genericMessage };
|
return { message: genericMessage };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,16 +159,10 @@ export class PasswordRecoveryService {
|
|||||||
{ used_at: new Date() },
|
{ used_at: new Date() },
|
||||||
);
|
);
|
||||||
|
|
||||||
// 6. Invalidar todas las sesiones (logout global de seguridad)
|
// 6. Invalidar todas las sesiones (logout global)
|
||||||
try {
|
// TODO: Implementar con SessionManagementService
|
||||||
// Revocar todas las sesiones excepto ninguna (currentSessionId vacío = revocar todas)
|
// await this.sessionService.revokeAllSessions(user.id);
|
||||||
// Usamos un UUID inexistente para asegurar que se cierren TODAS las sesiones
|
console.log(`[DEV] Should revoke all sessions for user ${user.id}`);
|
||||||
const result = await this.sessionManagementService.revokeAllSessions(user.id, '00000000-0000-0000-0000-000000000000');
|
|
||||||
this.logger.log(`Revoked ${result.count} sessions for user ${user.id} after password reset`);
|
|
||||||
} catch (error) {
|
|
||||||
// Log pero no fallar - la contraseña ya fue cambiada
|
|
||||||
this.logger.error(`Failed to revoke sessions for user ${user.id}:`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: 'Contraseña actualizada exitosamente' };
|
return { message: 'Contraseña actualizada exitosamente' };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, NotFoundException, BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common';
|
import { Injectable, NotFoundException, BadRequestException, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
|
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
|
||||||
import { Repository, EntityManager } from 'typeorm';
|
import { Repository, EntityManager } from 'typeorm';
|
||||||
import { ExerciseSubmission } from '../entities';
|
import { ExerciseSubmission } from '../entities';
|
||||||
@ -79,8 +79,6 @@ interface FragmentState {
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExerciseSubmissionService {
|
export class ExerciseSubmissionService {
|
||||||
private readonly logger = new Logger(ExerciseSubmissionService.name);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(ExerciseSubmission, 'progress')
|
@InjectRepository(ExerciseSubmission, 'progress')
|
||||||
private readonly submissionRepo: Repository<ExerciseSubmission>,
|
private readonly submissionRepo: Repository<ExerciseSubmission>,
|
||||||
@ -244,7 +242,7 @@ export class ExerciseSubmissionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[BE-P2-009] Diario multimedia validation passed: ${wordCount} palabras`);
|
console.log(`[BE-P2-009] Diario multimedia validation passed: ${wordCount} palabras`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exercise.exercise_type === 'comic_digital') {
|
if (exercise.exercise_type === 'comic_digital') {
|
||||||
@ -271,7 +269,7 @@ export class ExerciseSubmissionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[BE-P2-009] Comic digital validation passed: ${panels.length} paneles`);
|
console.log(`[BE-P2-009] Comic digital validation passed: ${panels.length} paneles`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exercise.exercise_type === 'video_carta') {
|
if (exercise.exercise_type === 'video_carta') {
|
||||||
@ -292,11 +290,11 @@ export class ExerciseSubmissionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[BE-P2-009] Video carta validation passed: ${videoUrl}`);
|
console.log(`[BE-P2-009] Video carta validation passed: ${videoUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FE-059: Validate answer structure BEFORE saving to database
|
// FE-059: Validate answer structure BEFORE saving to database
|
||||||
this.logger.log(`[FE-059] Validating answer structure for exercise type: ${exercise.exercise_type}`);
|
console.log(`[FE-059] Validating answer structure for exercise type: ${exercise.exercise_type}`);
|
||||||
await ExerciseAnswerValidator.validate(exercise.exercise_type, answers);
|
await ExerciseAnswerValidator.validate(exercise.exercise_type, answers);
|
||||||
|
|
||||||
// Verificar si ya existe un envío previo
|
// Verificar si ya existe un envío previo
|
||||||
@ -328,7 +326,7 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
// ✅ FIX BUG-001: Auto-claim rewards después de calificar
|
// ✅ FIX BUG-001: Auto-claim rewards después de calificar
|
||||||
if (submission.is_correct && submission.status === 'graded') {
|
if (submission.is_correct && submission.status === 'graded') {
|
||||||
this.logger.log(`[BUG-001 FIX] Auto-claiming rewards for submission ${submission.id}`);
|
console.log(`[BUG-001 FIX] Auto-claiming rewards for submission ${submission.id}`);
|
||||||
const rewards = await this.claimRewards(submission.id);
|
const rewards = await this.claimRewards(submission.id);
|
||||||
|
|
||||||
// Los campos ya están persistidos en la submission por claimRewards()
|
// Los campos ya están persistidos en la submission por claimRewards()
|
||||||
@ -338,13 +336,13 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
// BE-P2-008: Notificar al docente si el ejercicio requiere revisión manual
|
// BE-P2-008: Notificar al docente si el ejercicio requiere revisión manual
|
||||||
if (exercise.requires_manual_grading) {
|
if (exercise.requires_manual_grading) {
|
||||||
this.logger.log(`[BE-P2-008] Exercise ${exerciseId} requires manual grading - notifying teacher`);
|
console.log(`[BE-P2-008] Exercise ${exerciseId} requires manual grading - notifying teacher`);
|
||||||
try {
|
try {
|
||||||
await this.notifyTeacherOfSubmission(submission, exercise, profileId);
|
await this.notifyTeacherOfSubmission(submission, exercise, profileId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log error but don't fail submission
|
// Log error but don't fail submission
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
this.logger.error(`[BE-P2-008] Failed to notify teacher: ${errorMessage}`);
|
console.error(`[BE-P2-008] Failed to notify teacher: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,7 +371,7 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
// P1-003: Check if manual grading is requested
|
// P1-003: Check if manual grading is requested
|
||||||
if (manualGrade?.final_score !== undefined) {
|
if (manualGrade?.final_score !== undefined) {
|
||||||
this.logger.log(`[P1-003] Manual grading requested: score=${manualGrade.final_score}, grader=${manualGrade.grader_id}`);
|
console.log(`[P1-003] Manual grading requested: score=${manualGrade.final_score}, grader=${manualGrade.grader_id}`);
|
||||||
|
|
||||||
// Validate manual score range
|
// Validate manual score range
|
||||||
if (manualGrade.final_score < 0 || manualGrade.final_score > submission.max_score) {
|
if (manualGrade.final_score < 0 || manualGrade.final_score > submission.max_score) {
|
||||||
@ -400,7 +398,7 @@ export class ExerciseSubmissionService {
|
|||||||
submission.feedback = `Calificación manual: ${manualGrade.final_score}/${submission.max_score}`;
|
submission.feedback = `Calificación manual: ${manualGrade.final_score}/${submission.max_score}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[P1-003] Manual grading applied: ${submission.score}/${submission.max_score}, correct=${submission.is_correct}`);
|
console.log(`[P1-003] Manual grading applied: ${submission.score}/${submission.max_score}, correct=${submission.is_correct}`);
|
||||||
|
|
||||||
const savedSubmission = await this.submissionRepo.save(submission);
|
const savedSubmission = await this.submissionRepo.save(submission);
|
||||||
|
|
||||||
@ -408,17 +406,17 @@ export class ExerciseSubmissionService {
|
|||||||
try {
|
try {
|
||||||
const earned = await this.achievementsService.detectAndGrantEarned(submission.user_id);
|
const earned = await this.achievementsService.detectAndGrantEarned(submission.user_id);
|
||||||
if (earned.length > 0) {
|
if (earned.length > 0) {
|
||||||
this.logger.log(`[IMPL-004] ✅ Granted ${earned.length} achievements to user ${submission.user_id} after manual grading`);
|
console.log(`[IMPL-004] ✅ Granted ${earned.length} achievements to user ${submission.user_id} after manual grading`);
|
||||||
}
|
}
|
||||||
} catch (achievementError) {
|
} catch (achievementError) {
|
||||||
this.logger.error(`[IMPL-004] ❌ Error detecting achievements: ${achievementError instanceof Error ? achievementError.message : String(achievementError)}`);
|
console.error(`[IMPL-004] ❌ Error detecting achievements: ${achievementError instanceof Error ? achievementError.message : String(achievementError)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return savedSubmission;
|
return savedSubmission;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: Auto-grading using SQL validate_and_audit()
|
// Default: Auto-grading using SQL validate_and_audit()
|
||||||
this.logger.log('[P1-003] No manual score provided - executing auto-grading');
|
console.log('[P1-003] No manual score provided - executing auto-grading');
|
||||||
|
|
||||||
const { score, isCorrect, correctAnswers, totalQuestions, feedback, details, auditId } = await this.autoGrade(
|
const { score, isCorrect, correctAnswers, totalQuestions, feedback, details, auditId } = await this.autoGrade(
|
||||||
submission.user_id, // userId (profiles.id)
|
submission.user_id, // userId (profiles.id)
|
||||||
@ -435,7 +433,7 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
// FE-059: Audit ID is stored in educational_content.exercise_validation_audit
|
// FE-059: Audit ID is stored in educational_content.exercise_validation_audit
|
||||||
// Can be queried using: exercise_id + user_id + attempt_number
|
// Can be queried using: exercise_id + user_id + attempt_number
|
||||||
this.logger.log(`[FE-059] Validation audit saved with ID: ${auditId}`);
|
console.log(`[FE-059] Validation audit saved with ID: ${auditId}`);
|
||||||
|
|
||||||
// Store validation results in submission
|
// Store validation results in submission
|
||||||
(submission as any).correctAnswers = correctAnswers;
|
(submission as any).correctAnswers = correctAnswers;
|
||||||
@ -468,10 +466,10 @@ export class ExerciseSubmissionService {
|
|||||||
try {
|
try {
|
||||||
const earned = await this.achievementsService.detectAndGrantEarned(submission.user_id);
|
const earned = await this.achievementsService.detectAndGrantEarned(submission.user_id);
|
||||||
if (earned.length > 0) {
|
if (earned.length > 0) {
|
||||||
this.logger.log(`[IMPL-004] ✅ Granted ${earned.length} achievements to user ${submission.user_id} after auto-grading`);
|
console.log(`[IMPL-004] ✅ Granted ${earned.length} achievements to user ${submission.user_id} after auto-grading`);
|
||||||
}
|
}
|
||||||
} catch (achievementError) {
|
} catch (achievementError) {
|
||||||
this.logger.error(`[IMPL-004] ❌ Error detecting achievements: ${achievementError instanceof Error ? achievementError.message : String(achievementError)}`);
|
console.error(`[IMPL-004] ❌ Error detecting achievements: ${achievementError instanceof Error ? achievementError.message : String(achievementError)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return savedSubmission;
|
return savedSubmission;
|
||||||
@ -514,7 +512,7 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
// SPECIAL CASE: Completar Espacios - Anti-redundancy validation (Exercise 1.3)
|
// SPECIAL CASE: Completar Espacios - Anti-redundancy validation (Exercise 1.3)
|
||||||
if (exercise.exercise_type === 'completar_espacios') {
|
if (exercise.exercise_type === 'completar_espacios') {
|
||||||
this.logger.log('[autoGrade] Checking anti-redundancy for Completar Espacios (Exercise 1.3)');
|
console.log('[autoGrade] Checking anti-redundancy for Completar Espacios (Exercise 1.3)');
|
||||||
|
|
||||||
// Check if blanks.5 and blanks.6 exist and are identical (case-insensitive)
|
// Check if blanks.5 and blanks.6 exist and are identical (case-insensitive)
|
||||||
const blanks = (answerData.blanks || {}) as Record<string, unknown>;
|
const blanks = (answerData.blanks || {}) as Record<string, unknown>;
|
||||||
@ -523,7 +521,7 @@ export class ExerciseSubmissionService {
|
|||||||
const space6 = String(blanks['6']).toLowerCase().trim();
|
const space6 = String(blanks['6']).toLowerCase().trim();
|
||||||
|
|
||||||
if (space5 === space6) {
|
if (space5 === space6) {
|
||||||
this.logger.log(`[autoGrade] REDUNDANCY DETECTED: space5="${space5}" === space6="${space6}"`);
|
console.log(`[autoGrade] REDUNDANCY DETECTED: space5="${space5}" === space6="${space6}"`);
|
||||||
|
|
||||||
// Create audit record for failed validation
|
// Create audit record for failed validation
|
||||||
const auditId = 'redundancy-' + Date.now();
|
const auditId = 'redundancy-' + Date.now();
|
||||||
@ -547,12 +545,12 @@ export class ExerciseSubmissionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log('[autoGrade] Anti-redundancy check passed, proceeding with normal validation');
|
console.log('[autoGrade] Anti-redundancy check passed, proceeding with normal validation');
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPECIAL CASE: Rueda de Inferencias custom validation
|
// SPECIAL CASE: Rueda de Inferencias custom validation
|
||||||
if (exercise.exercise_type === 'rueda_inferencias') {
|
if (exercise.exercise_type === 'rueda_inferencias') {
|
||||||
this.logger.log('[autoGrade] Using custom validation for Rueda de Inferencias');
|
console.log('[autoGrade] Using custom validation for Rueda de Inferencias');
|
||||||
|
|
||||||
// Extract fragmentStates from answerData if available
|
// Extract fragmentStates from answerData if available
|
||||||
const fragmentStates = answerData.fragmentStates as FragmentState[] | undefined;
|
const fragmentStates = answerData.fragmentStates as FragmentState[] | undefined;
|
||||||
@ -585,7 +583,7 @@ export class ExerciseSubmissionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DEFAULT CASE: Use SQL validate_and_audit() for other exercise types
|
// DEFAULT CASE: Use SQL validate_and_audit() for other exercise types
|
||||||
this.logger.log(`[FE-059] Validating exercise ${exerciseId} using SQL validate_and_audit()`);
|
console.log(`[FE-059] Validating exercise ${exerciseId} using SQL validate_and_audit()`);
|
||||||
|
|
||||||
// Call PostgreSQL validate_and_audit() function
|
// Call PostgreSQL validate_and_audit() function
|
||||||
const query = `
|
const query = `
|
||||||
@ -613,7 +611,7 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
const validation = result[0];
|
const validation = result[0];
|
||||||
|
|
||||||
this.logger.log(`[FE-059] Validation result: score=${validation.score}/${validation.max_score}, correct=${validation.is_correct}, audit_id=${validation.audit_id}`);
|
console.log(`[FE-059] Validation result: score=${validation.score}/${validation.max_score}, correct=${validation.is_correct}, audit_id=${validation.audit_id}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
score: validation.score,
|
score: validation.score,
|
||||||
@ -625,7 +623,7 @@ export class ExerciseSubmissionService {
|
|||||||
auditId: validation.audit_id,
|
auditId: validation.audit_id,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('[FE-059] Error calling validate_and_audit():', error);
|
console.error('[FE-059] Error calling validate_and_audit():', error);
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
throw new InternalServerErrorException(`Exercise validation failed: ${errorMessage}`);
|
throw new InternalServerErrorException(`Exercise validation failed: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
@ -782,7 +780,7 @@ export class ExerciseSubmissionService {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
} {
|
} {
|
||||||
this.logger.log('[validateRuedaInferencias] Starting validation for Rueda de Inferencias exercise');
|
console.log('[validateRuedaInferencias] Starting validation for Rueda de Inferencias exercise');
|
||||||
|
|
||||||
// Cast solution to ExerciseSolution interface
|
// Cast solution to ExerciseSolution interface
|
||||||
const solution = exercise.solution as unknown as ExerciseSolution;
|
const solution = exercise.solution as unknown as ExerciseSolution;
|
||||||
@ -818,7 +816,7 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
// Skip if no answer provided
|
// Skip if no answer provided
|
||||||
if (!userAnswer) {
|
if (!userAnswer) {
|
||||||
this.logger.log(`[validateRuedaInferencias] No answer provided for fragment ${fragment.id}, skipping`);
|
console.log(`[validateRuedaInferencias] No answer provided for fragment ${fragment.id}, skipping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -832,14 +830,14 @@ export class ExerciseSubmissionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[validateRuedaInferencias] Fragment ${fragment.id} using category: ${categoryId}`);
|
console.log(`[validateRuedaInferencias] Fragment ${fragment.id} using category: ${categoryId}`);
|
||||||
|
|
||||||
// Get expectations for this category (with type safety)
|
// Get expectations for this category (with type safety)
|
||||||
type CategoryId = 'cat-literal' | 'cat-inferencial' | 'cat-critico' | 'cat-creativo';
|
type CategoryId = 'cat-literal' | 'cat-inferencial' | 'cat-critico' | 'cat-creativo';
|
||||||
let categoryExpectation = fragment.categoryExpectations?.[categoryId as CategoryId];
|
let categoryExpectation = fragment.categoryExpectations?.[categoryId as CategoryId];
|
||||||
|
|
||||||
if (!categoryExpectation) {
|
if (!categoryExpectation) {
|
||||||
this.logger.warn(`[validateRuedaInferencias] No expectations found for category ${categoryId} in fragment ${fragment.id}, using default`);
|
console.warn(`[validateRuedaInferencias] No expectations found for category ${categoryId} in fragment ${fragment.id}, using default`);
|
||||||
// Fallback: use literal category if available
|
// Fallback: use literal category if available
|
||||||
categoryExpectation = fragment.categoryExpectations?.['cat-literal'];
|
categoryExpectation = fragment.categoryExpectations?.['cat-literal'];
|
||||||
if (!categoryExpectation) {
|
if (!categoryExpectation) {
|
||||||
@ -849,7 +847,7 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
// Validate categoryExpectation structure
|
// Validate categoryExpectation structure
|
||||||
if (!categoryExpectation.keywords || !Array.isArray(categoryExpectation.keywords)) {
|
if (!categoryExpectation.keywords || !Array.isArray(categoryExpectation.keywords)) {
|
||||||
this.logger.warn(`[validateRuedaInferencias] Invalid category expectation for ${categoryId} in fragment ${fragment.id}`);
|
console.warn(`[validateRuedaInferencias] Invalid category expectation for ${categoryId} in fragment ${fragment.id}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -863,7 +861,7 @@ export class ExerciseSubmissionService {
|
|||||||
userAnswerLower.includes(keyword.toLowerCase()),
|
userAnswerLower.includes(keyword.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`[validateRuedaInferencias] Fragment ${fragment.id}: Found ${foundKeywords.length}/${expectedKeywords.length} keywords`);
|
console.log(`[validateRuedaInferencias] Fragment ${fragment.id}: Found ${foundKeywords.length}/${expectedKeywords.length} keywords`);
|
||||||
|
|
||||||
// Calculate score based on keywords found
|
// Calculate score based on keywords found
|
||||||
let fragmentScore = 0;
|
let fragmentScore = 0;
|
||||||
@ -912,7 +910,7 @@ export class ExerciseSubmissionService {
|
|||||||
overallFeedback = 'Necesitas practicar más. Revisa las categorías de inferencias y los ejemplos proporcionados.';
|
overallFeedback = 'Necesitas practicar más. Revisa las categorías de inferencias y los ejemplos proporcionados.';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[validateRuedaInferencias] Validation complete: ${totalScore}/${maxScore} points (${overallPercentage.toFixed(1)}%)`);
|
console.log(`[validateRuedaInferencias] Validation complete: ${totalScore}/${maxScore} points (${overallPercentage.toFixed(1)}%)`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
score: totalScore,
|
score: totalScore,
|
||||||
@ -975,7 +973,7 @@ export class ExerciseSubmissionService {
|
|||||||
let xpEarned = Math.floor(baseXpReward * scoreMultiplier * rankMultiplier);
|
let xpEarned = Math.floor(baseXpReward * scoreMultiplier * rankMultiplier);
|
||||||
let mlCoinsEarned = Math.floor(baseMlCoinsReward * scoreMultiplier);
|
let mlCoinsEarned = Math.floor(baseMlCoinsReward * scoreMultiplier);
|
||||||
|
|
||||||
this.logger.log(`[claimRewards] XP calculation: base=${baseXpReward}, score=${scoreMultiplier.toFixed(2)}, rank=${rankMultiplier}x, total=${xpEarned}`);
|
console.log(`[claimRewards] XP calculation: base=${baseXpReward}, score=${scoreMultiplier.toFixed(2)}, rank=${rankMultiplier}x, total=${xpEarned}`);
|
||||||
|
|
||||||
// Bonificación por perfect score
|
// Bonificación por perfect score
|
||||||
if (submission.score === submission.max_score && !submission.hint_used) {
|
if (submission.score === submission.max_score && !submission.hint_used) {
|
||||||
@ -991,7 +989,7 @@ export class ExerciseSubmissionService {
|
|||||||
mlCoinsEarned = Math.max(0, mlCoinsEarned - submission.ml_coins_spent);
|
mlCoinsEarned = Math.max(0, mlCoinsEarned - submission.ml_coins_spent);
|
||||||
|
|
||||||
// ✅ FIX BUG-001: Actualizar user_stats con XP y ML Coins
|
// ✅ FIX BUG-001: Actualizar user_stats con XP y ML Coins
|
||||||
this.logger.log(`[BUG-001 FIX] Claiming rewards for user ${submission.user_id}: +${xpEarned} XP, +${mlCoinsEarned} ML Coins`);
|
console.log(`[BUG-001 FIX] Claiming rewards for user ${submission.user_id}: +${xpEarned} XP, +${mlCoinsEarned} ML Coins`);
|
||||||
|
|
||||||
// Obtener rango ANTES de agregar XP
|
// Obtener rango ANTES de agregar XP
|
||||||
const userStatsBefore = await this.userStatsService.findByUserId(submission.user_id);
|
const userStatsBefore = await this.userStatsService.findByUserId(submission.user_id);
|
||||||
@ -1050,7 +1048,7 @@ export class ExerciseSubmissionService {
|
|||||||
newMultiplier: rankMultipliers[newRank] || 1.0,
|
newMultiplier: rankMultipliers[newRank] || 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.log(`[RANK UP] User ${submission.user_id} promoted from ${previousRank} to ${newRank}`);
|
console.log(`[RANK UP] User ${submission.user_id} promoted from ${previousRank} to ${newRank}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ FIX BUG-002: Actualizar module_progress después de completar ejercicio
|
// ✅ FIX BUG-002: Actualizar module_progress después de completar ejercicio
|
||||||
@ -1109,7 +1107,7 @@ export class ExerciseSubmissionService {
|
|||||||
|
|
||||||
return 1.00; // Default si no encuentra
|
return 1.00; // Default si no encuentra
|
||||||
} catch {
|
} catch {
|
||||||
this.logger.warn(`[getRankXpMultiplier] Error getting multiplier for user ${userId}, using 1.00`);
|
console.warn(`[getRankXpMultiplier] Error getting multiplier for user ${userId}, using 1.00`);
|
||||||
return 1.00;
|
return 1.00;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1135,12 +1133,12 @@ export class ExerciseSubmissionService {
|
|||||||
// Obtener module_id del ejercicio
|
// Obtener module_id del ejercicio
|
||||||
const exercise = await this.exerciseRepo.findOne({ where: { id: exerciseId } });
|
const exercise = await this.exerciseRepo.findOne({ where: { id: exerciseId } });
|
||||||
if (!exercise?.module_id) {
|
if (!exercise?.module_id) {
|
||||||
this.logger.warn(`[BUG-002 FIX] Exercise ${exerciseId} has no module_id - skipping progress update`);
|
console.warn(`[BUG-002 FIX] Exercise ${exerciseId} has no module_id - skipping progress update`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleId = exercise.module_id;
|
const moduleId = exercise.module_id;
|
||||||
this.logger.log(`[BUG-002 FIX] Updating module progress for user ${userId}, module ${moduleId}`);
|
console.log(`[BUG-002 FIX] Updating module progress for user ${userId}, module ${moduleId}`);
|
||||||
|
|
||||||
// Verificar si es el primer envío correcto para ESTE ejercicio específico
|
// Verificar si es el primer envío correcto para ESTE ejercicio específico
|
||||||
const previousCorrectSubmissions = await this.submissionRepo.count({
|
const previousCorrectSubmissions = await this.submissionRepo.count({
|
||||||
@ -1158,7 +1156,7 @@ export class ExerciseSubmissionService {
|
|||||||
SET last_accessed_at = NOW(), updated_at = NOW()
|
SET last_accessed_at = NOW(), updated_at = NOW()
|
||||||
WHERE user_id = $1 AND module_id = $2
|
WHERE user_id = $1 AND module_id = $2
|
||||||
`, [userId, moduleId]);
|
`, [userId, moduleId]);
|
||||||
this.logger.log('[BUG-002 FIX] Not first correct submission - only updated timestamps');
|
console.log('[BUG-002 FIX] Not first correct submission - only updated timestamps');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1194,7 +1192,7 @@ export class ExerciseSubmissionService {
|
|||||||
newStatus = 'not_started';
|
newStatus = 'not_started';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[BUG-002 FIX] Module progress: ${completedExercises}/${totalExercises} (${progressPercentage}%) - Status: ${newStatus}`);
|
console.log(`[BUG-002 FIX] Module progress: ${completedExercises}/${totalExercises} (${progressPercentage}%) - Status: ${newStatus}`);
|
||||||
|
|
||||||
// Actualizar o insertar module_progress usando UPSERT
|
// Actualizar o insertar module_progress usando UPSERT
|
||||||
await this.entityManager.query(`
|
await this.entityManager.query(`
|
||||||
@ -1230,12 +1228,12 @@ export class ExerciseSubmissionService {
|
|||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
`, [userId, moduleId, newStatus, progressPercentage, completedExercises, totalExercises, xpEarned, mlCoinsEarned]);
|
`, [userId, moduleId, newStatus, progressPercentage, completedExercises, totalExercises, xpEarned, mlCoinsEarned]);
|
||||||
|
|
||||||
this.logger.log('[BUG-002 FIX] ✅ Module progress updated successfully');
|
console.log('[BUG-002 FIX] ✅ Module progress updated successfully');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log error pero no bloquear el claim de rewards
|
// Log error pero no bloquear el claim de rewards
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
this.logger.error(`[BUG-002 FIX] ❌ Error updating module progress: ${errorMessage}`);
|
console.error(`[BUG-002 FIX] ❌ Error updating module progress: ${errorMessage}`);
|
||||||
// No throw - la actualización de progreso no debe bloquear la respuesta
|
// No throw - la actualización de progreso no debe bloquear la respuesta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1254,7 +1252,7 @@ export class ExerciseSubmissionService {
|
|||||||
*/
|
*/
|
||||||
private async updateMissionsProgressAfterCompletion(userId: string, xpEarned: number = 0): Promise<void> {
|
private async updateMissionsProgressAfterCompletion(userId: string, xpEarned: number = 0): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.logger.log(`[BUG-003 FIX] Updating missions progress for user ${userId}`);
|
console.log(`[BUG-003 FIX] Updating missions progress for user ${userId}`);
|
||||||
|
|
||||||
// Buscar misiones activas del usuario con objetivo 'complete_exercises'
|
// Buscar misiones activas del usuario con objetivo 'complete_exercises'
|
||||||
const missions = await this.missionsService.findByTypeAndUser(userId, MissionTypeEnum.DAILY);
|
const missions = await this.missionsService.findByTypeAndUser(userId, MissionTypeEnum.DAILY);
|
||||||
@ -1269,7 +1267,7 @@ export class ExerciseSubmissionService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (activeMissions.length === 0) {
|
if (activeMissions.length === 0) {
|
||||||
this.logger.log('[BUG-003 FIX] No active missions with \'complete_exercises\' objective found');
|
console.log('[BUG-003 FIX] No active missions with \'complete_exercises\' objective found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1282,11 +1280,11 @@ export class ExerciseSubmissionService {
|
|||||||
'complete_exercises',
|
'complete_exercises',
|
||||||
1, // Incrementar en 1 por cada ejercicio completado
|
1, // Incrementar en 1 por cada ejercicio completado
|
||||||
);
|
);
|
||||||
this.logger.log(`[BUG-003 FIX] ✅ Mission ${mission.id} (complete_exercises) updated`);
|
console.log(`[BUG-003 FIX] ✅ Mission ${mission.id} (complete_exercises) updated`);
|
||||||
} catch (missionError) {
|
} catch (missionError) {
|
||||||
// Log pero continuar con otras misiones
|
// Log pero continuar con otras misiones
|
||||||
const errorMessage = missionError instanceof Error ? missionError.message : String(missionError);
|
const errorMessage = missionError instanceof Error ? missionError.message : String(missionError);
|
||||||
this.logger.warn(`[BUG-003 FIX] ⚠️ Error updating mission ${mission.id}: ${errorMessage}`);
|
console.warn(`[BUG-003 FIX] ⚠️ Error updating mission ${mission.id}: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1306,20 +1304,20 @@ export class ExerciseSubmissionService {
|
|||||||
'earn_xp',
|
'earn_xp',
|
||||||
xpEarned, // Incrementar por cantidad de XP ganado
|
xpEarned, // Incrementar por cantidad de XP ganado
|
||||||
);
|
);
|
||||||
this.logger.log(`[BUG-003 FIX] ✅ Mission ${mission.id} (earn_xp) updated with +${xpEarned} XP`);
|
console.log(`[BUG-003 FIX] ✅ Mission ${mission.id} (earn_xp) updated with +${xpEarned} XP`);
|
||||||
} catch (missionError) {
|
} catch (missionError) {
|
||||||
const errorMessage = missionError instanceof Error ? missionError.message : String(missionError);
|
const errorMessage = missionError instanceof Error ? missionError.message : String(missionError);
|
||||||
this.logger.warn(`[BUG-003 FIX] ⚠️ Error updating earn_xp mission ${mission.id}: ${errorMessage}`);
|
console.warn(`[BUG-003 FIX] ⚠️ Error updating earn_xp mission ${mission.id}: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log('[BUG-003 FIX] ✅ Missions progress update completed');
|
console.log('[BUG-003 FIX] ✅ Missions progress update completed');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log error pero no bloquear el claim de rewards
|
// Log error pero no bloquear el claim de rewards
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
this.logger.error(`[BUG-003 FIX] ❌ Error updating missions progress: ${errorMessage}`);
|
console.error(`[BUG-003 FIX] ❌ Error updating missions progress: ${errorMessage}`);
|
||||||
// No throw - la actualización de misiones no debe bloquear la respuesta
|
// No throw - la actualización de misiones no debe bloquear la respuesta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1488,7 +1486,7 @@ export class ExerciseSubmissionService {
|
|||||||
exercise: Exercise,
|
exercise: Exercise,
|
||||||
studentProfileId: string,
|
studentProfileId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.log(`[BE-P2-008] Notifying teacher about submission ${submission.id}`);
|
console.log(`[BE-P2-008] Notifying teacher about submission ${submission.id}`);
|
||||||
|
|
||||||
// 1. Obtener datos del estudiante
|
// 1. Obtener datos del estudiante
|
||||||
const studentProfile = await this.profileRepo.findOne({
|
const studentProfile = await this.profileRepo.findOne({
|
||||||
@ -1497,7 +1495,7 @@ export class ExerciseSubmissionService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!studentProfile) {
|
if (!studentProfile) {
|
||||||
this.logger.warn(`[BE-P2-008] Student profile ${studentProfileId} not found - skipping notification`);
|
console.warn(`[BE-P2-008] Student profile ${studentProfileId} not found - skipping notification`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1526,7 +1524,7 @@ export class ExerciseSubmissionService {
|
|||||||
const teacherResult = await this.entityManager.query(teacherQuery, [studentProfileId]);
|
const teacherResult = await this.entityManager.query(teacherQuery, [studentProfileId]);
|
||||||
|
|
||||||
if (!teacherResult || teacherResult.length === 0) {
|
if (!teacherResult || teacherResult.length === 0) {
|
||||||
this.logger.warn(`[BE-P2-008] No active teacher found for student ${studentProfileId} - skipping notification`);
|
console.warn(`[BE-P2-008] No active teacher found for student ${studentProfileId} - skipping notification`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1537,7 +1535,7 @@ export class ExerciseSubmissionService {
|
|||||||
const classroomName = teacher.classroom_name || 'tu aula';
|
const classroomName = teacher.classroom_name || 'tu aula';
|
||||||
const teacherPreferences = teacher.teacher_preferences || {};
|
const teacherPreferences = teacher.teacher_preferences || {};
|
||||||
|
|
||||||
this.logger.log(`[BE-P2-008] Found teacher ${teacherId} (${teacherEmail}) for student ${studentProfileId}`);
|
console.log(`[BE-P2-008] Found teacher ${teacherId} (${teacherEmail}) for student ${studentProfileId}`);
|
||||||
|
|
||||||
// 3. Construir URL de revisión
|
// 3. Construir URL de revisión
|
||||||
const reviewUrl = `/teacher/reviews/${submission.id}`;
|
const reviewUrl = `/teacher/reviews/${submission.id}`;
|
||||||
@ -1567,10 +1565,10 @@ export class ExerciseSubmissionService {
|
|||||||
priority: 'high',
|
priority: 'high',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`[BE-P2-008] ✅ In-app notification sent to teacher ${teacherId}`);
|
console.log(`[BE-P2-008] ✅ In-app notification sent to teacher ${teacherId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
this.logger.error(`[BE-P2-008] ❌ Failed to send in-app notification: ${errorMessage}`);
|
console.error(`[BE-P2-008] ❌ Failed to send in-app notification: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Enviar email si está habilitado en preferencias
|
// 5. Enviar email si está habilitado en preferencias
|
||||||
@ -1578,7 +1576,7 @@ export class ExerciseSubmissionService {
|
|||||||
const exerciseFeedbackEmailEnabled = teacherPreferences?.email_notifications?.exercise_feedback !== false;
|
const exerciseFeedbackEmailEnabled = teacherPreferences?.email_notifications?.exercise_feedback !== false;
|
||||||
|
|
||||||
if (emailNotificationsEnabled && exerciseFeedbackEmailEnabled && teacherEmail) {
|
if (emailNotificationsEnabled && exerciseFeedbackEmailEnabled && teacherEmail) {
|
||||||
this.logger.log(`[BE-P2-008] Email notifications enabled for teacher ${teacherId} - sending email to ${teacherEmail}`);
|
console.log(`[BE-P2-008] Email notifications enabled for teacher ${teacherId} - sending email to ${teacherEmail}`);
|
||||||
|
|
||||||
const emailSubject = `Nuevo ejercicio para revisar: ${exercise.title}`;
|
const emailSubject = `Nuevo ejercicio para revisar: ${exercise.title}`;
|
||||||
const emailMessage = `
|
const emailMessage = `
|
||||||
@ -1599,19 +1597,19 @@ export class ExerciseSubmissionService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (emailSent) {
|
if (emailSent) {
|
||||||
this.logger.log(`[BE-P2-008] ✅ Email notification sent to teacher ${teacherEmail}`);
|
console.log(`[BE-P2-008] ✅ Email notification sent to teacher ${teacherEmail}`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`[BE-P2-008] ⚠️ Email notification logged but not sent (SMTP not configured)`);
|
console.warn(`[BE-P2-008] ⚠️ Email notification logged but not sent (SMTP not configured)`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
this.logger.error(`[BE-P2-008] ❌ Failed to send email notification: ${errorMessage}`);
|
console.error(`[BE-P2-008] ❌ Failed to send email notification: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger.log(`[BE-P2-008] Email notifications disabled for teacher ${teacherId} - skipping email`);
|
console.log(`[BE-P2-008] Email notifications disabled for teacher ${teacherId} - skipping email`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[BE-P2-008] ✅ Teacher notification process completed for submission ${submission.id}`);
|
console.log(`[BE-P2-008] ✅ Teacher notification process completed for submission ${submission.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -578,158 +578,6 @@ export class AnalyticsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// P1-08: CACHE INVALIDATION METHODS (2025-12-18)
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate economy analytics cache for a teacher
|
|
||||||
*
|
|
||||||
* Call this when:
|
|
||||||
* - Student earns/spends ML Coins
|
|
||||||
* - New student joins classroom
|
|
||||||
* - Student removed from classroom
|
|
||||||
*
|
|
||||||
* @param teacherId - Teacher's user ID
|
|
||||||
* @param classroomId - Optional specific classroom ID
|
|
||||||
*/
|
|
||||||
async invalidateEconomyAnalyticsCache(teacherId: string, classroomId?: string): Promise<void> {
|
|
||||||
const cacheKeys = [
|
|
||||||
`economy-analytics:${teacherId}:all`,
|
|
||||||
`students-economy:${teacherId}:all`,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (classroomId) {
|
|
||||||
cacheKeys.push(`economy-analytics:${teacherId}:${classroomId}`);
|
|
||||||
cacheKeys.push(`students-economy:${teacherId}:${classroomId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(cacheKeys.map(key => this.cacheManager.del(key)));
|
|
||||||
this.logger.debug(`Invalidated economy cache for teacher ${teacherId}${classroomId ? `, classroom ${classroomId}` : ''}`);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.warn(`Error invalidating economy cache: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate achievements stats cache for a teacher
|
|
||||||
*
|
|
||||||
* Call this when:
|
|
||||||
* - Student unlocks achievement
|
|
||||||
* - Achievement configuration changes
|
|
||||||
* - Student joins/leaves classroom
|
|
||||||
*
|
|
||||||
* @param teacherId - Teacher's user ID
|
|
||||||
* @param classroomId - Optional specific classroom ID
|
|
||||||
*/
|
|
||||||
async invalidateAchievementsStatsCache(teacherId: string, classroomId?: string): Promise<void> {
|
|
||||||
const cacheKeys = [
|
|
||||||
`achievements-stats:${teacherId}:all`,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (classroomId) {
|
|
||||||
cacheKeys.push(`achievements-stats:${teacherId}:${classroomId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(cacheKeys.map(key => this.cacheManager.del(key)));
|
|
||||||
this.logger.debug(`Invalidated achievements cache for teacher ${teacherId}${classroomId ? `, classroom ${classroomId}` : ''}`);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.warn(`Error invalidating achievements cache: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate all analytics caches for a teacher
|
|
||||||
*
|
|
||||||
* Call this on major data changes that affect multiple analytics:
|
|
||||||
* - New submission completed
|
|
||||||
* - Score/grade updated
|
|
||||||
* - Bulk student changes
|
|
||||||
*
|
|
||||||
* @param teacherId - Teacher's user ID
|
|
||||||
* @param studentId - Optional student whose insights should be invalidated
|
|
||||||
* @param classroomId - Optional specific classroom ID
|
|
||||||
*/
|
|
||||||
async invalidateAllAnalyticsCache(
|
|
||||||
teacherId: string,
|
|
||||||
studentId?: string,
|
|
||||||
classroomId?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const invalidations = [
|
|
||||||
this.invalidateEconomyAnalyticsCache(teacherId, classroomId),
|
|
||||||
this.invalidateAchievementsStatsCache(teacherId, classroomId),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (studentId) {
|
|
||||||
invalidations.push(this.invalidateStudentInsightsCache(studentId));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(invalidations);
|
|
||||||
this.logger.debug(
|
|
||||||
`Invalidated all analytics caches for teacher ${teacherId}` +
|
|
||||||
(studentId ? `, student ${studentId}` : '') +
|
|
||||||
(classroomId ? `, classroom ${classroomId}` : ''),
|
|
||||||
);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.warn(`Error invalidating all analytics cache: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for ExerciseSubmissionService to call when a submission is created/updated
|
|
||||||
*
|
|
||||||
* @param studentId - Student who made the submission
|
|
||||||
* @param teacherIds - Array of teacher IDs who teach this student
|
|
||||||
*/
|
|
||||||
async onSubmissionChange(studentId: string, teacherIds: string[]): Promise<void> {
|
|
||||||
try {
|
|
||||||
// Invalidate student's insights
|
|
||||||
await this.invalidateStudentInsightsCache(studentId);
|
|
||||||
|
|
||||||
// Invalidate each teacher's analytics
|
|
||||||
await Promise.all(
|
|
||||||
teacherIds.map(teacherId =>
|
|
||||||
this.invalidateAllAnalyticsCache(teacherId, studentId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.debug(`Invalidated caches after submission change for student ${studentId}`);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.warn(`Error in onSubmissionChange cache invalidation: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for ClassroomMemberService to call when membership changes
|
|
||||||
*
|
|
||||||
* @param classroomId - Classroom where change occurred
|
|
||||||
* @param teacherId - Teacher who owns the classroom
|
|
||||||
* @param studentId - Student whose membership changed
|
|
||||||
*/
|
|
||||||
async onMembershipChange(classroomId: string, teacherId: string, studentId: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await Promise.all([
|
|
||||||
this.invalidateStudentInsightsCache(studentId),
|
|
||||||
this.invalidateEconomyAnalyticsCache(teacherId, classroomId),
|
|
||||||
this.invalidateAchievementsStatsCache(teacherId, classroomId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Invalidated caches after membership change in classroom ${classroomId} for student ${studentId}`,
|
|
||||||
);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.warn(`Error in onMembershipChange cache invalidation: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// ECONOMY ANALYTICS (GAP-ST-005)
|
// ECONOMY ANALYTICS (GAP-ST-005)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@ -14,9 +14,6 @@ import { ClassroomMember } from '@/modules/social/entities/classroom-member.enti
|
|||||||
import { Classroom } from '@/modules/social/entities/classroom.entity';
|
import { Classroom } from '@/modules/social/entities/classroom.entity';
|
||||||
import { User } from '@/modules/auth/entities/user.entity';
|
import { User } from '@/modules/auth/entities/user.entity';
|
||||||
import { UserStats } from '@/modules/gamification/entities/user-stats.entity';
|
import { UserStats } from '@/modules/gamification/entities/user-stats.entity';
|
||||||
// P1-05: Added 2025-12-18 - Educational entities for data enrichment
|
|
||||||
import { Module as EducationalModule } from '@/modules/educational/entities/module.entity';
|
|
||||||
import { Exercise } from '@/modules/educational/entities/exercise.entity';
|
|
||||||
import { GetStudentProgressQueryDto, AddTeacherNoteDto, StudentNoteResponseDto } from '../dto';
|
import { GetStudentProgressQueryDto, AddTeacherNoteDto, StudentNoteResponseDto } from '../dto';
|
||||||
|
|
||||||
export interface StudentOverview {
|
export interface StudentOverview {
|
||||||
@ -104,11 +101,6 @@ export class StudentProgressService {
|
|||||||
private readonly userRepository: Repository<User>,
|
private readonly userRepository: Repository<User>,
|
||||||
@InjectRepository(UserStats, 'gamification')
|
@InjectRepository(UserStats, 'gamification')
|
||||||
private readonly userStatsRepository: Repository<UserStats>,
|
private readonly userStatsRepository: Repository<UserStats>,
|
||||||
// P1-05: Added 2025-12-18 - Educational repositories for data enrichment
|
|
||||||
@InjectRepository(EducationalModule, 'educational')
|
|
||||||
private readonly moduleRepository: Repository<EducationalModule>,
|
|
||||||
@InjectRepository(Exercise, 'educational')
|
|
||||||
private readonly exerciseRepository: Repository<Exercise>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -263,45 +255,20 @@ export class StudentProgressService {
|
|||||||
where: { user_id: profile.id },
|
where: { user_id: profile.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
// P1-05: Get module data for enrichment
|
// TODO: Join with actual module data to get names and details
|
||||||
const moduleIds = moduleProgresses.map(mp => mp.module_id);
|
return moduleProgresses.map((mp, index) => ({
|
||||||
const modules = moduleIds.length > 0
|
module_id: mp.module_id,
|
||||||
? await this.moduleRepository.find({ where: { id: In(moduleIds) } })
|
module_name: `Módulo ${index + 1}`, // TODO: Get from modules table
|
||||||
: [];
|
module_order: index + 1,
|
||||||
const moduleMap = new Map(modules.map(m => [m.id, m]));
|
total_activities: 15, // TODO: Get from module
|
||||||
|
completed_activities: Math.round(
|
||||||
// P1-05: Get submissions for time calculation
|
(mp.progress_percentage / 100) * 15,
|
||||||
const submissions = await this.submissionRepository.find({
|
),
|
||||||
where: { user_id: profile.id },
|
average_score: Math.round(mp.progress_percentage * 0.8), // Estimate
|
||||||
});
|
time_spent_minutes: 0, // TODO: Calculate from submissions
|
||||||
const timeByModule = new Map<string, number>();
|
last_activity_date: mp.updated_at, // Using updated_at as proxy for last_activity
|
||||||
for (const sub of submissions) {
|
status: this.calculateModuleStatus(mp.progress_percentage),
|
||||||
// Get exercise to find module
|
}));
|
||||||
const exercise = await this.exerciseRepository.findOne({ where: { id: sub.exercise_id } });
|
|
||||||
if (exercise) {
|
|
||||||
const currentTime = timeByModule.get(exercise.module_id) || 0;
|
|
||||||
timeByModule.set(exercise.module_id, currentTime + (sub.time_spent_seconds || 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// P1-05: Enriched response with real module data
|
|
||||||
return moduleProgresses.map((mp) => {
|
|
||||||
const moduleData = moduleMap.get(mp.module_id);
|
|
||||||
const timeSpentSeconds = timeByModule.get(mp.module_id) || 0;
|
|
||||||
const totalActivities = moduleData?.total_exercises || 15;
|
|
||||||
|
|
||||||
return {
|
|
||||||
module_id: mp.module_id,
|
|
||||||
module_name: moduleData?.title || `Módulo ${moduleData?.order_index || 1}`,
|
|
||||||
module_order: moduleData?.order_index || 1,
|
|
||||||
total_activities: totalActivities,
|
|
||||||
completed_activities: Math.round((mp.progress_percentage / 100) * totalActivities),
|
|
||||||
average_score: Math.round(mp.average_score || mp.progress_percentage * 0.8),
|
|
||||||
time_spent_minutes: Math.round(timeSpentSeconds / 60),
|
|
||||||
last_activity_date: mp.updated_at,
|
|
||||||
status: this.calculateModuleStatus(mp.progress_percentage),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -349,37 +316,19 @@ export class StudentProgressService {
|
|||||||
order: { submitted_at: 'DESC' },
|
order: { submitted_at: 'DESC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// P1-05: Get exercise and module data for enrichment
|
// TODO: Join with exercise data to get titles and types
|
||||||
const exerciseIds = [...new Set(submissions.map(s => s.exercise_id))];
|
return submissions.map((sub) => ({
|
||||||
const exercises = exerciseIds.length > 0
|
id: sub.id,
|
||||||
? await this.exerciseRepository.find({ where: { id: In(exerciseIds) } })
|
exercise_title: 'Ejercicio', // TODO: Get from exercises table
|
||||||
: [];
|
module_name: 'Módulo', // TODO: Get from modules table
|
||||||
const exerciseMap = new Map(exercises.map(e => [e.id, e]));
|
exercise_type: 'multiple_choice', // TODO: Get from exercises table
|
||||||
|
is_correct: sub.is_correct || false,
|
||||||
// Get module data for exercise modules
|
// Protect against division by zero
|
||||||
const moduleIds = [...new Set(exercises.map(e => e.module_id))];
|
score_percentage: Math.round((sub.score / (sub.max_score || 1)) * 100),
|
||||||
const modules = moduleIds.length > 0
|
time_spent_seconds: sub.time_spent_seconds || 0,
|
||||||
? await this.moduleRepository.find({ where: { id: In(moduleIds) } })
|
hints_used: sub.hints_count || 0,
|
||||||
: [];
|
submitted_at: sub.submitted_at,
|
||||||
const moduleMap = new Map(modules.map(m => [m.id, m]));
|
}));
|
||||||
|
|
||||||
// P1-05: Enriched response with real exercise/module data
|
|
||||||
return submissions.map((sub) => {
|
|
||||||
const exercise = exerciseMap.get(sub.exercise_id);
|
|
||||||
const moduleData = exercise ? moduleMap.get(exercise.module_id) : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: sub.id,
|
|
||||||
exercise_title: exercise?.title || 'Ejercicio',
|
|
||||||
module_name: moduleData?.title || 'Módulo',
|
|
||||||
exercise_type: exercise?.exercise_type || 'multiple_choice',
|
|
||||||
is_correct: sub.is_correct || false,
|
|
||||||
score_percentage: Math.round((sub.score / (sub.max_score || 1)) * 100),
|
|
||||||
time_spent_seconds: sub.time_spent_seconds || 0,
|
|
||||||
hints_used: sub.hints_count || 0,
|
|
||||||
submitted_at: sub.submitted_at,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -402,48 +351,32 @@ export class StudentProgressService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Group by exercise to find struggles
|
// Group by exercise to find struggles
|
||||||
const submissionsByExercise = new Map<string, ExerciseSubmission[]>();
|
const exerciseMap = new Map<string, ExerciseSubmission[]>();
|
||||||
submissions.forEach((sub) => {
|
submissions.forEach((sub) => {
|
||||||
const key = sub.exercise_id;
|
const key = sub.exercise_id;
|
||||||
if (!submissionsByExercise.has(key)) {
|
if (!exerciseMap.has(key)) {
|
||||||
submissionsByExercise.set(key, []);
|
exerciseMap.set(key, []);
|
||||||
}
|
}
|
||||||
submissionsByExercise.get(key)!.push(sub);
|
exerciseMap.get(key)!.push(sub);
|
||||||
});
|
});
|
||||||
|
|
||||||
// P1-05: Get exercise and module data for enrichment
|
|
||||||
const exerciseIds = [...submissionsByExercise.keys()];
|
|
||||||
const exercises = exerciseIds.length > 0
|
|
||||||
? await this.exerciseRepository.find({ where: { id: In(exerciseIds) } })
|
|
||||||
: [];
|
|
||||||
const exerciseDataMap = new Map(exercises.map(e => [e.id, e]));
|
|
||||||
|
|
||||||
const moduleIds = [...new Set(exercises.map(e => e.module_id))];
|
|
||||||
const modules = moduleIds.length > 0
|
|
||||||
? await this.moduleRepository.find({ where: { id: In(moduleIds) } })
|
|
||||||
: [];
|
|
||||||
const moduleDataMap = new Map(modules.map(m => [m.id, m]));
|
|
||||||
|
|
||||||
const struggles: StruggleArea[] = [];
|
const struggles: StruggleArea[] = [];
|
||||||
|
|
||||||
submissionsByExercise.forEach((subs, exerciseId) => {
|
exerciseMap.forEach((subs) => {
|
||||||
const attempts = subs.length;
|
const attempts = subs.length;
|
||||||
const correctAttempts = subs.filter((s) => s.is_correct).length;
|
const correctAttempts = subs.filter((s) => s.is_correct).length;
|
||||||
const successRate = (correctAttempts / attempts) * 100;
|
const successRate = (correctAttempts / attempts) * 100;
|
||||||
|
|
||||||
// Consider it a struggle if success rate < 70% and multiple attempts
|
// Consider it a struggle if success rate < 70% and multiple attempts
|
||||||
if (successRate < 70 && attempts >= 2) {
|
if (successRate < 70 && attempts >= 2) {
|
||||||
|
// Protect against division by zero in score calculation
|
||||||
const avgScore =
|
const avgScore =
|
||||||
subs.reduce((sum, s) => sum + (s.score / (s.max_score || 1)) * 100, 0) /
|
subs.reduce((sum, s) => sum + (s.score / (s.max_score || 1)) * 100, 0) /
|
||||||
attempts;
|
attempts;
|
||||||
|
|
||||||
// P1-05: Get real exercise/module names
|
|
||||||
const exercise = exerciseDataMap.get(exerciseId);
|
|
||||||
const moduleData = exercise ? moduleDataMap.get(exercise.module_id) : undefined;
|
|
||||||
|
|
||||||
struggles.push({
|
struggles.push({
|
||||||
topic: exercise?.title || 'Tema del ejercicio',
|
topic: 'Tema del ejercicio', // TODO: Get from exercise data
|
||||||
module_name: moduleData?.title || 'Módulo',
|
module_name: 'Módulo', // TODO: Get from module data
|
||||||
attempts,
|
attempts,
|
||||||
success_rate: Math.round(successRate),
|
success_rate: Math.round(successRate),
|
||||||
average_score: Math.round(avgScore),
|
average_score: Math.round(avgScore),
|
||||||
@ -457,7 +390,6 @@ export class StudentProgressService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare student with class averages
|
* Compare student with class averages
|
||||||
* P1-05: Updated 2025-12-18 - Calculate real class averages
|
|
||||||
*/
|
*/
|
||||||
async getClassComparison(studentId: string): Promise<ClassComparison[]> {
|
async getClassComparison(studentId: string): Promise<ClassComparison[]> {
|
||||||
const studentStats = await this.getStudentStats(studentId);
|
const studentStats = await this.getStudentStats(studentId);
|
||||||
@ -466,14 +398,12 @@ export class StudentProgressService {
|
|||||||
const allProfiles = await this.profileRepository.find();
|
const allProfiles = await this.profileRepository.find();
|
||||||
const allSubmissions = await this.submissionRepository.find();
|
const allSubmissions = await this.submissionRepository.find();
|
||||||
|
|
||||||
// P1-05: Get all user stats for real averages
|
|
||||||
const allUserStats = await this.userStatsRepository.find();
|
|
||||||
|
|
||||||
// Calculate class averages (with division by zero protection)
|
// Calculate class averages (with division by zero protection)
|
||||||
const classAvgScore = allSubmissions.length > 0
|
const classAvgScore = allSubmissions.length > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
allSubmissions.reduce(
|
allSubmissions.reduce(
|
||||||
(sum, sub) => {
|
(sum, sub) => {
|
||||||
|
// Protect against division by zero in score calculation
|
||||||
const maxScore = sub.max_score || 1;
|
const maxScore = sub.max_score || 1;
|
||||||
return sum + (sub.score / maxScore) * 100;
|
return sum + (sub.score / maxScore) * 100;
|
||||||
},
|
},
|
||||||
@ -487,24 +417,6 @@ export class StudentProgressService {
|
|||||||
? allSubmissions.length / allProfiles.length
|
? allSubmissions.length / allProfiles.length
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// P1-05: Calculate real time average from submissions
|
|
||||||
const totalTimeAllStudents = allSubmissions.reduce(
|
|
||||||
(sum, sub) => sum + (sub.time_spent_seconds || 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const classAvgTimeMinutes = allProfiles.length > 0
|
|
||||||
? Math.round(totalTimeAllStudents / 60 / allProfiles.length)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// P1-05: Calculate real streak average from user_stats
|
|
||||||
const totalStreaks = allUserStats.reduce(
|
|
||||||
(sum, stats) => sum + (stats.current_streak || 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const classAvgStreak = allUserStats.length > 0
|
|
||||||
? Math.round(totalStreaks / allUserStats.length)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
metric: 'Puntuación Promedio',
|
metric: 'Puntuación Promedio',
|
||||||
@ -527,19 +439,19 @@ export class StudentProgressService {
|
|||||||
{
|
{
|
||||||
metric: 'Tiempo de Estudio (min)',
|
metric: 'Tiempo de Estudio (min)',
|
||||||
student_value: studentStats.total_time_spent_minutes,
|
student_value: studentStats.total_time_spent_minutes,
|
||||||
class_average: classAvgTimeMinutes,
|
class_average: 1100, // TODO: Calculate actual class average
|
||||||
percentile: this.calculatePercentile(
|
percentile: this.calculatePercentile(
|
||||||
studentStats.total_time_spent_minutes,
|
studentStats.total_time_spent_minutes,
|
||||||
classAvgTimeMinutes,
|
1100,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
metric: 'Racha Actual (días)',
|
metric: 'Racha Actual (días)',
|
||||||
student_value: studentStats.current_streak_days,
|
student_value: studentStats.current_streak_days,
|
||||||
class_average: classAvgStreak,
|
class_average: 5, // TODO: Calculate actual class average
|
||||||
percentile: this.calculatePercentile(
|
percentile: this.calculatePercentile(
|
||||||
studentStats.current_streak_days,
|
studentStats.current_streak_days,
|
||||||
classAvgStreak,
|
5,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -13,9 +13,7 @@ import { Profile } from '@/modules/auth/entities/profile.entity';
|
|||||||
import { Classroom } from '@/modules/social/entities/classroom.entity';
|
import { Classroom } from '@/modules/social/entities/classroom.entity';
|
||||||
import { ClassroomMember } from '@/modules/social/entities/classroom-member.entity';
|
import { ClassroomMember } from '@/modules/social/entities/classroom-member.entity';
|
||||||
import { AnalyticsService } from './analytics.service';
|
import { AnalyticsService } from './analytics.service';
|
||||||
// P0-04: Added 2025-12-18 - NotificationsService integration
|
import { GamilityRoleEnum } from '@/shared/constants/enums.constants';
|
||||||
import { NotificationsService } from '@/modules/notifications/services/notifications.service';
|
|
||||||
import { GamilityRoleEnum, NotificationTypeEnum } from '@/shared/constants/enums.constants';
|
|
||||||
|
|
||||||
export interface RiskAlert {
|
export interface RiskAlert {
|
||||||
student_id: string;
|
student_id: string;
|
||||||
@ -51,8 +49,6 @@ export class StudentRiskAlertService {
|
|||||||
@InjectRepository(ClassroomMember, 'social')
|
@InjectRepository(ClassroomMember, 'social')
|
||||||
private readonly classroomMemberRepository: Repository<ClassroomMember>,
|
private readonly classroomMemberRepository: Repository<ClassroomMember>,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly analyticsService: AnalyticsService,
|
||||||
// P0-04: Added 2025-12-18 - NotificationsService injection
|
|
||||||
private readonly notificationsService: NotificationsService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -209,45 +205,29 @@ export class StudentRiskAlertService {
|
|||||||
/**
|
/**
|
||||||
* Send alert notification to teacher about at-risk students
|
* Send alert notification to teacher about at-risk students
|
||||||
*
|
*
|
||||||
* P0-04: Implemented 2025-12-18 - NotificationsService integration
|
* @TODO: Replace with actual notification service call
|
||||||
*/
|
*/
|
||||||
private async sendTeacherAlert(teacherId: string, alerts: RiskAlert[]): Promise<void> {
|
private async sendTeacherAlert(teacherId: string, alerts: RiskAlert[]): Promise<void> {
|
||||||
const highRiskCount = alerts.filter(a => a.risk_level === 'high').length;
|
const highRiskCount = alerts.filter(a => a.risk_level === 'high').length;
|
||||||
const mediumRiskCount = alerts.filter(a => a.risk_level === 'medium').length;
|
const mediumRiskCount = alerts.filter(a => a.risk_level === 'medium').length;
|
||||||
const totalAlerts = highRiskCount + mediumRiskCount;
|
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[NOTIFICATION] Teacher ${teacherId}: ${highRiskCount} high-risk, ${mediumRiskCount} medium-risk students`,
|
`[NOTIFICATION] Teacher ${teacherId}: ${highRiskCount} high-risk, ${mediumRiskCount} medium-risk students`,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
// TODO: Integrate with NotificationService
|
||||||
// P0-04: Send notification via NotificationsService
|
// Example:
|
||||||
await this.notificationsService.sendNotification({
|
// await this.notificationService.create({
|
||||||
userId: teacherId,
|
// recipient_id: teacherId,
|
||||||
type: NotificationTypeEnum.SYSTEM_ANNOUNCEMENT,
|
// type: 'student_risk_alert',
|
||||||
title: highRiskCount > 0
|
// title: `${highRiskCount + mediumRiskCount} estudiantes requieren atención`,
|
||||||
? `⚠️ Alerta: ${totalAlerts} estudiantes requieren atención urgente`
|
// message: this.formatAlertMessage(alerts),
|
||||||
: `📊 ${totalAlerts} estudiantes requieren seguimiento`,
|
// priority: highRiskCount > 0 ? 'high' : 'medium',
|
||||||
message: this.formatAlertMessage(alerts),
|
// action_url: '/teacher/alerts',
|
||||||
data: {
|
// metadata: { alerts }
|
||||||
alertType: 'student_risk',
|
// });
|
||||||
highRiskCount,
|
|
||||||
mediumRiskCount,
|
|
||||||
studentIds: alerts.map(a => a.student_id),
|
|
||||||
action: {
|
|
||||||
type: 'navigate',
|
|
||||||
url: '/teacher/alerts',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(`✅ Notification sent to teacher ${teacherId}`);
|
// For now, just log detailed info
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.error(`Failed to send notification to teacher ${teacherId}: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log detailed info for debugging
|
|
||||||
for (const alert of alerts) {
|
for (const alert of alerts) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
` - ${alert.student_name}: ${alert.risk_level} risk, ${alert.overall_score}% score, ${alert.dropout_risk * 100}% dropout risk`,
|
` - ${alert.student_name}: ${alert.risk_level} risk, ${alert.overall_score}% score, ${alert.dropout_risk * 100}% dropout risk`,
|
||||||
@ -257,70 +237,21 @@ export class StudentRiskAlertService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send summary to admins about high-risk students across the platform
|
* Send summary to admins about high-risk students across the platform
|
||||||
*
|
|
||||||
* P0-04: Implemented 2025-12-18 - NotificationsService integration
|
|
||||||
*/
|
*/
|
||||||
private async sendAdminSummary(highRiskAlerts: RiskAlert[]): Promise<void> {
|
private async sendAdminSummary(highRiskAlerts: RiskAlert[]): Promise<void> {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[ADMIN SUMMARY] ${highRiskAlerts.length} high-risk students detected across platform`,
|
`[ADMIN SUMMARY] ${highRiskAlerts.length} high-risk students detected across platform`,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
// TODO: Integrate with NotificationService for admins
|
||||||
// Get all super admins
|
// Example:
|
||||||
const admins = await this.profileRepository.find({
|
// await this.notificationService.createForRole({
|
||||||
where: { role: GamilityRoleEnum.SUPER_ADMIN },
|
// role: GamilityRoleEnum.SUPER_ADMIN,
|
||||||
});
|
// type: 'platform_risk_summary',
|
||||||
|
// title: `Alerta: ${highRiskAlerts.length} estudiantes en alto riesgo`,
|
||||||
if (admins.length === 0) {
|
// message: this.formatAdminSummary(highRiskAlerts),
|
||||||
this.logger.warn('No super admins found to receive risk summary');
|
// priority: 'high'
|
||||||
return;
|
// });
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare summary message
|
|
||||||
const summaryLines = [
|
|
||||||
`🚨 Resumen de Riesgo de Plataforma`,
|
|
||||||
``,
|
|
||||||
`Se han detectado ${highRiskAlerts.length} estudiantes en ALTO RIESGO:`,
|
|
||||||
``,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const alert of highRiskAlerts.slice(0, 10)) { // Limit to first 10
|
|
||||||
summaryLines.push(`• ${alert.student_name} - Riesgo abandono: ${Math.round(alert.dropout_risk * 100)}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (highRiskAlerts.length > 10) {
|
|
||||||
summaryLines.push(`... y ${highRiskAlerts.length - 10} más`);
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryLines.push(``);
|
|
||||||
summaryLines.push(`Revisa el panel de administración para acciones.`);
|
|
||||||
|
|
||||||
const summaryMessage = summaryLines.join('\n');
|
|
||||||
|
|
||||||
// P0-04: Send notification to each admin
|
|
||||||
const notifications = admins.map(admin => ({
|
|
||||||
userId: admin.id,
|
|
||||||
type: NotificationTypeEnum.SYSTEM_ANNOUNCEMENT,
|
|
||||||
title: `🚨 Alerta Crítica: ${highRiskAlerts.length} estudiantes en alto riesgo`,
|
|
||||||
message: summaryMessage,
|
|
||||||
data: {
|
|
||||||
alertType: 'platform_risk_summary',
|
|
||||||
highRiskCount: highRiskAlerts.length,
|
|
||||||
studentIds: highRiskAlerts.map(a => a.student_id),
|
|
||||||
action: {
|
|
||||||
type: 'navigate',
|
|
||||||
url: '/admin/alerts',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
await this.notificationsService.sendBulkNotifications(notifications);
|
|
||||||
|
|
||||||
this.logger.log(`✅ Admin summary sent to ${admins.length} administrators`);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.error(`Failed to send admin summary: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -72,8 +72,6 @@ import { TeacherGuard, ClassroomOwnershipGuard } from './guards';
|
|||||||
|
|
||||||
// External modules
|
// External modules
|
||||||
import { ProgressModule } from '@modules/progress/progress.module';
|
import { ProgressModule } from '@modules/progress/progress.module';
|
||||||
// P0-04: Added 2025-12-18 - NotificationsModule for StudentRiskAlertService
|
|
||||||
import { NotificationsModule } from '@modules/notifications/notifications.module';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TeacherModule
|
* TeacherModule
|
||||||
@ -126,9 +124,6 @@ import { NotificationsModule } from '@modules/notifications/notifications.module
|
|||||||
// Import ProgressModule for ExerciseSubmissionService (needed for reward distribution)
|
// Import ProgressModule for ExerciseSubmissionService (needed for reward distribution)
|
||||||
ProgressModule,
|
ProgressModule,
|
||||||
|
|
||||||
// P0-04: Import NotificationsModule for StudentRiskAlertService
|
|
||||||
NotificationsModule,
|
|
||||||
|
|
||||||
// Entities from 'auth' datasource
|
// Entities from 'auth' datasource
|
||||||
TypeOrmModule.forFeature([Profile, User], 'auth'),
|
TypeOrmModule.forFeature([Profile, User], 'auth'),
|
||||||
|
|
||||||
|
|||||||
@ -17,14 +17,7 @@ import {
|
|||||||
import { Logger, UseGuards } from '@nestjs/common';
|
import { Logger, UseGuards } from '@nestjs/common';
|
||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
import { WsJwtGuard, AuthenticatedSocket } from './guards/ws-jwt.guard';
|
import { WsJwtGuard, AuthenticatedSocket } from './guards/ws-jwt.guard';
|
||||||
import {
|
import { SocketEvent } from './types/websocket.types';
|
||||||
SocketEvent,
|
|
||||||
StudentActivityPayload,
|
|
||||||
ClassroomUpdatePayload,
|
|
||||||
NewSubmissionPayload,
|
|
||||||
AlertTriggeredPayload,
|
|
||||||
StudentOnlineStatusPayload,
|
|
||||||
} from './types/websocket.types';
|
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
cors: {
|
cors: {
|
||||||
@ -182,166 +175,4 @@ implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
|||||||
getUserSocketCount(userId: string): number {
|
getUserSocketCount(userId: string): number {
|
||||||
return this.userSockets.get(userId)?.size || 0;
|
return this.userSockets.get(userId)?.size || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// TEACHER PORTAL METHODS (P2-01: 2025-12-18)
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe teacher to classroom updates
|
|
||||||
*/
|
|
||||||
@UseGuards(WsJwtGuard)
|
|
||||||
@SubscribeMessage('teacher:subscribe_classroom')
|
|
||||||
async handleSubscribeClassroom(
|
|
||||||
@ConnectedSocket() client: AuthenticatedSocket,
|
|
||||||
@MessageBody() data: { classroomId: string },
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const userId = client.userData!.userId;
|
|
||||||
const { classroomId } = data;
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
|
|
||||||
await client.join(room);
|
|
||||||
this.logger.debug(`Teacher ${userId} subscribed to classroom ${classroomId}`);
|
|
||||||
|
|
||||||
return { success: true, room };
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.error('Error subscribing to classroom:', error);
|
|
||||||
return { success: false, error: errorMessage };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe teacher from classroom updates
|
|
||||||
*/
|
|
||||||
@UseGuards(WsJwtGuard)
|
|
||||||
@SubscribeMessage('teacher:unsubscribe_classroom')
|
|
||||||
async handleUnsubscribeClassroom(
|
|
||||||
@ConnectedSocket() client: AuthenticatedSocket,
|
|
||||||
@MessageBody() data: { classroomId: string },
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const userId = client.userData!.userId;
|
|
||||||
const { classroomId } = data;
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
|
|
||||||
await client.leave(room);
|
|
||||||
this.logger.debug(`Teacher ${userId} unsubscribed from classroom ${classroomId}`);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
this.logger.error('Error unsubscribing from classroom:', error);
|
|
||||||
return { success: false, error: errorMessage };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit student activity to classroom teachers
|
|
||||||
*/
|
|
||||||
emitStudentActivity(classroomId: string, payload: Omit<StudentActivityPayload, 'timestamp'>) {
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
this.server.to(room).emit(SocketEvent.STUDENT_ACTIVITY, {
|
|
||||||
...payload,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
this.logger.debug(`Student activity emitted to classroom ${classroomId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit classroom update to subscribed teachers
|
|
||||||
*/
|
|
||||||
emitClassroomUpdate(classroomId: string, payload: Omit<ClassroomUpdatePayload, 'timestamp'>) {
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
this.server.to(room).emit(SocketEvent.CLASSROOM_UPDATE, {
|
|
||||||
...payload,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
this.logger.debug(`Classroom update emitted to ${classroomId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit new submission notification to classroom teachers
|
|
||||||
*/
|
|
||||||
emitNewSubmission(classroomId: string, payload: Omit<NewSubmissionPayload, 'timestamp'>) {
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
this.server.to(room).emit(SocketEvent.NEW_SUBMISSION, {
|
|
||||||
...payload,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
this.logger.debug(`New submission emitted to classroom ${classroomId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit alert to specific teacher and classroom
|
|
||||||
*/
|
|
||||||
emitAlertTriggered(
|
|
||||||
teacherId: string,
|
|
||||||
classroomId: string,
|
|
||||||
payload: Omit<AlertTriggeredPayload, 'timestamp'>,
|
|
||||||
) {
|
|
||||||
// Emit to teacher's personal room
|
|
||||||
this.emitToUser(teacherId, SocketEvent.ALERT_TRIGGERED, payload);
|
|
||||||
// Also emit to classroom room for other subscribed teachers
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
this.server.to(room).emit(SocketEvent.ALERT_TRIGGERED, {
|
|
||||||
...payload,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
this.logger.debug(`Alert triggered for teacher ${teacherId} in classroom ${classroomId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit student online/offline status to classroom
|
|
||||||
*/
|
|
||||||
emitStudentOnlineStatus(classroomId: string, payload: Omit<StudentOnlineStatusPayload, 'timestamp'>) {
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
const event = payload.isOnline ? SocketEvent.STUDENT_ONLINE : SocketEvent.STUDENT_OFFLINE;
|
|
||||||
this.server.to(room).emit(event, {
|
|
||||||
...payload,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
this.logger.debug(`Student ${payload.studentId} is now ${payload.isOnline ? 'online' : 'offline'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit progress update for a student
|
|
||||||
*/
|
|
||||||
emitProgressUpdate(
|
|
||||||
teacherIds: string[],
|
|
||||||
classroomId: string,
|
|
||||||
data: {
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
progressType: 'module_complete' | 'exercise_complete' | 'level_up' | 'achievement';
|
|
||||||
details: Record<string, unknown>;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const payload = {
|
|
||||||
...data,
|
|
||||||
classroomId,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Emit to all relevant teachers
|
|
||||||
teacherIds.forEach((teacherId) => {
|
|
||||||
this.emitToUser(teacherId, SocketEvent.PROGRESS_UPDATE, payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also emit to classroom room
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
this.server.to(room).emit(SocketEvent.PROGRESS_UPDATE, payload);
|
|
||||||
|
|
||||||
this.logger.debug(`Progress update emitted for student ${data.studentId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get count of teachers subscribed to a classroom
|
|
||||||
*/
|
|
||||||
async getClassroomSubscriberCount(classroomId: string): Promise<number> {
|
|
||||||
const room = `classroom:${classroomId}`;
|
|
||||||
const sockets = await this.server.in(room).fetchSockets();
|
|
||||||
return sockets.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,15 +25,6 @@ export enum SocketEvent {
|
|||||||
// Missions
|
// Missions
|
||||||
MISSION_COMPLETED = 'mission:completed',
|
MISSION_COMPLETED = 'mission:completed',
|
||||||
MISSION_PROGRESS = 'mission:progress',
|
MISSION_PROGRESS = 'mission:progress',
|
||||||
|
|
||||||
// Teacher Portal (P2-01: 2025-12-18)
|
|
||||||
STUDENT_ACTIVITY = 'teacher:student_activity',
|
|
||||||
CLASSROOM_UPDATE = 'teacher:classroom_update',
|
|
||||||
NEW_SUBMISSION = 'teacher:new_submission',
|
|
||||||
ALERT_TRIGGERED = 'teacher:alert_triggered',
|
|
||||||
STUDENT_ONLINE = 'teacher:student_online',
|
|
||||||
STUDENT_OFFLINE = 'teacher:student_offline',
|
|
||||||
PROGRESS_UPDATE = 'teacher:progress_update',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketUserData {
|
export interface SocketUserData {
|
||||||
@ -60,60 +51,3 @@ export interface LeaderboardPayload {
|
|||||||
leaderboard: any[]; // Will be typed from gamification module
|
leaderboard: any[]; // Will be typed from gamification module
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Teacher Portal Payloads (P2-01: 2025-12-18)
|
|
||||||
|
|
||||||
export interface StudentActivityPayload {
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
classroomId: string;
|
|
||||||
activityType: 'exercise_start' | 'exercise_complete' | 'hint_used' | 'comodin_used' | 'module_start';
|
|
||||||
exerciseId?: string;
|
|
||||||
exerciseTitle?: string;
|
|
||||||
moduleId?: string;
|
|
||||||
moduleTitle?: string;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassroomUpdatePayload {
|
|
||||||
classroomId: string;
|
|
||||||
classroomName: string;
|
|
||||||
updateType: 'student_joined' | 'student_left' | 'stats_changed';
|
|
||||||
data: Record<string, unknown>;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NewSubmissionPayload {
|
|
||||||
submissionId: string;
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
exerciseId: string;
|
|
||||||
exerciseTitle: string;
|
|
||||||
classroomId: string;
|
|
||||||
score: number;
|
|
||||||
maxScore: number;
|
|
||||||
requiresReview: boolean;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlertTriggeredPayload {
|
|
||||||
alertId: string;
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
classroomId: string;
|
|
||||||
alertType: 'at_risk' | 'low_performance' | 'inactive' | 'struggling';
|
|
||||||
severity: 'low' | 'medium' | 'high';
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StudentOnlineStatusPayload {
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
classroomId: string;
|
|
||||||
isOnline: boolean;
|
|
||||||
lastActivity?: string;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,208 +0,0 @@
|
|||||||
# FLUJO DE CARGA LIMPIA - GAMILIT DATABASE
|
|
||||||
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Version:** 1.0
|
|
||||||
**Cumple con:** DIRECTIVA-POLITICA-CARGA-LIMPIA.md
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RESUMEN
|
|
||||||
|
|
||||||
Este documento describe los 3 escenarios de inicializacion de base de datos y que script usar en cada caso.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ESCENARIOS Y SCRIPTS
|
|
||||||
|
|
||||||
### Escenario 1: INSTALACION NUEVA (Usuario y BD no existen)
|
|
||||||
|
|
||||||
**Usar:** `init-database.sh` o `init-database-v3.sh`
|
|
||||||
|
|
||||||
**Ubicacion:** `apps/database/scripts/`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/projects/gamilit/apps/database/scripts
|
|
||||||
|
|
||||||
# Opcion A: Con password manual
|
|
||||||
./init-database.sh --env dev --password "tu_password_seguro"
|
|
||||||
|
|
||||||
# Opcion B: Con dotenv-vault (recomendado para produccion)
|
|
||||||
./manage-secrets.sh generate --env prod
|
|
||||||
./manage-secrets.sh sync --env prod
|
|
||||||
./init-database-v3.sh --env prod
|
|
||||||
```
|
|
||||||
|
|
||||||
**Que hace:**
|
|
||||||
1. Crea usuario PostgreSQL `gamilit_user`
|
|
||||||
2. Crea base de datos `gamilit_platform`
|
|
||||||
3. Ejecuta todos los DDL (16 fases)
|
|
||||||
4. Carga todos los Seeds (38+ archivos)
|
|
||||||
5. Genera archivo de credenciales
|
|
||||||
6. Actualiza .env de backend/frontend
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Escenario 2: RECREACION COMPLETA (Usuario existe, BD se resetea)
|
|
||||||
|
|
||||||
**Usar:** `drop-and-recreate-database.sh`
|
|
||||||
|
|
||||||
**Ubicacion:** `apps/database/`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/projects/gamilit/apps/database
|
|
||||||
|
|
||||||
# Con DATABASE_URL
|
|
||||||
export DATABASE_URL="postgresql://gamilit_user:password@localhost:5432/gamilit_platform"
|
|
||||||
./drop-and-recreate-database.sh
|
|
||||||
|
|
||||||
# O pasando como argumento
|
|
||||||
./drop-and-recreate-database.sh "postgresql://gamilit_user:password@localhost:5432/gamilit_platform"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Que hace:**
|
|
||||||
1. Desconecta usuarios activos
|
|
||||||
2. DROP DATABASE gamilit_platform
|
|
||||||
3. CREATE DATABASE gamilit_platform
|
|
||||||
4. Llama a `create-database.sh` automaticamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Escenario 3: SOLO DDL + SEEDS (BD limpia ya existe)
|
|
||||||
|
|
||||||
**Usar:** `create-database.sh`
|
|
||||||
|
|
||||||
**Ubicacion:** `apps/database/`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/projects/gamilit/apps/database
|
|
||||||
|
|
||||||
export DATABASE_URL="postgresql://gamilit_user:password@localhost:5432/gamilit_platform"
|
|
||||||
./create-database.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Que hace:**
|
|
||||||
1. Habilita extensiones (pgcrypto, uuid-ossp)
|
|
||||||
2. Ejecuta DDL en 16 fases ordenadas
|
|
||||||
3. Carga Seeds de produccion (38+ archivos)
|
|
||||||
4. Genera reporte de objetos creados
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DIAGRAMA DE DECISION
|
|
||||||
|
|
||||||
```
|
|
||||||
¿Existe el usuario gamilit_user?
|
|
||||||
│
|
|
||||||
├── NO ──► Escenario 1: ./scripts/init-database.sh --env dev
|
|
||||||
│
|
|
||||||
└── SI ──► ¿Necesitas eliminar TODOS los datos?
|
|
||||||
│
|
|
||||||
├── SI ──► Escenario 2: ./drop-and-recreate-database.sh
|
|
||||||
│
|
|
||||||
└── NO ──► ¿La BD esta vacia (recien creada)?
|
|
||||||
│
|
|
||||||
├── SI ──► Escenario 3: ./create-database.sh
|
|
||||||
│
|
|
||||||
└── NO ──► Escenario 2: ./drop-and-recreate-database.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMANDOS RAPIDOS POR AMBIENTE
|
|
||||||
|
|
||||||
### Desarrollo (primera vez)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/database/scripts
|
|
||||||
./init-database.sh --env dev --password "dev_password_123"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Desarrollo (recrear BD)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/database
|
|
||||||
export DATABASE_URL="postgresql://gamilit_user:dev_password_123@localhost:5432/gamilit_platform"
|
|
||||||
./drop-and-recreate-database.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Produccion (primera vez)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/database/scripts
|
|
||||||
./manage-secrets.sh generate --env prod
|
|
||||||
./manage-secrets.sh sync --env prod
|
|
||||||
./init-database-v3.sh --env prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Produccion (recrear BD)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/database
|
|
||||||
export DATABASE_URL="postgresql://gamilit_user:$DB_PASSWORD@localhost:5432/gamilit_platform"
|
|
||||||
./drop-and-recreate-database.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## VALIDACION POST-CARGA
|
|
||||||
|
|
||||||
Despues de cualquier escenario, validar con:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar conteo de objetos
|
|
||||||
psql "$DATABASE_URL" -c "
|
|
||||||
SELECT
|
|
||||||
(SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog','information_schema','pg_toast')) as schemas,
|
|
||||||
(SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog','information_schema')) as tables,
|
|
||||||
(SELECT COUNT(*) FROM pg_type WHERE typcategory = 'E') as enums
|
|
||||||
;"
|
|
||||||
|
|
||||||
# Verificar datos criticos
|
|
||||||
psql "$DATABASE_URL" -c "
|
|
||||||
SELECT 'tenants' as tabla, COUNT(*) FROM auth_management.tenants
|
|
||||||
UNION ALL SELECT 'users', COUNT(*) FROM auth.users
|
|
||||||
UNION ALL SELECT 'modules', COUNT(*) FROM educational_content.modules
|
|
||||||
UNION ALL SELECT 'maya_ranks', COUNT(*) FROM gamification_system.maya_ranks
|
|
||||||
UNION ALL SELECT 'feature_flags', COUNT(*) FROM system_configuration.feature_flags;
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Valores esperados:**
|
|
||||||
- Schemas: 15+
|
|
||||||
- Tablas: 60+
|
|
||||||
- ENUMs: 35+
|
|
||||||
- Tenants: 14+
|
|
||||||
- Users: 20+
|
|
||||||
- Modules: 5
|
|
||||||
- Maya Ranks: 5
|
|
||||||
- Feature Flags: 26+
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SCRIPTS DISPONIBLES
|
|
||||||
|
|
||||||
| Script | Ubicacion | Proposito |
|
|
||||||
|--------|-----------|-----------|
|
|
||||||
| `init-database.sh` | scripts/ | Crear usuario + BD + DDL + Seeds |
|
|
||||||
| `init-database-v3.sh` | scripts/ | Igual pero con dotenv-vault |
|
|
||||||
| `drop-and-recreate-database.sh` | ./ | Drop BD + Recrear + DDL + Seeds |
|
|
||||||
| `create-database.sh` | ./ | Solo DDL + Seeds (BD debe existir) |
|
|
||||||
| `reset-database.sh` | scripts/ | Reset BD (mantiene usuario) |
|
|
||||||
| `recreate-database.sh` | scripts/ | Drop completo (usuario + BD) |
|
|
||||||
| `manage-secrets.sh` | scripts/ | Gestionar passwords con vault |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CUMPLIMIENTO DE DIRECTIVA
|
|
||||||
|
|
||||||
Este flujo cumple con DIRECTIVA-POLITICA-CARGA-LIMPIA.md:
|
|
||||||
|
|
||||||
- ✅ DDL es fuente de verdad
|
|
||||||
- ✅ BD es resultado de ejecutar DDL
|
|
||||||
- ✅ No se usan migrations
|
|
||||||
- ✅ Recreacion completa en cualquier momento
|
|
||||||
- ✅ Un comando = BD lista
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
@ -72,6 +72,7 @@ INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, e
|
|||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', '5ae21325-7450-4c37-82f1-3f9bcd7b6f45', 'authenticated', NULL, 'omarcitogonzalezzavaleta@gmail.com', '$2b$10$RRk3DAgQdiikxVImFIMqquqB.TNpKs3E.RNFtt1rwwTzO24uShri.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:17:07.610076+00', '2025-11-25 08:17:07.610076+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', '5ae21325-7450-4c37-82f1-3f9bcd7b6f45', 'authenticated', NULL, 'omarcitogonzalezzavaleta@gmail.com', '$2b$10$RRk3DAgQdiikxVImFIMqquqB.TNpKs3E.RNFtt1rwwTzO24uShri.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:17:07.610076+00', '2025-11-25 08:17:07.610076+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', 'a4d27774-8a51-4660-ad2f-81d0dfd3a5a7', 'authenticated', NULL, 'gustavobm2024cbtis@gmail.com', '$2b$10$lg7KRUTPofcx4Rtyey8J7.XO0gmdBLCFIfK5uP08mqT0qUIl1aTJq', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:20:49.649184+00', '2025-11-25 08:20:49.649184+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', 'a4d27774-8a51-4660-ad2f-81d0dfd3a5a7', 'authenticated', NULL, 'gustavobm2024cbtis@gmail.com', '$2b$10$lg7KRUTPofcx4Rtyey8J7.XO0gmdBLCFIfK5uP08mqT0qUIl1aTJq', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:20:49.649184+00', '2025-11-25 08:20:49.649184+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', '6e30164a-78b0-49b0-bd21-23d7c6c03349', 'authenticated', NULL, 'marianaxsotoxt22@gmail.com', '$2b$10$GQC9yTWiP2vP9GUp0gnhUeLjmw70EI4JQhfJBZbMOlCNXGXb/bt5O', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:33:18.150784+00', '2025-11-25 08:33:18.150784+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', '6e30164a-78b0-49b0-bd21-23d7c6c03349', 'authenticated', NULL, 'marianaxsotoxt22@gmail.com', '$2b$10$GQC9yTWiP2vP9GUp0gnhUeLjmw70EI4JQhfJBZbMOlCNXGXb/bt5O', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:33:18.150784+00', '2025-11-25 08:33:18.150784+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, '0ae1bf21-39e3-4168-9632-457418c7a07d', 'authenticated', NULL, 'rckrdmrd@gmail.com', '$2b$10$LiDdaJLA.ZvdFleamkMuvOcIrW0PQMEh5aVZ5Wg5pzhm7gwc5s.1C', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-09 01:22:42.784+00', NULL, '{}', false, '2025-11-29 13:37:09.271457+00', '2025-12-09 01:22:42.785367+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'authenticated', NULL, 'admin@gamilit.com', '$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga', '2025-11-29 13:26:50.289631+00', NULL, '', NULL, '', NULL, '', '', NULL, '2025-12-01 00:54:19.615+00', '{"provider": "email", "providers": ["email"]}', '{"name": "Admin GAMILIT", "role": "super_admin", "description": "Usuario administrador de testing"}', false, '2025-11-29 13:26:50.289631+00', '2025-12-01 00:54:19.617766+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'super_admin', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'authenticated', NULL, 'admin@gamilit.com', '$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga', '2025-11-29 13:26:50.289631+00', NULL, '', NULL, '', NULL, '', '', NULL, '2025-12-01 00:54:19.615+00', '{"provider": "email", "providers": ["email"]}', '{"name": "Admin GAMILIT", "role": "super_admin", "description": "Usuario administrador de testing"}', false, '2025-11-29 13:26:50.289631+00', '2025-12-01 00:54:19.617766+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'super_admin', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, '69681b09-5077-4f77-84cc-67606abd9755', 'authenticated', NULL, 'javiermar06@hotmail.com', '$2b$10$3RHyXnR4BG3NaxP8Ez82FuiGDMNCG7GhNaOsMFigy3BpIVOzCqHMW', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-14 03:51:04.122+00', NULL, '{}', false, '2025-12-08 19:24:06.266895+00', '2025-12-14 03:51:04.123886+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, '69681b09-5077-4f77-84cc-67606abd9755', 'authenticated', NULL, 'javiermar06@hotmail.com', '$2b$10$3RHyXnR4BG3NaxP8Ez82FuiGDMNCG7GhNaOsMFigy3BpIVOzCqHMW', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-14 03:51:04.122+00', NULL, '{}', false, '2025-12-08 19:24:06.266895+00', '2025-12-14 03:51:04.123886+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, 'f929d6df-8c29-461f-88f5-264facd879e9', 'authenticated', NULL, 'ju188an@gmail.com', '$2b$10$9vUERFnXApdfXuAI7DFve.aa8uDjI5bfm4CI75/EZ2cUre83RytKe', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-17 23:51:43.553+00', NULL, '{}', false, '2025-12-17 17:51:43.530434+00', '2025-12-17 23:51:43.55475+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, 'f929d6df-8c29-461f-88f5-264facd879e9', 'authenticated', NULL, 'ju188an@gmail.com', '$2b$10$9vUERFnXApdfXuAI7DFve.aa8uDjI5bfm4CI75/EZ2cUre83RytKe', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-17 23:51:43.553+00', NULL, '{}', false, '2025-12-17 17:51:43.530434+00', '2025-12-17 23:51:43.55475+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
@ -126,6 +127,7 @@ INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, fi
|
|||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('c0aecfcc-3b2f-4117-9f20-e0920df97dc0', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'segurauriel235@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '5d1839f6-b03f-4e12-b236-eca43f4674f2', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('c0aecfcc-3b2f-4117-9f20-e0920df97dc0', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'segurauriel235@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '5d1839f6-b03f-4e12-b236-eca43f4674f2', NULL);
|
||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('3dfcdc9d-de8a-45b3-a05f-b83b51097ef5', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'omarcitogonzalezzavaleta@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '5ae21325-7450-4c37-82f1-3f9bcd7b6f45', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('3dfcdc9d-de8a-45b3-a05f-b83b51097ef5', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'omarcitogonzalezzavaleta@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '5ae21325-7450-4c37-82f1-3f9bcd7b6f45', NULL);
|
||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('bb74b280-db90-4240-ab09-b8c6cf63d553', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'erickfranco462@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '2d9f05d4-44dd-42cd-97aa-d57bd06fecd0', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('bb74b280-db90-4240-ab09-b8c6cf63d553', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'erickfranco462@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '2d9f05d4-44dd-42cd-97aa-d57bd06fecd0', NULL);
|
||||||
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('0ae1bf21-39e3-4168-9632-457418c7a07d', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'rckrdmrd@gmail.com', NULL, 'rckrdmrd@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:37:09.278078+00', '2025-11-29 13:37:09.278078+00', '0ae1bf21-39e3-4168-9632-457418c7a07d', NULL);
|
||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('69681b09-5077-4f77-84cc-67606abd9755', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'Javier', ' Mar', 'javiermar06@hotmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-12-08 19:24:06.272257+00', '2025-12-08 19:24:06.272257+00', '69681b09-5077-4f77-84cc-67606abd9755', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('69681b09-5077-4f77-84cc-67606abd9755', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'Javier', ' Mar', 'javiermar06@hotmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-12-08 19:24:06.272257+00', '2025-12-08 19:24:06.272257+00', '69681b09-5077-4f77-84cc-67606abd9755', NULL);
|
||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('f929d6df-8c29-461f-88f5-264facd879e9', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'Juan', 'pa', 'ju188an@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-12-17 17:51:43.536295+00', '2025-12-17 17:51:43.536295+00', 'f929d6df-8c29-461f-88f5-264facd879e9', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('f929d6df-8c29-461f-88f5-264facd879e9', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'Juan', 'pa', 'ju188an@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-12-17 17:51:43.536295+00', '2025-12-17 17:51:43.536295+00', 'f929d6df-8c29-461f-88f5-264facd879e9', NULL);
|
||||||
|
|
||||||
|
|||||||
@ -45,5 +45,6 @@ instance_id,id,aud,role,email,encrypted_password,email_confirmed_at,invited_at,c
|
|||||||
00000000-0000-0000-0000-000000000000,cccccccc-cccc-cccc-cccc-cccccccccccc,authenticated,,student@gamilit.com,$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga,2025-11-29 13:26:50.289631+00,,"",,"",,"","",,2025-12-07 03:42:02.528+00,"{""provider"": ""email"", ""providers"": [""email""]}","{""name"": ""Estudiante Testing"", ""role"": ""student"", ""description"": ""Usuario estudiante de testing""}",f,2025-11-29 13:26:50.289631+00,2025-12-07 03:42:02.529507+00,,,,,,,,0,,,,f,,student,active
|
00000000-0000-0000-0000-000000000000,cccccccc-cccc-cccc-cccc-cccccccccccc,authenticated,,student@gamilit.com,$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga,2025-11-29 13:26:50.289631+00,,"",,"",,"","",,2025-12-07 03:42:02.528+00,"{""provider"": ""email"", ""providers"": [""email""]}","{""name"": ""Estudiante Testing"", ""role"": ""student"", ""description"": ""Usuario estudiante de testing""}",f,2025-11-29 13:26:50.289631+00,2025-12-07 03:42:02.529507+00,,,,,,,,0,,,,f,,student,active
|
||||||
00000000-0000-0000-0000-000000000000,bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb,authenticated,,teacher@gamilit.com,$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga,2025-11-29 13:26:50.289631+00,,"",,"",,"","",,,"{""provider"": ""email"", ""providers"": [""email""]}","{""name"": ""Profesor Testing"", ""role"": ""admin_teacher"", ""description"": ""Usuario profesor de testing""}",f,2025-11-29 13:26:50.289631+00,2025-11-29 13:26:50.289631+00,,,,,,,,0,,,,f,,admin_teacher,active
|
00000000-0000-0000-0000-000000000000,bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb,authenticated,,teacher@gamilit.com,$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga,2025-11-29 13:26:50.289631+00,,"",,"",,"","",,,"{""provider"": ""email"", ""providers"": [""email""]}","{""name"": ""Profesor Testing"", ""role"": ""admin_teacher"", ""description"": ""Usuario profesor de testing""}",f,2025-11-29 13:26:50.289631+00,2025-11-29 13:26:50.289631+00,,,,,,,,0,,,,f,,admin_teacher,active
|
||||||
00000000-0000-0000-0000-000000000000,aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa,authenticated,,admin@gamilit.com,$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga,2025-11-29 13:26:50.289631+00,,"",,"",,"","",,2025-12-01 00:54:19.615+00,"{""provider"": ""email"", ""providers"": [""email""]}","{""name"": ""Admin GAMILIT"", ""role"": ""super_admin"", ""description"": ""Usuario administrador de testing""}",f,2025-11-29 13:26:50.289631+00,2025-12-01 00:54:19.617766+00,,,,,,,,0,,,,f,,super_admin,active
|
00000000-0000-0000-0000-000000000000,aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa,authenticated,,admin@gamilit.com,$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga,2025-11-29 13:26:50.289631+00,,"",,"",,"","",,2025-12-01 00:54:19.615+00,"{""provider"": ""email"", ""providers"": [""email""]}","{""name"": ""Admin GAMILIT"", ""role"": ""super_admin"", ""description"": ""Usuario administrador de testing""}",f,2025-11-29 13:26:50.289631+00,2025-12-01 00:54:19.617766+00,,,,,,,,0,,,,f,,super_admin,active
|
||||||
|
,0ae1bf21-39e3-4168-9632-457418c7a07d,authenticated,,rckrdmrd@gmail.com,$2b$10$LiDdaJLA.ZvdFleamkMuvOcIrW0PQMEh5aVZ5Wg5pzhm7gwc5s.1C,,,,,,,,,,2025-12-09 01:22:42.784+00,,{},f,2025-11-29 13:37:09.271457+00,2025-12-09 01:22:42.785367+00,,,,,,,,0,,,,f,,student,active
|
||||||
,69681b09-5077-4f77-84cc-67606abd9755,authenticated,,javiermar06@hotmail.com,$2b$10$3RHyXnR4BG3NaxP8Ez82FuiGDMNCG7GhNaOsMFigy3BpIVOzCqHMW,,,,,,,,,,2025-12-14 03:51:04.122+00,,{},f,2025-12-08 19:24:06.266895+00,2025-12-14 03:51:04.123886+00,,,,,,,,0,,,,f,,student,active
|
,69681b09-5077-4f77-84cc-67606abd9755,authenticated,,javiermar06@hotmail.com,$2b$10$3RHyXnR4BG3NaxP8Ez82FuiGDMNCG7GhNaOsMFigy3BpIVOzCqHMW,,,,,,,,,,2025-12-14 03:51:04.122+00,,{},f,2025-12-08 19:24:06.266895+00,2025-12-14 03:51:04.123886+00,,,,,,,,0,,,,f,,student,active
|
||||||
,f929d6df-8c29-461f-88f5-264facd879e9,authenticated,,ju188an@gmail.com,$2b$10$9vUERFnXApdfXuAI7DFve.aa8uDjI5bfm4CI75/EZ2cUre83RytKe,,,,,,,,,,2025-12-17 23:51:43.553+00,,{},f,2025-12-17 17:51:43.530434+00,2025-12-17 23:51:43.55475+00,,,,,,,,0,,,,f,,student,active
|
,f929d6df-8c29-461f-88f5-264facd879e9,authenticated,,ju188an@gmail.com,$2b$10$9vUERFnXApdfXuAI7DFve.aa8uDjI5bfm4CI75/EZ2cUre83RytKe,,,,,,,,,,2025-12-17 23:51:43.553+00,,{},f,2025-12-17 17:51:43.530434+00,2025-12-17 23:51:43.55475+00,,,,,,,,0,,,,f,,student,active
|
||||||
|
|||||||
|
@ -45,5 +45,6 @@ de1511df-f963-4ff6-8e3f-2225ba493879,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,"","
|
|||||||
26168044-3b5c-43f6-a757-833ba1485d41,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,"","",enriquecuevascbtis136@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-11-29 13:30:54.277737+00,2025-11-29 13:30:54.277737+00,1efe491d-98ef-4c02-acd1-3135f7289072,
|
26168044-3b5c-43f6-a757-833ba1485d41,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,"","",enriquecuevascbtis136@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-11-29 13:30:54.277737+00,2025-11-29 13:30:54.277737+00,1efe491d-98ef-4c02-acd1-3135f7289072,
|
||||||
e742724a-0ff6-4760-884b-866835460045,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,"","",fl432025@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-11-29 13:30:54.277737+00,2025-11-29 13:30:54.277737+00,547eb778-4782-4681-b198-c731bba36147,
|
e742724a-0ff6-4760-884b-866835460045,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,"","",fl432025@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-11-29 13:30:54.277737+00,2025-11-29 13:30:54.277737+00,547eb778-4782-4681-b198-c731bba36147,
|
||||||
3ce354c8-bcac-44c6-9a94-5274e5f9b389,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,"","",abdallahxelhaneriavega@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-11-29 13:30:54.277737+00,2025-11-29 13:30:54.277737+00,f4c46f46-3fb9-40bf-a52b-a8ad2e6a92e1,
|
3ce354c8-bcac-44c6-9a94-5274e5f9b389,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,"","",abdallahxelhaneriavega@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-11-29 13:30:54.277737+00,2025-11-29 13:30:54.277737+00,f4c46f46-3fb9-40bf-a52b-a8ad2e6a92e1,
|
||||||
|
0ae1bf21-39e3-4168-9632-457418c7a07d,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,rckrdmrd@gmail.com,,rckrdmrd@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-11-29 13:37:09.278078+00,2025-11-29 13:37:09.278078+00,0ae1bf21-39e3-4168-9632-457418c7a07d,
|
||||||
69681b09-5077-4f77-84cc-67606abd9755,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,Javier, Mar,javiermar06@hotmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-12-08 19:24:06.272257+00,2025-12-08 19:24:06.272257+00,69681b09-5077-4f77-84cc-67606abd9755,
|
69681b09-5077-4f77-84cc-67606abd9755,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,Javier, Mar,javiermar06@hotmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-12-08 19:24:06.272257+00,2025-12-08 19:24:06.272257+00,69681b09-5077-4f77-84cc-67606abd9755,
|
||||||
f929d6df-8c29-461f-88f5-264facd879e9,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,Juan,pa,ju188an@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-12-17 17:51:43.536295+00,2025-12-17 17:51:43.536295+00,f929d6df-8c29-461f-88f5-264facd879e9,
|
f929d6df-8c29-461f-88f5-264facd879e9,a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11,,,Juan,pa,ju188an@gmail.com,,,,,,,,student,active,f,f,"{""theme"": ""detective"", ""language"": ""es"", ""timezone"": ""America/Mexico_City"", ""sound_enabled"": true, ""notifications_enabled"": true}",,,{},2025-12-17 17:51:43.536295+00,2025-12-17 17:51:43.536295+00,f929d6df-8c29-461f-88f5-264facd879e9,
|
||||||
|
|||||||
|
@ -67,6 +67,7 @@ INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, e
|
|||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', '5ae21325-7450-4c37-82f1-3f9bcd7b6f45', 'authenticated', NULL, 'omarcitogonzalezzavaleta@gmail.com', '$2b$10$RRk3DAgQdiikxVImFIMqquqB.TNpKs3E.RNFtt1rwwTzO24uShri.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:17:07.610076+00', '2025-11-25 08:17:07.610076+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', '5ae21325-7450-4c37-82f1-3f9bcd7b6f45', 'authenticated', NULL, 'omarcitogonzalezzavaleta@gmail.com', '$2b$10$RRk3DAgQdiikxVImFIMqquqB.TNpKs3E.RNFtt1rwwTzO24uShri.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:17:07.610076+00', '2025-11-25 08:17:07.610076+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', 'a4d27774-8a51-4660-ad2f-81d0dfd3a5a7', 'authenticated', NULL, 'gustavobm2024cbtis@gmail.com', '$2b$10$lg7KRUTPofcx4Rtyey8J7.XO0gmdBLCFIfK5uP08mqT0qUIl1aTJq', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:20:49.649184+00', '2025-11-25 08:20:49.649184+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', 'a4d27774-8a51-4660-ad2f-81d0dfd3a5a7', 'authenticated', NULL, 'gustavobm2024cbtis@gmail.com', '$2b$10$lg7KRUTPofcx4Rtyey8J7.XO0gmdBLCFIfK5uP08mqT0qUIl1aTJq', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:20:49.649184+00', '2025-11-25 08:20:49.649184+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', '6e30164a-78b0-49b0-bd21-23d7c6c03349', 'authenticated', NULL, 'marianaxsotoxt22@gmail.com', '$2b$10$GQC9yTWiP2vP9GUp0gnhUeLjmw70EI4JQhfJBZbMOlCNXGXb/bt5O', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:33:18.150784+00', '2025-11-25 08:33:18.150784+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', '6e30164a-78b0-49b0-bd21-23d7c6c03349', 'authenticated', NULL, 'marianaxsotoxt22@gmail.com', '$2b$10$GQC9yTWiP2vP9GUp0gnhUeLjmw70EI4JQhfJBZbMOlCNXGXb/bt5O', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '{"provider": "email", "providers": ["email"]}', '{"last_name": "", "first_name": ""}', false, '2025-11-25 08:33:18.150784+00', '2025-11-25 08:33:18.150784+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, '0ae1bf21-39e3-4168-9632-457418c7a07d', 'authenticated', NULL, 'rckrdmrd@gmail.com', '$2b$10$LiDdaJLA.ZvdFleamkMuvOcIrW0PQMEh5aVZ5Wg5pzhm7gwc5s.1C', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-09 01:22:42.784+00', NULL, '{}', false, '2025-11-29 13:37:09.271457+00', '2025-12-09 01:22:42.785367+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'authenticated', NULL, 'admin@gamilit.com', '$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga', '2025-11-29 13:26:50.289631+00', NULL, '', NULL, '', NULL, '', '', NULL, '2025-12-01 00:54:19.615+00', '{"provider": "email", "providers": ["email"]}', '{"name": "Admin GAMILIT", "role": "super_admin", "description": "Usuario administrador de testing"}', false, '2025-11-29 13:26:50.289631+00', '2025-12-01 00:54:19.617766+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'super_admin', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES ('00000000-0000-0000-0000-000000000000', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'authenticated', NULL, 'admin@gamilit.com', '$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga', '2025-11-29 13:26:50.289631+00', NULL, '', NULL, '', NULL, '', '', NULL, '2025-12-01 00:54:19.615+00', '{"provider": "email", "providers": ["email"]}', '{"name": "Admin GAMILIT", "role": "super_admin", "description": "Usuario administrador de testing"}', false, '2025-11-29 13:26:50.289631+00', '2025-12-01 00:54:19.617766+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'super_admin', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, '69681b09-5077-4f77-84cc-67606abd9755', 'authenticated', NULL, 'javiermar06@hotmail.com', '$2b$10$3RHyXnR4BG3NaxP8Ez82FuiGDMNCG7GhNaOsMFigy3BpIVOzCqHMW', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-14 03:51:04.122+00', NULL, '{}', false, '2025-12-08 19:24:06.266895+00', '2025-12-14 03:51:04.123886+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, '69681b09-5077-4f77-84cc-67606abd9755', 'authenticated', NULL, 'javiermar06@hotmail.com', '$2b$10$3RHyXnR4BG3NaxP8Ez82FuiGDMNCG7GhNaOsMFigy3BpIVOzCqHMW', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-14 03:51:04.122+00', NULL, '{}', false, '2025-12-08 19:24:06.266895+00', '2025-12-14 03:51:04.123886+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, 'f929d6df-8c29-461f-88f5-264facd879e9', 'authenticated', NULL, 'ju188an@gmail.com', '$2b$10$9vUERFnXApdfXuAI7DFve.aa8uDjI5bfm4CI75/EZ2cUre83RytKe', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-17 23:51:43.553+00', NULL, '{}', false, '2025-12-17 17:51:43.530434+00', '2025-12-17 23:51:43.55475+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
INSERT INTO auth.users (instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, invited_at, confirmation_token, confirmation_sent_at, recovery_token, recovery_sent_at, email_change_token_new, email_change, email_change_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_super_admin, created_at, updated_at, phone, phone_confirmed_at, phone_change, phone_change_token, phone_change_sent_at, confirmed_at, email_change_token_current, email_change_confirm_status, banned_until, reauthentication_token, reauthentication_sent_at, is_sso_user, deleted_at, gamilit_role, status) VALUES (NULL, 'f929d6df-8c29-461f-88f5-264facd879e9', 'authenticated', NULL, 'ju188an@gmail.com', '$2b$10$9vUERFnXApdfXuAI7DFve.aa8uDjI5bfm4CI75/EZ2cUre83RytKe', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2025-12-17 23:51:43.553+00', NULL, '{}', false, '2025-12-17 17:51:43.530434+00', '2025-12-17 23:51:43.55475+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, false, NULL, 'student', 'active');
|
||||||
@ -122,6 +123,7 @@ INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, fi
|
|||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('c0aecfcc-3b2f-4117-9f20-e0920df97dc0', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'segurauriel235@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '5d1839f6-b03f-4e12-b236-eca43f4674f2', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('c0aecfcc-3b2f-4117-9f20-e0920df97dc0', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'segurauriel235@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '5d1839f6-b03f-4e12-b236-eca43f4674f2', NULL);
|
||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('3dfcdc9d-de8a-45b3-a05f-b83b51097ef5', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'omarcitogonzalezzavaleta@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '5ae21325-7450-4c37-82f1-3f9bcd7b6f45', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('3dfcdc9d-de8a-45b3-a05f-b83b51097ef5', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'omarcitogonzalezzavaleta@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '5ae21325-7450-4c37-82f1-3f9bcd7b6f45', NULL);
|
||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('bb74b280-db90-4240-ab09-b8c6cf63d553', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'erickfranco462@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '2d9f05d4-44dd-42cd-97aa-d57bd06fecd0', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('bb74b280-db90-4240-ab09-b8c6cf63d553', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, '', '', 'erickfranco462@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:30:54.277737+00', '2025-11-29 13:30:54.277737+00', '2d9f05d4-44dd-42cd-97aa-d57bd06fecd0', NULL);
|
||||||
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('0ae1bf21-39e3-4168-9632-457418c7a07d', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'rckrdmrd@gmail.com', NULL, 'rckrdmrd@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-11-29 13:37:09.278078+00', '2025-11-29 13:37:09.278078+00', '0ae1bf21-39e3-4168-9632-457418c7a07d', NULL);
|
||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('69681b09-5077-4f77-84cc-67606abd9755', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'Javier', ' Mar', 'javiermar06@hotmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-12-08 19:24:06.272257+00', '2025-12-08 19:24:06.272257+00', '69681b09-5077-4f77-84cc-67606abd9755', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('69681b09-5077-4f77-84cc-67606abd9755', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'Javier', ' Mar', 'javiermar06@hotmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-12-08 19:24:06.272257+00', '2025-12-08 19:24:06.272257+00', '69681b09-5077-4f77-84cc-67606abd9755', NULL);
|
||||||
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('f929d6df-8c29-461f-88f5-264facd879e9', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'Juan', 'pa', 'ju188an@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-12-17 17:51:43.536295+00', '2025-12-17 17:51:43.536295+00', 'f929d6df-8c29-461f-88f5-264facd879e9', NULL);
|
INSERT INTO auth_management.profiles (id, tenant_id, display_name, full_name, first_name, last_name, email, avatar_url, bio, phone, date_of_birth, grade_level, student_id, school_id, role, status, email_verified, phone_verified, preferences, last_sign_in_at, last_activity_at, metadata, created_at, updated_at, user_id, deleted_at) VALUES ('f929d6df-8c29-461f-88f5-264facd879e9', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NULL, NULL, 'Juan', 'pa', 'ju188an@gmail.com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'student', 'active', false, false, '{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "sound_enabled": true, "notifications_enabled": true}', NULL, NULL, '{}', '2025-12-17 17:51:43.536295+00', '2025-12-17 17:51:43.536295+00', 'f929d6df-8c29-461f-88f5-264facd879e9', NULL);
|
||||||
|
|
||||||
|
|||||||
@ -77,15 +77,5 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA gamilit GRANT EXECUTE ON FUNCTIONS TO gamilit
|
|||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA auth GRANT EXECUTE ON FUNCTIONS TO gamilit_user;
|
ALTER DEFAULT PRIVILEGES IN SCHEMA auth GRANT EXECUTE ON FUNCTIONS TO gamilit_user;
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO gamilit_user;
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO gamilit_user;
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- BYPASS RLS for gamilit_user
|
|
||||||
-- =====================================================
|
|
||||||
-- Added: 2025-12-18 (FIX: Application user needs to bypass RLS)
|
|
||||||
-- Reason: The application manages RLS context via app.current_user_id
|
|
||||||
-- but needs BYPASSRLS to perform operations on behalf of users
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
ALTER ROLE gamilit_user BYPASSRLS;
|
|
||||||
|
|
||||||
-- Verification
|
-- Verification
|
||||||
SELECT 'Permisos otorgados exitosamente a gamilit_user (incluyendo BYPASSRLS)' as status;
|
SELECT 'Permisos otorgados exitosamente a gamilit_user' as status;
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Indexes for Teacher Portal Optimization
|
|
||||||
-- Schema: progress_tracking
|
|
||||||
-- Created: 2025-12-18 (P1-02 - FASE 5 Implementation)
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
-- Index: module_progress by classroom and status
|
|
||||||
-- Purpose: Fast lookup of progress data for classroom analytics
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_module_progress_classroom_status
|
|
||||||
ON progress_tracking.module_progress(classroom_id, status);
|
|
||||||
|
|
||||||
COMMENT ON INDEX progress_tracking.idx_module_progress_classroom_status IS
|
|
||||||
'P1-02: Optimizes classroom progress overview queries';
|
|
||||||
|
|
||||||
-- Index: intervention_alerts by teacher and status
|
|
||||||
-- Purpose: Fast lookup of pending alerts for teacher dashboard
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_intervention_alerts_teacher_status
|
|
||||||
ON progress_tracking.student_intervention_alerts(teacher_id, status)
|
|
||||||
WHERE status IN ('pending', 'acknowledged');
|
|
||||||
|
|
||||||
COMMENT ON INDEX progress_tracking.idx_intervention_alerts_teacher_status IS
|
|
||||||
'P1-02: Optimizes teacher alerts panel queries';
|
|
||||||
|
|
||||||
-- Index: exercise_submissions by student and date
|
|
||||||
-- Purpose: Fast lookup of recent submissions for progress tracking
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_exercise_submissions_student_date
|
|
||||||
ON progress_tracking.exercise_submissions(student_id, submitted_at DESC);
|
|
||||||
|
|
||||||
COMMENT ON INDEX progress_tracking.idx_exercise_submissions_student_date IS
|
|
||||||
'P1-02: Optimizes student timeline and recent activity queries';
|
|
||||||
|
|
||||||
-- Index: exercise_submissions needing review
|
|
||||||
-- Purpose: Fast lookup of submissions pending manual review
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_exercise_submissions_needs_review
|
|
||||||
ON progress_tracking.exercise_submissions(needs_review, submitted_at DESC)
|
|
||||||
WHERE needs_review = true;
|
|
||||||
|
|
||||||
COMMENT ON INDEX progress_tracking.idx_exercise_submissions_needs_review IS
|
|
||||||
'P1-02: Optimizes teacher review queue';
|
|
||||||
@ -14,12 +14,9 @@ ALTER TABLE progress_tracking.module_progress ENABLE ROW LEVEL SECURITY;
|
|||||||
ALTER TABLE progress_tracking.exercise_attempts ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE progress_tracking.exercise_attempts ENABLE ROW LEVEL SECURITY;
|
||||||
ALTER TABLE progress_tracking.exercise_submissions ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE progress_tracking.exercise_submissions ENABLE ROW LEVEL SECURITY;
|
||||||
ALTER TABLE progress_tracking.learning_sessions ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE progress_tracking.learning_sessions ENABLE ROW LEVEL SECURITY;
|
||||||
-- P1-01: Added 2025-12-18 - Teacher notes RLS
|
|
||||||
ALTER TABLE progress_tracking.teacher_notes ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Comentarios
|
-- Comentarios
|
||||||
COMMENT ON TABLE progress_tracking.module_progress IS 'RLS enabled: Progreso de módulos - lectura propia + teacher + admin';
|
COMMENT ON TABLE progress_tracking.module_progress IS 'RLS enabled: Progreso de módulos - lectura propia + teacher + admin';
|
||||||
COMMENT ON TABLE progress_tracking.exercise_attempts IS 'RLS enabled: Intentos de ejercicios - lectura propia + teacher';
|
COMMENT ON TABLE progress_tracking.exercise_attempts IS 'RLS enabled: Intentos de ejercicios - lectura propia + teacher';
|
||||||
COMMENT ON TABLE progress_tracking.exercise_submissions IS 'RLS enabled: Entregas de ejercicios - gestión propia + calificación teacher';
|
COMMENT ON TABLE progress_tracking.exercise_submissions IS 'RLS enabled: Entregas de ejercicios - gestión propia + calificación teacher';
|
||||||
COMMENT ON TABLE progress_tracking.learning_sessions IS 'RLS enabled: Sesiones de aprendizaje de usuarios';
|
COMMENT ON TABLE progress_tracking.learning_sessions IS 'RLS enabled: Sesiones de aprendizaje de usuarios';
|
||||||
COMMENT ON TABLE progress_tracking.teacher_notes IS 'RLS enabled: Notas de profesores - lectura/escritura propia';
|
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- RLS Policies for: progress_tracking.teacher_notes
|
|
||||||
-- Description: Teacher notes with self-access only
|
|
||||||
-- Created: 2025-12-18 (P1-01 - FASE 5 Implementation)
|
|
||||||
-- Policies: 4 (SELECT, INSERT, UPDATE, DELETE)
|
|
||||||
-- =====================================================
|
|
||||||
--
|
|
||||||
-- Security Strategy:
|
|
||||||
-- - Self-service: Teachers can CRUD their own notes
|
|
||||||
-- - No student access: Notes are private to teachers
|
|
||||||
-- - No cross-teacher access: Teachers cannot see other teachers' notes
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- TABLE: progress_tracking.teacher_notes
|
|
||||||
-- Policies: 4 (SELECT: 1, INSERT: 1, UPDATE: 1, DELETE: 1)
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
DROP POLICY IF EXISTS teacher_notes_select_own ON progress_tracking.teacher_notes;
|
|
||||||
DROP POLICY IF EXISTS teacher_notes_insert_own ON progress_tracking.teacher_notes;
|
|
||||||
DROP POLICY IF EXISTS teacher_notes_update_own ON progress_tracking.teacher_notes;
|
|
||||||
DROP POLICY IF EXISTS teacher_notes_delete_own ON progress_tracking.teacher_notes;
|
|
||||||
|
|
||||||
-- Policy: teacher_notes_select_own
|
|
||||||
-- Purpose: Teachers can read their own notes
|
|
||||||
CREATE POLICY teacher_notes_select_own
|
|
||||||
ON progress_tracking.teacher_notes
|
|
||||||
AS PERMISSIVE
|
|
||||||
FOR SELECT
|
|
||||||
TO public
|
|
||||||
USING (teacher_id = current_setting('app.current_user_id', true)::uuid);
|
|
||||||
|
|
||||||
COMMENT ON POLICY teacher_notes_select_own ON progress_tracking.teacher_notes IS
|
|
||||||
'Teachers can see their own notes about students';
|
|
||||||
|
|
||||||
-- Policy: teacher_notes_insert_own
|
|
||||||
-- Purpose: Teachers can create notes for any student
|
|
||||||
CREATE POLICY teacher_notes_insert_own
|
|
||||||
ON progress_tracking.teacher_notes
|
|
||||||
AS PERMISSIVE
|
|
||||||
FOR INSERT
|
|
||||||
TO public
|
|
||||||
WITH CHECK (
|
|
||||||
teacher_id = current_setting('app.current_user_id', true)::uuid
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1 FROM auth_management.user_roles ur
|
|
||||||
WHERE ur.user_id = current_setting('app.current_user_id', true)::uuid
|
|
||||||
AND ur.role = 'admin_teacher'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
COMMENT ON POLICY teacher_notes_insert_own ON progress_tracking.teacher_notes IS
|
|
||||||
'Teachers can create notes about students (teacher role required)';
|
|
||||||
|
|
||||||
-- Policy: teacher_notes_update_own
|
|
||||||
-- Purpose: Teachers can update their own notes
|
|
||||||
CREATE POLICY teacher_notes_update_own
|
|
||||||
ON progress_tracking.teacher_notes
|
|
||||||
AS PERMISSIVE
|
|
||||||
FOR UPDATE
|
|
||||||
TO public
|
|
||||||
USING (teacher_id = current_setting('app.current_user_id', true)::uuid)
|
|
||||||
WITH CHECK (teacher_id = current_setting('app.current_user_id', true)::uuid);
|
|
||||||
|
|
||||||
COMMENT ON POLICY teacher_notes_update_own ON progress_tracking.teacher_notes IS
|
|
||||||
'Teachers can update their own notes';
|
|
||||||
|
|
||||||
-- Policy: teacher_notes_delete_own
|
|
||||||
-- Purpose: Teachers can delete their own notes
|
|
||||||
CREATE POLICY teacher_notes_delete_own
|
|
||||||
ON progress_tracking.teacher_notes
|
|
||||||
AS PERMISSIVE
|
|
||||||
FOR DELETE
|
|
||||||
TO public
|
|
||||||
USING (teacher_id = current_setting('app.current_user_id', true)::uuid);
|
|
||||||
|
|
||||||
COMMENT ON POLICY teacher_notes_delete_own ON progress_tracking.teacher_notes IS
|
|
||||||
'Teachers can delete their own notes';
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- SUMMARY: teacher_notes RLS
|
|
||||||
-- =====================================================
|
|
||||||
-- Total policies: 4
|
|
||||||
-- - SELECT: 1 (own notes only)
|
|
||||||
-- - INSERT: 1 (teacher role required)
|
|
||||||
-- - UPDATE: 1 (own notes only)
|
|
||||||
-- - DELETE: 1 (own notes only)
|
|
||||||
-- =====================================================
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- TABLE: progress_tracking.teacher_interventions
|
|
||||||
-- =============================================================================
|
|
||||||
-- Purpose: Track teacher actions/interventions in response to student alerts
|
|
||||||
-- Priority: P2-04 - Teacher Portal intervention tracking
|
|
||||||
-- Created: 2025-12-18
|
|
||||||
--
|
|
||||||
-- DESCRIPTION:
|
|
||||||
-- This table records the specific actions teachers take when responding to
|
|
||||||
-- student intervention alerts. Unlike the resolution in student_intervention_alerts
|
|
||||||
-- which only captures the final resolution, this table captures the full history
|
|
||||||
-- of all interventions taken, including follow-ups and multi-step interventions.
|
|
||||||
--
|
|
||||||
-- USE CASES:
|
|
||||||
-- - Parent contact records
|
|
||||||
-- - One-on-one sessions scheduled/completed
|
|
||||||
-- - Resource assignments
|
|
||||||
-- - Peer tutoring arrangements
|
|
||||||
-- - Accommodation adjustments
|
|
||||||
-- - Follow-up tracking
|
|
||||||
-- =============================================================================
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS progress_tracking.teacher_interventions (
|
|
||||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
|
|
||||||
-- Alert reference (optional - can also be standalone intervention)
|
|
||||||
alert_id uuid,
|
|
||||||
|
|
||||||
-- Core identifiers
|
|
||||||
student_id uuid NOT NULL,
|
|
||||||
teacher_id uuid NOT NULL,
|
|
||||||
classroom_id uuid,
|
|
||||||
|
|
||||||
-- Intervention details
|
|
||||||
intervention_type text NOT NULL,
|
|
||||||
title text NOT NULL,
|
|
||||||
description text,
|
|
||||||
|
|
||||||
-- Action tracking
|
|
||||||
action_taken text NOT NULL,
|
|
||||||
outcome text,
|
|
||||||
|
|
||||||
-- Scheduling
|
|
||||||
scheduled_date timestamp with time zone,
|
|
||||||
completed_date timestamp with time zone,
|
|
||||||
|
|
||||||
-- Status tracking
|
|
||||||
status text DEFAULT 'planned'::text NOT NULL,
|
|
||||||
priority text DEFAULT 'medium'::text NOT NULL,
|
|
||||||
|
|
||||||
-- Follow-up
|
|
||||||
follow_up_required boolean DEFAULT false,
|
|
||||||
follow_up_date timestamp with time zone,
|
|
||||||
follow_up_notes text,
|
|
||||||
|
|
||||||
-- Communication records
|
|
||||||
parent_contacted boolean DEFAULT false,
|
|
||||||
parent_contact_date timestamp with time zone,
|
|
||||||
parent_contact_notes text,
|
|
||||||
|
|
||||||
-- Effectiveness tracking
|
|
||||||
effectiveness_rating integer,
|
|
||||||
student_response text,
|
|
||||||
|
|
||||||
-- Metadata
|
|
||||||
notes text,
|
|
||||||
metadata jsonb DEFAULT '{}'::jsonb,
|
|
||||||
|
|
||||||
-- Multi-tenant
|
|
||||||
tenant_id uuid NOT NULL,
|
|
||||||
|
|
||||||
-- Audit
|
|
||||||
created_at timestamp with time zone DEFAULT gamilit.now_mexico() NOT NULL,
|
|
||||||
updated_at timestamp with time zone DEFAULT gamilit.now_mexico() NOT NULL,
|
|
||||||
|
|
||||||
-- Constraints
|
|
||||||
CONSTRAINT teacher_interventions_pkey PRIMARY KEY (id),
|
|
||||||
CONSTRAINT teacher_interventions_type_check CHECK (intervention_type IN (
|
|
||||||
'one_on_one_session', -- Individual tutoring session
|
|
||||||
'parent_contact', -- Parent/guardian communication
|
|
||||||
'resource_assignment', -- Additional materials assigned
|
|
||||||
'peer_tutoring', -- Paired with another student
|
|
||||||
'accommodation', -- Learning accommodations
|
|
||||||
'referral', -- Referral to specialist
|
|
||||||
'behavior_plan', -- Behavioral intervention
|
|
||||||
'progress_check', -- Scheduled progress review
|
|
||||||
'encouragement', -- Motivational intervention
|
|
||||||
'schedule_adjustment', -- Modified schedule/deadlines
|
|
||||||
'other' -- Custom intervention
|
|
||||||
)),
|
|
||||||
CONSTRAINT teacher_interventions_status_check CHECK (status IN (
|
|
||||||
'planned', -- Scheduled but not started
|
|
||||||
'in_progress', -- Currently ongoing
|
|
||||||
'completed', -- Successfully completed
|
|
||||||
'cancelled', -- Cancelled before completion
|
|
||||||
'rescheduled' -- Moved to new date
|
|
||||||
)),
|
|
||||||
CONSTRAINT teacher_interventions_priority_check CHECK (priority IN (
|
|
||||||
'low',
|
|
||||||
'medium',
|
|
||||||
'high',
|
|
||||||
'urgent'
|
|
||||||
)),
|
|
||||||
CONSTRAINT teacher_interventions_effectiveness_check CHECK (
|
|
||||||
effectiveness_rating IS NULL OR
|
|
||||||
(effectiveness_rating >= 1 AND effectiveness_rating <= 5)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Set ownership
|
|
||||||
ALTER TABLE progress_tracking.teacher_interventions OWNER TO gamilit_user;
|
|
||||||
|
|
||||||
-- Comments
|
|
||||||
COMMENT ON TABLE progress_tracking.teacher_interventions IS
|
|
||||||
'Records teacher intervention actions for at-risk students. Tracks full intervention history including scheduling, outcomes, parent contact, and effectiveness.';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN progress_tracking.teacher_interventions.intervention_type IS
|
|
||||||
'Type of intervention: one_on_one_session, parent_contact, resource_assignment, peer_tutoring, accommodation, referral, behavior_plan, progress_check, encouragement, schedule_adjustment, other';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN progress_tracking.teacher_interventions.status IS
|
|
||||||
'Current status: planned, in_progress, completed, cancelled, rescheduled';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN progress_tracking.teacher_interventions.effectiveness_rating IS
|
|
||||||
'Teacher rating of intervention effectiveness (1-5 scale). NULL if not yet rated.';
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
CREATE INDEX idx_teacher_interventions_alert ON progress_tracking.teacher_interventions(alert_id);
|
|
||||||
CREATE INDEX idx_teacher_interventions_student ON progress_tracking.teacher_interventions(student_id);
|
|
||||||
CREATE INDEX idx_teacher_interventions_teacher ON progress_tracking.teacher_interventions(teacher_id);
|
|
||||||
CREATE INDEX idx_teacher_interventions_classroom ON progress_tracking.teacher_interventions(classroom_id);
|
|
||||||
CREATE INDEX idx_teacher_interventions_status ON progress_tracking.teacher_interventions(status);
|
|
||||||
CREATE INDEX idx_teacher_interventions_type ON progress_tracking.teacher_interventions(intervention_type);
|
|
||||||
CREATE INDEX idx_teacher_interventions_tenant ON progress_tracking.teacher_interventions(tenant_id);
|
|
||||||
CREATE INDEX idx_teacher_interventions_scheduled ON progress_tracking.teacher_interventions(scheduled_date)
|
|
||||||
WHERE status IN ('planned', 'in_progress');
|
|
||||||
CREATE INDEX idx_teacher_interventions_follow_up ON progress_tracking.teacher_interventions(follow_up_date)
|
|
||||||
WHERE follow_up_required = true;
|
|
||||||
|
|
||||||
-- Foreign keys
|
|
||||||
ALTER TABLE progress_tracking.teacher_interventions
|
|
||||||
ADD CONSTRAINT teacher_interventions_alert_fkey
|
|
||||||
FOREIGN KEY (alert_id) REFERENCES progress_tracking.student_intervention_alerts(id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
ALTER TABLE progress_tracking.teacher_interventions
|
|
||||||
ADD CONSTRAINT teacher_interventions_student_fkey
|
|
||||||
FOREIGN KEY (student_id) REFERENCES auth_management.profiles(id) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE progress_tracking.teacher_interventions
|
|
||||||
ADD CONSTRAINT teacher_interventions_teacher_fkey
|
|
||||||
FOREIGN KEY (teacher_id) REFERENCES auth_management.profiles(id) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE progress_tracking.teacher_interventions
|
|
||||||
ADD CONSTRAINT teacher_interventions_classroom_fkey
|
|
||||||
FOREIGN KEY (classroom_id) REFERENCES social_features.classrooms(id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
ALTER TABLE progress_tracking.teacher_interventions
|
|
||||||
ADD CONSTRAINT teacher_interventions_tenant_fkey
|
|
||||||
FOREIGN KEY (tenant_id) REFERENCES auth_management.tenants(id) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
-- Trigger for updated_at
|
|
||||||
CREATE TRIGGER trg_teacher_interventions_updated_at
|
|
||||||
BEFORE UPDATE ON progress_tracking.teacher_interventions
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION gamilit.update_updated_at_column();
|
|
||||||
|
|
||||||
-- =============================================================================
|
|
||||||
-- ROW LEVEL SECURITY
|
|
||||||
-- =============================================================================
|
|
||||||
|
|
||||||
ALTER TABLE progress_tracking.teacher_interventions ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Teachers can view and manage their own interventions
|
|
||||||
CREATE POLICY teacher_manage_own_interventions ON progress_tracking.teacher_interventions
|
|
||||||
FOR ALL
|
|
||||||
USING (teacher_id = gamilit.get_current_user_id())
|
|
||||||
WITH CHECK (teacher_id = gamilit.get_current_user_id());
|
|
||||||
|
|
||||||
-- Teachers can view interventions for students in their classrooms
|
|
||||||
CREATE POLICY teacher_view_classroom_interventions ON progress_tracking.teacher_interventions
|
|
||||||
FOR SELECT
|
|
||||||
USING (EXISTS (
|
|
||||||
SELECT 1 FROM social_features.teacher_classrooms tc
|
|
||||||
WHERE tc.classroom_id = teacher_interventions.classroom_id
|
|
||||||
AND tc.teacher_id = gamilit.get_current_user_id()
|
|
||||||
AND tc.tenant_id = teacher_interventions.tenant_id
|
|
||||||
));
|
|
||||||
|
|
||||||
-- Admins can view all interventions in their tenant
|
|
||||||
CREATE POLICY admin_view_tenant_interventions ON progress_tracking.teacher_interventions
|
|
||||||
FOR SELECT
|
|
||||||
USING (
|
|
||||||
tenant_id IN (
|
|
||||||
SELECT p.tenant_id FROM auth_management.profiles p
|
|
||||||
WHERE p.id = gamilit.get_current_user_id()
|
|
||||||
)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1 FROM auth_management.profiles p
|
|
||||||
WHERE p.id = gamilit.get_current_user_id()
|
|
||||||
AND p.role IN ('SUPER_ADMIN', 'ADMIN_TEACHER')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Grants
|
|
||||||
GRANT ALL ON TABLE progress_tracking.teacher_interventions TO gamilit_user;
|
|
||||||
GRANT SELECT ON TABLE progress_tracking.teacher_interventions TO authenticated;
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- VIEW: progress_tracking.teacher_pending_reviews
|
|
||||||
-- =============================================================================
|
|
||||||
-- Purpose: Consolidated view of student submissions requiring teacher review
|
|
||||||
-- Priority: P2-05 - Teacher Portal pending reviews dashboard
|
|
||||||
-- Created: 2025-12-18
|
|
||||||
--
|
|
||||||
-- DESCRIPTION:
|
|
||||||
-- This view aggregates exercise submissions that need manual grading or review,
|
|
||||||
-- providing teachers with a prioritized queue of work items. It includes
|
|
||||||
-- student info, exercise details, submission timestamps, and urgency indicators.
|
|
||||||
--
|
|
||||||
-- USE CASES:
|
|
||||||
-- - Teacher dashboard pending review count
|
|
||||||
-- - Grading queue interface
|
|
||||||
-- - Priority-based review workflow
|
|
||||||
-- - Bulk grading operations
|
|
||||||
-- =============================================================================
|
|
||||||
|
|
||||||
DROP VIEW IF EXISTS progress_tracking.teacher_pending_reviews CASCADE;
|
|
||||||
|
|
||||||
CREATE VIEW progress_tracking.teacher_pending_reviews AS
|
|
||||||
SELECT
|
|
||||||
-- Submission identifiers
|
|
||||||
es.id AS submission_id,
|
|
||||||
es.exercise_id,
|
|
||||||
es.user_id AS student_id,
|
|
||||||
|
|
||||||
-- Student info
|
|
||||||
p.full_name AS student_name,
|
|
||||||
p.username AS student_username,
|
|
||||||
|
|
||||||
-- Exercise info
|
|
||||||
e.title AS exercise_title,
|
|
||||||
e.mechanic_type,
|
|
||||||
e.exercise_type,
|
|
||||||
m.title AS module_title,
|
|
||||||
m.module_order,
|
|
||||||
|
|
||||||
-- Classroom info
|
|
||||||
cm.classroom_id,
|
|
||||||
c.name AS classroom_name,
|
|
||||||
|
|
||||||
-- Submission details
|
|
||||||
es.score,
|
|
||||||
es.time_spent,
|
|
||||||
es.attempts,
|
|
||||||
es.answers,
|
|
||||||
es.feedback,
|
|
||||||
es.submitted_at,
|
|
||||||
es.created_at AS submission_date,
|
|
||||||
|
|
||||||
-- Review status
|
|
||||||
CASE
|
|
||||||
WHEN es.graded_at IS NOT NULL THEN 'graded'
|
|
||||||
WHEN es.submitted_at IS NOT NULL THEN 'pending'
|
|
||||||
ELSE 'in_progress'
|
|
||||||
END AS review_status,
|
|
||||||
es.graded_at,
|
|
||||||
es.graded_by,
|
|
||||||
|
|
||||||
-- Priority calculation
|
|
||||||
CASE
|
|
||||||
WHEN es.submitted_at < NOW() - INTERVAL '7 days' THEN 'urgent'
|
|
||||||
WHEN es.submitted_at < NOW() - INTERVAL '3 days' THEN 'high'
|
|
||||||
WHEN es.submitted_at < NOW() - INTERVAL '1 day' THEN 'medium'
|
|
||||||
ELSE 'normal'
|
|
||||||
END AS priority,
|
|
||||||
|
|
||||||
-- Days waiting
|
|
||||||
EXTRACT(DAY FROM (NOW() - es.submitted_at))::integer AS days_waiting,
|
|
||||||
|
|
||||||
-- Metadata
|
|
||||||
es.metadata,
|
|
||||||
es.tenant_id
|
|
||||||
|
|
||||||
FROM progress_tracking.exercise_submissions es
|
|
||||||
-- Join to get student profile
|
|
||||||
INNER JOIN auth_management.profiles p ON es.user_id = p.id
|
|
||||||
-- Join to get exercise details
|
|
||||||
INNER JOIN educational_content.exercises e ON es.exercise_id = e.id
|
|
||||||
-- Join to get module info
|
|
||||||
INNER JOIN educational_content.modules m ON e.module_id = m.id
|
|
||||||
-- Join to find student's classroom(s)
|
|
||||||
LEFT JOIN social_features.classroom_members cm ON es.user_id = cm.student_id
|
|
||||||
AND cm.is_active = true
|
|
||||||
-- Join to get classroom name
|
|
||||||
LEFT JOIN social_features.classrooms c ON cm.classroom_id = c.id
|
|
||||||
|
|
||||||
WHERE
|
|
||||||
-- Only submissions that need review
|
|
||||||
es.graded_at IS NULL
|
|
||||||
AND es.submitted_at IS NOT NULL
|
|
||||||
-- Only exercises that require manual grading
|
|
||||||
AND (
|
|
||||||
e.requires_manual_grading = true
|
|
||||||
OR e.mechanic_type IN (
|
|
||||||
'respuesta_abierta',
|
|
||||||
'escritura_creativa',
|
|
||||||
'debate_guiado',
|
|
||||||
'mapa_mental',
|
|
||||||
'proyecto_multimedia',
|
|
||||||
'reflexion_metacognitiva',
|
|
||||||
'podcast_educativo',
|
|
||||||
'infografia_interactiva',
|
|
||||||
'creacion_storyboard',
|
|
||||||
'argumentacion_estructurada'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
ORDER BY
|
|
||||||
-- Urgent items first
|
|
||||||
CASE
|
|
||||||
WHEN es.submitted_at < NOW() - INTERVAL '7 days' THEN 1
|
|
||||||
WHEN es.submitted_at < NOW() - INTERVAL '3 days' THEN 2
|
|
||||||
WHEN es.submitted_at < NOW() - INTERVAL '1 day' THEN 3
|
|
||||||
ELSE 4
|
|
||||||
END,
|
|
||||||
-- Then by submission date (oldest first)
|
|
||||||
es.submitted_at ASC;
|
|
||||||
|
|
||||||
-- Set ownership
|
|
||||||
ALTER VIEW progress_tracking.teacher_pending_reviews OWNER TO gamilit_user;
|
|
||||||
|
|
||||||
-- Comments
|
|
||||||
COMMENT ON VIEW progress_tracking.teacher_pending_reviews IS
|
|
||||||
'Consolidated view of student submissions requiring teacher review. Shows pending items with priority based on wait time.';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN progress_tracking.teacher_pending_reviews.priority IS
|
|
||||||
'Priority based on wait time: urgent (>7 days), high (3-7 days), medium (1-3 days), normal (<1 day)';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN progress_tracking.teacher_pending_reviews.days_waiting IS
|
|
||||||
'Number of days the submission has been waiting for review';
|
|
||||||
|
|
||||||
-- Grants
|
|
||||||
GRANT SELECT ON progress_tracking.teacher_pending_reviews TO gamilit_user;
|
|
||||||
GRANT SELECT ON progress_tracking.teacher_pending_reviews TO authenticated;
|
|
||||||
|
|
||||||
-- =============================================================================
|
|
||||||
-- HELPER FUNCTION: Get pending reviews count for a teacher
|
|
||||||
-- =============================================================================
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION progress_tracking.get_teacher_pending_reviews_count(
|
|
||||||
p_teacher_id uuid,
|
|
||||||
p_classroom_id uuid DEFAULT NULL
|
|
||||||
)
|
|
||||||
RETURNS TABLE (
|
|
||||||
total_pending bigint,
|
|
||||||
urgent_count bigint,
|
|
||||||
high_count bigint,
|
|
||||||
medium_count bigint,
|
|
||||||
normal_count bigint
|
|
||||||
)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
SECURITY DEFINER
|
|
||||||
AS $$
|
|
||||||
BEGIN
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT
|
|
||||||
COUNT(*)::bigint AS total_pending,
|
|
||||||
COUNT(*) FILTER (WHERE pr.priority = 'urgent')::bigint AS urgent_count,
|
|
||||||
COUNT(*) FILTER (WHERE pr.priority = 'high')::bigint AS high_count,
|
|
||||||
COUNT(*) FILTER (WHERE pr.priority = 'medium')::bigint AS medium_count,
|
|
||||||
COUNT(*) FILTER (WHERE pr.priority = 'normal')::bigint AS normal_count
|
|
||||||
FROM progress_tracking.teacher_pending_reviews pr
|
|
||||||
WHERE EXISTS (
|
|
||||||
SELECT 1 FROM social_features.teacher_classrooms tc
|
|
||||||
WHERE tc.teacher_id = p_teacher_id
|
|
||||||
AND tc.classroom_id = pr.classroom_id
|
|
||||||
AND tc.is_active = true
|
|
||||||
)
|
|
||||||
AND (p_classroom_id IS NULL OR pr.classroom_id = p_classroom_id);
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
ALTER FUNCTION progress_tracking.get_teacher_pending_reviews_count(uuid, uuid) OWNER TO gamilit_user;
|
|
||||||
|
|
||||||
COMMENT ON FUNCTION progress_tracking.get_teacher_pending_reviews_count IS
|
|
||||||
'Returns count of pending reviews for a teacher, optionally filtered by classroom. Includes breakdown by priority level.';
|
|
||||||
|
|
||||||
GRANT EXECUTE ON FUNCTION progress_tracking.get_teacher_pending_reviews_count(uuid, uuid) TO gamilit_user;
|
|
||||||
GRANT EXECUTE ON FUNCTION progress_tracking.get_teacher_pending_reviews_count(uuid, uuid) TO authenticated;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Indexes for Teacher Portal Optimization
|
|
||||||
-- Schema: social_features
|
|
||||||
-- Created: 2025-12-18 (P1-02 - FASE 5 Implementation)
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
-- Index: classroom_members active by classroom
|
|
||||||
-- Purpose: Fast lookup of active students in a classroom (Teacher Portal monitoring)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_classroom_members_classroom_active
|
|
||||||
ON social_features.classroom_members(classroom_id, status)
|
|
||||||
WHERE status = 'active';
|
|
||||||
|
|
||||||
COMMENT ON INDEX social_features.idx_classroom_members_classroom_active IS
|
|
||||||
'P1-02: Optimizes teacher queries for active students in classrooms';
|
|
||||||
|
|
||||||
-- Index: classrooms by teacher
|
|
||||||
-- Purpose: Fast lookup of classrooms owned by a teacher
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_classrooms_teacher_active
|
|
||||||
ON social_features.classrooms(teacher_id, is_active)
|
|
||||||
WHERE is_active = true;
|
|
||||||
|
|
||||||
COMMENT ON INDEX social_features.idx_classrooms_teacher_active IS
|
|
||||||
'P1-02: Optimizes teacher dashboard classroom listing';
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
-- =============================================================================
|
|
||||||
-- VIEW: social_features.classroom_progress_overview
|
|
||||||
-- =============================================================================
|
|
||||||
-- Purpose: Aggregated progress view for Teacher Portal classroom monitoring
|
|
||||||
-- Priority: P1-03 - Teacher Portal optimization
|
|
||||||
-- Created: 2025-12-18
|
|
||||||
-- =============================================================================
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW social_features.classroom_progress_overview AS
|
|
||||||
SELECT
|
|
||||||
c.id AS classroom_id,
|
|
||||||
c.name AS classroom_name,
|
|
||||||
c.teacher_id,
|
|
||||||
|
|
||||||
-- Student counts
|
|
||||||
COUNT(DISTINCT cm.student_id) FILTER (WHERE cm.status = 'active') AS total_students,
|
|
||||||
COUNT(DISTINCT mp.student_id) FILTER (WHERE mp.status = 'completed') AS students_completed,
|
|
||||||
|
|
||||||
-- Progress metrics
|
|
||||||
COALESCE(ROUND(AVG(mp.progress_percentage)::numeric, 2), 0) AS avg_progress,
|
|
||||||
COALESCE(ROUND(AVG(mp.score)::numeric, 2), 0) AS avg_score,
|
|
||||||
|
|
||||||
-- Alert counts
|
|
||||||
COUNT(DISTINCT sia.id) FILTER (WHERE sia.status = 'pending') AS pending_alerts,
|
|
||||||
COUNT(DISTINCT sia.id) FILTER (WHERE sia.status = 'acknowledged') AS acknowledged_alerts,
|
|
||||||
|
|
||||||
-- Review counts
|
|
||||||
COUNT(DISTINCT es.id) FILTER (WHERE es.needs_review = true) AS pending_reviews,
|
|
||||||
|
|
||||||
-- Activity metrics
|
|
||||||
COUNT(DISTINCT es.id) AS total_submissions,
|
|
||||||
MAX(es.submitted_at) AS last_activity,
|
|
||||||
|
|
||||||
-- Module completion
|
|
||||||
COUNT(DISTINCT mp.module_id) FILTER (WHERE mp.status = 'completed') AS modules_completed,
|
|
||||||
COUNT(DISTINCT mp.module_id) AS modules_started,
|
|
||||||
|
|
||||||
-- Timestamps
|
|
||||||
c.created_at AS classroom_created_at,
|
|
||||||
c.updated_at AS classroom_updated_at
|
|
||||||
|
|
||||||
FROM social_features.classrooms c
|
|
||||||
LEFT JOIN social_features.classroom_members cm
|
|
||||||
ON c.id = cm.classroom_id AND cm.status = 'active'
|
|
||||||
LEFT JOIN progress_tracking.module_progress mp
|
|
||||||
ON cm.student_id = mp.student_id
|
|
||||||
LEFT JOIN progress_tracking.student_intervention_alerts sia
|
|
||||||
ON cm.student_id = sia.student_id AND sia.teacher_id = c.teacher_id
|
|
||||||
LEFT JOIN progress_tracking.exercise_submissions es
|
|
||||||
ON cm.student_id = es.student_id
|
|
||||||
WHERE
|
|
||||||
c.is_deleted = FALSE
|
|
||||||
GROUP BY
|
|
||||||
c.id, c.name, c.teacher_id, c.created_at, c.updated_at;
|
|
||||||
|
|
||||||
-- Grant permissions
|
|
||||||
GRANT SELECT ON social_features.classroom_progress_overview TO authenticated;
|
|
||||||
|
|
||||||
-- Documentation
|
|
||||||
COMMENT ON VIEW social_features.classroom_progress_overview IS
|
|
||||||
'Teacher Portal view for classroom progress monitoring.
|
|
||||||
|
|
||||||
Columns:
|
|
||||||
- classroom_id/name: Classroom identification
|
|
||||||
- teacher_id: Owner teacher
|
|
||||||
- total_students: Active students in classroom
|
|
||||||
- students_completed: Students who completed at least one module
|
|
||||||
- avg_progress: Average progress percentage (0-100)
|
|
||||||
- avg_score: Average score across all modules
|
|
||||||
- pending_alerts: Count of pending intervention alerts
|
|
||||||
- acknowledged_alerts: Count of acknowledged alerts
|
|
||||||
- pending_reviews: Submissions needing manual review
|
|
||||||
- total_submissions: Total exercise submissions
|
|
||||||
- last_activity: Most recent submission timestamp
|
|
||||||
- modules_completed/started: Module completion stats
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
-- Get all classrooms for a teacher
|
|
||||||
SELECT * FROM social_features.classroom_progress_overview
|
|
||||||
WHERE teacher_id = :teacher_id;
|
|
||||||
|
|
||||||
-- Find classrooms needing attention
|
|
||||||
SELECT * FROM social_features.classroom_progress_overview
|
|
||||||
WHERE pending_alerts > 0 OR pending_reviews > 5
|
|
||||||
ORDER BY pending_alerts DESC;
|
|
||||||
|
|
||||||
Created: 2025-12-18 (P1-03 Teacher Portal Analysis)';
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ============================================================================
|
|
||||||
# Script: Validación de Gaps DB-127
|
|
||||||
# Fecha: 2025-11-24
|
|
||||||
# Autor: Database-Agent
|
|
||||||
# ============================================================================
|
|
||||||
#
|
|
||||||
# DESCRIPCIÓN:
|
|
||||||
# Valida que los 3 gaps Database↔Backend estén resueltos
|
|
||||||
#
|
|
||||||
# USO:
|
|
||||||
# ./scripts/DB-127-validar-gaps.sh [DATABASE_URL]
|
|
||||||
#
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -e # Exit on error
|
|
||||||
set -u # Exit on undefined variable
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Get database URL from argument or environment
|
|
||||||
DATABASE_URL="${1:-${DATABASE_URL:-}}"
|
|
||||||
|
|
||||||
if [ -z "$DATABASE_URL" ]; then
|
|
||||||
echo -e "${RED}ERROR: DATABASE_URL no está configurada${NC}"
|
|
||||||
echo "Uso: ./scripts/DB-127-validar-gaps.sh <DATABASE_URL>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${BLUE}============================================================================${NC}"
|
|
||||||
echo -e "${BLUE}VALIDACIÓN DE GAPS DB-127${NC}"
|
|
||||||
echo -e "${BLUE}============================================================================${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Extract database name from URL
|
|
||||||
DB_NAME=$(echo "$DATABASE_URL" | sed -n 's|.*://[^/]*/\([^?]*\).*|\1|p')
|
|
||||||
echo -e "Base de datos: ${YELLOW}$DB_NAME${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Run validation SQL script
|
|
||||||
echo -e "${YELLOW}Ejecutando validación...${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
psql "$DATABASE_URL" -f "$(dirname "$0")/validate-gap-fixes.sql"
|
|
||||||
|
|
||||||
EXIT_CODE=$?
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
if [ $EXIT_CODE -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}✅ VALIDACIÓN COMPLETADA${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}Próximos pasos:${NC}"
|
|
||||||
echo "1. Verificar que los 3 gaps muestran estado '✅ RESUELTO'"
|
|
||||||
echo "2. Probar endpoints backend:"
|
|
||||||
echo " - GET /api/admin/dashboard/actions/recent"
|
|
||||||
echo " - GET /api/admin/dashboard/alerts"
|
|
||||||
echo " - GET /api/admin/tenants"
|
|
||||||
echo " - GET /api/classrooms?is_deleted=false"
|
|
||||||
echo ""
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ ERROR EN VALIDACIÓN${NC}"
|
|
||||||
echo "Revisar logs arriba para detalles del error"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@ -1,396 +0,0 @@
|
|||||||
# 📚 ÍNDICE MAESTRO - Scripts de Base de Datos GAMILIT
|
|
||||||
|
|
||||||
**Actualizado:** 2025-11-08
|
|
||||||
**Versión:** 3.0
|
|
||||||
**Estado:** ✅ Consolidado y Funcional
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 INICIO RÁPIDO
|
|
||||||
|
|
||||||
¿Nuevo en el proyecto? Empieza aquí:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Lee la guía rápida
|
|
||||||
cat QUICK-START.md
|
|
||||||
|
|
||||||
# 2. Inicializa la BD
|
|
||||||
./init-database.sh --env dev --force
|
|
||||||
|
|
||||||
# 3. ¡Listo!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📖 DOCUMENTACIÓN DISPONIBLE
|
|
||||||
|
|
||||||
### Documentos Principales
|
|
||||||
|
|
||||||
| Archivo | Propósito | ¿Cuándo leer? |
|
|
||||||
|---------|-----------|---------------|
|
|
||||||
| **QUICK-START.md** | Guía rápida de uso | ⭐ Primero - Setup inicial |
|
|
||||||
| **README.md** | Documentación completa | Segunda lectura - Detalles |
|
|
||||||
| **ANALISIS-SCRIPTS-2025-11-08.md** | Análisis técnico | Referencia técnica |
|
|
||||||
| **INDEX.md** | Este índice | Navegación general |
|
|
||||||
| **README-SETUP.md** | Guía de setup detallada | Setup avanzado |
|
|
||||||
|
|
||||||
### Orden Recomendado de Lectura
|
|
||||||
|
|
||||||
```
|
|
||||||
1. INDEX.md (este archivo) ← Estás aquí
|
|
||||||
2. QUICK-START.md ← Guía rápida para empezar
|
|
||||||
3. README.md ← Documentación completa
|
|
||||||
4. ANALISIS-SCRIPTS-2025-11-08.md ← Detalles técnicos (opcional)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ SCRIPTS DISPONIBLES
|
|
||||||
|
|
||||||
### Scripts Principales (3) ⭐
|
|
||||||
|
|
||||||
| Script | Tamaño | Estado | Propósito |
|
|
||||||
|--------|--------|--------|-----------|
|
|
||||||
| `init-database.sh` | 36K | ✅ Activo | Inicialización completa (v3.0) |
|
|
||||||
| `reset-database.sh` | 16K | ✅ Activo | Reset rápido (mantiene usuario) |
|
|
||||||
| `recreate-database.sh` | 8.9K | ✅ Activo | Recreación completa (elimina todo) |
|
|
||||||
|
|
||||||
### Scripts de Gestión (3)
|
|
||||||
|
|
||||||
| Script | Tamaño | Estado | Propósito |
|
|
||||||
|--------|--------|--------|-----------|
|
|
||||||
| `manage-secrets.sh` | 18K | ✅ Activo | Gestión de secrets con dotenv-vault |
|
|
||||||
| `update-env-files.sh` | 16K | ✅ Activo | Sincronización de .env |
|
|
||||||
| `cleanup-duplicados.sh` | 12K | ✅ Activo | Limpieza de duplicados |
|
|
||||||
|
|
||||||
### Scripts de Inventario (8)
|
|
||||||
|
|
||||||
Ubicación: `inventory/`
|
|
||||||
|
|
||||||
| Script | Propósito |
|
|
||||||
|--------|-----------|
|
|
||||||
| `list-tables.sh` | Listar todas las tablas |
|
|
||||||
| `list-functions.sh` | Listar todas las funciones |
|
|
||||||
| `list-enums.sh` | Listar todos los ENUMs |
|
|
||||||
| `list-rls.sh` | Listar RLS policies |
|
|
||||||
| `list-indexes.sh` | Listar índices |
|
|
||||||
| `list-views.sh` | Listar vistas |
|
|
||||||
| `list-triggers.sh` | Listar triggers |
|
|
||||||
| `list-seeds.sh` | Listar seeds disponibles |
|
|
||||||
| `generate-all-inventories.sh` | Generar todos los inventarios |
|
|
||||||
|
|
||||||
### Scripts Obsoletos (deprecated/)
|
|
||||||
|
|
||||||
| Script | Estado | Notas |
|
|
||||||
|--------|--------|-------|
|
|
||||||
| `init-database-v1.sh` | 📦 Deprecated | Versión original (21K) |
|
|
||||||
| `init-database-v2.sh` | 📦 Deprecated | Versión intermedia (32K) |
|
|
||||||
| `init-database.sh.backup-*` | 📦 Deprecated | Backup de v1.0 |
|
|
||||||
|
|
||||||
⚠️ **NO eliminar archivos en deprecated/** - Son históricos y de referencia
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 COMPARACIÓN RÁPIDA DE SCRIPTS PRINCIPALES
|
|
||||||
|
|
||||||
| Característica | init-database.sh | reset-database.sh | recreate-database.sh |
|
|
||||||
|----------------|------------------|-------------------|----------------------|
|
|
||||||
| **Elimina usuario** | ❌ | ❌ | ✅ |
|
|
||||||
| **Elimina BD** | ⚠️ Si existe | ✅ | ✅ |
|
|
||||||
| **Crea usuario** | ✅ Si no existe | ❌ | ✅ |
|
|
||||||
| **Genera password** | ✅ | ❌ | ✅ |
|
|
||||||
| **Requiere password** | ❌ | ✅ | ❌ |
|
|
||||||
| **Actualiza .env** | ✅ | ❌ | ✅ |
|
|
||||||
| **Soporta dotenv-vault** | ✅ | ❌ | ✅ (vía init) |
|
|
||||||
| **Tiempo ejecución** | 30-60s | 20-30s | 40-70s |
|
|
||||||
| **Riesgo de pérdida datos** | Bajo | Medio | Alto |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 GUÍA DE DECISIÓN RÁPIDA
|
|
||||||
|
|
||||||
### ¿Qué script debo usar?
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ ¿Es la primera vez en el proyecto? │
|
|
||||||
└──────────┬──────────────────────────┘
|
|
||||||
│
|
|
||||||
├─ SÍ ──> init-database.sh --env dev --force
|
|
||||||
│
|
|
||||||
└─ NO ──┐
|
|
||||||
│
|
|
||||||
┌──────────┴────────────────────────┐
|
|
||||||
│ ¿Conoces el password del usuario? │
|
|
||||||
└──────────┬────────────────────────┘
|
|
||||||
│
|
|
||||||
├─ SÍ ──> reset-database.sh --env dev --password "pass"
|
|
||||||
│
|
|
||||||
└─ NO ──> recreate-database.sh --env dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Casos de Uso Específicos
|
|
||||||
|
|
||||||
| Situación | Script Recomendado | Comando |
|
|
||||||
|-----------|-------------------|---------|
|
|
||||||
| **Primera vez** | init-database.sh | `./init-database.sh --env dev --force` |
|
|
||||||
| **Aplicar cambios DDL** | reset-database.sh | `./reset-database.sh --env dev --password "pass"` |
|
|
||||||
| **Olvidé password** | recreate-database.sh | `./recreate-database.sh --env dev` |
|
|
||||||
| **Deployment producción** | init-database.sh + vault | `./manage-secrets.sh generate --env prod && ./init-database.sh --env prod` |
|
|
||||||
| **Desarrollo diario** | reset-database.sh | `./reset-database.sh --env dev --password "$(grep Password ../database-credentials-dev.txt | cut -d: -f2 | xargs)"` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 ESTRUCTURA DEL DIRECTORIO
|
|
||||||
|
|
||||||
```
|
|
||||||
/apps/database/scripts/
|
|
||||||
│
|
|
||||||
├── 📖 Documentación
|
|
||||||
│ ├── INDEX.md ← Estás aquí
|
|
||||||
│ ├── QUICK-START.md ⭐ Guía rápida
|
|
||||||
│ ├── README.md 📚 Documentación completa
|
|
||||||
│ ├── README-SETUP.md 🔧 Setup avanzado
|
|
||||||
│ └── ANALISIS-SCRIPTS-2025-11-08.md 📊 Análisis técnico
|
|
||||||
│
|
|
||||||
├── 🛠️ Scripts Principales
|
|
||||||
│ ├── init-database.sh ⭐ Inicialización (v3.0)
|
|
||||||
│ ├── reset-database.sh 🔄 Reset rápido
|
|
||||||
│ └── recreate-database.sh ⚠️ Recreación completa
|
|
||||||
│
|
|
||||||
├── 🔐 Scripts de Gestión
|
|
||||||
│ ├── manage-secrets.sh 🔑 Gestión de secrets
|
|
||||||
│ ├── update-env-files.sh 🔧 Sincronización .env
|
|
||||||
│ └── cleanup-duplicados.sh 🧹 Limpieza
|
|
||||||
│
|
|
||||||
├── ⚙️ Configuración
|
|
||||||
│ └── config/
|
|
||||||
│ ├── dev.conf 🛠️ Config desarrollo
|
|
||||||
│ └── prod.conf 🚀 Config producción
|
|
||||||
│
|
|
||||||
├── 📊 Inventario
|
|
||||||
│ └── inventory/
|
|
||||||
│ ├── list-tables.sh 📋 Listar tablas
|
|
||||||
│ ├── list-functions.sh ⚙️ Listar funciones
|
|
||||||
│ ├── list-enums.sh 🏷️ Listar ENUMs
|
|
||||||
│ ├── list-rls.sh 🔒 Listar RLS
|
|
||||||
│ ├── list-indexes.sh 📈 Listar índices
|
|
||||||
│ ├── list-views.sh 👁️ Listar vistas
|
|
||||||
│ ├── list-triggers.sh ⚡ Listar triggers
|
|
||||||
│ ├── list-seeds.sh 🌱 Listar seeds
|
|
||||||
│ └── generate-all-inventories.sh 📊 Generar todos
|
|
||||||
│
|
|
||||||
├── 🔄 Migraciones
|
|
||||||
│ └── migrations/
|
|
||||||
│ └── *.sql 📝 Migraciones SQL
|
|
||||||
│
|
|
||||||
├── 💾 Backup y Restore
|
|
||||||
│ ├── backup/ 💾 Scripts de backup
|
|
||||||
│ └── restore/ ♻️ Scripts de restore
|
|
||||||
│
|
|
||||||
├── 🛠️ Utilidades
|
|
||||||
│ └── utilities/ 🔧 Herramientas varias
|
|
||||||
│
|
|
||||||
└── 📦 Obsoletos
|
|
||||||
└── deprecated/
|
|
||||||
├── init-database-v1.sh 📦 Versión 1.0
|
|
||||||
├── init-database-v2.sh 📦 Versión 2.0
|
|
||||||
└── init-database.sh.backup-* 📦 Backups
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 BÚSQUEDA RÁPIDA
|
|
||||||
|
|
||||||
### ¿Cómo hacer...?
|
|
||||||
|
|
||||||
**Inicializar BD por primera vez:**
|
|
||||||
```bash
|
|
||||||
./init-database.sh --env dev --force
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resetear datos rápidamente:**
|
|
||||||
```bash
|
|
||||||
PASSWORD=$(grep 'Database Password' ../database-credentials-dev.txt | cut -d: -f2 | xargs)
|
|
||||||
./reset-database.sh --env dev --password "$PASSWORD"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ver credenciales actuales:**
|
|
||||||
```bash
|
|
||||||
cat ../database-credentials-dev.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
**Listar todos los objetos de BD:**
|
|
||||||
```bash
|
|
||||||
cd inventory/
|
|
||||||
./generate-all-inventories.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Aplicar migración SQL:**
|
|
||||||
```bash
|
|
||||||
# Agregar migración a migrations/
|
|
||||||
# Luego resetear BD
|
|
||||||
./reset-database.sh --env dev --password "pass"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verificar estado de BD:**
|
|
||||||
```bash
|
|
||||||
# Verificar conexión
|
|
||||||
psql -U gamilit_user -d gamilit_platform -c "SELECT version();"
|
|
||||||
|
|
||||||
# Contar objetos
|
|
||||||
psql -U gamilit_user -d gamilit_platform -c "\dt *.*" | wc -l # Tablas
|
|
||||||
psql -U gamilit_user -d gamilit_platform -c "\df *.*" | wc -l # Funciones
|
|
||||||
psql -U gamilit_user -d gamilit_platform -c "\dn" | wc -l # Schemas
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 ESTADO DE LA BASE DE DATOS
|
|
||||||
|
|
||||||
### Objetos Implementados (según INVENTARIO-COMPLETO-BD-2025-11-07.md)
|
|
||||||
|
|
||||||
| Tipo de Objeto | Cantidad | Estado |
|
|
||||||
|----------------|----------|--------|
|
|
||||||
| **Schemas** | 13 | ✅ Completo |
|
|
||||||
| **Tablas** | 61 | ✅ Completo |
|
|
||||||
| **Funciones** | 61 | ✅ Completo |
|
|
||||||
| **Vistas** | 12 | ✅ Completo |
|
|
||||||
| **Vistas Materializadas** | 4 | ✅ Completo |
|
|
||||||
| **Triggers** | 49 | ✅ Completo |
|
|
||||||
| **Índices** | 74 archivos | ✅ Completo |
|
|
||||||
| **RLS Policies** | 24 archivos | ✅ Completo |
|
|
||||||
| **ENUMs** | 36 | ✅ Completo |
|
|
||||||
|
|
||||||
**Total:** 285 archivos SQL
|
|
||||||
|
|
||||||
**Calidad:** A+ (98.8%)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ ADVERTENCIAS IMPORTANTES
|
|
||||||
|
|
||||||
### Desarrollo (dev)
|
|
||||||
|
|
||||||
✅ **Puedes:**
|
|
||||||
- Usar `--force` libremente
|
|
||||||
- Recrear BD frecuentemente
|
|
||||||
- Experimentar con scripts
|
|
||||||
|
|
||||||
❌ **Evita:**
|
|
||||||
- Usar secrets de producción
|
|
||||||
- Omitir logs de errores
|
|
||||||
|
|
||||||
### Producción (prod)
|
|
||||||
|
|
||||||
✅ **Debes:**
|
|
||||||
- SIEMPRE hacer backup primero
|
|
||||||
- Usar dotenv-vault
|
|
||||||
- Validar dos veces
|
|
||||||
- Notificar al equipo
|
|
||||||
|
|
||||||
❌ **NUNCA:**
|
|
||||||
- Usar `--force` sin validación
|
|
||||||
- Recrear sin backup
|
|
||||||
- Ejecutar sin pruebas previas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 TROUBLESHOOTING RÁPIDO
|
|
||||||
|
|
||||||
| Error | Solución Rápida |
|
|
||||||
|-------|----------------|
|
|
||||||
| "psql no encontrado" | `sudo apt install postgresql-client` |
|
|
||||||
| "No se puede conectar" | `sudo systemctl start postgresql` |
|
|
||||||
| "Usuario ya existe" | `./recreate-database.sh --env dev` |
|
|
||||||
| "Permisos denegados" | `chmod +x *.sh` |
|
|
||||||
| "BD en uso" | `sudo -u postgres psql -c "SELECT pg_terminate_backend..."` |
|
|
||||||
|
|
||||||
Para más detalles: `cat QUICK-START.md | grep -A 10 "Troubleshooting"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 OBTENER AYUDA
|
|
||||||
|
|
||||||
### Orden de consulta
|
|
||||||
|
|
||||||
1. **QUICK-START.md** - Casos de uso comunes
|
|
||||||
2. **README.md** - Documentación detallada
|
|
||||||
3. **ANALISIS-SCRIPTS-2025-11-08.md** - Detalles técnicos
|
|
||||||
4. **Logs del script** - Revisa el output del comando
|
|
||||||
5. **Equipo de BD** - Si todo falla
|
|
||||||
|
|
||||||
### Comandos de ayuda
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver ayuda de cualquier script
|
|
||||||
./init-database.sh --help
|
|
||||||
./reset-database.sh --help
|
|
||||||
./recreate-database.sh --help
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ CHECKLIST RÁPIDO
|
|
||||||
|
|
||||||
### Primera Vez en el Proyecto
|
|
||||||
|
|
||||||
- [ ] Leí QUICK-START.md
|
|
||||||
- [ ] PostgreSQL está instalado y corriendo
|
|
||||||
- [ ] Ejecuté `./init-database.sh --env dev --force`
|
|
||||||
- [ ] Verifiqué credenciales en `../database-credentials-dev.txt`
|
|
||||||
- [ ] Backend puede conectarse a la BD
|
|
||||||
|
|
||||||
### Antes de Deployment Producción
|
|
||||||
|
|
||||||
- [ ] Leí README.md completo
|
|
||||||
- [ ] Tengo backup completo de BD actual
|
|
||||||
- [ ] Generé secrets con `manage-secrets.sh`
|
|
||||||
- [ ] Probé en staging
|
|
||||||
- [ ] Tengo plan de rollback
|
|
||||||
- [ ] Notifiqué al equipo
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 HISTORIAL DE CAMBIOS
|
|
||||||
|
|
||||||
### 2025-11-08 - Consolidación v3.0
|
|
||||||
|
|
||||||
- ✅ Unificadas versiones múltiples de init-database.sh
|
|
||||||
- ✅ Movidos scripts obsoletos a deprecated/
|
|
||||||
- ✅ Creado QUICK-START.md
|
|
||||||
- ✅ Creado ANALISIS-SCRIPTS-2025-11-08.md
|
|
||||||
- ✅ Creado INDEX.md (este archivo)
|
|
||||||
- ✅ Actualizada documentación completa
|
|
||||||
|
|
||||||
### Versiones Anteriores
|
|
||||||
|
|
||||||
- v2.0 (2025-11-02) - Integración con update-env-files
|
|
||||||
- v1.0 (Original) - Scripts base
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎓 RECURSOS ADICIONALES
|
|
||||||
|
|
||||||
### Documentación de BD
|
|
||||||
|
|
||||||
- `INVENTARIO-COMPLETO-BD-2025-11-07.md` - Inventario exhaustivo
|
|
||||||
- `REPORTE-VALIDACION-BD-COMPLETO-2025-11-08.md` - Validación completa
|
|
||||||
- `MATRIZ-COBERTURA-MODULOS-PLATAFORMA-2025-11-07.md` - Cobertura
|
|
||||||
|
|
||||||
### Validaciones Cruzadas
|
|
||||||
|
|
||||||
- `VALIDACION-CRUZADA-INFORME-MIGRACION-2025-11-08.md` - Validación de migración
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Última actualización:** 2025-11-08
|
|
||||||
**Mantenido por:** Equipo de Base de Datos GAMILIT
|
|
||||||
**Versión:** 3.0
|
|
||||||
**Estado:** ✅ Consolidado y Funcional
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
🎉 **¡Bienvenido a los Scripts de Base de Datos GAMILIT!** 🎉
|
|
||||||
|
|
||||||
**Próximo paso:** Lee `QUICK-START.md` para empezar
|
|
||||||
@ -1,317 +0,0 @@
|
|||||||
# 🚀 GUÍA RÁPIDA - Scripts de Base de Datos GAMILIT
|
|
||||||
|
|
||||||
**Actualizado:** 2025-11-08
|
|
||||||
**Versión:** 3.0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚡ Inicio Rápido
|
|
||||||
|
|
||||||
### Para Desarrollo (Primera Vez)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/projects/gamilit/apps/database/scripts
|
|
||||||
|
|
||||||
# Inicializar BD completa (crea usuario + BD + DDL + seeds)
|
|
||||||
./init-database.sh --env dev --force
|
|
||||||
```
|
|
||||||
|
|
||||||
### Para Producción (Primera Vez)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Con dotenv-vault (RECOMENDADO)
|
|
||||||
./manage-secrets.sh generate --env prod
|
|
||||||
./manage-secrets.sh sync --env prod
|
|
||||||
./init-database.sh --env prod
|
|
||||||
|
|
||||||
# O con password manual
|
|
||||||
./init-database.sh --env prod --password "tu_password_seguro_32chars"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Scripts Disponibles (3 principales)
|
|
||||||
|
|
||||||
### 1. `init-database.sh` - Inicialización Completa ⭐
|
|
||||||
|
|
||||||
**¿Cuándo usar?** Primera vez, o cuando el usuario NO existe
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./init-database.sh --env dev # Desarrollo
|
|
||||||
./init-database.sh --env prod # Producción
|
|
||||||
./init-database.sh --env dev --force # Sin confirmación
|
|
||||||
```
|
|
||||||
|
|
||||||
**¿Qué hace?**
|
|
||||||
- ✅ Crea usuario `gamilit_user` (si no existe)
|
|
||||||
- ✅ Genera password seguro de 32 caracteres
|
|
||||||
- ✅ Crea base de datos `gamilit_platform`
|
|
||||||
- ✅ Ejecuta DDL (13 schemas, 61 tablas, 61 funciones, 288 índices, 114 RLS policies)
|
|
||||||
- ✅ Carga seeds del ambiente
|
|
||||||
- ✅ Actualiza archivos .env automáticamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. `reset-database.sh` - Reset Rápido (Mantiene Usuario)
|
|
||||||
|
|
||||||
**¿Cuándo usar?** Usuario ya existe, solo quieres resetear datos
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./reset-database.sh --env dev --password "password_existente"
|
|
||||||
./reset-database.sh --env prod --password "prod_pass"
|
|
||||||
```
|
|
||||||
|
|
||||||
**¿Qué hace?**
|
|
||||||
- ⚠️ Elimina la BD `gamilit_platform`
|
|
||||||
- ✅ Mantiene el usuario `gamilit_user` (NO cambia password)
|
|
||||||
- ✅ Recrea BD con DDL y seeds
|
|
||||||
- ℹ️ NO actualiza .env (credenciales no cambian)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. `recreate-database.sh` - Recreación Completa (DESTRUYE TODO)
|
|
||||||
|
|
||||||
**¿Cuándo usar?** Cuando quieres empezar desde cero COMPLETAMENTE
|
|
||||||
|
|
||||||
⚠️ **ADVERTENCIA: ELIMINA USUARIO Y TODOS LOS DATOS**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./recreate-database.sh --env dev
|
|
||||||
./recreate-database.sh --env prod # Requiere confirmación adicional
|
|
||||||
```
|
|
||||||
|
|
||||||
**¿Qué hace?**
|
|
||||||
- ⚠️ Termina todas las conexiones
|
|
||||||
- ⚠️ Elimina completamente la BD
|
|
||||||
- ⚠️ Elimina el usuario
|
|
||||||
- ✅ Ejecuta `init-database.sh` para recrear todo
|
|
||||||
- ✅ Actualiza archivos .env automáticamente
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Casos de Uso Comunes
|
|
||||||
|
|
||||||
### Caso 1: Primera vez en proyecto (Setup inicial)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./init-database.sh --env dev --force
|
|
||||||
```
|
|
||||||
|
|
||||||
### Caso 2: Resetear datos pero mantener usuario
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Si conoces el password
|
|
||||||
./reset-database.sh --env dev --password "mi_password"
|
|
||||||
|
|
||||||
# Si no conoces el password, usa recreate
|
|
||||||
./recreate-database.sh --env dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Caso 3: Actualizar estructura de BD (nueva migración)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Opción A: Reset rápido (si tienes password)
|
|
||||||
./reset-database.sh --env dev --password "password"
|
|
||||||
|
|
||||||
# Opción B: Recrear completo (genera nuevo password)
|
|
||||||
./recreate-database.sh --env dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Caso 4: Aplicar cambios de DDL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Si solo cambiaron DDL/seeds (sin cambios de usuario)
|
|
||||||
./reset-database.sh --env dev --password "password_actual"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Caso 5: Olvidé el password del usuario
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Única opción: recrear todo
|
|
||||||
./recreate-database.sh --env dev
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Comparación Rápida
|
|
||||||
|
|
||||||
| Acción | init-database.sh | reset-database.sh | recreate-database.sh |
|
|
||||||
|--------|------------------|-------------------|----------------------|
|
|
||||||
| **Elimina usuario** | ❌ | ❌ | ✅ |
|
|
||||||
| **Elimina BD** | ⚠️ Si existe | ✅ | ✅ |
|
|
||||||
| **Crea usuario** | ✅ Si no existe | ❌ | ✅ |
|
|
||||||
| **Genera password** | ✅ | ❌ | ✅ |
|
|
||||||
| **Requiere password** | ❌ | ✅ | ❌ |
|
|
||||||
| **Actualiza .env** | ✅ | ❌ | ✅ |
|
|
||||||
| **Tiempo aprox** | 30-60s | 20-30s | 40-70s |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔑 Gestión de Credenciales
|
|
||||||
|
|
||||||
### ¿Dónde están las credenciales?
|
|
||||||
|
|
||||||
Después de ejecutar `init-database.sh` o `recreate-database.sh`:
|
|
||||||
|
|
||||||
```
|
|
||||||
apps/database/database-credentials-{env}.txt ← Credenciales guardadas aquí
|
|
||||||
```
|
|
||||||
|
|
||||||
Ejemplo de contenido:
|
|
||||||
```
|
|
||||||
Database Host: localhost
|
|
||||||
Database Port: 5432
|
|
||||||
Database Name: gamilit_platform
|
|
||||||
Database User: gamilit_user
|
|
||||||
Database Password: xB9k2mN...Zp8Q
|
|
||||||
Connection String: postgresql://gamilit_user:xB9k2mN...@localhost:5432/gamilit_platform
|
|
||||||
```
|
|
||||||
|
|
||||||
### Archivos .env actualizados automáticamente
|
|
||||||
|
|
||||||
- `apps/backend/.env.{env}`
|
|
||||||
- `apps/database/.env.{env}`
|
|
||||||
- `../../gamilit-deployment-scripts/.env.{env}` (si existe)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Advertencias de Seguridad
|
|
||||||
|
|
||||||
### Desarrollo
|
|
||||||
|
|
||||||
- ✅ OK usar `--force` para automatización
|
|
||||||
- ✅ OK regenerar passwords
|
|
||||||
- ✅ OK recrear BD frecuentemente
|
|
||||||
|
|
||||||
### Producción
|
|
||||||
|
|
||||||
- ⚠️ NUNCA usar `--force` sin validación
|
|
||||||
- ⚠️ SIEMPRE hacer backup antes de `recreate-database.sh`
|
|
||||||
- ⚠️ Confirmar que tienes backup antes de eliminar
|
|
||||||
- ✅ Usar dotenv-vault para gestión de secrets
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Error: "No se puede conectar a PostgreSQL"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar que PostgreSQL está corriendo
|
|
||||||
sudo systemctl status postgresql
|
|
||||||
|
|
||||||
# O verificar proceso
|
|
||||||
ps aux | grep postgres
|
|
||||||
|
|
||||||
# Iniciar si está detenido
|
|
||||||
sudo systemctl start postgresql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: "Usuario ya existe"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Opción A: Usar reset (si conoces password)
|
|
||||||
./reset-database.sh --env dev --password "password_existente"
|
|
||||||
|
|
||||||
# Opción B: Recrear todo
|
|
||||||
./recreate-database.sh --env dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: "Base de datos no se puede eliminar (conexiones activas)"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# El script ya maneja esto, pero si falla:
|
|
||||||
sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='gamilit_platform';"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: "Permisos denegados"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Dar permisos de ejecución
|
|
||||||
chmod +x *.sh
|
|
||||||
|
|
||||||
# Verificar permisos de PostgreSQL
|
|
||||||
sudo -u postgres psql -c "SELECT version();"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Estructura de Archivos
|
|
||||||
|
|
||||||
```
|
|
||||||
scripts/
|
|
||||||
├── init-database.sh ⭐ Script principal (v3.0)
|
|
||||||
├── reset-database.sh 🔄 Reset rápido
|
|
||||||
├── recreate-database.sh ⚠️ Recreación completa
|
|
||||||
├── manage-secrets.sh 🔐 Gestión de secrets
|
|
||||||
├── update-env-files.sh 🔧 Sincronización .env
|
|
||||||
├── cleanup-duplicados.sh 🧹 Limpieza
|
|
||||||
├── QUICK-START.md 📖 Esta guía
|
|
||||||
├── README.md 📚 Documentación completa
|
|
||||||
├── deprecated/ 📦 Scripts antiguos
|
|
||||||
│ ├── init-database-v1.sh
|
|
||||||
│ ├── init-database-v2.sh
|
|
||||||
│ └── init-database.sh.backup-*
|
|
||||||
├── config/ ⚙️ Configuraciones
|
|
||||||
│ ├── dev.conf
|
|
||||||
│ └── prod.conf
|
|
||||||
├── inventory/ 📊 Scripts de inventario
|
|
||||||
└── utilities/ 🛠️ Utilidades
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎓 Flujo Recomendado para Nuevos Desarrolladores
|
|
||||||
|
|
||||||
### Día 1 - Setup Inicial
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Clonar repositorio
|
|
||||||
git clone <repo-url>
|
|
||||||
cd gamilit/projects/gamilit/apps/database/scripts
|
|
||||||
|
|
||||||
# 2. Inicializar BD
|
|
||||||
./init-database.sh --env dev --force
|
|
||||||
|
|
||||||
# 3. Verificar credenciales
|
|
||||||
cat ../database-credentials-dev.txt
|
|
||||||
|
|
||||||
# 4. ¡Listo! Backend puede conectarse
|
|
||||||
```
|
|
||||||
|
|
||||||
### Día a Día - Desarrollo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Aplicar cambios de DDL
|
|
||||||
./reset-database.sh --env dev --password "$(grep 'Database Password' ../database-credentials-dev.txt | cut -d: -f2 | xargs)"
|
|
||||||
|
|
||||||
# O más simple: recrear todo
|
|
||||||
./recreate-database.sh --env dev --force
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Checklist Pre-Deployment Producción
|
|
||||||
|
|
||||||
- [ ] Backup completo de BD actual
|
|
||||||
- [ ] Verificar que `manage-secrets.sh` tiene secrets generados
|
|
||||||
- [ ] Probar script en staging primero
|
|
||||||
- [ ] Tener plan de rollback
|
|
||||||
- [ ] Notificar al equipo del deployment
|
|
||||||
- [ ] Ejecutar con --env prod (SIN --force)
|
|
||||||
- [ ] Validar conexiones post-deployment
|
|
||||||
- [ ] Verificar que seeds de producción se cargaron
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Soporte
|
|
||||||
|
|
||||||
- **Documentación completa:** `README.md`
|
|
||||||
- **Scripts de inventario:** `inventory/`
|
|
||||||
- **Logs:** Revisa output del script (se muestra en consola)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Última actualización:** 2025-11-08
|
|
||||||
**Versión de scripts:** 3.0
|
|
||||||
**Mantenido por:** Equipo de Base de Datos GAMILIT
|
|
||||||
@ -1,289 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ==============================================================================
|
|
||||||
# Script: cleanup-duplicados.sh
|
|
||||||
# Propósito: Eliminar duplicados detectados en análisis de dependencias
|
|
||||||
# Generado: 2025-11-07
|
|
||||||
# Autor: NEXUS-DATABASE-AVANZADO
|
|
||||||
# Documentación: /gamilit/orchestration/05-validaciones/database/ANALISIS-DEPENDENCIAS-DUPLICADOS-2025-11-07.md
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
set -e # Exit on error
|
|
||||||
set -u # Exit on undefined variable
|
|
||||||
|
|
||||||
# Colores para output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Configuración
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
||||||
BACKUP_DIR="$PROJECT_ROOT/apps/database/backups/duplicados/2025-11-07"
|
|
||||||
DDL_DIR="$PROJECT_ROOT/apps/database/ddl"
|
|
||||||
|
|
||||||
echo -e "${BLUE}================================${NC}"
|
|
||||||
echo -e "${BLUE} CLEANUP DE DUPLICADOS - DATABASE${NC}"
|
|
||||||
echo -e "${BLUE}================================${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# PASO 0: Verificar ubicación
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${YELLOW}📍 Verificando ubicación...${NC}"
|
|
||||||
if [ ! -d "$PROJECT_ROOT/apps/database" ]; then
|
|
||||||
echo -e "${RED}❌ Error: No se encuentra el directorio de database${NC}"
|
|
||||||
echo -e "${RED} Ejecutar desde: /gamilit/apps/database/scripts/${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -e "${GREEN}✅ Ubicación correcta${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# PASO 1: Crear estructura de backups
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${YELLOW}📦 PASO 1: Creando estructura de backups...${NC}"
|
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
|
||||||
|
|
||||||
cat > "$BACKUP_DIR/README.md" << 'EOF'
|
|
||||||
# Backups de Archivos Duplicados - 2025-11-07
|
|
||||||
|
|
||||||
## Razón del Backup
|
|
||||||
Archivos duplicados detectados por análisis de dependencias.
|
|
||||||
Estos archivos fueron eliminados tras confirmar que no tienen referencias activas.
|
|
||||||
|
|
||||||
## Archivos en este backup
|
|
||||||
1. `auth_get_current_user_id.sql` - Duplicado de gamilit/functions/02-get_current_user_id.sql (0 referencias)
|
|
||||||
2. `public_trg_feature_flags_updated_at.sql` - Duplicado en schema incorrecto
|
|
||||||
3. `public_trg_system_settings_updated_at.sql` - Duplicado en schema incorrecto
|
|
||||||
|
|
||||||
## Análisis Completo
|
|
||||||
Ver: `/gamilit/orchestration/05-validaciones/database/ANALISIS-DEPENDENCIAS-DUPLICADOS-2025-11-07.md`
|
|
||||||
|
|
||||||
## Versiones Canónicas (MANTENER)
|
|
||||||
1. `gamilit/functions/02-get_current_user_id.sql` - 73 referencias en DDL
|
|
||||||
2. `system_configuration/triggers/29-trg_feature_flags_updated_at.sql` - Ubicación correcta
|
|
||||||
3. `system_configuration/triggers/30-trg_system_settings_updated_at.sql` - Ubicación correcta
|
|
||||||
|
|
||||||
## Restauración (solo si es necesario)
|
|
||||||
```bash
|
|
||||||
# Restaurar función (NO RECOMENDADO - 0 referencias)
|
|
||||||
cp auth_get_current_user_id.sql ../../ddl/schemas/auth/functions/get_current_user_id.sql
|
|
||||||
|
|
||||||
# Restaurar triggers (NO RECOMENDADO - schema incorrecto)
|
|
||||||
cp public_trg_feature_flags_updated_at.sql ../../ddl/schemas/public/triggers/29-trg_feature_flags_updated_at.sql
|
|
||||||
cp public_trg_system_settings_updated_at.sql ../../ddl/schemas/public/triggers/30-trg_system_settings_updated_at.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**IMPORTANTE:** Los archivos eliminados NO tienen referencias activas o están en ubicación incorrecta.
|
|
||||||
La restauración solo debe hacerse si se detecta un error específico.
|
|
||||||
|
|
||||||
## Timestamp
|
|
||||||
- **Fecha backup:** 2025-11-07T18:45:00Z
|
|
||||||
- **Análisis basado en:** 73 referencias medidas en DDL, Backend, Frontend y Docs
|
|
||||||
- **Decisión:** Data-driven
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Estructura de backups creada${NC}"
|
|
||||||
echo -e " Ubicación: $BACKUP_DIR"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# PASO 2: Realizar backups
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${YELLOW}💾 PASO 2: Creando backups de duplicados...${NC}"
|
|
||||||
|
|
||||||
DUPLICADOS=(
|
|
||||||
"schemas/auth/functions/get_current_user_id.sql:auth_get_current_user_id.sql"
|
|
||||||
"schemas/public/triggers/29-trg_feature_flags_updated_at.sql:public_trg_feature_flags_updated_at.sql"
|
|
||||||
"schemas/public/triggers/30-trg_system_settings_updated_at.sql:public_trg_system_settings_updated_at.sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
BACKUP_COUNT=0
|
|
||||||
for DUP in "${DUPLICADOS[@]}"; do
|
|
||||||
SOURCE_PATH="${DUP%%:*}"
|
|
||||||
BACKUP_NAME="${DUP##*:}"
|
|
||||||
FULL_PATH="$DDL_DIR/$SOURCE_PATH"
|
|
||||||
|
|
||||||
if [ -f "$FULL_PATH" ]; then
|
|
||||||
cp "$FULL_PATH" "$BACKUP_DIR/$BACKUP_NAME"
|
|
||||||
echo -e "${GREEN} ✅ Backup: $BACKUP_NAME${NC}"
|
|
||||||
BACKUP_COUNT=$((BACKUP_COUNT + 1))
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW} ⚠️ No encontrado: $SOURCE_PATH (posiblemente ya eliminado)${NC}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Backups completados: $BACKUP_COUNT archivos${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# PASO 3: Verificar estado antes de eliminar
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${YELLOW}🔍 PASO 3: Verificando estado ANTES de eliminar...${NC}"
|
|
||||||
|
|
||||||
# Contar referencias actuales
|
|
||||||
AUTH_REFS_BEFORE=$(grep -r "auth\.get_current_user_id" "$DDL_DIR" --include="*.sql" 2>/dev/null | wc -l || echo "0")
|
|
||||||
GAMILIT_REFS_BEFORE=$(grep -r "gamilit\.get_current_user_id" "$DDL_DIR" --include="*.sql" 2>/dev/null | wc -l || echo "0")
|
|
||||||
|
|
||||||
echo -e " Referencias auth.get_current_user_id: $AUTH_REFS_BEFORE"
|
|
||||||
echo -e " Referencias gamilit.get_current_user_id: $GAMILIT_REFS_BEFORE"
|
|
||||||
|
|
||||||
# Contar archivos de triggers
|
|
||||||
FEATURE_FLAGS_COUNT=$(find "$DDL_DIR" -name "*trg_feature_flags_updated_at*" 2>/dev/null | wc -l || echo "0")
|
|
||||||
SYSTEM_SETTINGS_COUNT=$(find "$DDL_DIR" -name "*trg_system_settings_updated_at*" 2>/dev/null | wc -l || echo "0")
|
|
||||||
|
|
||||||
echo -e " Archivos trg_feature_flags_updated_at: $FEATURE_FLAGS_COUNT"
|
|
||||||
echo -e " Archivos trg_system_settings_updated_at: $SYSTEM_SETTINGS_COUNT"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# PASO 4: Eliminar duplicados
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${YELLOW}🗑️ PASO 4: Eliminando duplicados...${NC}"
|
|
||||||
|
|
||||||
DELETED_COUNT=0
|
|
||||||
for DUP in "${DUPLICADOS[@]}"; do
|
|
||||||
SOURCE_PATH="${DUP%%:*}"
|
|
||||||
FULL_PATH="$DDL_DIR/$SOURCE_PATH"
|
|
||||||
|
|
||||||
if [ -f "$FULL_PATH" ]; then
|
|
||||||
rm "$FULL_PATH"
|
|
||||||
echo -e "${GREEN} ✅ Eliminado: $SOURCE_PATH${NC}"
|
|
||||||
DELETED_COUNT=$((DELETED_COUNT + 1))
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW} ⚠️ Ya eliminado: $SOURCE_PATH${NC}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Duplicados eliminados: $DELETED_COUNT archivos${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# PASO 5: Verificar integridad POST-eliminación
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${YELLOW}✅ PASO 5: Verificando integridad POST-eliminación...${NC}"
|
|
||||||
|
|
||||||
# Verificar referencias
|
|
||||||
AUTH_REFS_AFTER=$(grep -r "auth\.get_current_user_id" "$DDL_DIR" --include="*.sql" 2>/dev/null | wc -l || echo "0")
|
|
||||||
GAMILIT_REFS_AFTER=$(grep -r "gamilit\.get_current_user_id" "$DDL_DIR" --include="*.sql" 2>/dev/null | wc -l || echo "0")
|
|
||||||
|
|
||||||
echo -e " Referencias auth.get_current_user_id: $AUTH_REFS_AFTER (esperado: 0)"
|
|
||||||
echo -e " Referencias gamilit.get_current_user_id: $GAMILIT_REFS_AFTER (esperado: 73)"
|
|
||||||
|
|
||||||
# Verificar archivos de triggers
|
|
||||||
FEATURE_FLAGS_AFTER=$(find "$DDL_DIR" -name "*trg_feature_flags_updated_at*" 2>/dev/null | wc -l || echo "0")
|
|
||||||
SYSTEM_SETTINGS_AFTER=$(find "$DDL_DIR" -name "*trg_system_settings_updated_at*" 2>/dev/null | wc -l || echo "0")
|
|
||||||
|
|
||||||
echo -e " Archivos trg_feature_flags_updated_at: $FEATURE_FLAGS_AFTER (esperado: 1)"
|
|
||||||
echo -e " Archivos trg_system_settings_updated_at: $SYSTEM_SETTINGS_AFTER (esperado: 1)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# PASO 6: Validación de resultados
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${YELLOW}🎯 PASO 6: Validando resultados...${NC}"
|
|
||||||
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
# Validar función
|
|
||||||
if [ "$AUTH_REFS_AFTER" -ne 0 ]; then
|
|
||||||
echo -e "${RED} ❌ FALLO: auth.get_current_user_id tiene $AUTH_REFS_AFTER referencias (esperado: 0)${NC}"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
echo -e "${GREEN} ✅ auth.get_current_user_id: 0 referencias${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$GAMILIT_REFS_AFTER" -eq 73 ]; then
|
|
||||||
echo -e "${GREEN} ✅ gamilit.get_current_user_id: 73 referencias${NC}"
|
|
||||||
elif [ "$GAMILIT_REFS_AFTER" -gt 70 ]; then
|
|
||||||
echo -e "${YELLOW} ⚠️ gamilit.get_current_user_id: $GAMILIT_REFS_AFTER referencias (esperado: 73, aceptable)${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED} ❌ FALLO: gamilit.get_current_user_id tiene $GAMILIT_REFS_AFTER referencias (esperado: 73)${NC}"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validar triggers
|
|
||||||
if [ "$FEATURE_FLAGS_AFTER" -eq 1 ]; then
|
|
||||||
echo -e "${GREEN} ✅ trg_feature_flags_updated_at: 1 archivo${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED} ❌ FALLO: trg_feature_flags_updated_at tiene $FEATURE_FLAGS_AFTER archivos (esperado: 1)${NC}"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SYSTEM_SETTINGS_AFTER" -eq 1 ]; then
|
|
||||||
echo -e "${GREEN} ✅ trg_system_settings_updated_at: 1 archivo${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED} ❌ FALLO: trg_system_settings_updated_at tiene $SYSTEM_SETTINGS_AFTER archivos (esperado: 1)${NC}"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# PASO 7: Listar archivos preservados
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${YELLOW}📋 PASO 7: Verificando archivos preservados...${NC}"
|
|
||||||
|
|
||||||
CANONICOS=(
|
|
||||||
"schemas/gamilit/functions/02-get_current_user_id.sql:gamilit.get_current_user_id()"
|
|
||||||
"schemas/system_configuration/triggers/29-trg_feature_flags_updated_at.sql:trg_feature_flags_updated_at"
|
|
||||||
"schemas/system_configuration/triggers/30-trg_system_settings_updated_at.sql:trg_system_settings_updated_at"
|
|
||||||
)
|
|
||||||
|
|
||||||
for CANONICO in "${CANONICOS[@]}"; do
|
|
||||||
FILE_PATH="${CANONICO%%:*}"
|
|
||||||
FUNC_NAME="${CANONICO##*:}"
|
|
||||||
FULL_PATH="$DDL_DIR/$FILE_PATH"
|
|
||||||
|
|
||||||
if [ -f "$FULL_PATH" ]; then
|
|
||||||
echo -e "${GREEN} ✅ $FUNC_NAME${NC}"
|
|
||||||
echo -e " $FILE_PATH"
|
|
||||||
else
|
|
||||||
echo -e "${RED} ❌ FALLO: No se encuentra $FUNC_NAME${NC}"
|
|
||||||
echo -e "${RED} $FILE_PATH${NC}"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# RESUMEN FINAL
|
|
||||||
# ==============================================================================
|
|
||||||
echo -e "${BLUE}================================${NC}"
|
|
||||||
echo -e "${BLUE} RESUMEN FINAL${NC}"
|
|
||||||
echo -e "${BLUE}================================${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo -e "📊 Estadísticas:"
|
|
||||||
echo -e " - Archivos respaldados: $BACKUP_COUNT"
|
|
||||||
echo -e " - Archivos eliminados: $DELETED_COUNT"
|
|
||||||
echo -e " - Archivos preservados: 3"
|
|
||||||
echo -e " - Errores detectados: $ERRORS"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo -e "📁 Backups guardados en:"
|
|
||||||
echo -e " $BACKUP_DIR"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ $ERRORS -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}🎉 PROCESO COMPLETADO EXITOSAMENTE${NC}"
|
|
||||||
echo -e "${GREEN}✅ 0 duplicados restantes${NC}"
|
|
||||||
echo -e "${GREEN}✅ Integridad verificada${NC}"
|
|
||||||
echo -e ""
|
|
||||||
echo -e "${BLUE}📚 Ver análisis completo:${NC}"
|
|
||||||
echo -e " orchestration/05-validaciones/database/ANALISIS-DEPENDENCIAS-DUPLICADOS-2025-11-07.md"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ PROCESO COMPLETADO CON ERRORES${NC}"
|
|
||||||
echo -e "${RED} Errores encontrados: $ERRORS${NC}"
|
|
||||||
echo -e ""
|
|
||||||
echo -e "${YELLOW}⚠️ Acciones recomendadas:${NC}"
|
|
||||||
echo -e " 1. Revisar archivos preservados"
|
|
||||||
echo -e " 2. Verificar backups en: $BACKUP_DIR"
|
|
||||||
echo -e " 3. Consultar análisis: orchestration/05-validaciones/database/ANALISIS-DEPENDENCIAS-DUPLICADOS-2025-11-07.md"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# =====================================================
|
|
||||||
# Script: fix-duplicate-triggers.sh
|
|
||||||
# Purpose: Remove duplicate triggers from table files
|
|
||||||
# Date: 2025-11-24
|
|
||||||
# Author: Architecture-Analyst
|
|
||||||
#
|
|
||||||
# This script comments out CREATE TRIGGER statements from
|
|
||||||
# table definition files, as they should only exist in
|
|
||||||
# separate trigger files (ddl/schemas/*/triggers/*.sql)
|
|
||||||
# =====================================================
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
DDL_PATH="/home/isem/workspace/projects/gamilit/apps/database/ddl/schemas"
|
|
||||||
LOG_FILE="/tmp/fix-duplicate-triggers-$(date +%Y%m%d_%H%M%S).log"
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo "Fix Duplicate Triggers Script"
|
|
||||||
echo "Date: $(date)"
|
|
||||||
echo "Log: $LOG_FILE"
|
|
||||||
echo "=========================================="
|
|
||||||
|
|
||||||
# List of files to process
|
|
||||||
declare -a TABLE_FILES=(
|
|
||||||
# auth_management
|
|
||||||
"auth_management/tables/01-tenants.sql"
|
|
||||||
"auth_management/tables/04-roles.sql"
|
|
||||||
"auth_management/tables/10-memberships.sql"
|
|
||||||
|
|
||||||
# progress_tracking
|
|
||||||
"progress_tracking/tables/01-module_progress.sql"
|
|
||||||
"progress_tracking/tables/03-exercise_attempts.sql"
|
|
||||||
"progress_tracking/tables/04-exercise_submissions.sql"
|
|
||||||
|
|
||||||
# gamification_system
|
|
||||||
"gamification_system/tables/01-user_stats.sql"
|
|
||||||
"gamification_system/tables/02-user_ranks.sql"
|
|
||||||
"gamification_system/tables/03-achievements.sql"
|
|
||||||
"gamification_system/tables/06-missions.sql"
|
|
||||||
"gamification_system/tables/07-comodines_inventory.sql"
|
|
||||||
"gamification_system/tables/08-notifications.sql"
|
|
||||||
|
|
||||||
# educational_content
|
|
||||||
"educational_content/tables/01-modules.sql"
|
|
||||||
"educational_content/tables/02-exercises.sql"
|
|
||||||
"educational_content/tables/03-assessment_rubrics.sql"
|
|
||||||
"educational_content/tables/04-media_resources.sql"
|
|
||||||
|
|
||||||
# content_management
|
|
||||||
"content_management/tables/01-content_templates.sql"
|
|
||||||
"content_management/tables/02-marie_curie_content.sql"
|
|
||||||
"content_management/tables/03-media_files.sql"
|
|
||||||
|
|
||||||
# social_features
|
|
||||||
"social_features/tables/02-schools.sql"
|
|
||||||
"social_features/tables/03-classrooms.sql"
|
|
||||||
"social_features/tables/04-classroom_members.sql"
|
|
||||||
"social_features/tables/05-teams.sql"
|
|
||||||
|
|
||||||
# audit_logging
|
|
||||||
"audit_logging/tables/03-system_alerts.sql"
|
|
||||||
|
|
||||||
# system_configuration
|
|
||||||
"system_configuration/tables/01-system_settings.sql"
|
|
||||||
"system_configuration/tables/01-feature_flags.sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
process_file() {
|
|
||||||
local file="$DDL_PATH/$1"
|
|
||||||
|
|
||||||
if [ ! -f "$file" ]; then
|
|
||||||
echo "SKIP: $1 (file not found)" | tee -a "$LOG_FILE"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if file has CREATE TRIGGER
|
|
||||||
if ! grep -q "CREATE TRIGGER\|CREATE OR REPLACE TRIGGER" "$file"; then
|
|
||||||
echo "SKIP: $1 (no triggers)" | tee -a "$LOG_FILE"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "PROCESSING: $1" | tee -a "$LOG_FILE"
|
|
||||||
|
|
||||||
# Create backup
|
|
||||||
cp "$file" "${file}.bak"
|
|
||||||
|
|
||||||
# Comment out CREATE TRIGGER blocks (from CREATE TRIGGER to ;)
|
|
||||||
# This is a simplified approach - for complex cases, manual review is needed
|
|
||||||
sed -i 's/^CREATE TRIGGER/-- [DUPLICATE] CREATE TRIGGER/g' "$file"
|
|
||||||
sed -i 's/^CREATE OR REPLACE TRIGGER/-- [DUPLICATE] CREATE OR REPLACE TRIGGER/g' "$file"
|
|
||||||
|
|
||||||
# Add note about trigger location
|
|
||||||
if ! grep -q "NOTE: Triggers moved to separate files" "$file"; then
|
|
||||||
# Add note after "-- Triggers" comment if exists
|
|
||||||
sed -i '/^-- Triggers$/a -- NOTE: Triggers moved to separate files in triggers/ directory' "$file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " - Commented out CREATE TRIGGER statements" | tee -a "$LOG_FILE"
|
|
||||||
echo " - Backup created: ${file}.bak" | tee -a "$LOG_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Processing ${#TABLE_FILES[@]} files..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
for file in "${TABLE_FILES[@]}"; do
|
|
||||||
process_file "$file"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo "COMPLETED"
|
|
||||||
echo "Files processed: ${#TABLE_FILES[@]}"
|
|
||||||
echo "Log saved to: $LOG_FILE"
|
|
||||||
echo ""
|
|
||||||
echo "NEXT STEPS:"
|
|
||||||
echo "1. Review changes in git diff"
|
|
||||||
echo "2. Test with: ./drop-and-recreate-database.sh"
|
|
||||||
echo "3. Remove .bak files if successful"
|
|
||||||
echo "=========================================="
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,172 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ============================================================================
|
|
||||||
# Script: load-users-and-profiles.sh
|
|
||||||
# Descripción: Carga usuarios y perfiles correctamente
|
|
||||||
# Versión: 2.0 (con correcciones para tablas faltantes)
|
|
||||||
# Fecha: 2025-11-09
|
|
||||||
# Autor: Claude Code (AI Assistant)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
DB_DIR="$(dirname "$SCRIPT_DIR")"
|
|
||||||
|
|
||||||
cd "$DB_DIR"
|
|
||||||
|
|
||||||
# Cargar credenciales
|
|
||||||
if [ ! -f "database-credentials-dev.txt" ]; then
|
|
||||||
echo "❌ Error: database-credentials-dev.txt no encontrado"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DB_PASSWORD=$(grep "^Password:" database-credentials-dev.txt | awk '{print $2}')
|
|
||||||
export PGPASSWORD="$DB_PASSWORD"
|
|
||||||
|
|
||||||
PSQL="psql -h localhost -p 5432 -U gamilit_user -d gamilit_platform"
|
|
||||||
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo " CARGANDO USUARIOS Y PERFILES - GAMILIT PLATFORM"
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# PASO 1: Verificar tablas de gamificación
|
|
||||||
echo "📋 PASO 1: Verificando tablas de gamificación..."
|
|
||||||
TABLES_COUNT=$($PSQL -t -c "
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'gamification_system'
|
|
||||||
AND table_name IN ('user_stats', 'user_ranks');
|
|
||||||
" | tr -d ' ')
|
|
||||||
|
|
||||||
if [ "$TABLES_COUNT" -lt 2 ]; then
|
|
||||||
echo "⚠️ Tablas de gamificación faltantes ($TABLES_COUNT/2). Creando..."
|
|
||||||
bash "$SCRIPT_DIR/fix-missing-gamification-tables.sh"
|
|
||||||
else
|
|
||||||
echo "✅ Tablas de gamificación presentes (2/2)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# PASO 2: Cargar usuarios en auth.users
|
|
||||||
echo "👥 PASO 2: Cargando usuarios en auth.users..."
|
|
||||||
|
|
||||||
if [ -f "seeds/dev/auth/01-demo-users.sql" ]; then
|
|
||||||
$PSQL -f seeds/dev/auth/01-demo-users.sql > /dev/null 2>&1
|
|
||||||
echo " ✅ Demo users cargados"
|
|
||||||
else
|
|
||||||
echo " ⚠️ seeds/dev/auth/01-demo-users.sql no encontrado"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "seeds/dev/auth/02-test-users.sql" ]; then
|
|
||||||
$PSQL -f seeds/dev/auth/02-test-users.sql > /dev/null 2>&1
|
|
||||||
echo " ✅ Test users cargados"
|
|
||||||
else
|
|
||||||
echo " ⚠️ seeds/dev/auth/02-test-users.sql no encontrado"
|
|
||||||
fi
|
|
||||||
|
|
||||||
USERS_COUNT=$($PSQL -t -c "
|
|
||||||
SELECT COUNT(*) FROM auth.users
|
|
||||||
WHERE email LIKE '%@glit.edu.mx'
|
|
||||||
OR email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR email LIKE '%@gamilit.com';
|
|
||||||
" | tr -d ' ')
|
|
||||||
echo " 📊 Total usuarios: $USERS_COUNT"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# PASO 3: Cargar profiles en auth_management.profiles
|
|
||||||
echo "📝 PASO 3: Cargando profiles..."
|
|
||||||
|
|
||||||
if [ -f "seeds/dev/auth_management/03-profiles.sql" ]; then
|
|
||||||
# Ejecutar y capturar errores
|
|
||||||
if $PSQL -f seeds/dev/auth_management/03-profiles.sql 2>&1 | grep -q "ERROR"; then
|
|
||||||
echo " ⚠️ Error al cargar profiles. Intentando método alternativo..."
|
|
||||||
|
|
||||||
# Deshabilitar trigger temporalmente
|
|
||||||
$PSQL -c "ALTER TABLE auth_management.profiles DISABLE TRIGGER trg_initialize_user_stats;" > /dev/null 2>&1
|
|
||||||
|
|
||||||
# Re-intentar carga
|
|
||||||
$PSQL -f seeds/dev/auth_management/03-profiles.sql > /dev/null 2>&1
|
|
||||||
|
|
||||||
# Re-habilitar trigger
|
|
||||||
$PSQL -c "ALTER TABLE auth_management.profiles ENABLE TRIGGER trg_initialize_user_stats;" > /dev/null 2>&1
|
|
||||||
|
|
||||||
echo " ✅ Profiles cargados (método alternativo)"
|
|
||||||
else
|
|
||||||
echo " ✅ Profiles cargados"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ⚠️ seeds/dev/auth_management/03-profiles.sql no encontrado"
|
|
||||||
fi
|
|
||||||
|
|
||||||
PROFILES_COUNT=$($PSQL -t -c "
|
|
||||||
SELECT COUNT(*) FROM auth_management.profiles
|
|
||||||
WHERE email LIKE '%@glit.edu.mx'
|
|
||||||
OR email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR email LIKE '%@gamilit.com';
|
|
||||||
" | tr -d ' ')
|
|
||||||
echo " 📊 Total profiles: $PROFILES_COUNT"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# PASO 4: Verificación final
|
|
||||||
echo "✅ PASO 4: Verificación final..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
$PSQL -c "
|
|
||||||
SELECT
|
|
||||||
'auth.users' as tabla,
|
|
||||||
COUNT(*) as total,
|
|
||||||
COUNT(*) FILTER (WHERE role = 'super_admin') as admins,
|
|
||||||
COUNT(*) FILTER (WHERE role = 'admin_teacher') as teachers,
|
|
||||||
COUNT(*) FILTER (WHERE role = 'student') as students
|
|
||||||
FROM auth.users
|
|
||||||
WHERE email LIKE '%@glit.edu.mx'
|
|
||||||
OR email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR email LIKE '%@gamilit.com'
|
|
||||||
UNION ALL
|
|
||||||
SELECT
|
|
||||||
'auth_management.profiles' as tabla,
|
|
||||||
COUNT(*) as total,
|
|
||||||
COUNT(*) FILTER (WHERE role = 'super_admin') as admins,
|
|
||||||
COUNT(*) FILTER (WHERE role = 'admin_teacher') as teachers,
|
|
||||||
COUNT(*) FILTER (WHERE role = 'student') as students
|
|
||||||
FROM auth_management.profiles
|
|
||||||
WHERE email LIKE '%@glit.edu.mx'
|
|
||||||
OR email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR email LIKE '%@gamilit.com';
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Verificar vinculación
|
|
||||||
UNLINKED=$($PSQL -t -c "
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM auth.users u
|
|
||||||
LEFT JOIN auth_management.profiles p ON u.id = p.user_id
|
|
||||||
WHERE p.user_id IS NULL
|
|
||||||
AND (u.email LIKE '%@glit.edu.mx'
|
|
||||||
OR u.email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR u.email LIKE '%@gamilit.com');
|
|
||||||
" | tr -d ' ')
|
|
||||||
|
|
||||||
if [ "$UNLINKED" -gt 0 ]; then
|
|
||||||
echo "⚠️ Advertencia: $UNLINKED usuarios sin perfil"
|
|
||||||
else
|
|
||||||
echo "✅ Todos los usuarios tienen perfil vinculado"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo " ✅ CARGA COMPLETADA"
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
echo "📊 Resumen:"
|
|
||||||
echo " Usuarios cargados: $USERS_COUNT"
|
|
||||||
echo " Profiles cargados: $PROFILES_COUNT"
|
|
||||||
echo " Sin vincular: $UNLINKED"
|
|
||||||
echo ""
|
|
||||||
echo "📝 Para ver detalles, ejecutar:"
|
|
||||||
echo " bash scripts/verify-users.sh"
|
|
||||||
echo ""
|
|
||||||
@ -1,329 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
##############################################################################
|
|
||||||
# GAMILIT Platform - Database Recreation Script
|
|
||||||
#
|
|
||||||
# Propósito: ELIMINACIÓN COMPLETA y recreación (usuario + BD)
|
|
||||||
# ⚠️ DESTRUYE TODOS LOS DATOS ⚠️
|
|
||||||
#
|
|
||||||
# Uso:
|
|
||||||
# ./recreate-database.sh # Modo interactivo
|
|
||||||
# ./recreate-database.sh --env dev # Desarrollo
|
|
||||||
# ./recreate-database.sh --env prod # Producción
|
|
||||||
# ./recreate-database.sh --env dev --force # Sin confirmación
|
|
||||||
#
|
|
||||||
# Funcionalidades:
|
|
||||||
# 1. ⚠️ Elimina completamente la BD gamilit_platform
|
|
||||||
# 2. ⚠️ Elimina el usuario gamilit_user
|
|
||||||
# 3. Ejecuta init-database.sh para recrear todo
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colores
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
# Configuración
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
INIT_SCRIPT="$SCRIPT_DIR/init-database.sh"
|
|
||||||
|
|
||||||
DB_NAME="gamilit_platform"
|
|
||||||
DB_USER="gamilit_user"
|
|
||||||
DB_HOST="localhost"
|
|
||||||
DB_PORT="5432"
|
|
||||||
POSTGRES_USER="postgres"
|
|
||||||
|
|
||||||
ENVIRONMENT=""
|
|
||||||
FORCE_MODE=false
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FUNCIONES AUXILIARES
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
print_header() {
|
|
||||||
echo ""
|
|
||||||
echo -e "${RED}========================================${NC}"
|
|
||||||
echo -e "${RED}$1${NC}"
|
|
||||||
echo -e "${RED}========================================${NC}"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
print_step() {
|
|
||||||
echo -e "${CYAN}▶ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}✓ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}✗ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}⚠ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_info() {
|
|
||||||
echo " $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
show_help() {
|
|
||||||
cat << EOF
|
|
||||||
GAMILIT Platform - Recreación Completa de Base de Datos
|
|
||||||
|
|
||||||
⚠️ ADVERTENCIA: Este script ELIMINA TODOS LOS DATOS
|
|
||||||
|
|
||||||
Uso: $0 [OPCIONES]
|
|
||||||
|
|
||||||
Opciones:
|
|
||||||
--env dev|prod Ambiente
|
|
||||||
--force No pedir confirmación
|
|
||||||
--help Mostrar ayuda
|
|
||||||
|
|
||||||
Ejemplos:
|
|
||||||
$0 --env dev
|
|
||||||
$0 --env prod --force
|
|
||||||
|
|
||||||
Este script:
|
|
||||||
1. Elimina la base de datos gamilit_platform
|
|
||||||
2. Elimina el usuario gamilit_user
|
|
||||||
3. Ejecuta init-database.sh para recrear todo
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FUNCIONES SQL
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
execute_as_postgres() {
|
|
||||||
local sql="$1"
|
|
||||||
if [ "$USE_SUDO" = true ]; then
|
|
||||||
echo "$sql" | sudo -u postgres psql 2>&1
|
|
||||||
else
|
|
||||||
PGPASSWORD="$PGPASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$POSTGRES_USER" -c "$sql" 2>&1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
query_as_postgres() {
|
|
||||||
local sql="$1"
|
|
||||||
if [ "$USE_SUDO" = true ]; then
|
|
||||||
echo "$sql" | sudo -u postgres psql -t | xargs
|
|
||||||
else
|
|
||||||
PGPASSWORD="$PGPASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$POSTGRES_USER" -t -c "$sql" | xargs
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# VERIFICACIÓN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
check_prerequisites() {
|
|
||||||
print_step "Verificando prerequisitos..."
|
|
||||||
|
|
||||||
if ! command -v psql &> /dev/null; then
|
|
||||||
print_error "psql no encontrado"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$INIT_SCRIPT" ]; then
|
|
||||||
print_error "Script de inicialización no encontrado: $INIT_SCRIPT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar conexión PostgreSQL
|
|
||||||
if sudo -n -u postgres psql -c "SELECT 1" &> /dev/null 2>&1; then
|
|
||||||
USE_SUDO=true
|
|
||||||
print_success "Conectado a PostgreSQL (sudo)"
|
|
||||||
elif [ -n "$PGPASSWORD" ] && psql -h "$DB_HOST" -p "$DB_PORT" -U "$POSTGRES_USER" -c "SELECT 1" &> /dev/null 2>&1; then
|
|
||||||
USE_SUDO=false
|
|
||||||
print_success "Conectado a PostgreSQL (TCP)"
|
|
||||||
else
|
|
||||||
print_error "No se puede conectar a PostgreSQL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PASO 1: ELIMINAR BASE DE DATOS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
drop_database() {
|
|
||||||
print_step "PASO 1/3: Eliminando base de datos..."
|
|
||||||
|
|
||||||
db_exists=$(query_as_postgres "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'")
|
|
||||||
|
|
||||||
if [ -z "$db_exists" ]; then
|
|
||||||
print_info "Base de datos '$DB_NAME' no existe"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_info "Terminando conexiones activas..."
|
|
||||||
execute_as_postgres "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DB_NAME' AND pid <> pg_backend_pid();" > /dev/null 2>&1 || true
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
print_info "Eliminando base de datos '$DB_NAME'..."
|
|
||||||
if execute_as_postgres "DROP DATABASE IF EXISTS $DB_NAME;" > /dev/null 2>&1; then
|
|
||||||
print_success "Base de datos eliminada"
|
|
||||||
else
|
|
||||||
print_error "Error al eliminar base de datos"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PASO 2: ELIMINAR USUARIO
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
drop_user() {
|
|
||||||
print_step "PASO 2/3: Eliminando usuario..."
|
|
||||||
|
|
||||||
user_exists=$(query_as_postgres "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'")
|
|
||||||
|
|
||||||
if [ -z "$user_exists" ]; then
|
|
||||||
print_info "Usuario '$DB_USER' no existe"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_info "Eliminando objetos del usuario..."
|
|
||||||
execute_as_postgres "DROP OWNED BY $DB_USER CASCADE;" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
print_info "Eliminando usuario '$DB_USER'..."
|
|
||||||
if execute_as_postgres "DROP USER IF EXISTS $DB_USER;" > /dev/null 2>&1; then
|
|
||||||
print_success "Usuario eliminado"
|
|
||||||
else
|
|
||||||
print_warning "No se pudo eliminar el usuario"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PASO 3: REINICIALIZAR
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
reinitialize() {
|
|
||||||
print_step "PASO 3/3: Reinicializando..."
|
|
||||||
|
|
||||||
print_info "Ejecutando init-database.sh..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
local init_args="--env $ENVIRONMENT"
|
|
||||||
if [ "$FORCE_MODE" = true ]; then
|
|
||||||
init_args="$init_args --force"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if bash "$INIT_SCRIPT" $init_args; then
|
|
||||||
print_success "Reinicialización completada"
|
|
||||||
else
|
|
||||||
print_error "Error durante reinicialización"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# CONFIRMACIÓN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
confirm_deletion() {
|
|
||||||
print_header "⚠️ ADVERTENCIA: ELIMINACIÓN DE DATOS"
|
|
||||||
|
|
||||||
echo -e "${RED}Este script eliminará PERMANENTEMENTE:${NC}"
|
|
||||||
echo -e " • Base de datos: ${YELLOW}$DB_NAME${NC}"
|
|
||||||
echo -e " • Usuario: ${YELLOW}$DB_USER${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${RED}TODOS LOS DATOS SERÁN ELIMINADOS${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ "$FORCE_MODE" = false ]; then
|
|
||||||
echo -e "${RED}¿Estás COMPLETAMENTE seguro?${NC}"
|
|
||||||
read -p "Escribe 'DELETE ALL' para confirmar: " confirmation
|
|
||||||
|
|
||||||
if [ "$confirmation" != "DELETE ALL" ]; then
|
|
||||||
print_info "Operación cancelada"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
read -p "¿Continuar? (yes/no): " final_confirm
|
|
||||||
if [ "$final_confirm" != "yes" ]; then
|
|
||||||
print_info "Operación cancelada"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_warning "Iniciando en 3 segundos..."
|
|
||||||
sleep 1
|
|
||||||
echo -n "3... "
|
|
||||||
sleep 1
|
|
||||||
echo -n "2... "
|
|
||||||
sleep 1
|
|
||||||
echo "1..."
|
|
||||||
sleep 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# MAIN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
main() {
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--env)
|
|
||||||
ENVIRONMENT="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--force)
|
|
||||||
FORCE_MODE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help)
|
|
||||||
show_help
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
print_error "Opción desconocida: $1"
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$ENVIRONMENT" ]; then
|
|
||||||
print_header "GAMILIT Platform - Recreación de BD"
|
|
||||||
echo "Selecciona ambiente:"
|
|
||||||
echo " 1) dev"
|
|
||||||
echo " 2) prod"
|
|
||||||
read -p "Opción: " env_option
|
|
||||||
|
|
||||||
case $env_option in
|
|
||||||
1) ENVIRONMENT="dev" ;;
|
|
||||||
2) ENVIRONMENT="prod" ;;
|
|
||||||
*)
|
|
||||||
print_error "Opción inválida"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$ENVIRONMENT" != "dev" ] && [ "$ENVIRONMENT" != "prod" ]; then
|
|
||||||
print_error "Ambiente inválido: $ENVIRONMENT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
confirm_deletion
|
|
||||||
check_prerequisites
|
|
||||||
drop_database
|
|
||||||
drop_user
|
|
||||||
reinitialize
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
print_header "✅ BASE DE DATOS RECREADA"
|
|
||||||
echo -e "${GREEN}Base de datos y usuario recreados desde cero${NC}"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@ -1,503 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
##############################################################################
|
|
||||||
# GAMILIT Platform - Database Reset Script
|
|
||||||
#
|
|
||||||
# Propósito: Reiniciar SOLO la base de datos (mantiene usuario existente)
|
|
||||||
# ⚠️ Elimina datos pero NO el usuario PostgreSQL
|
|
||||||
#
|
|
||||||
# Uso:
|
|
||||||
# ./reset-database.sh # Modo interactivo
|
|
||||||
# ./reset-database.sh --env dev # Desarrollo
|
|
||||||
# ./reset-database.sh --env prod # Producción
|
|
||||||
# ./reset-database.sh --env dev --force # Sin confirmación
|
|
||||||
# ./reset-database.sh --password "mi_pass" # Con password conocido
|
|
||||||
#
|
|
||||||
# Funcionalidades:
|
|
||||||
# 1. ⚠️ Elimina la BD gamilit_platform
|
|
||||||
# 2. ✅ Mantiene el usuario gamilit_user
|
|
||||||
# 3. Recrea BD, ejecuta DDL y carga seeds
|
|
||||||
#
|
|
||||||
# Ideal para:
|
|
||||||
# - Usuario ya existe con password conocido
|
|
||||||
# - Resetear datos sin tocar configuración de usuario
|
|
||||||
# - Ambientes donde el usuario tiene permisos específicos
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colores
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
# Configuración de rutas
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
DATABASE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
DDL_DIR="$DATABASE_ROOT/ddl"
|
|
||||||
SEEDS_DIR="$DATABASE_ROOT/seeds"
|
|
||||||
|
|
||||||
# Configuración de base de datos
|
|
||||||
DB_NAME="gamilit_platform"
|
|
||||||
DB_USER="gamilit_user"
|
|
||||||
DB_HOST="localhost"
|
|
||||||
DB_PORT="5432"
|
|
||||||
POSTGRES_USER="postgres"
|
|
||||||
|
|
||||||
# Variables
|
|
||||||
ENVIRONMENT=""
|
|
||||||
FORCE_MODE=false
|
|
||||||
DB_PASSWORD=""
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FUNCIONES AUXILIARES
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
print_header() {
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}========================================${NC}"
|
|
||||||
echo -e "${YELLOW}$1${NC}"
|
|
||||||
echo -e "${YELLOW}========================================${NC}"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
print_step() {
|
|
||||||
echo -e "${CYAN}▶ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}✓ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}✗ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}⚠ $1${NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_info() {
|
|
||||||
echo " $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
show_help() {
|
|
||||||
cat << EOF
|
|
||||||
GAMILIT Platform - Reset de Base de Datos (Mantiene Usuario)
|
|
||||||
|
|
||||||
Uso: $0 [OPCIONES]
|
|
||||||
|
|
||||||
Opciones:
|
|
||||||
--env dev|prod Ambiente
|
|
||||||
--password PASS Password del usuario existente (requerido)
|
|
||||||
--force No pedir confirmación
|
|
||||||
--help Mostrar ayuda
|
|
||||||
|
|
||||||
Ejemplos:
|
|
||||||
$0 --env dev --password "mi_password"
|
|
||||||
$0 --env prod --password "prod_pass" --force
|
|
||||||
|
|
||||||
Este script:
|
|
||||||
1. Elimina la base de datos gamilit_platform
|
|
||||||
2. Mantiene el usuario gamilit_user
|
|
||||||
3. Recrea la BD con DDL y seeds
|
|
||||||
|
|
||||||
⚠️ Requiere conocer el password del usuario existente
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FUNCIONES SQL
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
execute_as_postgres() {
|
|
||||||
local sql="$1"
|
|
||||||
if [ "$USE_SUDO" = true ]; then
|
|
||||||
echo "$sql" | sudo -u postgres psql 2>&1
|
|
||||||
else
|
|
||||||
PGPASSWORD="$PGPASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$POSTGRES_USER" -c "$sql" 2>&1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
query_as_postgres() {
|
|
||||||
local sql="$1"
|
|
||||||
if [ "$USE_SUDO" = true ]; then
|
|
||||||
echo "$sql" | sudo -u postgres psql -t | xargs
|
|
||||||
else
|
|
||||||
PGPASSWORD="$PGPASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$POSTGRES_USER" -t -c "$sql" | xargs
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
execute_sql_file() {
|
|
||||||
local file="$1"
|
|
||||||
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$file" 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# VERIFICACIÓN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
check_prerequisites() {
|
|
||||||
print_step "Verificando prerequisitos..."
|
|
||||||
|
|
||||||
if ! command -v psql &> /dev/null; then
|
|
||||||
print_error "psql no encontrado"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
print_success "psql encontrado"
|
|
||||||
|
|
||||||
if [ ! -d "$DDL_DIR" ]; then
|
|
||||||
print_error "Directorio DDL no encontrado: $DDL_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "$SEEDS_DIR" ]; then
|
|
||||||
print_error "Directorio seeds no encontrado: $SEEDS_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar conexión PostgreSQL
|
|
||||||
if sudo -n -u postgres psql -c "SELECT 1" &> /dev/null 2>&1; then
|
|
||||||
USE_SUDO=true
|
|
||||||
print_success "Conectado a PostgreSQL (sudo)"
|
|
||||||
elif [ -n "$PGPASSWORD" ] && psql -h "$DB_HOST" -p "$DB_PORT" -U "$POSTGRES_USER" -c "SELECT 1" &> /dev/null 2>&1; then
|
|
||||||
USE_SUDO=false
|
|
||||||
print_success "Conectado a PostgreSQL (TCP)"
|
|
||||||
else
|
|
||||||
print_error "No se puede conectar a PostgreSQL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar que el usuario existe
|
|
||||||
user_exists=$(query_as_postgres "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'")
|
|
||||||
if [ -z "$user_exists" ]; then
|
|
||||||
print_error "Usuario '$DB_USER' no existe"
|
|
||||||
print_info "Usa init-database.sh para crear el usuario primero"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
print_success "Usuario $DB_USER existe"
|
|
||||||
|
|
||||||
# Verificar password del usuario
|
|
||||||
if [ -z "$DB_PASSWORD" ]; then
|
|
||||||
print_error "Password requerido (usar --password)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Probar conexión con el password
|
|
||||||
if ! PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT 1" > /dev/null 2>&1; then
|
|
||||||
print_error "Password incorrecto para usuario $DB_USER"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
print_success "Password verificado"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PASO 1: ELIMINAR BASE DE DATOS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
drop_database() {
|
|
||||||
print_step "PASO 1/4: Eliminando base de datos..."
|
|
||||||
|
|
||||||
db_exists=$(query_as_postgres "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'")
|
|
||||||
|
|
||||||
if [ -z "$db_exists" ]; then
|
|
||||||
print_info "Base de datos '$DB_NAME' no existe, se creará nueva"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_info "Terminando conexiones activas..."
|
|
||||||
execute_as_postgres "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DB_NAME' AND pid <> pg_backend_pid();" > /dev/null 2>&1 || true
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
print_info "Eliminando base de datos '$DB_NAME'..."
|
|
||||||
if execute_as_postgres "DROP DATABASE IF EXISTS $DB_NAME;" > /dev/null 2>&1; then
|
|
||||||
print_success "Base de datos eliminada"
|
|
||||||
else
|
|
||||||
print_error "Error al eliminar base de datos"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PASO 2: CREAR BASE DE DATOS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
create_database() {
|
|
||||||
print_step "PASO 2/4: Creando base de datos..."
|
|
||||||
|
|
||||||
print_info "Creando base de datos $DB_NAME..."
|
|
||||||
execute_as_postgres "CREATE DATABASE $DB_NAME OWNER $DB_USER ENCODING 'UTF8';" > /dev/null
|
|
||||||
print_success "Base de datos creada"
|
|
||||||
|
|
||||||
execute_as_postgres "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" > /dev/null
|
|
||||||
print_success "Privilegios otorgados"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PASO 3: EJECUTAR DDL Y SEEDS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
execute_ddl() {
|
|
||||||
print_step "PASO 3/4: Ejecutando DDL..."
|
|
||||||
|
|
||||||
export PGPASSWORD="$DB_PASSWORD"
|
|
||||||
|
|
||||||
local schemas=(
|
|
||||||
"auth"
|
|
||||||
"auth_management"
|
|
||||||
"system_configuration"
|
|
||||||
"gamification_system"
|
|
||||||
"educational_content"
|
|
||||||
"content_management"
|
|
||||||
"social_features"
|
|
||||||
"progress_tracking"
|
|
||||||
"audit_logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Crear schemas
|
|
||||||
print_info "Creando schemas..."
|
|
||||||
for schema in "${schemas[@]}"; do
|
|
||||||
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "CREATE SCHEMA IF NOT EXISTS $schema;" > /dev/null 2>&1
|
|
||||||
done
|
|
||||||
print_success "9 schemas creados"
|
|
||||||
|
|
||||||
# Crear ENUMs
|
|
||||||
print_info "Creando ENUMs..."
|
|
||||||
if [ -d "$DDL_DIR/schemas/gamification_system/enums" ]; then
|
|
||||||
for enum_file in "$DDL_DIR/schemas/gamification_system/enums"/*.sql; do
|
|
||||||
if [ -f "$enum_file" ]; then
|
|
||||||
execute_sql_file "$enum_file" > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
print_success "ENUMs creados"
|
|
||||||
|
|
||||||
# Crear tablas
|
|
||||||
print_info "Creando tablas..."
|
|
||||||
local table_count=0
|
|
||||||
for schema in "${schemas[@]}"; do
|
|
||||||
local tables_dir="$DDL_DIR/schemas/$schema/tables"
|
|
||||||
if [ -d "$tables_dir" ]; then
|
|
||||||
for table_file in "$tables_dir"/*.sql; do
|
|
||||||
if [ -f "$table_file" ]; then
|
|
||||||
if execute_sql_file "$table_file" > /dev/null 2>&1; then
|
|
||||||
((table_count++))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
print_success "$table_count tablas creadas"
|
|
||||||
|
|
||||||
# Otorgar permisos a gamilit_user
|
|
||||||
print_info "Otorgando permisos a gamilit_user..."
|
|
||||||
local perms_file="$DDL_DIR/99-post-ddl-permissions.sql"
|
|
||||||
if [ -f "$perms_file" ]; then
|
|
||||||
execute_sql_file "$perms_file" > /dev/null 2>&1
|
|
||||||
print_success "Permisos otorgados"
|
|
||||||
else
|
|
||||||
print_warning "Archivo de permisos no encontrado: $perms_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
unset PGPASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
load_seeds() {
|
|
||||||
print_step "PASO 4/4: Cargando seeds..."
|
|
||||||
|
|
||||||
export PGPASSWORD="$DB_PASSWORD"
|
|
||||||
|
|
||||||
local seeds_base="$SEEDS_DIR/dev"
|
|
||||||
local loaded=0
|
|
||||||
local failed=0
|
|
||||||
|
|
||||||
# Array con orden específico respetando dependencias
|
|
||||||
# IMPORTANTE: Este orden es crítico para evitar errores de FK
|
|
||||||
local seed_files=(
|
|
||||||
# 1. Tenants y auth providers (sin dependencias)
|
|
||||||
"$seeds_base/auth_management/01-tenants.sql"
|
|
||||||
"$seeds_base/auth_management/02-auth_providers.sql"
|
|
||||||
|
|
||||||
# 2. Users (depende de tenants - opcional)
|
|
||||||
"$seeds_base/auth/01-demo-users.sql"
|
|
||||||
|
|
||||||
# 3. Profiles (CRÍTICO: depende de users y tenants)
|
|
||||||
"$seeds_base/auth_management/03-profiles.sql"
|
|
||||||
|
|
||||||
# 4. Resto de auth_management
|
|
||||||
"$seeds_base/auth_management/04-user_roles.sql"
|
|
||||||
"$seeds_base/auth_management/05-user_preferences.sql"
|
|
||||||
"$seeds_base/auth_management/06-auth_attempts.sql"
|
|
||||||
"$seeds_base/auth_management/07-security_events.sql"
|
|
||||||
|
|
||||||
# 5. System configuration
|
|
||||||
"$seeds_base/system_configuration/01-system_settings.sql"
|
|
||||||
"$seeds_base/system_configuration/02-feature_flags.sql"
|
|
||||||
|
|
||||||
# 6. Gamificación (depende de users/profiles)
|
|
||||||
"$seeds_base/gamification_system/01-achievement_categories.sql"
|
|
||||||
"$seeds_base/gamification_system/02-achievements.sql"
|
|
||||||
"$seeds_base/gamification_system/03-leaderboard_metadata.sql"
|
|
||||||
"$seeds_base/gamification_system/04-initialize_user_gamification.sql"
|
|
||||||
|
|
||||||
# 7. Educational content
|
|
||||||
"$seeds_base/educational_content/01-modules.sql"
|
|
||||||
"$seeds_base/educational_content/02-exercises-module1.sql"
|
|
||||||
"$seeds_base/educational_content/03-exercises-module2.sql"
|
|
||||||
"$seeds_base/educational_content/04-exercises-module3.sql"
|
|
||||||
"$seeds_base/educational_content/05-exercises-module4.sql"
|
|
||||||
"$seeds_base/educational_content/06-exercises-module5.sql"
|
|
||||||
"$seeds_base/educational_content/07-assessment-rubrics.sql"
|
|
||||||
|
|
||||||
# 8. Content management
|
|
||||||
"$seeds_base/content_management/01-marie-curie-bio.sql"
|
|
||||||
"$seeds_base/content_management/02-media-files.sql"
|
|
||||||
"$seeds_base/content_management/03-tags.sql"
|
|
||||||
|
|
||||||
# 9. Social features
|
|
||||||
"$seeds_base/social_features/01-schools.sql"
|
|
||||||
"$seeds_base/social_features/02-classrooms.sql"
|
|
||||||
"$seeds_base/social_features/03-classroom-members.sql"
|
|
||||||
"$seeds_base/social_features/04-teams.sql"
|
|
||||||
|
|
||||||
# 10. Progress tracking
|
|
||||||
"$seeds_base/progress_tracking/01-demo-progress.sql"
|
|
||||||
"$seeds_base/progress_tracking/02-exercise-attempts.sql"
|
|
||||||
|
|
||||||
# 11. Audit logging
|
|
||||||
"$seeds_base/audit_logging/01-audit-logs.sql"
|
|
||||||
"$seeds_base/audit_logging/02-system-metrics.sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
for seed_file in "${seed_files[@]}"; do
|
|
||||||
if [ -f "$seed_file" ]; then
|
|
||||||
local basename_file=$(basename "$seed_file")
|
|
||||||
print_info " $basename_file"
|
|
||||||
|
|
||||||
# CRÍTICO: NO ocultar errores - ejecutar y mostrar salida
|
|
||||||
if execute_sql_file "$seed_file" 2>&1 | grep -i "error" > /dev/null; then
|
|
||||||
((failed++))
|
|
||||||
print_warning " ⚠️ Errores en $basename_file (continuando...)"
|
|
||||||
else
|
|
||||||
((loaded++))
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_warning "Seed no encontrado: $(basename $seed_file)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $failed -gt 0 ]; then
|
|
||||||
print_warning "$loaded seeds cargados, $failed con errores"
|
|
||||||
else
|
|
||||||
print_success "$loaded seeds cargados exitosamente"
|
|
||||||
fi
|
|
||||||
|
|
||||||
unset PGPASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# CONFIRMACIÓN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
confirm_reset() {
|
|
||||||
print_header "⚠️ ADVERTENCIA: RESET DE BASE DE DATOS"
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Este script:${NC}"
|
|
||||||
echo -e " • Eliminará la base de datos: ${RED}$DB_NAME${NC}"
|
|
||||||
echo -e " • Mantendrá el usuario: ${GREEN}$DB_USER${NC}"
|
|
||||||
echo -e " • Recreará schemas, tablas y datos"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ "$FORCE_MODE" = false ]; then
|
|
||||||
read -p "¿Continuar con el reset? (yes/no): " confirm
|
|
||||||
if [ "$confirm" != "yes" ]; then
|
|
||||||
print_info "Operación cancelada"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# MAIN
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
main() {
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--env)
|
|
||||||
ENVIRONMENT="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--password)
|
|
||||||
DB_PASSWORD="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--force)
|
|
||||||
FORCE_MODE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help)
|
|
||||||
show_help
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
print_error "Opción desconocida: $1"
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$ENVIRONMENT" ]; then
|
|
||||||
print_header "GAMILIT Platform - Reset de BD"
|
|
||||||
echo "Selecciona ambiente:"
|
|
||||||
echo " 1) dev"
|
|
||||||
echo " 2) prod"
|
|
||||||
read -p "Opción: " env_option
|
|
||||||
|
|
||||||
case $env_option in
|
|
||||||
1) ENVIRONMENT="dev" ;;
|
|
||||||
2) ENVIRONMENT="prod" ;;
|
|
||||||
*)
|
|
||||||
print_error "Opción inválida"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$ENVIRONMENT" != "dev" ] && [ "$ENVIRONMENT" != "prod" ]; then
|
|
||||||
print_error "Ambiente inválido: $ENVIRONMENT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Si no se proveyó password, preguntar
|
|
||||||
if [ -z "$DB_PASSWORD" ]; then
|
|
||||||
read -sp "Password para usuario $DB_USER: " DB_PASSWORD
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
confirm_reset
|
|
||||||
check_prerequisites
|
|
||||||
drop_database
|
|
||||||
create_database
|
|
||||||
execute_ddl
|
|
||||||
load_seeds
|
|
||||||
|
|
||||||
print_header "✅ BASE DE DATOS RESETEADA"
|
|
||||||
|
|
||||||
echo -e "${GREEN}Base de datos recreada exitosamente${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${CYAN}Conexión:${NC}"
|
|
||||||
echo -e " Database: $DB_NAME"
|
|
||||||
echo -e " User: $DB_USER"
|
|
||||||
echo -e " Host: $DB_HOST:$DB_PORT"
|
|
||||||
echo ""
|
|
||||||
print_success "¡Listo para usar!"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@ -1,395 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- SCRIPT DE EMERGENCIA: CREAR USUARIOS DE TESTING
|
|
||||||
-- =====================================================
|
|
||||||
-- Fecha: 2025-11-11
|
|
||||||
-- Propósito: Crear usuarios de testing manualmente
|
|
||||||
-- Usuarios: admin@gamilit.com, teacher@gamilit.com, student@gamilit.com
|
|
||||||
-- Password: Test1234 (para todos)
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
-- Habilitar extensión pgcrypto si no está habilitada
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
|
|
||||||
-- Verificar tenant por defecto
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
default_tenant_id uuid;
|
|
||||||
BEGIN
|
|
||||||
-- Buscar o crear tenant por defecto
|
|
||||||
SELECT id INTO default_tenant_id
|
|
||||||
FROM auth_management.tenants
|
|
||||||
WHERE name = 'GAMILIT Platform'
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
IF default_tenant_id IS NULL THEN
|
|
||||||
INSERT INTO auth_management.tenants (
|
|
||||||
id, name, slug, status, settings, created_at, updated_at
|
|
||||||
) VALUES (
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'GAMILIT Platform',
|
|
||||||
'gamilit-platform',
|
|
||||||
'active',
|
|
||||||
'{}'::jsonb,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
) ON CONFLICT (id) DO NOTHING;
|
|
||||||
|
|
||||||
default_tenant_id := '00000000-0000-0000-0000-000000000001'::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Tenant ID: %', default_tenant_id;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- PASO 1: CREAR USUARIOS EN auth.users
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO auth.users (
|
|
||||||
id,
|
|
||||||
instance_id,
|
|
||||||
email,
|
|
||||||
encrypted_password,
|
|
||||||
email_confirmed_at,
|
|
||||||
raw_app_meta_data,
|
|
||||||
raw_user_meta_data,
|
|
||||||
gamilit_role,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
confirmation_token,
|
|
||||||
email_change,
|
|
||||||
email_change_token_new,
|
|
||||||
recovery_token
|
|
||||||
) VALUES
|
|
||||||
-- ADMIN
|
|
||||||
(
|
|
||||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000000'::uuid,
|
|
||||||
'admin@gamilit.com',
|
|
||||||
crypt('Test1234', gen_salt('bf', 10)),
|
|
||||||
NOW(),
|
|
||||||
jsonb_build_object(
|
|
||||||
'provider', 'email',
|
|
||||||
'providers', ARRAY['email']
|
|
||||||
),
|
|
||||||
jsonb_build_object(
|
|
||||||
'name', 'Admin GAMILIT',
|
|
||||||
'role', 'super_admin'
|
|
||||||
),
|
|
||||||
'super_admin'::auth_management.gamilit_role,
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
''
|
|
||||||
),
|
|
||||||
-- TEACHER
|
|
||||||
(
|
|
||||||
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000000'::uuid,
|
|
||||||
'teacher@gamilit.com',
|
|
||||||
crypt('Test1234', gen_salt('bf', 10)),
|
|
||||||
NOW(),
|
|
||||||
jsonb_build_object(
|
|
||||||
'provider', 'email',
|
|
||||||
'providers', ARRAY['email']
|
|
||||||
),
|
|
||||||
jsonb_build_object(
|
|
||||||
'name', 'Profesor Testing',
|
|
||||||
'role', 'teacher'
|
|
||||||
),
|
|
||||||
'teacher'::auth_management.gamilit_role,
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
''
|
|
||||||
),
|
|
||||||
-- STUDENT
|
|
||||||
(
|
|
||||||
'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000000'::uuid,
|
|
||||||
'student@gamilit.com',
|
|
||||||
crypt('Test1234', gen_salt('bf', 10)),
|
|
||||||
NOW(),
|
|
||||||
jsonb_build_object(
|
|
||||||
'provider', 'email',
|
|
||||||
'providers', ARRAY['email']
|
|
||||||
),
|
|
||||||
jsonb_build_object(
|
|
||||||
'name', 'Estudiante Testing',
|
|
||||||
'role', 'student'
|
|
||||||
),
|
|
||||||
'student'::auth_management.gamilit_role,
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
''
|
|
||||||
)
|
|
||||||
ON CONFLICT (email) DO UPDATE SET
|
|
||||||
encrypted_password = EXCLUDED.encrypted_password,
|
|
||||||
updated_at = NOW();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- PASO 2: CREAR PROFILES EN auth_management.profiles
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO auth_management.profiles (
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
tenant_id,
|
|
||||||
email,
|
|
||||||
full_name,
|
|
||||||
first_name,
|
|
||||||
last_name,
|
|
||||||
role,
|
|
||||||
status,
|
|
||||||
email_verified,
|
|
||||||
preferences,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES
|
|
||||||
-- ADMIN PROFILE
|
|
||||||
(
|
|
||||||
'aaaaaaaa-aaaa-aaaa-bbbb-aaaaaaaaaaaa'::uuid,
|
|
||||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'admin@gamilit.com',
|
|
||||||
'Admin GAMILIT',
|
|
||||||
'Admin',
|
|
||||||
'GAMILIT',
|
|
||||||
'super_admin'::auth_management.gamilit_role,
|
|
||||||
'active'::auth_management.user_status,
|
|
||||||
true,
|
|
||||||
jsonb_build_object(
|
|
||||||
'theme', 'detective',
|
|
||||||
'language', 'es',
|
|
||||||
'timezone', 'America/Mexico_City',
|
|
||||||
'sound_enabled', true,
|
|
||||||
'notifications_enabled', true
|
|
||||||
),
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
-- TEACHER PROFILE
|
|
||||||
(
|
|
||||||
'bbbbbbbb-bbbb-bbbb-cccc-bbbbbbbbbbbb'::uuid,
|
|
||||||
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'teacher@gamilit.com',
|
|
||||||
'Profesor Testing',
|
|
||||||
'Profesor',
|
|
||||||
'Testing',
|
|
||||||
'teacher'::auth_management.gamilit_role,
|
|
||||||
'active'::auth_management.user_status,
|
|
||||||
true,
|
|
||||||
jsonb_build_object(
|
|
||||||
'theme', 'detective',
|
|
||||||
'language', 'es',
|
|
||||||
'timezone', 'America/Mexico_City',
|
|
||||||
'sound_enabled', true,
|
|
||||||
'notifications_enabled', true
|
|
||||||
),
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
-- STUDENT PROFILE
|
|
||||||
(
|
|
||||||
'cccccccc-cccc-cccc-dddd-cccccccccccc'::uuid,
|
|
||||||
'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'student@gamilit.com',
|
|
||||||
'Estudiante Testing',
|
|
||||||
'Estudiante',
|
|
||||||
'Testing',
|
|
||||||
'student'::auth_management.gamilit_role,
|
|
||||||
'active'::auth_management.user_status,
|
|
||||||
true,
|
|
||||||
jsonb_build_object(
|
|
||||||
'theme', 'detective',
|
|
||||||
'language', 'es',
|
|
||||||
'timezone', 'America/Mexico_City',
|
|
||||||
'sound_enabled', true,
|
|
||||||
'notifications_enabled', true,
|
|
||||||
'grade_level', '5'
|
|
||||||
),
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
full_name = EXCLUDED.full_name,
|
|
||||||
updated_at = NOW();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- PASO 3: INICIALIZAR user_stats (gamification)
|
|
||||||
-- =====================================================
|
|
||||||
-- Nota: El trigger trg_initialize_user_stats debería hacer esto automáticamente
|
|
||||||
-- pero lo agregamos manualmente por si acaso
|
|
||||||
|
|
||||||
INSERT INTO gamification_system.user_stats (
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
tenant_id,
|
|
||||||
level,
|
|
||||||
total_xp,
|
|
||||||
xp_to_next_level,
|
|
||||||
current_rank,
|
|
||||||
ml_coins,
|
|
||||||
ml_coins_earned_total,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES
|
|
||||||
(
|
|
||||||
'aaaaaaaa-aaaa-stat-aaaa-aaaaaaaaaaaa'::uuid,
|
|
||||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
'Ajaw'::gamification_system.maya_rank,
|
|
||||||
100,
|
|
||||||
100,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'bbbbbbbb-bbbb-stat-bbbb-bbbbbbbbbbbb'::uuid,
|
|
||||||
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
'Ajaw'::gamification_system.maya_rank,
|
|
||||||
100,
|
|
||||||
100,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'cccccccc-cccc-stat-cccc-cccccccccccc'::uuid,
|
|
||||||
'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
'Ajaw'::gamification_system.maya_rank,
|
|
||||||
100,
|
|
||||||
100,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
updated_at = NOW();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- PASO 4: INICIALIZAR user_ranks (gamification)
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO gamification_system.user_ranks (
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
tenant_id,
|
|
||||||
current_rank,
|
|
||||||
rank_level,
|
|
||||||
total_rank_points,
|
|
||||||
rank_achieved_at,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES
|
|
||||||
(
|
|
||||||
'aaaaaaaa-aaaa-rank-aaaa-aaaaaaaaaaaa'::uuid,
|
|
||||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'Ajaw'::gamification_system.maya_rank,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'bbbbbbbb-bbbb-rank-bbbb-bbbbbbbbbbbb'::uuid,
|
|
||||||
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'Ajaw'::gamification_system.maya_rank,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'cccccccc-cccc-rank-cccc-cccccccccccc'::uuid,
|
|
||||||
'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid,
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'Ajaw'::gamification_system.maya_rank,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
updated_at = NOW();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- VERIFICACIÓN FINAL
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
users_count INTEGER;
|
|
||||||
profiles_count INTEGER;
|
|
||||||
stats_count INTEGER;
|
|
||||||
ranks_count INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO users_count FROM auth.users
|
|
||||||
WHERE email IN ('admin@gamilit.com', 'teacher@gamilit.com', 'student@gamilit.com');
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO profiles_count FROM auth_management.profiles
|
|
||||||
WHERE email IN ('admin@gamilit.com', 'teacher@gamilit.com', 'student@gamilit.com');
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO stats_count FROM gamification_system.user_stats
|
|
||||||
WHERE user_id IN (
|
|
||||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid,
|
|
||||||
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
|
|
||||||
'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid
|
|
||||||
);
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO ranks_count FROM gamification_system.user_ranks
|
|
||||||
WHERE user_id IN (
|
|
||||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid,
|
|
||||||
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
|
|
||||||
'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid
|
|
||||||
);
|
|
||||||
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE 'USUARIOS DE TESTING CREADOS';
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE 'auth.users: % usuarios', users_count;
|
|
||||||
RAISE NOTICE 'auth_management.profiles: % profiles', profiles_count;
|
|
||||||
RAISE NOTICE 'gamification_system.user_stats: % stats', stats_count;
|
|
||||||
RAISE NOTICE 'gamification_system.user_ranks: % ranks', ranks_count;
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
|
|
||||||
IF users_count = 3 AND profiles_count = 3 AND stats_count = 3 AND ranks_count = 3 THEN
|
|
||||||
RAISE NOTICE '✅ TODOS LOS USUARIOS CREADOS EXITOSAMENTE';
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE 'Credenciales de testing:';
|
|
||||||
RAISE NOTICE ' - admin@gamilit.com / Test1234';
|
|
||||||
RAISE NOTICE ' - teacher@gamilit.com / Test1234';
|
|
||||||
RAISE NOTICE ' - student@gamilit.com / Test1234';
|
|
||||||
ELSE
|
|
||||||
RAISE WARNING '⚠️ ALGUNOS USUARIOS NO SE CREARON CORRECTAMENTE';
|
|
||||||
RAISE WARNING 'Esperado: 3 users, 3 profiles, 3 stats, 3 ranks';
|
|
||||||
RAISE WARNING 'Creado: % users, % profiles, % stats, % ranks',
|
|
||||||
users_count, profiles_count, stats_count, ranks_count;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- FIN DEL SCRIPT
|
|
||||||
-- =====================================================
|
|
||||||
@ -1,205 +0,0 @@
|
|||||||
# Scripts de Validación de Integridad - GAMILIT Database
|
|
||||||
|
|
||||||
**Fecha:** 2025-11-24
|
|
||||||
**Mantenido por:** Database-Agent
|
|
||||||
**Propósito:** Scripts para validar y mantener la integridad de datos de XP y ML Coins
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Scripts Disponibles
|
|
||||||
|
|
||||||
### 1. `quick-validate-xp.sql`
|
|
||||||
|
|
||||||
**Descripción:** Validación rápida (30 segundos) para detectar problemas de integridad en XP.
|
|
||||||
|
|
||||||
**Uso:**
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/apps/database
|
|
||||||
PGPASSWORD='C5hq7253pdVyVKUC' psql -h localhost -U gamilit_user -d gamilit_platform -f scripts/quick-validate-xp.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**Salida esperada (sistema saludable):**
|
|
||||||
```
|
|
||||||
1. Intentos con score > 0 pero xp_earned = 0:
|
|
||||||
intentos_problematicos = 0
|
|
||||||
|
|
||||||
2. Usuarios con discrepancias:
|
|
||||||
usuarios_con_discrepancias = 0
|
|
||||||
|
|
||||||
3. Estado de integridad:
|
|
||||||
estado = "✅ INTEGRIDAD OK"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frecuencia recomendada:** Diaria o después de cada deployment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. `validate-xp-integrity.sql`
|
|
||||||
|
|
||||||
**Descripción:** Validación completa (2-3 minutos) con reporte detallado de todos los aspectos de integridad.
|
|
||||||
|
|
||||||
**Uso:**
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/apps/database
|
|
||||||
PGPASSWORD='C5hq7253pdVyVKUC' psql -h localhost -U gamilit_user -d gamilit_platform -f scripts/validate-xp-integrity.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**Validaciones incluidas:**
|
|
||||||
1. Intentos con score > 0 pero xp_earned = 0
|
|
||||||
2. Usuarios con discrepancias entre attempts y user_stats
|
|
||||||
3. Intentos donde xp_earned no coincide con la fórmula esperada
|
|
||||||
4. User stats sin attempts registrados
|
|
||||||
5. Resumen general del sistema
|
|
||||||
|
|
||||||
**Frecuencia recomendada:** Semanal o cuando se detecten anomalías
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. `fix-historical-xp-ml-coins-v2.sql`
|
|
||||||
|
|
||||||
**Descripción:** Script de corrección automática de datos históricos (solo si se detectan problemas).
|
|
||||||
|
|
||||||
**⚠️ ADVERTENCIA:** Solo ejecutar si `quick-validate-xp.sql` reporta problemas.
|
|
||||||
|
|
||||||
**Uso:**
|
|
||||||
```bash
|
|
||||||
cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/apps/database
|
|
||||||
PGPASSWORD='C5hq7253pdVyVKUC' psql -h localhost -U gamilit_user -d gamilit_platform -f scripts/fix-historical-xp-ml-coins-v2.sql
|
|
||||||
|
|
||||||
# Revisar output cuidadosamente antes de confirmar
|
|
||||||
# Si todo se ve bien, ejecutar en otra sesión:
|
|
||||||
PGPASSWORD='C5hq7253pdVyVKUC' psql -h localhost -U gamilit_user -d gamilit_platform -c "COMMIT;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Acciones que realiza:**
|
|
||||||
1. Deshabilita trigger `trg_check_rank_promotion_on_xp_gain`
|
|
||||||
2. Corrige `xp_earned` y `ml_coins_earned` en `exercise_attempts`
|
|
||||||
3. Recalcula `user_stats` basado en suma de attempts
|
|
||||||
4. Rehabilita trigger
|
|
||||||
5. Valida integridad final
|
|
||||||
|
|
||||||
**Frecuencia recomendada:** Solo cuando sea necesario (no es una tarea periódica)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Interpretación de Resultados
|
|
||||||
|
|
||||||
### Estado: ✅ INTEGRIDAD OK
|
|
||||||
|
|
||||||
Todo funciona correctamente. No se requiere acción.
|
|
||||||
|
|
||||||
### Estado: ❌ HAY PROBLEMAS
|
|
||||||
|
|
||||||
**Pasos a seguir:**
|
|
||||||
|
|
||||||
1. **Ejecutar validación completa:**
|
|
||||||
```bash
|
|
||||||
psql -d gamilit_platform -f scripts/validate-xp-integrity.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Analizar reporte detallado:**
|
|
||||||
- ¿Cuántos intentos afectados?
|
|
||||||
- ¿Cuántos usuarios tienen discrepancias?
|
|
||||||
- ¿Cuál es la magnitud del problema?
|
|
||||||
|
|
||||||
3. **Si hay pocos casos aislados (< 5 usuarios):**
|
|
||||||
- Ejecutar script de corrección automática
|
|
||||||
- Revisar logs antes de confirmar
|
|
||||||
- Validar resultado
|
|
||||||
|
|
||||||
4. **Si hay muchos casos (> 5 usuarios):**
|
|
||||||
- NO ejecutar script automático
|
|
||||||
- Investigar la causa raíz
|
|
||||||
- Consultar con Database-Agent o Tech Lead
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Fórmulas de XP y ML Coins
|
|
||||||
|
|
||||||
### XP Earned
|
|
||||||
```sql
|
|
||||||
xp_earned = GREATEST(0, score - (hints_used * 10))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ejemplos:**
|
|
||||||
- Score: 100, hints: 0 → XP: 100
|
|
||||||
- Score: 100, hints: 2 → XP: 80
|
|
||||||
- Score: 50, hints: 10 → XP: 0 (no negativo)
|
|
||||||
|
|
||||||
### ML Coins Earned
|
|
||||||
```sql
|
|
||||||
ml_coins_earned = GREATEST(0, FLOOR(score / 10) - (comodines_used * 2))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ejemplos:**
|
|
||||||
- Score: 100, comodines: 0 → ML Coins: 10
|
|
||||||
- Score: 100, comodines: 2 → ML Coins: 6
|
|
||||||
- Score: 50, comodines: 0 → ML Coins: 5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 Problemas Comunes
|
|
||||||
|
|
||||||
### 1. Intentos con xp_earned = 0
|
|
||||||
|
|
||||||
**Causa:** Bug en el código que crea el attempt sin calcular XP.
|
|
||||||
|
|
||||||
**Solución:** Ejecutar script de corrección automática.
|
|
||||||
|
|
||||||
**Prevención:** Agregar validación en backend antes de insertar attempt.
|
|
||||||
|
|
||||||
### 2. Discrepancia entre attempts y user_stats
|
|
||||||
|
|
||||||
**Causa:** Trigger `update_user_stats_on_exercise_complete` no se ejecutó correctamente.
|
|
||||||
|
|
||||||
**Solución:** Recalcular user_stats con script de corrección.
|
|
||||||
|
|
||||||
**Prevención:** Monitorear logs de triggers.
|
|
||||||
|
|
||||||
### 3. Fórmulas inconsistentes
|
|
||||||
|
|
||||||
**Causa:** Cambio en lógica de negocio sin migración de datos históricos.
|
|
||||||
|
|
||||||
**Solución:** Decidir si mantener datos históricos o migrar.
|
|
||||||
|
|
||||||
**Prevención:** Documentar cambios en fórmulas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Historial de Correcciones
|
|
||||||
|
|
||||||
### 2025-11-24: Corrección inicial de datos históricos
|
|
||||||
|
|
||||||
- **Usuario afectado:** `85a2d456-a07d-4be9-b9ce-4a46b183a2a0`
|
|
||||||
- **Intentos corregidos:** 1
|
|
||||||
- **XP recuperado:** +600 XP (500 → 1100)
|
|
||||||
- **ML Coins recuperados:** +90 ML Coins (220 → 310)
|
|
||||||
- **Bug adicional:** Corregida función `promote_to_next_rank()`
|
|
||||||
|
|
||||||
**Ver detalles completos:**
|
|
||||||
- `/apps/database/REPORTE-CORRECCION-XP-ML-COINS-2025-11-24.md`
|
|
||||||
- `/apps/database/RESUMEN-EJECUTIVO-CORRECCION-XP-2025-11-24.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔗 Referencias
|
|
||||||
|
|
||||||
- **Política DDL-First:** `/orchestration/directivas/DIRECTIVA-POLITICA-CARGA-LIMPIA.md`
|
|
||||||
- **Traza de tareas:** `/orchestration/trazas/TRAZA-TAREAS-DATABASE.md`
|
|
||||||
- **Prompt Database-Agent:** `/orchestration/prompts/PROMPT-DATABASE-AGENT.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Contacto
|
|
||||||
|
|
||||||
**Mantenido por:** Database-Agent
|
|
||||||
**Última actualización:** 2025-11-24
|
|
||||||
|
|
||||||
Para preguntas o problemas, consultar:
|
|
||||||
1. Documentación en `/apps/database/docs/`
|
|
||||||
2. Trazas en `/orchestration/trazas/`
|
|
||||||
3. Tech Lead del proyecto GAMILIT
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**FIN DEL README**
|
|
||||||
@ -1,219 +0,0 @@
|
|||||||
-- ============================================================================
|
|
||||||
-- VALIDACIONES RAPIDAS POST-RECREACION DE BASE DE DATOS
|
|
||||||
-- Fecha: 2025-11-24
|
|
||||||
-- Database: gamilit_platform
|
|
||||||
-- ============================================================================
|
|
||||||
--
|
|
||||||
-- Este archivo contiene queries SQL para validar rapidamente la integridad
|
|
||||||
-- de la base de datos despues de una recreacion.
|
|
||||||
--
|
|
||||||
-- Uso:
|
|
||||||
-- psql -h localhost -U gamilit_user -d gamilit_platform -f VALIDACIONES-RAPIDAS-POST-RECREACION.sql
|
|
||||||
-- ============================================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 1: CONTEO DE SCHEMAS'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT count(*) as total_schemas
|
|
||||||
FROM information_schema.schemata
|
|
||||||
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast');
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 2: CONTEO DE TABLAS POR SCHEMA'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT table_schema, count(*) as tables
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
||||||
AND table_type = 'BASE TABLE'
|
|
||||||
GROUP BY table_schema
|
|
||||||
ORDER BY table_schema;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 3: CONTEO DE FUNCIONES'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT count(*) as total_functions
|
|
||||||
FROM information_schema.routines
|
|
||||||
WHERE routine_schema NOT IN ('pg_catalog', 'information_schema');
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 4: CONTEO DE TRIGGERS'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT count(DISTINCT trigger_name) as total_triggers
|
|
||||||
FROM information_schema.triggers;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 5: RLS POLICIES EN GAMIFICATION_SYSTEM.NOTIFICATIONS'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT policyname, cmd
|
|
||||||
FROM pg_policies
|
|
||||||
WHERE tablename = 'notifications'
|
|
||||||
AND schemaname = 'gamification_system'
|
|
||||||
ORDER BY policyname;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 6: RLS POLICIES EN STUDENT_INTERVENTION_ALERTS'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT policyname, cmd
|
|
||||||
FROM pg_policies
|
|
||||||
WHERE tablename = 'student_intervention_alerts'
|
|
||||||
AND schemaname = 'progress_tracking'
|
|
||||||
ORDER BY policyname;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 7: CONTEO DE USUARIOS POR ROL'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT
|
|
||||||
role,
|
|
||||||
count(*) as total
|
|
||||||
FROM auth_management.profiles
|
|
||||||
GROUP BY role
|
|
||||||
ORDER BY role;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 8: MODULOS EDUCATIVOS'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
status,
|
|
||||||
(SELECT count(*) FROM educational_content.exercises e WHERE e.module_id = m.id) as ejercicios
|
|
||||||
FROM educational_content.modules m
|
|
||||||
ORDER BY title;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 9: RANGOS MAYA'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT
|
|
||||||
rank_name,
|
|
||||||
display_name,
|
|
||||||
min_xp_required,
|
|
||||||
max_xp_threshold,
|
|
||||||
ml_coins_bonus,
|
|
||||||
is_active
|
|
||||||
FROM gamification_system.maya_ranks
|
|
||||||
ORDER BY min_xp_required;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 10: USER STATS INICIALIZADOS'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT
|
|
||||||
count(*) as total_users,
|
|
||||||
count(*) FILTER (WHERE total_xp > 0) as con_xp,
|
|
||||||
count(*) FILTER (WHERE ml_coins > 0) as con_coins,
|
|
||||||
sum(total_xp)::bigint as total_xp_sistema,
|
|
||||||
sum(ml_coins)::bigint as total_coins_sistema
|
|
||||||
FROM gamification_system.user_stats;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 11: MODULE PROGRESS INICIALIZADO'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT
|
|
||||||
count(*) as total_records,
|
|
||||||
count(DISTINCT user_id) as usuarios,
|
|
||||||
count(DISTINCT module_id) as modulos,
|
|
||||||
count(*) FILTER (WHERE status = 'completed') as completados,
|
|
||||||
count(*) FILTER (WHERE status = 'in_progress') as en_progreso,
|
|
||||||
count(*) FILTER (WHERE status = 'not_started') as no_iniciados
|
|
||||||
FROM progress_tracking.module_progress;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 12: SOCIAL FEATURES'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT 'Schools' as tipo, count(*)::text as total FROM social_features.schools
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'Classrooms', count(*)::text FROM social_features.classrooms
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'Teacher-Classroom Links', count(*)::text FROM social_features.teacher_classrooms;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 13: RESUMEN DE OBJETOS POR SCHEMA'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT
|
|
||||||
s.schema_name,
|
|
||||||
COALESCE(t.tables, 0) as tables,
|
|
||||||
COALESCE(v.views, 0) as views,
|
|
||||||
COALESCE(f.functions, 0) as functions,
|
|
||||||
COALESCE(tr.triggers, 0) as triggers,
|
|
||||||
COALESCE(p.policies, 0) as rls_policies
|
|
||||||
FROM (
|
|
||||||
SELECT schema_name
|
|
||||||
FROM information_schema.schemata
|
|
||||||
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'pg_temp_3', 'pg_toast_temp_3')
|
|
||||||
) s
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT table_schema, count(*) as tables
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_type = 'BASE TABLE'
|
|
||||||
GROUP BY table_schema
|
|
||||||
) t ON s.schema_name = t.table_schema
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT table_schema, count(*) as views
|
|
||||||
FROM information_schema.views
|
|
||||||
GROUP BY table_schema
|
|
||||||
) v ON s.schema_name = v.table_schema
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT routine_schema, count(*) as functions
|
|
||||||
FROM information_schema.routines
|
|
||||||
GROUP BY routine_schema
|
|
||||||
) f ON s.schema_name = f.routine_schema
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT trigger_schema, count(DISTINCT trigger_name) as triggers
|
|
||||||
FROM information_schema.triggers
|
|
||||||
GROUP BY trigger_schema
|
|
||||||
) tr ON s.schema_name = tr.trigger_schema
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT schemaname, count(*) as policies
|
|
||||||
FROM pg_policies
|
|
||||||
GROUP BY schemaname
|
|
||||||
) p ON s.schema_name = p.schemaname
|
|
||||||
WHERE s.schema_name IN (
|
|
||||||
'admin_dashboard',
|
|
||||||
'audit_logging',
|
|
||||||
'auth',
|
|
||||||
'auth_management',
|
|
||||||
'communication',
|
|
||||||
'content_management',
|
|
||||||
'educational_content',
|
|
||||||
'gamification_system',
|
|
||||||
'gamilit',
|
|
||||||
'lti_integration',
|
|
||||||
'notifications',
|
|
||||||
'progress_tracking',
|
|
||||||
'social_features',
|
|
||||||
'system_configuration'
|
|
||||||
)
|
|
||||||
ORDER BY s.schema_name;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION 14: TABLAS CON RLS HABILITADO'
|
|
||||||
\echo '============================================================================'
|
|
||||||
SELECT
|
|
||||||
schemaname,
|
|
||||||
tablename,
|
|
||||||
rowsecurity as rls_enabled
|
|
||||||
FROM pg_tables
|
|
||||||
WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
||||||
AND rowsecurity = true
|
|
||||||
ORDER BY schemaname, tablename;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'VALIDACION COMPLETA'
|
|
||||||
\echo '============================================================================'
|
|
||||||
\echo 'Si todas las validaciones anteriores se ejecutaron sin errores,'
|
|
||||||
\echo 'la base de datos esta correctamente recreada y operacional.'
|
|
||||||
\echo ''
|
|
||||||
@ -1,234 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Script de Validación: Gaps Database-Backend
|
|
||||||
-- Fecha: 2025-11-24
|
|
||||||
-- Tarea: DB-127 Corrección Gaps Coherencia
|
|
||||||
-- =====================================================
|
|
||||||
--
|
|
||||||
-- Este script valida que los 3 gaps identificados estén resueltos:
|
|
||||||
-- - GAP-DB-001: activity_log con entity_type, entity_id
|
|
||||||
-- - GAP-DB-002: auth.tenants vista alias
|
|
||||||
-- - GAP-DB-003: classrooms.is_deleted
|
|
||||||
--
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo '==================================================='
|
|
||||||
\echo 'VALIDACIÓN DE GAPS DATABASE-BACKEND'
|
|
||||||
\echo '==================================================='
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- GAP-DB-001: Validar tabla activity_log
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo '--- GAP-DB-001: Tabla audit_logging.activity_log ---'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- Validar que tabla existe
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'audit_logging'
|
|
||||||
AND table_name = 'activity_log'
|
|
||||||
)
|
|
||||||
THEN '✅ Tabla audit_logging.activity_log existe'
|
|
||||||
ELSE '❌ ERROR: Tabla audit_logging.activity_log NO existe'
|
|
||||||
END as status;
|
|
||||||
|
|
||||||
-- Validar columnas requeridas
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validar columnas en activity_log:'
|
|
||||||
SELECT
|
|
||||||
column_name,
|
|
||||||
data_type,
|
|
||||||
is_nullable,
|
|
||||||
column_default,
|
|
||||||
CASE
|
|
||||||
WHEN column_name IN ('id', 'user_id', 'action_type', 'entity_type', 'entity_id', 'description', 'metadata', 'created_at')
|
|
||||||
THEN '✅ Requerida'
|
|
||||||
ELSE ' Opcional'
|
|
||||||
END as importancia
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'audit_logging'
|
|
||||||
AND table_name = 'activity_log'
|
|
||||||
ORDER BY ordinal_position;
|
|
||||||
|
|
||||||
-- Validar indices
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validar índices en activity_log:'
|
|
||||||
SELECT
|
|
||||||
indexname,
|
|
||||||
indexdef
|
|
||||||
FROM pg_indexes
|
|
||||||
WHERE schemaname = 'audit_logging'
|
|
||||||
AND tablename = 'activity_log'
|
|
||||||
ORDER BY indexname;
|
|
||||||
|
|
||||||
-- Validar query backend (query de admin-dashboard.service.ts:184)
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validar query backend - Actividad por tipo (últimos 7 días):'
|
|
||||||
SELECT
|
|
||||||
action_type,
|
|
||||||
COUNT(*) as count
|
|
||||||
FROM audit_logging.activity_log
|
|
||||||
WHERE created_at > NOW() - INTERVAL '7 days'
|
|
||||||
GROUP BY action_type
|
|
||||||
ORDER BY count DESC
|
|
||||||
LIMIT 5;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- GAP-DB-002: Validar vista auth.tenants
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '--- GAP-DB-002: Vista auth.tenants (alias) ---'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- Validar que vista existe
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.views
|
|
||||||
WHERE table_schema = 'auth'
|
|
||||||
AND table_name = 'tenants'
|
|
||||||
)
|
|
||||||
THEN '✅ Vista auth.tenants existe'
|
|
||||||
ELSE '❌ ERROR: Vista auth.tenants NO existe'
|
|
||||||
END as status;
|
|
||||||
|
|
||||||
-- Validar query backend (query de admin-dashboard.service.ts:95)
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validar query backend - Tenants actualizados (últimos 7 días):'
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
slug,
|
|
||||||
updated_at
|
|
||||||
FROM auth.tenants
|
|
||||||
WHERE updated_at >= NOW() - INTERVAL '7 days'
|
|
||||||
ORDER BY updated_at DESC
|
|
||||||
LIMIT 3;
|
|
||||||
|
|
||||||
-- Validar que apunta a auth_management.tenants
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validar definición de vista:'
|
|
||||||
SELECT
|
|
||||||
schemaname,
|
|
||||||
viewname,
|
|
||||||
LEFT(definition, 100) || '...' as definition_preview
|
|
||||||
FROM pg_views
|
|
||||||
WHERE schemaname = 'auth'
|
|
||||||
AND viewname = 'tenants';
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- GAP-DB-003: Validar classrooms.is_deleted
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '--- GAP-DB-003: Columna classrooms.is_deleted ---'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- Validar que columna existe
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'social_features'
|
|
||||||
AND table_name = 'classrooms'
|
|
||||||
AND column_name = 'is_deleted'
|
|
||||||
)
|
|
||||||
THEN '✅ Columna social_features.classrooms.is_deleted existe'
|
|
||||||
ELSE '❌ ERROR: Columna is_deleted NO existe en classrooms'
|
|
||||||
END as status;
|
|
||||||
|
|
||||||
-- Validar tipo y default de columna
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validar columna is_deleted:'
|
|
||||||
SELECT
|
|
||||||
column_name,
|
|
||||||
data_type,
|
|
||||||
is_nullable,
|
|
||||||
column_default
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'social_features'
|
|
||||||
AND table_name = 'classrooms'
|
|
||||||
AND column_name IN ('is_deleted', 'is_archived', 'is_active')
|
|
||||||
ORDER BY column_name;
|
|
||||||
|
|
||||||
-- Validar índice parcial para is_deleted
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validar índice parcial para is_deleted:'
|
|
||||||
SELECT
|
|
||||||
indexname,
|
|
||||||
indexdef
|
|
||||||
FROM pg_indexes
|
|
||||||
WHERE schemaname = 'social_features'
|
|
||||||
AND tablename = 'classrooms'
|
|
||||||
AND indexdef LIKE '%is_deleted%'
|
|
||||||
ORDER BY indexname;
|
|
||||||
|
|
||||||
-- Validar query backend (query de classrooms.service.ts:67)
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validar query backend - Classrooms no eliminados:'
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
code,
|
|
||||||
is_active,
|
|
||||||
is_archived,
|
|
||||||
is_deleted
|
|
||||||
FROM social_features.classrooms
|
|
||||||
WHERE is_deleted = FALSE
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 3;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- RESUMEN FINAL
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '==================================================='
|
|
||||||
\echo 'RESUMEN DE VALIDACIÓN'
|
|
||||||
\echo '==================================================='
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'✅ GAP-DB-001: activity_log' as gap,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) >= 8 THEN '✅ RESUELTO'
|
|
||||||
ELSE '❌ PENDIENTE'
|
|
||||||
END as estado
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'audit_logging'
|
|
||||||
AND table_name = 'activity_log'
|
|
||||||
AND column_name IN ('id', 'user_id', 'action_type', 'entity_type', 'entity_id', 'description', 'metadata', 'created_at')
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'✅ GAP-DB-002: auth.tenants' as gap,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 1 THEN '✅ RESUELTO'
|
|
||||||
ELSE '❌ PENDIENTE'
|
|
||||||
END as estado
|
|
||||||
FROM information_schema.views
|
|
||||||
WHERE table_schema = 'auth'
|
|
||||||
AND table_name = 'tenants'
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'✅ GAP-DB-003: classrooms.is_deleted' as gap,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 1 THEN '✅ RESUELTO'
|
|
||||||
ELSE '❌ PENDIENTE'
|
|
||||||
END as estado
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'social_features'
|
|
||||||
AND table_name = 'classrooms'
|
|
||||||
AND column_name = 'is_deleted';
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '==================================================='
|
|
||||||
\echo 'VALIDACIÓN COMPLETADA'
|
|
||||||
\echo '==================================================='
|
|
||||||
@ -1,253 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Script de Validación: generate_student_alerts() JOINs
|
|
||||||
-- Descripción: Valida que los JOINs arquitectónicos sean correctos
|
|
||||||
-- Fecha: 2025-11-24
|
|
||||||
-- Agente: Database-Agent
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo '========================================='
|
|
||||||
\echo 'VALIDACIÓN: generate_student_alerts()'
|
|
||||||
\echo 'Verificando JOINs arquitectónicos'
|
|
||||||
\echo '========================================='
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- 1. VERIFICAR QUE LA FUNCIÓN EXISTE
|
|
||||||
-- =====================================================
|
|
||||||
\echo '1. Verificando que la función existe...'
|
|
||||||
SELECT
|
|
||||||
p.proname as function_name,
|
|
||||||
n.nspname as schema_name,
|
|
||||||
pg_get_functiondef(p.oid) LIKE '%auth_management.profiles%' as uses_profiles_join,
|
|
||||||
pg_get_functiondef(p.oid) LIKE '%JOIN auth.users%' as uses_auth_users_join
|
|
||||||
FROM pg_proc p
|
|
||||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
||||||
WHERE p.proname = 'generate_student_alerts'
|
|
||||||
AND n.nspname = 'progress_tracking';
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo ' ✓ Si uses_profiles_join = t y uses_auth_users_join = f → CORRECTO'
|
|
||||||
\echo ' ✗ Si uses_auth_users_join = t → INCORRECTO (todavía usa JOINs antiguos)'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- 2. VERIFICAR FOREIGN KEYS RELEVANTES
|
|
||||||
-- =====================================================
|
|
||||||
\echo '2. Verificando Foreign Keys relevantes...'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
\echo ' 2.1 module_progress.user_id → profiles(id)'
|
|
||||||
SELECT
|
|
||||||
tc.table_schema,
|
|
||||||
tc.table_name,
|
|
||||||
kcu.column_name,
|
|
||||||
ccu.table_schema AS foreign_table_schema,
|
|
||||||
ccu.table_name AS foreign_table_name,
|
|
||||||
ccu.column_name AS foreign_column_name
|
|
||||||
FROM information_schema.table_constraints AS tc
|
|
||||||
JOIN information_schema.key_column_usage AS kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name
|
|
||||||
AND tc.table_schema = kcu.table_schema
|
|
||||||
JOIN information_schema.constraint_column_usage AS ccu
|
|
||||||
ON ccu.constraint_name = tc.constraint_name
|
|
||||||
AND ccu.table_schema = tc.table_schema
|
|
||||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
||||||
AND tc.table_schema = 'progress_tracking'
|
|
||||||
AND tc.table_name = 'module_progress'
|
|
||||||
AND kcu.column_name = 'user_id';
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo ' 2.2 exercise_submissions.user_id → profiles(id)'
|
|
||||||
SELECT
|
|
||||||
tc.table_schema,
|
|
||||||
tc.table_name,
|
|
||||||
kcu.column_name,
|
|
||||||
ccu.table_schema AS foreign_table_schema,
|
|
||||||
ccu.table_name AS foreign_table_name,
|
|
||||||
ccu.column_name AS foreign_column_name
|
|
||||||
FROM information_schema.table_constraints AS tc
|
|
||||||
JOIN information_schema.key_column_usage AS kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name
|
|
||||||
AND tc.table_schema = kcu.table_schema
|
|
||||||
JOIN information_schema.constraint_column_usage AS ccu
|
|
||||||
ON ccu.constraint_name = tc.constraint_name
|
|
||||||
AND ccu.table_schema = tc.table_schema
|
|
||||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
||||||
AND tc.table_schema = 'progress_tracking'
|
|
||||||
AND tc.table_name = 'exercise_submissions'
|
|
||||||
AND kcu.column_name = 'user_id';
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo ' 2.3 student_intervention_alerts.student_id → auth.users(id)'
|
|
||||||
SELECT
|
|
||||||
tc.table_schema,
|
|
||||||
tc.table_name,
|
|
||||||
kcu.column_name,
|
|
||||||
ccu.table_schema AS foreign_table_schema,
|
|
||||||
ccu.table_name AS foreign_table_name,
|
|
||||||
ccu.column_name AS foreign_column_name
|
|
||||||
FROM information_schema.table_constraints AS tc
|
|
||||||
JOIN information_schema.key_column_usage AS kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name
|
|
||||||
AND tc.table_schema = kcu.table_schema
|
|
||||||
JOIN information_schema.constraint_column_usage AS ccu
|
|
||||||
ON ccu.constraint_name = tc.constraint_name
|
|
||||||
AND ccu.table_schema = tc.table_schema
|
|
||||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
||||||
AND tc.table_schema = 'progress_tracking'
|
|
||||||
AND tc.table_name = 'student_intervention_alerts'
|
|
||||||
AND kcu.column_name = 'student_id';
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo ' 2.4 profiles.user_id → auth.users(id)'
|
|
||||||
SELECT
|
|
||||||
tc.table_schema,
|
|
||||||
tc.table_name,
|
|
||||||
kcu.column_name,
|
|
||||||
ccu.table_schema AS foreign_table_schema,
|
|
||||||
ccu.table_name AS foreign_table_name,
|
|
||||||
ccu.column_name AS foreign_column_name
|
|
||||||
FROM information_schema.table_constraints AS tc
|
|
||||||
JOIN information_schema.key_column_usage AS kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name
|
|
||||||
AND tc.table_schema = kcu.table_schema
|
|
||||||
JOIN information_schema.constraint_column_usage AS ccu
|
|
||||||
ON ccu.constraint_name = tc.constraint_name
|
|
||||||
AND ccu.table_schema = tc.table_schema
|
|
||||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
||||||
AND tc.table_schema = 'auth_management'
|
|
||||||
AND tc.table_name = 'profiles'
|
|
||||||
AND kcu.column_name = 'user_id';
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- 3. RECREAR LA FUNCIÓN ACTUALIZADA
|
|
||||||
-- =====================================================
|
|
||||||
\echo ''
|
|
||||||
\echo '3. Recreando la función con JOINs corregidos...'
|
|
||||||
\i apps/database/ddl/schemas/progress_tracking/functions/15-generate_student_alerts.sql
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- 4. VERIFICAR DEFINICIÓN DE LA FUNCIÓN
|
|
||||||
-- =====================================================
|
|
||||||
\echo ''
|
|
||||||
\echo '4. Verificando definición de la función...'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- Contar ocurrencias de JOIN auth_management.profiles
|
|
||||||
\echo ' 4.1 Ocurrencias de "JOIN auth_management.profiles":'
|
|
||||||
SELECT COUNT(*)::text || ' ocurrencias (esperado: 3)' as resultado
|
|
||||||
FROM regexp_matches(
|
|
||||||
pg_get_functiondef(
|
|
||||||
(SELECT oid FROM pg_proc WHERE proname = 'generate_student_alerts' AND pronamespace = 'progress_tracking'::regnamespace)
|
|
||||||
),
|
|
||||||
'JOIN auth_management\.profiles',
|
|
||||||
'g'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Contar ocurrencias de JOIN auth.users (debería ser 0)
|
|
||||||
\echo ''
|
|
||||||
\echo ' 4.2 Ocurrencias de "JOIN auth.users" (debe ser 0):'
|
|
||||||
SELECT COALESCE(COUNT(*)::text, '0') || ' ocurrencias (esperado: 0)' as resultado
|
|
||||||
FROM regexp_matches(
|
|
||||||
pg_get_functiondef(
|
|
||||||
(SELECT oid FROM pg_proc WHERE proname = 'generate_student_alerts' AND pronamespace = 'progress_tracking'::regnamespace)
|
|
||||||
),
|
|
||||||
'JOIN auth\.users',
|
|
||||||
'g'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Contar ocurrencias de p.tenant_id
|
|
||||||
\echo ''
|
|
||||||
\echo ' 4.3 Ocurrencias de "p.tenant_id":'
|
|
||||||
SELECT COUNT(*)::text || ' ocurrencias (esperado: 3)' as resultado
|
|
||||||
FROM regexp_matches(
|
|
||||||
pg_get_functiondef(
|
|
||||||
(SELECT oid FROM pg_proc WHERE proname = 'generate_student_alerts' AND pronamespace = 'progress_tracking'::regnamespace)
|
|
||||||
),
|
|
||||||
'p\.tenant_id',
|
|
||||||
'g'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Contar ocurrencias de p.user_id (debe aparecer en los 3 INSERTs)
|
|
||||||
\echo ''
|
|
||||||
\echo ' 4.4 Ocurrencias de "p.user_id":'
|
|
||||||
SELECT COUNT(*)::text || ' ocurrencias (esperado: 3)' as resultado
|
|
||||||
FROM regexp_matches(
|
|
||||||
pg_get_functiondef(
|
|
||||||
(SELECT oid FROM pg_proc WHERE proname = 'generate_student_alerts' AND pronamespace = 'progress_tracking'::regnamespace)
|
|
||||||
),
|
|
||||||
'p\.user_id',
|
|
||||||
'g'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- 5. PRUEBA FUNCIONAL (OPCIONAL)
|
|
||||||
-- =====================================================
|
|
||||||
\echo ''
|
|
||||||
\echo '5. Prueba funcional (opcional)...'
|
|
||||||
\echo ' Si desea ejecutar la función, ejecute:'
|
|
||||||
\echo ' SELECT progress_tracking.generate_student_alerts();'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- 6. ANÁLISIS DE DATOS DE PRUEBA (SI EXISTEN)
|
|
||||||
-- =====================================================
|
|
||||||
\echo '6. Analizando datos existentes...'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
\echo ' 6.1 Estudiantes con progreso en módulos:'
|
|
||||||
SELECT COUNT(DISTINCT user_id) as total_students
|
|
||||||
FROM progress_tracking.module_progress;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo ' 6.2 Estudiantes con ejercicios intentados:'
|
|
||||||
SELECT COUNT(DISTINCT user_id) as total_students
|
|
||||||
FROM progress_tracking.exercise_submissions;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo ' 6.3 Verificar que profiles.user_id mapea a auth.users.id:'
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total_profiles,
|
|
||||||
COUNT(DISTINCT p.id) as unique_profile_ids,
|
|
||||||
COUNT(DISTINCT p.user_id) as unique_user_ids,
|
|
||||||
COUNT(DISTINCT u.id) as unique_auth_user_ids,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(DISTINCT p.user_id) = COUNT(DISTINCT u.id) THEN 'CORRECTO ✓'
|
|
||||||
ELSE 'INCONSISTENTE ✗'
|
|
||||||
END as mapping_status
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN auth.users u ON p.user_id = u.id;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo ' 6.4 Alertas generadas actualmente:'
|
|
||||||
SELECT
|
|
||||||
alert_type,
|
|
||||||
COUNT(*) as total,
|
|
||||||
COUNT(DISTINCT student_id) as unique_students
|
|
||||||
FROM progress_tracking.student_intervention_alerts
|
|
||||||
GROUP BY alert_type
|
|
||||||
ORDER BY alert_type;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- 7. RESUMEN DE VALIDACIÓN
|
|
||||||
-- =====================================================
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================='
|
|
||||||
\echo 'RESUMEN DE VALIDACIÓN'
|
|
||||||
\echo '========================================='
|
|
||||||
\echo ''
|
|
||||||
\echo 'CRITERIOS DE ACEPTACIÓN:'
|
|
||||||
\echo ' ✓ Función usa JOIN auth_management.profiles (3 veces)'
|
|
||||||
\echo ' ✓ Función NO usa JOIN auth.users (0 veces)'
|
|
||||||
\echo ' ✓ Función usa p.tenant_id (3 veces)'
|
|
||||||
\echo ' ✓ Función usa p.user_id (3 veces)'
|
|
||||||
\echo ' ✓ FKs verificadas:'
|
|
||||||
\echo ' - module_progress.user_id → profiles(id)'
|
|
||||||
\echo ' - exercise_submissions.user_id → profiles(id)'
|
|
||||||
\echo ' - student_intervention_alerts.student_id → auth.users(id)'
|
|
||||||
\echo ' - profiles.user_id → auth.users(id)'
|
|
||||||
\echo ''
|
|
||||||
\echo 'Si todos los criterios se cumplen: ✅ VALIDACIÓN EXITOSA'
|
|
||||||
\echo 'Si algún criterio falla: ✗ REVISAR IMPLEMENTACIÓN'
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================='
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Script de Validación: Estructura de Objectives en Missions
|
|
||||||
-- Fecha: 2025-11-26
|
|
||||||
-- Propósito: Validar que initialize_user_missions crea objectives como ARRAY
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo '=================================================='
|
|
||||||
\echo 'VALIDACIÓN: Estructura de objectives en missions'
|
|
||||||
\echo '=================================================='
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
v_test_user_id UUID;
|
|
||||||
v_objectives JSONB;
|
|
||||||
v_objectives_type TEXT;
|
|
||||||
v_mission_count INT;
|
|
||||||
v_total_missions INT;
|
|
||||||
v_test_passed BOOLEAN := true;
|
|
||||||
BEGIN
|
|
||||||
RAISE NOTICE '1. Buscando usuario de prueba...';
|
|
||||||
|
|
||||||
-- Buscar un usuario existente
|
|
||||||
SELECT id INTO v_test_user_id
|
|
||||||
FROM auth_management.profiles
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
IF v_test_user_id IS NULL THEN
|
|
||||||
RAISE EXCEPTION 'No hay usuarios en la base de datos para testing';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Usuario de prueba: %', v_test_user_id;
|
|
||||||
|
|
||||||
RAISE NOTICE '2. Limpiando misiones previas...';
|
|
||||||
DELETE FROM gamification_system.missions WHERE user_id = v_test_user_id;
|
|
||||||
|
|
||||||
RAISE NOTICE '3. Ejecutando initialize_user_missions()...';
|
|
||||||
PERFORM gamilit.initialize_user_missions(v_test_user_id);
|
|
||||||
|
|
||||||
RAISE NOTICE '4. Verificando estructura de objectives...';
|
|
||||||
|
|
||||||
-- Verificar que se crearon 8 misiones
|
|
||||||
SELECT COUNT(*) INTO v_total_missions
|
|
||||||
FROM gamification_system.missions
|
|
||||||
WHERE user_id = v_test_user_id;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Misiones creadas: %', v_total_missions;
|
|
||||||
|
|
||||||
IF v_total_missions != 8 THEN
|
|
||||||
RAISE NOTICE '❌ ERROR: Se esperaban 8 misiones, se crearon %', v_total_missions;
|
|
||||||
v_test_passed := false;
|
|
||||||
ELSE
|
|
||||||
RAISE NOTICE '✅ OK: Se crearon las 8 misiones esperadas';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Verificar que TODAS las misiones tienen objectives como ARRAY
|
|
||||||
SELECT objectives, jsonb_typeof(objectives)
|
|
||||||
INTO v_objectives, v_objectives_type
|
|
||||||
FROM gamification_system.missions
|
|
||||||
WHERE user_id = v_test_user_id
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Tipo de objectives: %', v_objectives_type;
|
|
||||||
RAISE NOTICE 'Ejemplo de objectives: %', v_objectives;
|
|
||||||
|
|
||||||
IF v_objectives_type != 'array' THEN
|
|
||||||
RAISE NOTICE '❌ ERROR: objectives NO es un array, es: %', v_objectives_type;
|
|
||||||
v_test_passed := false;
|
|
||||||
ELSE
|
|
||||||
RAISE NOTICE '✅ OK: objectives es un ARRAY';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE '5. Verificando compatibilidad con operador @>...';
|
|
||||||
|
|
||||||
-- Test: Buscar misiones de tipo complete_exercises
|
|
||||||
SELECT COUNT(*) INTO v_mission_count
|
|
||||||
FROM gamification_system.missions
|
|
||||||
WHERE user_id = v_test_user_id
|
|
||||||
AND objectives @> '[{"type": "complete_exercises"}]'::jsonb;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Misiones con type=complete_exercises encontradas con @>: %', v_mission_count;
|
|
||||||
|
|
||||||
IF v_mission_count = 0 THEN
|
|
||||||
RAISE NOTICE '❌ ERROR: El operador @> NO encuentra misiones';
|
|
||||||
v_test_passed := false;
|
|
||||||
ELSE
|
|
||||||
RAISE NOTICE '✅ OK: El operador @> funciona correctamente';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Test: Buscar misión de earn_xp
|
|
||||||
SELECT COUNT(*) INTO v_mission_count
|
|
||||||
FROM gamification_system.missions
|
|
||||||
WHERE user_id = v_test_user_id
|
|
||||||
AND objectives @> '[{"type": "earn_xp"}]'::jsonb;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Misiones con type=earn_xp encontradas: %', v_mission_count;
|
|
||||||
|
|
||||||
IF v_mission_count = 0 THEN
|
|
||||||
RAISE NOTICE '❌ ERROR: No se encuentra misión de earn_xp';
|
|
||||||
v_test_passed := false;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Test: Verificar misión weekly_explorer con modules_visited
|
|
||||||
SELECT COUNT(*) INTO v_mission_count
|
|
||||||
FROM gamification_system.missions
|
|
||||||
WHERE user_id = v_test_user_id
|
|
||||||
AND template_id = 'weekly_explorer'
|
|
||||||
AND objectives @> '[{"type": "explore_modules"}]'::jsonb
|
|
||||||
AND objectives::text LIKE '%modules_visited%';
|
|
||||||
|
|
||||||
RAISE NOTICE 'Misión weekly_explorer con modules_visited: %', v_mission_count;
|
|
||||||
|
|
||||||
IF v_mission_count = 0 THEN
|
|
||||||
RAISE NOTICE '❌ ERROR: weekly_explorer no tiene modules_visited';
|
|
||||||
v_test_passed := false;
|
|
||||||
ELSE
|
|
||||||
RAISE NOTICE '✅ OK: weekly_explorer tiene modules_visited';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE '6. Limpiando datos de prueba...';
|
|
||||||
DELETE FROM gamification_system.missions WHERE user_id = v_test_user_id;
|
|
||||||
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE '==================================================';
|
|
||||||
IF v_test_passed THEN
|
|
||||||
RAISE NOTICE '✅ ✅ ✅ VALIDACIÓN EXITOSA ✅ ✅ ✅';
|
|
||||||
RAISE NOTICE 'La función initialize_user_missions está correcta';
|
|
||||||
ELSE
|
|
||||||
RAISE NOTICE '❌ ❌ ❌ VALIDACIÓN FALLIDA ❌ ❌ ❌';
|
|
||||||
RAISE NOTICE 'Hay problemas en la función initialize_user_missions';
|
|
||||||
END IF;
|
|
||||||
RAISE NOTICE '==================================================';
|
|
||||||
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
@ -1,248 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Script: Validate Seeds Integrity
|
|
||||||
-- Description: Valida integridad referencial de todos los seeds
|
|
||||||
-- Created: 2025-11-15
|
|
||||||
-- Version: 1.0
|
|
||||||
-- =====================================================
|
|
||||||
--
|
|
||||||
-- PROPÓSITO:
|
|
||||||
-- Este script verifica que:
|
|
||||||
-- 1. No haya registros huérfanos (FK rotas)
|
|
||||||
-- 2. Conteos de registros coincidan (users = profiles = user_stats)
|
|
||||||
-- 3. Triggers funcionaron correctamente
|
|
||||||
-- 4. Seeds sociales tienen datos suficientes
|
|
||||||
--
|
|
||||||
-- EJECUCIÓN:
|
|
||||||
-- psql -U gamilit_user -d gamilit_platform -f validate-seeds-integrity.sql
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\set QUIET on
|
|
||||||
\timing off
|
|
||||||
|
|
||||||
-- Configurar search path
|
|
||||||
SET search_path TO auth_management, gamification_system, social_features, educational_content, public;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Sección 1: Conteos Básicos
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo '1. CONTEOS BÁSICOS'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
users_count INTEGER;
|
|
||||||
profiles_count INTEGER;
|
|
||||||
user_stats_count INTEGER;
|
|
||||||
user_ranks_count INTEGER;
|
|
||||||
comodines_count INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO users_count FROM auth.users;
|
|
||||||
SELECT COUNT(*) INTO profiles_count FROM auth_management.profiles;
|
|
||||||
SELECT COUNT(*) INTO user_stats_count FROM gamification_system.user_stats;
|
|
||||||
SELECT COUNT(*) INTO user_ranks_count FROM gamification_system.user_ranks;
|
|
||||||
SELECT COUNT(*) INTO comodines_count FROM gamification_system.comodines_inventory;
|
|
||||||
|
|
||||||
RAISE NOTICE 'auth.users: %', users_count;
|
|
||||||
RAISE NOTICE 'auth_management.profiles: %', profiles_count;
|
|
||||||
RAISE NOTICE 'gamification_system.user_stats: %', user_stats_count;
|
|
||||||
RAISE NOTICE 'gamification_system.user_ranks: %', user_ranks_count;
|
|
||||||
RAISE NOTICE 'gamification_system.comodines_inventory: %', comodines_count;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
|
|
||||||
IF users_count = profiles_count AND profiles_count = user_stats_count AND user_stats_count = user_ranks_count THEN
|
|
||||||
RAISE NOTICE '✓ PASS: Todos los conteos coinciden (%)', users_count;
|
|
||||||
ELSE
|
|
||||||
RAISE WARNING '✗ FAIL: Conteos no coinciden';
|
|
||||||
RAISE WARNING 'Diferencias detectadas - verificar triggers y seeds';
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Sección 2: Integridad Referencial
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo '2. INTEGRIDAD REFERENCIAL'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
orphan_profiles INTEGER;
|
|
||||||
orphan_user_stats INTEGER;
|
|
||||||
orphan_user_ranks INTEGER;
|
|
||||||
orphan_comodines INTEGER;
|
|
||||||
BEGIN
|
|
||||||
-- Profiles sin user
|
|
||||||
SELECT COUNT(*) INTO orphan_profiles
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN auth.users u ON u.id = p.user_id
|
|
||||||
WHERE u.id IS NULL;
|
|
||||||
|
|
||||||
-- User_stats sin profile
|
|
||||||
SELECT COUNT(*) INTO orphan_user_stats
|
|
||||||
FROM gamification_system.user_stats us
|
|
||||||
LEFT JOIN auth_management.profiles p ON p.user_id = us.user_id
|
|
||||||
WHERE p.id IS NULL;
|
|
||||||
|
|
||||||
-- User_ranks sin user_stats
|
|
||||||
SELECT COUNT(*) INTO orphan_user_ranks
|
|
||||||
FROM gamification_system.user_ranks ur
|
|
||||||
LEFT JOIN gamification_system.user_stats us ON us.user_id = ur.user_id
|
|
||||||
WHERE us.id IS NULL;
|
|
||||||
|
|
||||||
-- Comodines sin user
|
|
||||||
SELECT COUNT(*) INTO orphan_comodines
|
|
||||||
FROM gamification_system.comodines_inventory ci
|
|
||||||
LEFT JOIN auth_management.profiles p ON p.user_id = ci.user_id
|
|
||||||
WHERE p.id IS NULL;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Profiles huérfanos (sin user): %', orphan_profiles;
|
|
||||||
RAISE NOTICE 'User_stats huérfanos (sin profile): %', orphan_user_stats;
|
|
||||||
RAISE NOTICE 'User_ranks huérfanos (sin user_stats): %', orphan_user_ranks;
|
|
||||||
RAISE NOTICE 'Comodines huérfanos (sin user): %', orphan_comodines;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
|
|
||||||
IF orphan_profiles = 0 AND orphan_user_stats = 0 AND orphan_user_ranks = 0 AND orphan_comodines = 0 THEN
|
|
||||||
RAISE NOTICE '✓ PASS: No hay registros huérfanos';
|
|
||||||
ELSE
|
|
||||||
RAISE WARNING '✗ FAIL: Se encontraron registros huérfanos';
|
|
||||||
RAISE WARNING 'Ejecutar limpieza de huérfanos';
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Sección 3: Datos Educativos
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo '3. CONTENIDO EDUCATIVO'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
modules_count INTEGER;
|
|
||||||
published_modules INTEGER;
|
|
||||||
exercises_count INTEGER;
|
|
||||||
achievements_count INTEGER;
|
|
||||||
ranks_count INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO modules_count FROM educational_content.modules;
|
|
||||||
SELECT COUNT(*) INTO published_modules FROM educational_content.modules WHERE is_published = true;
|
|
||||||
SELECT COUNT(*) INTO exercises_count FROM educational_content.exercises;
|
|
||||||
SELECT COUNT(*) INTO achievements_count FROM gamification_system.achievements WHERE is_active = true;
|
|
||||||
SELECT COUNT(*) INTO ranks_count FROM gamification_system.maya_ranks WHERE is_active = true;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Módulos: % (% publicados)', modules_count, published_modules;
|
|
||||||
RAISE NOTICE 'Ejercicios: %', exercises_count;
|
|
||||||
RAISE NOTICE 'Achievements: %', achievements_count;
|
|
||||||
RAISE NOTICE 'Rangos Maya: %', ranks_count;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
|
|
||||||
IF modules_count >= 5 AND exercises_count >= 50 AND achievements_count >= 15 AND ranks_count >= 5 THEN
|
|
||||||
RAISE NOTICE '✓ PASS: Contenido educativo completo';
|
|
||||||
ELSE
|
|
||||||
RAISE WARNING '✗ FAIL: Contenido educativo incompleto';
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Sección 4: Features Sociales
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo '4. FEATURES SOCIALES'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
friendships_count INTEGER;
|
|
||||||
pending_requests INTEGER;
|
|
||||||
schools_count INTEGER;
|
|
||||||
classrooms_count INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO friendships_count FROM social_features.friendships WHERE status = 'accepted';
|
|
||||||
SELECT COUNT(*) INTO pending_requests FROM social_features.friendships WHERE status = 'pending';
|
|
||||||
SELECT COUNT(*) INTO schools_count FROM social_features.schools;
|
|
||||||
SELECT COUNT(*) INTO classrooms_count FROM social_features.classrooms;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Friendships aceptados: %', friendships_count;
|
|
||||||
RAISE NOTICE 'Friend requests pendientes: %', pending_requests;
|
|
||||||
RAISE NOTICE 'Escuelas: %', schools_count;
|
|
||||||
RAISE NOTICE 'Aulas: %', classrooms_count;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
|
|
||||||
IF friendships_count >= 8 AND schools_count >= 2 THEN
|
|
||||||
RAISE NOTICE '✓ PASS: Features sociales disponibles';
|
|
||||||
ELSE
|
|
||||||
RAISE WARNING '✗ FAIL: Features sociales incompletas';
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Sección 5: Resumen Final
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo 'RESUMEN FINAL'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
total_users INTEGER;
|
|
||||||
total_profiles INTEGER;
|
|
||||||
total_stats INTEGER;
|
|
||||||
avg_level NUMERIC;
|
|
||||||
total_coins INTEGER;
|
|
||||||
total_achievements INTEGER;
|
|
||||||
total_modules INTEGER;
|
|
||||||
total_friendships INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO total_users FROM auth.users;
|
|
||||||
SELECT COUNT(*) INTO total_profiles FROM auth_management.profiles;
|
|
||||||
SELECT COUNT(*) INTO total_stats FROM gamification_system.user_stats;
|
|
||||||
SELECT COUNT(*) INTO total_achievements FROM gamification_system.achievements;
|
|
||||||
SELECT COUNT(*) INTO total_modules FROM educational_content.modules;
|
|
||||||
SELECT COUNT(*) INTO total_friendships FROM social_features.friendships WHERE status = 'accepted';
|
|
||||||
|
|
||||||
SELECT AVG(level)::NUMERIC(5,2) INTO avg_level FROM gamification_system.user_stats;
|
|
||||||
SELECT SUM(ml_coins) INTO total_coins FROM gamification_system.user_stats;
|
|
||||||
|
|
||||||
RAISE NOTICE 'Base de Datos: gamilit_platform';
|
|
||||||
RAISE NOTICE 'Fecha validación: %', now();
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE 'Usuarios totales: %', total_users;
|
|
||||||
RAISE NOTICE 'Perfiles completos: %', total_profiles;
|
|
||||||
RAISE NOTICE 'User stats: %', total_stats;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE 'Nivel promedio usuarios: %', avg_level;
|
|
||||||
RAISE NOTICE 'ML Coins en circulación: %', total_coins;
|
|
||||||
RAISE NOTICE 'Achievements disponibles: %', total_achievements;
|
|
||||||
RAISE NOTICE 'Módulos educativos: %', total_modules;
|
|
||||||
RAISE NOTICE 'Amistades activas: %', total_friendships;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
|
|
||||||
IF total_users = total_profiles AND total_profiles = total_stats THEN
|
|
||||||
RAISE NOTICE '════════════════════════════════════════';
|
|
||||||
RAISE NOTICE '✓✓✓ VALIDACIÓN COMPLETA: SUCCESS ✓✓✓';
|
|
||||||
RAISE NOTICE '════════════════════════════════════════';
|
|
||||||
RAISE NOTICE 'Seeds están correctos y listos para desarrollo frontend';
|
|
||||||
ELSE
|
|
||||||
RAISE WARNING '════════════════════════════════════════';
|
|
||||||
RAISE WARNING '✗✗✗ VALIDACIÓN: PROBLEMAS DETECTADOS ✗✗✗';
|
|
||||||
RAISE WARNING '════════════════════════════════════════';
|
|
||||||
RAISE WARNING 'Revisar secciones anteriores para detalles';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE '';
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validación completada.'
|
|
||||||
\echo ''
|
|
||||||
@ -1,231 +0,0 @@
|
|||||||
-- =====================================================================================
|
|
||||||
-- Script: Validación de corrección update_user_rank()
|
|
||||||
-- Propósito: Validar que la función update_user_rank() incluye balance_before y balance_after
|
|
||||||
-- Fecha: 2025-11-24
|
|
||||||
-- Uso: psql -d gamilit_platform -f scripts/validate-update-user-rank-fix.sql
|
|
||||||
-- =====================================================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================='
|
|
||||||
\echo 'VALIDACIÓN: update_user_rank() - Balance Fields'
|
|
||||||
\echo '========================================='
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- PASO 1: Verificar existencia de la función
|
|
||||||
-- =====================================================================================
|
|
||||||
\echo '1. Verificando existencia de función...'
|
|
||||||
SELECT
|
|
||||||
p.proname AS function_name,
|
|
||||||
n.nspname AS schema_name,
|
|
||||||
pg_get_function_result(p.oid) AS return_type,
|
|
||||||
pg_get_function_arguments(p.oid) AS arguments
|
|
||||||
FROM pg_proc p
|
|
||||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
||||||
WHERE p.proname = 'update_user_rank'
|
|
||||||
AND n.nspname = 'gamification_system';
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- PASO 2: Verificar estructura de la tabla ml_coins_transactions
|
|
||||||
-- =====================================================================================
|
|
||||||
\echo '2. Verificando estructura de ml_coins_transactions...'
|
|
||||||
SELECT
|
|
||||||
column_name,
|
|
||||||
data_type,
|
|
||||||
is_nullable,
|
|
||||||
column_default
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'gamification_system'
|
|
||||||
AND table_name = 'ml_coins_transactions'
|
|
||||||
AND column_name IN ('balance_before', 'balance_after', 'transaction_type', 'amount', 'user_id')
|
|
||||||
ORDER BY ordinal_position;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- PASO 3: Verificar valores del ENUM transaction_type
|
|
||||||
-- =====================================================================================
|
|
||||||
\echo '3. Verificando ENUM transaction_type...'
|
|
||||||
SELECT
|
|
||||||
t.typname AS enum_name,
|
|
||||||
e.enumlabel AS enum_value,
|
|
||||||
e.enumsortorder AS sort_order
|
|
||||||
FROM pg_type t
|
|
||||||
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
||||||
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
||||||
WHERE t.typname = 'transaction_type'
|
|
||||||
AND n.nspname = 'gamification_system'
|
|
||||||
ORDER BY e.enumsortorder;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- PASO 4: Verificar que 'earned_rank' existe en el ENUM
|
|
||||||
-- =====================================================================================
|
|
||||||
\echo '4. Verificando que ''earned_rank'' existe en el ENUM...'
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_type t
|
|
||||||
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
||||||
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
||||||
WHERE t.typname = 'transaction_type'
|
|
||||||
AND n.nspname = 'gamification_system'
|
|
||||||
AND e.enumlabel = 'earned_rank'
|
|
||||||
) THEN '✅ ENUM ''earned_rank'' existe'
|
|
||||||
ELSE '❌ ERROR: ENUM ''earned_rank'' NO existe'
|
|
||||||
END AS validation_result;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- PASO 5: Ver código fuente de la función (últimas líneas del INSERT)
|
|
||||||
-- =====================================================================================
|
|
||||||
\echo '5. Verificando código fuente de la función (fragmento del INSERT)...'
|
|
||||||
SELECT
|
|
||||||
substring(
|
|
||||||
pg_get_functiondef(p.oid),
|
|
||||||
position('INSERT INTO gamification_system.ml_coins_transactions' in pg_get_functiondef(p.oid)),
|
|
||||||
500
|
|
||||||
) AS insert_statement_fragment
|
|
||||||
FROM pg_proc p
|
|
||||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
||||||
WHERE p.proname = 'update_user_rank'
|
|
||||||
AND n.nspname = 'gamification_system';
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- PASO 6: Test básico de sintaxis (sin ejecutar realmente)
|
|
||||||
-- =====================================================================================
|
|
||||||
\echo '6. Validación de sintaxis SQL...'
|
|
||||||
\echo 'Preparando transacción de prueba (se hará ROLLBACK)...'
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
-- Crear usuario de prueba temporal
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
v_test_user_id UUID := 'test-user-validate-rank-fix'::UUID;
|
|
||||||
BEGIN
|
|
||||||
-- Limpiar si existe
|
|
||||||
DELETE FROM gamification_system.user_stats WHERE user_id = v_test_user_id;
|
|
||||||
DELETE FROM gamification_system.user_ranks WHERE user_id = v_test_user_id;
|
|
||||||
DELETE FROM auth_management.profiles WHERE id = v_test_user_id;
|
|
||||||
|
|
||||||
-- Crear perfil de prueba
|
|
||||||
INSERT INTO auth_management.profiles (id, display_name, role, tenant_id)
|
|
||||||
VALUES (
|
|
||||||
v_test_user_id,
|
|
||||||
'Test User - Validate Rank Fix',
|
|
||||||
'student',
|
|
||||||
(SELECT id FROM auth_management.tenants LIMIT 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Crear user_stats inicial (debe disparar trigger de inicialización)
|
|
||||||
-- Si el trigger funciona, creará el registro automáticamente
|
|
||||||
|
|
||||||
RAISE NOTICE 'Usuario de prueba creado: %', v_test_user_id;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Verificar que el usuario se creó correctamente
|
|
||||||
\echo ''
|
|
||||||
\echo 'Verificando creación de user_stats...'
|
|
||||||
SELECT
|
|
||||||
user_id,
|
|
||||||
total_xp,
|
|
||||||
COALESCE(ml_coins, 0) AS ml_coins,
|
|
||||||
created_at
|
|
||||||
FROM gamification_system.user_stats
|
|
||||||
WHERE user_id = 'test-user-validate-rank-fix'::UUID;
|
|
||||||
|
|
||||||
-- Simular XP suficiente para ascender de rango
|
|
||||||
UPDATE gamification_system.user_stats
|
|
||||||
SET total_xp = 5000 -- Suficiente para pasar de Ajaw
|
|
||||||
WHERE user_id = 'test-user-validate-rank-fix'::UUID;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo 'Ejecutando update_user_rank()...'
|
|
||||||
SELECT * FROM gamification_system.update_user_rank('test-user-validate-rank-fix'::UUID);
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo 'Verificando transacción creada...'
|
|
||||||
SELECT
|
|
||||||
user_id,
|
|
||||||
amount,
|
|
||||||
balance_before,
|
|
||||||
balance_after,
|
|
||||||
transaction_type,
|
|
||||||
description,
|
|
||||||
created_at
|
|
||||||
FROM gamification_system.ml_coins_transactions
|
|
||||||
WHERE user_id = 'test-user-validate-rank-fix'::UUID
|
|
||||||
AND transaction_type = 'earned_rank'
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validando integridad de balance...'
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN balance_after = balance_before + amount THEN '✅ Balance correcto'
|
|
||||||
ELSE '❌ ERROR: Balance incorrecto'
|
|
||||||
END AS balance_validation,
|
|
||||||
balance_before,
|
|
||||||
amount,
|
|
||||||
balance_after,
|
|
||||||
(balance_before + amount) AS expected_balance_after
|
|
||||||
FROM gamification_system.ml_coins_transactions
|
|
||||||
WHERE user_id = 'test-user-validate-rank-fix'::UUID
|
|
||||||
AND transaction_type = 'earned_rank'
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- Rollback para no afectar la base de datos
|
|
||||||
ROLLBACK;
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '✅ Transacción de prueba revertida (ROLLBACK)'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- PASO 7: Resumen de validación
|
|
||||||
-- =====================================================================================
|
|
||||||
\echo '========================================='
|
|
||||||
\echo 'RESUMEN DE VALIDACIÓN'
|
|
||||||
\echo '========================================='
|
|
||||||
\echo ''
|
|
||||||
\echo 'Checklist de corrección:'
|
|
||||||
\echo ' [ ] Función update_user_rank() existe'
|
|
||||||
\echo ' [ ] Campos balance_before y balance_after existen en ml_coins_transactions'
|
|
||||||
\echo ' [ ] Campos son NOT NULL'
|
|
||||||
\echo ' [ ] ENUM transaction_type tiene valor ''earned_rank'''
|
|
||||||
\echo ' [ ] Función incluye balance_before y balance_after en INSERT'
|
|
||||||
\echo ' [ ] Balance calculado correctamente (balance_after = balance_before + amount)'
|
|
||||||
\echo ''
|
|
||||||
\echo 'Si todos los pasos anteriores mostraron ✅, la corrección es exitosa.'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- PASO 8: Instrucciones finales
|
|
||||||
-- =====================================================================================
|
|
||||||
\echo '========================================='
|
|
||||||
\echo 'INSTRUCCIONES'
|
|
||||||
\echo '========================================='
|
|
||||||
\echo ''
|
|
||||||
\echo 'Para aplicar la corrección a producción:'
|
|
||||||
\echo ' 1. Verificar que este script ejecuta sin errores'
|
|
||||||
\echo ' 2. Aplicar función: psql -d gamilit_platform -f ddl/schemas/gamification_system/functions/update_user_rank.sql'
|
|
||||||
\echo ' 3. Validar con usuarios reales en ambiente de staging'
|
|
||||||
\echo ' 4. Deploy a producción'
|
|
||||||
\echo ''
|
|
||||||
\echo 'Para revisar otras funciones que usan ml_coins_transactions:'
|
|
||||||
\echo ' grep -r "INSERT INTO.*ml_coins_transactions" apps/database/ddl/'
|
|
||||||
\echo ''
|
|
||||||
|
|
||||||
-- =====================================================================================
|
|
||||||
-- FIN DEL SCRIPT
|
|
||||||
-- =====================================================================================
|
|
||||||
@ -1,499 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Script: validate-user-initialization.sql
|
|
||||||
-- Description: Valida que TODOS los usuarios estén completamente inicializados
|
|
||||||
-- Version: 1.0
|
|
||||||
-- Created: 2025-11-24
|
|
||||||
-- Database-Agent Task: Análisis y corrección de inicialización de usuarios
|
|
||||||
-- =====================================================
|
|
||||||
--
|
|
||||||
-- OBJETIVO:
|
|
||||||
-- Validar que todos los usuarios (testing + demo + producción) tengan:
|
|
||||||
-- 1. auth.users (registro inicial)
|
|
||||||
-- 2. auth_management.profiles (con profiles.id = auth.users.id)
|
|
||||||
-- 3. gamification_system.user_stats (ML Coins inicializados)
|
|
||||||
-- 4. gamification_system.comodines_inventory (user_id → profiles.id)
|
|
||||||
-- 5. gamification_system.user_ranks (rango inicial Ajaw)
|
|
||||||
-- 6. progress_tracking.module_progress (todos los módulos publicados)
|
|
||||||
--
|
|
||||||
-- USUARIOS ESPERADOS:
|
|
||||||
-- - Testing PROD (@gamilit.com): 3 usuarios
|
|
||||||
-- - Demo PROD (@demo.glit.edu.mx): 20 usuarios (opcional según ambiente)
|
|
||||||
-- - Producción (emails reales): 13 usuarios
|
|
||||||
-- - TOTAL PROD: 16 usuarios (3 testing + 13 producción)
|
|
||||||
-- - TOTAL FULL: 36 usuarios (3 testing + 20 demo + 13 producción)
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\set ON_ERROR_STOP off
|
|
||||||
|
|
||||||
SET search_path TO auth, auth_management, gamification_system, progress_tracking, public;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- SECCIÓN 1: Validación de auth.users
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo '========================================'
|
|
||||||
\echo 'VALIDACIÓN 1: auth.users'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'1.1. Total usuarios en auth.users' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)'
|
|
||||||
ELSE '❌ ERROR: Menos de 16 usuarios'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth.users;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'1.2. Usuarios @gamilit.com (testing)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 3 THEN '✅ OK (3 esperados)'
|
|
||||||
ELSE '❌ ERROR: Se esperaban 3 usuarios @gamilit.com'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth.users
|
|
||||||
WHERE email LIKE '%@gamilit.com';
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'1.3. Usuarios productivos (no @gamilit.com)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 13 THEN '✅ OK (13 esperados)'
|
|
||||||
ELSE '⚠️ WARNING: Se esperaban 13 usuarios productivos'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth.users
|
|
||||||
WHERE email NOT LIKE '%@gamilit.com'
|
|
||||||
AND email NOT LIKE '%@demo.glit.edu.mx';
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'1.4. Usuarios DEMO (@demo.glit.edu.mx)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
'⏭️ OPCIONAL (ambiente)' AS resultado
|
|
||||||
FROM auth.users
|
|
||||||
WHERE email LIKE '%@demo.glit.edu.mx';
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- SECCIÓN 2: Validación de auth_management.profiles
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo 'VALIDACIÓN 2: auth_management.profiles'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'2.1. Total profiles' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)'
|
|
||||||
ELSE '❌ ERROR: Menos de 16 profiles'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth_management.profiles;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'2.2. Profiles con id = user_id (CRÍTICO)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = (SELECT COUNT(*) FROM auth_management.profiles)
|
|
||||||
THEN '✅ OK (100% consistente)'
|
|
||||||
ELSE '❌ ERROR: Hay profiles con id ≠ user_id'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth_management.profiles
|
|
||||||
WHERE id = user_id;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'2.3. Usuarios SIN profile (CRÍTICO)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 0 THEN '✅ OK (todos tienen profile)'
|
|
||||||
ELSE '❌ ERROR: Hay usuarios sin profile'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth.users u
|
|
||||||
LEFT JOIN auth_management.profiles p ON u.id = p.user_id
|
|
||||||
WHERE p.id IS NULL;
|
|
||||||
|
|
||||||
-- Mostrar usuarios sin profile (si existen)
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
usuarios_sin_profile INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO usuarios_sin_profile
|
|
||||||
FROM auth.users u
|
|
||||||
LEFT JOIN auth_management.profiles p ON u.id = p.user_id
|
|
||||||
WHERE p.id IS NULL;
|
|
||||||
|
|
||||||
IF usuarios_sin_profile > 0 THEN
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE '❌ USUARIOS SIN PROFILE DETECTADOS:';
|
|
||||||
FOR rec IN
|
|
||||||
SELECT u.id, u.email
|
|
||||||
FROM auth.users u
|
|
||||||
LEFT JOIN auth_management.profiles p ON u.id = p.user_id
|
|
||||||
WHERE p.id IS NULL
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE ' - % (%)', rec.email, rec.id;
|
|
||||||
END LOOP;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- SECCIÓN 3: Validación de gamification_system.user_stats
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo 'VALIDACIÓN 3: gamification_system.user_stats'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'3.1. Total user_stats' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)'
|
|
||||||
ELSE '❌ ERROR: Menos de 16 user_stats'
|
|
||||||
END AS resultado
|
|
||||||
FROM gamification_system.user_stats;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'3.2. Usuarios CON profile pero SIN user_stats (CRÍTICO)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 0 THEN '✅ OK (todos tienen user_stats)'
|
|
||||||
ELSE '❌ ERROR: Hay profiles sin user_stats'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.user_stats us ON p.user_id = us.user_id
|
|
||||||
WHERE us.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'3.3. user_stats con ML Coins = 100 (inicial)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
'⏭️ INFO (bonus inicial)' AS resultado
|
|
||||||
FROM gamification_system.user_stats
|
|
||||||
WHERE ml_coins = 100 AND ml_coins_earned_total = 100;
|
|
||||||
|
|
||||||
-- Mostrar profiles sin user_stats (si existen)
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
profiles_sin_stats INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO profiles_sin_stats
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.user_stats us ON p.user_id = us.user_id
|
|
||||||
WHERE us.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
IF profiles_sin_stats > 0 THEN
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE '❌ PROFILES SIN USER_STATS DETECTADOS:';
|
|
||||||
FOR rec IN
|
|
||||||
SELECT p.id, p.email, p.role
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.user_stats us ON p.user_id = us.user_id
|
|
||||||
WHERE us.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin')
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE ' - % (%, %)', rec.email, rec.role, rec.id;
|
|
||||||
END LOOP;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- SECCIÓN 4: Validación de gamification_system.comodines_inventory
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo 'VALIDACIÓN 4: gamification_system.comodines_inventory'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'4.1. Total comodines_inventory' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)'
|
|
||||||
ELSE '❌ ERROR: Menos de 16 inventarios'
|
|
||||||
END AS resultado
|
|
||||||
FROM gamification_system.comodines_inventory;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'4.2. Profiles SIN comodines_inventory (CRÍTICO)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 0 THEN '✅ OK (todos tienen inventario)'
|
|
||||||
ELSE '❌ ERROR: Hay profiles sin inventario'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.comodines_inventory ci ON p.id = ci.user_id
|
|
||||||
WHERE ci.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
-- IMPORTANTE: comodines_inventory.user_id apunta a profiles.id (NO auth.users.id)
|
|
||||||
SELECT
|
|
||||||
'4.3. comodines_inventory con user_id válido' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = (SELECT COUNT(*) FROM gamification_system.comodines_inventory)
|
|
||||||
THEN '✅ OK (100% válidos)'
|
|
||||||
ELSE '❌ ERROR: Hay inventarios con user_id inválido'
|
|
||||||
END AS resultado
|
|
||||||
FROM gamification_system.comodines_inventory ci
|
|
||||||
INNER JOIN auth_management.profiles p ON ci.user_id = p.id;
|
|
||||||
|
|
||||||
-- Mostrar profiles sin comodines_inventory (si existen)
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
profiles_sin_inventory INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO profiles_sin_inventory
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.comodines_inventory ci ON p.id = ci.user_id
|
|
||||||
WHERE ci.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
IF profiles_sin_inventory > 0 THEN
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE '❌ PROFILES SIN COMODINES_INVENTORY DETECTADOS:';
|
|
||||||
FOR rec IN
|
|
||||||
SELECT p.id, p.email, p.role
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.comodines_inventory ci ON p.id = ci.user_id
|
|
||||||
WHERE ci.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin')
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE ' - % (%, %)', rec.email, rec.role, rec.id;
|
|
||||||
END LOOP;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- SECCIÓN 5: Validación de gamification_system.user_ranks
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo 'VALIDACIÓN 5: gamification_system.user_ranks'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'5.1. Total user_ranks' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)'
|
|
||||||
ELSE '❌ ERROR: Menos de 16 user_ranks'
|
|
||||||
END AS resultado
|
|
||||||
FROM gamification_system.user_ranks;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'5.2. Usuarios CON profile pero SIN user_ranks (CRÍTICO)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = 0 THEN '✅ OK (todos tienen rank)'
|
|
||||||
ELSE '❌ ERROR: Hay profiles sin rank'
|
|
||||||
END AS resultado
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.user_ranks ur ON p.user_id = ur.user_id
|
|
||||||
WHERE ur.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'5.3. user_ranks con rango Ajaw (inicial)' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
'⏭️ INFO (rango inicial)' AS resultado
|
|
||||||
FROM gamification_system.user_ranks
|
|
||||||
WHERE current_rank = 'Ajaw'::gamification_system.maya_rank;
|
|
||||||
|
|
||||||
-- Mostrar profiles sin user_ranks (si existen)
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
profiles_sin_ranks INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO profiles_sin_ranks
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.user_ranks ur ON p.user_id = ur.user_id
|
|
||||||
WHERE ur.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
IF profiles_sin_ranks > 0 THEN
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE '❌ PROFILES SIN USER_RANKS DETECTADOS:';
|
|
||||||
FOR rec IN
|
|
||||||
SELECT p.id, p.email, p.role
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.user_ranks ur ON p.user_id = ur.user_id
|
|
||||||
WHERE ur.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin')
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE ' - % (%, %)', rec.email, rec.role, rec.id;
|
|
||||||
END LOOP;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- SECCIÓN 6: Validación de progress_tracking.module_progress
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo 'VALIDACIÓN 6: progress_tracking.module_progress'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'6.1. Total module_progress registros' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
'⏭️ INFO (depende de módulos publicados)' AS resultado
|
|
||||||
FROM progress_tracking.module_progress;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'6.2. Estudiantes CON module_progress' AS validacion,
|
|
||||||
COUNT(DISTINCT mp.user_id) AS cantidad,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(DISTINCT mp.user_id) >= 16 THEN '✅ OK (mínimo 16 esperados)'
|
|
||||||
ELSE '⚠️ WARNING: Menos de 16 estudiantes con progreso'
|
|
||||||
END AS resultado
|
|
||||||
FROM progress_tracking.module_progress mp
|
|
||||||
INNER JOIN auth_management.profiles p ON mp.user_id = p.id
|
|
||||||
WHERE p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
'6.3. Módulos publicados disponibles' AS validacion,
|
|
||||||
COUNT(*) AS cantidad,
|
|
||||||
'⏭️ INFO' AS resultado
|
|
||||||
FROM educational_content.modules
|
|
||||||
WHERE is_published = true AND status = 'published';
|
|
||||||
|
|
||||||
-- Mostrar estudiantes sin module_progress (si existen)
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
profiles_sin_progress INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO profiles_sin_progress
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN progress_tracking.module_progress mp ON p.id = mp.user_id
|
|
||||||
WHERE mp.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
IF profiles_sin_progress > 0 THEN
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE '⚠️ PROFILES SIN MODULE_PROGRESS DETECTADOS:';
|
|
||||||
FOR rec IN
|
|
||||||
SELECT p.id, p.email, p.role
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN progress_tracking.module_progress mp ON p.id = mp.user_id
|
|
||||||
WHERE mp.user_id IS NULL
|
|
||||||
AND p.role IN ('student', 'admin_teacher', 'super_admin')
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE ' - % (%, %)', rec.email, rec.role, rec.id;
|
|
||||||
END LOOP;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- SECCIÓN 7: Resumen Final
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo '========================================'
|
|
||||||
\echo 'RESUMEN FINAL'
|
|
||||||
\echo '========================================'
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
total_users INTEGER;
|
|
||||||
total_profiles INTEGER;
|
|
||||||
total_stats INTEGER;
|
|
||||||
total_inventory INTEGER;
|
|
||||||
total_ranks INTEGER;
|
|
||||||
total_progress_users INTEGER;
|
|
||||||
usuarios_sin_profile INTEGER;
|
|
||||||
profiles_sin_stats INTEGER;
|
|
||||||
profiles_sin_inventory INTEGER;
|
|
||||||
profiles_sin_ranks INTEGER;
|
|
||||||
profiles_sin_progress INTEGER;
|
|
||||||
errores_criticos INTEGER := 0;
|
|
||||||
BEGIN
|
|
||||||
-- Contar totales
|
|
||||||
SELECT COUNT(*) INTO total_users FROM auth.users;
|
|
||||||
SELECT COUNT(*) INTO total_profiles FROM auth_management.profiles;
|
|
||||||
SELECT COUNT(*) INTO total_stats FROM gamification_system.user_stats;
|
|
||||||
SELECT COUNT(*) INTO total_inventory FROM gamification_system.comodines_inventory;
|
|
||||||
SELECT COUNT(*) INTO total_ranks FROM gamification_system.user_ranks;
|
|
||||||
SELECT COUNT(DISTINCT mp.user_id) INTO total_progress_users
|
|
||||||
FROM progress_tracking.module_progress mp;
|
|
||||||
|
|
||||||
-- Contar problemas
|
|
||||||
SELECT COUNT(*) INTO usuarios_sin_profile
|
|
||||||
FROM auth.users u
|
|
||||||
LEFT JOIN auth_management.profiles p ON u.id = p.user_id
|
|
||||||
WHERE p.id IS NULL;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO profiles_sin_stats
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.user_stats us ON p.user_id = us.user_id
|
|
||||||
WHERE us.user_id IS NULL AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO profiles_sin_inventory
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.comodines_inventory ci ON p.id = ci.user_id
|
|
||||||
WHERE ci.user_id IS NULL AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO profiles_sin_ranks
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.user_ranks ur ON p.user_id = ur.user_id
|
|
||||||
WHERE ur.user_id IS NULL AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO profiles_sin_progress
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN progress_tracking.module_progress mp ON p.id = mp.user_id
|
|
||||||
WHERE mp.user_id IS NULL AND p.role IN ('student', 'admin_teacher', 'super_admin');
|
|
||||||
|
|
||||||
-- Calcular errores críticos
|
|
||||||
errores_criticos := usuarios_sin_profile + profiles_sin_stats +
|
|
||||||
profiles_sin_inventory + profiles_sin_ranks;
|
|
||||||
|
|
||||||
-- Mostrar resumen
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE 'TOTALES:';
|
|
||||||
RAISE NOTICE ' - auth.users: %', total_users;
|
|
||||||
RAISE NOTICE ' - auth_management.profiles: %', total_profiles;
|
|
||||||
RAISE NOTICE ' - gamification_system.user_stats: %', total_stats;
|
|
||||||
RAISE NOTICE ' - gamification_system.comodines_inventory: %', total_inventory;
|
|
||||||
RAISE NOTICE ' - gamification_system.user_ranks: %', total_ranks;
|
|
||||||
RAISE NOTICE ' - progress_tracking.module_progress (usuarios únicos): %', total_progress_users;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE 'PROBLEMAS DETECTADOS:';
|
|
||||||
RAISE NOTICE ' - Usuarios sin profile: %', usuarios_sin_profile;
|
|
||||||
RAISE NOTICE ' - Profiles sin user_stats: %', profiles_sin_stats;
|
|
||||||
RAISE NOTICE ' - Profiles sin comodines_inventory: %', profiles_sin_inventory;
|
|
||||||
RAISE NOTICE ' - Profiles sin user_ranks: %', profiles_sin_ranks;
|
|
||||||
RAISE NOTICE ' - Profiles sin module_progress: % (WARNING)', profiles_sin_progress;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
|
|
||||||
IF errores_criticos = 0 THEN
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE '✅ VALIDACIÓN EXITOSA';
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE 'Todos los usuarios están completamente inicializados.';
|
|
||||||
IF profiles_sin_progress > 0 THEN
|
|
||||||
RAISE NOTICE '⚠️ WARNING: Hay % usuarios sin module_progress', profiles_sin_progress;
|
|
||||||
RAISE NOTICE ' (Esto puede ser esperado si no hay módulos publicados)';
|
|
||||||
END IF;
|
|
||||||
ELSE
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE '❌ VALIDACIÓN FALLIDA';
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE 'Se detectaron % errores críticos.', errores_criticos;
|
|
||||||
RAISE NOTICE 'Revisa las secciones anteriores para más detalles.';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RAISE NOTICE '';
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- FIN DEL SCRIPT
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
\echo ''
|
|
||||||
\echo 'Validación completa. Revisa los resultados arriba.'
|
|
||||||
\echo ''
|
|
||||||
@ -1,474 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Script de validación exhaustiva de integridad de la base de datos GAMILIT
|
|
||||||
Fecha: 2025-11-07
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
from collections import defaultdict
|
|
||||||
from typing import Dict, List, Set, Tuple
|
|
||||||
|
|
||||||
# Colores para output
|
|
||||||
class Colors:
|
|
||||||
HEADER = '\033[95m'
|
|
||||||
OKBLUE = '\033[94m'
|
|
||||||
OKCYAN = '\033[96m'
|
|
||||||
OKGREEN = '\033[92m'
|
|
||||||
WARNING = '\033[93m'
|
|
||||||
FAIL = '\033[91m'
|
|
||||||
ENDC = '\033[0m'
|
|
||||||
BOLD = '\033[1m'
|
|
||||||
|
|
||||||
def print_section(title):
|
|
||||||
print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*80}{Colors.ENDC}")
|
|
||||||
print(f"{Colors.HEADER}{Colors.BOLD}{title}{Colors.ENDC}")
|
|
||||||
print(f"{Colors.HEADER}{Colors.BOLD}{'='*80}{Colors.ENDC}\n")
|
|
||||||
|
|
||||||
def print_error(severity, msg):
|
|
||||||
color = Colors.FAIL if severity == "CRÍTICO" else Colors.WARNING if severity == "ALTO" else Colors.OKCYAN
|
|
||||||
print(f"{color}[{severity}] {msg}{Colors.ENDC}")
|
|
||||||
|
|
||||||
def print_ok(msg):
|
|
||||||
print(f"{Colors.OKGREEN}✓ {msg}{Colors.ENDC}")
|
|
||||||
|
|
||||||
# Configuración de paths (usa variable de entorno o path relativo al script)
|
|
||||||
BASE_PATH = Path(os.environ.get('GAMILIT_DDL_PATH',
|
|
||||||
Path(__file__).resolve().parent.parent / 'ddl'))
|
|
||||||
SCHEMAS_PATH = BASE_PATH / "schemas"
|
|
||||||
|
|
||||||
# 1. EXTRAER TODOS LOS ENUMs DEFINIDOS
|
|
||||||
def extract_enums():
|
|
||||||
"""Extrae todos los ENUMs definidos en el sistema"""
|
|
||||||
enums = {}
|
|
||||||
|
|
||||||
# 1.1 ENUMs en 00-prerequisites.sql
|
|
||||||
prereq_file = BASE_PATH / "00-prerequisites.sql"
|
|
||||||
if prereq_file.exists():
|
|
||||||
content = prereq_file.read_text()
|
|
||||||
# Buscar CREATE TYPE schema.enum AS ENUM
|
|
||||||
pattern = r'CREATE TYPE\s+([\w.]+)\s+AS ENUM\s*\((.*?)\);'
|
|
||||||
matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
|
|
||||||
for enum_name, values in matches:
|
|
||||||
schema = "public"
|
|
||||||
name = enum_name
|
|
||||||
if "." in enum_name:
|
|
||||||
schema, name = enum_name.split(".", 1)
|
|
||||||
|
|
||||||
# Limpiar valores
|
|
||||||
vals = [v.strip().strip("'").strip('"') for v in re.findall(r"'([^']*)'", values)]
|
|
||||||
enums[f"{schema}.{name}"] = {
|
|
||||||
"file": str(prereq_file),
|
|
||||||
"schema": schema,
|
|
||||||
"name": name,
|
|
||||||
"values": vals,
|
|
||||||
"count": len(vals)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1.2 ENUMs en archivos individuales
|
|
||||||
for enum_file in SCHEMAS_PATH.rglob("enums/*.sql"):
|
|
||||||
content = enum_file.read_text()
|
|
||||||
|
|
||||||
# Extraer schema del path
|
|
||||||
parts = enum_file.parts
|
|
||||||
schema_idx = parts.index("schemas") + 1
|
|
||||||
schema = parts[schema_idx]
|
|
||||||
|
|
||||||
# Buscar CREATE TYPE
|
|
||||||
pattern = r'CREATE TYPE\s+([\w.]+)\s+AS ENUM\s*\((.*?)\);'
|
|
||||||
matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
|
|
||||||
|
|
||||||
for enum_name, values in matches:
|
|
||||||
if "." in enum_name:
|
|
||||||
schema, name = enum_name.split(".", 1)
|
|
||||||
else:
|
|
||||||
name = enum_name
|
|
||||||
|
|
||||||
vals = [v.strip().strip("'").strip('"') for v in re.findall(r"'([^']*)'", values)]
|
|
||||||
|
|
||||||
full_name = f"{schema}.{name}"
|
|
||||||
enums[full_name] = {
|
|
||||||
"file": str(enum_file),
|
|
||||||
"schema": schema,
|
|
||||||
"name": name,
|
|
||||||
"values": vals,
|
|
||||||
"count": len(vals)
|
|
||||||
}
|
|
||||||
|
|
||||||
return enums
|
|
||||||
|
|
||||||
# 2. EXTRAER TODAS LAS TABLAS DEFINIDAS
|
|
||||||
def extract_tables():
|
|
||||||
"""Extrae todas las tablas definidas"""
|
|
||||||
tables = {}
|
|
||||||
|
|
||||||
for table_file in SCHEMAS_PATH.rglob("tables/*.sql"):
|
|
||||||
content = table_file.read_text()
|
|
||||||
|
|
||||||
# Extraer schema del path
|
|
||||||
parts = table_file.parts
|
|
||||||
schema_idx = parts.index("schemas") + 1
|
|
||||||
schema = parts[schema_idx]
|
|
||||||
|
|
||||||
# Buscar CREATE TABLE
|
|
||||||
pattern = r'CREATE TABLE\s+(?:IF NOT EXISTS\s+)?([\w.]+)'
|
|
||||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
||||||
|
|
||||||
for table_name in matches:
|
|
||||||
if "." in table_name:
|
|
||||||
tbl_schema, tbl_name = table_name.split(".", 1)
|
|
||||||
else:
|
|
||||||
tbl_schema = schema
|
|
||||||
tbl_name = table_name
|
|
||||||
|
|
||||||
full_name = f"{tbl_schema}.{tbl_name}"
|
|
||||||
tables[full_name] = {
|
|
||||||
"file": str(table_file),
|
|
||||||
"schema": tbl_schema,
|
|
||||||
"name": tbl_name
|
|
||||||
}
|
|
||||||
|
|
||||||
return tables
|
|
||||||
|
|
||||||
# 3. VALIDAR FOREIGN KEYS
|
|
||||||
def validate_foreign_keys(tables):
|
|
||||||
"""Valida que todas las referencias de FK apunten a tablas existentes"""
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
print_section("VALIDACIÓN 1: INTEGRIDAD DE FOREIGN KEYS")
|
|
||||||
|
|
||||||
for table_file in SCHEMAS_PATH.rglob("tables/*.sql"):
|
|
||||||
content = table_file.read_text()
|
|
||||||
|
|
||||||
# Buscar REFERENCES
|
|
||||||
pattern = r'REFERENCES\s+([\w.]+)\s*\('
|
|
||||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
||||||
|
|
||||||
for ref_table in matches:
|
|
||||||
# Normalizar nombre
|
|
||||||
if "." not in ref_table:
|
|
||||||
# Buscar schema del archivo actual
|
|
||||||
parts = table_file.parts
|
|
||||||
schema_idx = parts.index("schemas") + 1
|
|
||||||
schema = parts[schema_idx]
|
|
||||||
ref_table = f"{schema}.{ref_table}"
|
|
||||||
|
|
||||||
if ref_table not in tables:
|
|
||||||
issues.append({
|
|
||||||
"severity": "CRÍTICO",
|
|
||||||
"type": "FK_BROKEN",
|
|
||||||
"file": str(table_file),
|
|
||||||
"message": f"Referencia a tabla inexistente: {ref_table}"
|
|
||||||
})
|
|
||||||
|
|
||||||
if not issues:
|
|
||||||
print_ok("Todas las Foreign Keys apuntan a tablas existentes")
|
|
||||||
else:
|
|
||||||
for issue in issues:
|
|
||||||
print_error(issue["severity"], f"{issue['message']}\n Archivo: {issue['file']}")
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
# 4. VALIDAR ENUMS EN TABLAS
|
|
||||||
def validate_enum_references(enums, tables):
|
|
||||||
"""Valida que todos los ENUMs usados en tablas existan"""
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
print_section("VALIDACIÓN 2: INTEGRIDAD DE ENUMs")
|
|
||||||
|
|
||||||
for table_file in SCHEMAS_PATH.rglob("tables/*.sql"):
|
|
||||||
content = table_file.read_text()
|
|
||||||
|
|
||||||
# Buscar columnas con tipo ENUM (schema.enum_name)
|
|
||||||
pattern = r'(\w+)\s+([\w.]+)(?:\s+(?:NOT NULL|DEFAULT|CHECK|UNIQUE|PRIMARY KEY))?'
|
|
||||||
|
|
||||||
# Buscar específicamente tipos que parecen ENUMs (esquema.tipo)
|
|
||||||
enum_pattern = r'\s+([\w]+\.\w+)(?:\s|,|\))'
|
|
||||||
enum_matches = re.findall(enum_pattern, content)
|
|
||||||
|
|
||||||
for enum_ref in enum_matches:
|
|
||||||
# Ignorar cosas que no son ENUMs
|
|
||||||
if enum_ref.startswith('auth.') or enum_ref.startswith('educational_content.') or enum_ref.startswith('social_features.'):
|
|
||||||
if '(' in enum_ref or ')' in enum_ref:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Verificar si es un ENUM conocido
|
|
||||||
if enum_ref not in enums:
|
|
||||||
# Podría ser una tabla, verificar
|
|
||||||
if enum_ref not in tables:
|
|
||||||
issues.append({
|
|
||||||
"severity": "ALTO",
|
|
||||||
"type": "ENUM_NOT_FOUND",
|
|
||||||
"file": str(table_file),
|
|
||||||
"message": f"Posible referencia a ENUM inexistente: {enum_ref}"
|
|
||||||
})
|
|
||||||
|
|
||||||
if not issues:
|
|
||||||
print_ok("Todos los ENUMs referenciados existen")
|
|
||||||
else:
|
|
||||||
for issue in issues:
|
|
||||||
print_error(issue["severity"], f"{issue['message']}\n Archivo: {issue['file']}")
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
# 5. VALIDAR CORRECCIONES APLICADAS
|
|
||||||
def validate_corrections():
|
|
||||||
"""Valida las correcciones específicas mencionadas en el tracking"""
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
print_section("VALIDACIÓN 3: CORRECCIONES APLICADAS")
|
|
||||||
|
|
||||||
# 5.1 notification_type - Debe tener 11 valores
|
|
||||||
print("\n--- notification_type ---")
|
|
||||||
enum_file = SCHEMAS_PATH / "public" / "enums" / "notification_type.sql"
|
|
||||||
if enum_file.exists():
|
|
||||||
content = enum_file.read_text()
|
|
||||||
values = re.findall(r"'([^']*)'", content)
|
|
||||||
if len(values) == 11:
|
|
||||||
print_ok(f"notification_type tiene 11 valores correctos")
|
|
||||||
expected = ['achievement_unlocked', 'rank_up', 'friend_request', 'guild_invitation',
|
|
||||||
'mission_completed', 'level_up', 'message_received', 'system_announcement',
|
|
||||||
'ml_coins_earned', 'streak_milestone', 'exercise_feedback']
|
|
||||||
missing = set(expected) - set(values)
|
|
||||||
if missing:
|
|
||||||
issues.append({
|
|
||||||
"severity": "ALTO",
|
|
||||||
"type": "ENUM_VALUES",
|
|
||||||
"file": str(enum_file),
|
|
||||||
"message": f"notification_type falta valores: {missing}"
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
issues.append({
|
|
||||||
"severity": "CRÍTICO",
|
|
||||||
"type": "ENUM_VALUES",
|
|
||||||
"file": str(enum_file),
|
|
||||||
"message": f"notification_type tiene {len(values)} valores, esperados 11"
|
|
||||||
})
|
|
||||||
|
|
||||||
# 5.2 achievement_category - Debe estar en gamification_system
|
|
||||||
print("\n--- achievement_category ---")
|
|
||||||
enum_file = SCHEMAS_PATH / "gamification_system" / "enums" / "achievement_category.sql"
|
|
||||||
if enum_file.exists():
|
|
||||||
print_ok(f"achievement_category está en gamification_system")
|
|
||||||
|
|
||||||
# Verificar que la tabla achievements lo usa
|
|
||||||
table_file = SCHEMAS_PATH / "gamification_system" / "tables" / "03-achievements.sql"
|
|
||||||
if table_file.exists():
|
|
||||||
content = table_file.read_text()
|
|
||||||
if "gamification_system.achievement_category" in content:
|
|
||||||
print_ok("achievements usa gamification_system.achievement_category")
|
|
||||||
elif "public.achievement_category" in content:
|
|
||||||
issues.append({
|
|
||||||
"severity": "CRÍTICO",
|
|
||||||
"type": "ENUM_SCHEMA",
|
|
||||||
"file": str(table_file),
|
|
||||||
"message": "achievements usa public.achievement_category (debe ser gamification_system)"
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
issues.append({
|
|
||||||
"severity": "CRÍTICO",
|
|
||||||
"type": "ENUM_MISSING",
|
|
||||||
"file": "N/A",
|
|
||||||
"message": "achievement_category no existe en gamification_system"
|
|
||||||
})
|
|
||||||
|
|
||||||
# 5.3 transaction_type - Debe estar en gamification_system
|
|
||||||
print("\n--- transaction_type ---")
|
|
||||||
enum_file = SCHEMAS_PATH / "gamification_system" / "enums" / "transaction_type.sql"
|
|
||||||
if enum_file.exists():
|
|
||||||
content = enum_file.read_text()
|
|
||||||
values = re.findall(r"'([^']*)'", content)
|
|
||||||
print_ok(f"transaction_type existe en gamification_system con {len(values)} valores")
|
|
||||||
|
|
||||||
# Debe tener 14 valores según tracking
|
|
||||||
if len(values) != 14:
|
|
||||||
issues.append({
|
|
||||||
"severity": "MEDIO",
|
|
||||||
"type": "ENUM_VALUES",
|
|
||||||
"file": str(enum_file),
|
|
||||||
"message": f"transaction_type tiene {len(values)} valores, esperados 14 según tracking"
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
issues.append({
|
|
||||||
"severity": "CRÍTICO",
|
|
||||||
"type": "ENUM_MISSING",
|
|
||||||
"file": "N/A",
|
|
||||||
"message": "transaction_type no existe en gamification_system"
|
|
||||||
})
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
# 6. BUSCAR FUNCIONES CON REFERENCIAS ROTAS
|
|
||||||
def validate_functions(tables):
|
|
||||||
"""Busca funciones que referencien tablas inexistentes"""
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
print_section("VALIDACIÓN 4: FUNCIONES CON REFERENCIAS ROTAS")
|
|
||||||
|
|
||||||
function_files = list(SCHEMAS_PATH.rglob("functions/*.sql"))
|
|
||||||
|
|
||||||
for func_file in function_files:
|
|
||||||
content = func_file.read_text()
|
|
||||||
|
|
||||||
# Buscar FROM, JOIN, INSERT INTO, UPDATE, DELETE FROM
|
|
||||||
patterns = [
|
|
||||||
r'FROM\s+([\w.]+)',
|
|
||||||
r'JOIN\s+([\w.]+)',
|
|
||||||
r'INSERT INTO\s+([\w.]+)',
|
|
||||||
r'UPDATE\s+([\w.]+)',
|
|
||||||
r'DELETE FROM\s+([\w.]+)'
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in patterns:
|
|
||||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
||||||
for table_ref in matches:
|
|
||||||
# Normalizar
|
|
||||||
if "." not in table_ref and table_ref not in ['NEW', 'OLD', 'RETURNING', 'VALUES']:
|
|
||||||
parts = func_file.parts
|
|
||||||
schema_idx = parts.index("schemas") + 1
|
|
||||||
schema = parts[schema_idx]
|
|
||||||
table_ref = f"{schema}.{table_ref}"
|
|
||||||
|
|
||||||
if "." in table_ref and table_ref not in tables:
|
|
||||||
# Verificar que no sea palabra clave SQL
|
|
||||||
if table_ref.lower() not in ['with.recursive', 'select.distinct']:
|
|
||||||
issues.append({
|
|
||||||
"severity": "ALTO",
|
|
||||||
"type": "FUNCTION_BROKEN_REF",
|
|
||||||
"file": str(func_file),
|
|
||||||
"message": f"Función referencia tabla inexistente: {table_ref}"
|
|
||||||
})
|
|
||||||
|
|
||||||
if not issues:
|
|
||||||
print_ok("Todas las funciones referencian tablas válidas")
|
|
||||||
else:
|
|
||||||
for issue in issues:
|
|
||||||
print_error(issue["severity"], f"{issue['message']}\n Archivo: {issue['file']}")
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
# 7. BUSCAR TRIGGERS CON REFERENCIAS ROTAS
|
|
||||||
def validate_triggers():
|
|
||||||
"""Busca triggers que llamen funciones inexistentes"""
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
print_section("VALIDACIÓN 5: TRIGGERS CON REFERENCIAS ROTAS")
|
|
||||||
|
|
||||||
# Primero extraer todas las funciones
|
|
||||||
functions = set()
|
|
||||||
for func_file in SCHEMAS_PATH.rglob("functions/*.sql"):
|
|
||||||
content = func_file.read_text()
|
|
||||||
pattern = r'CREATE\s+(?:OR REPLACE\s+)?FUNCTION\s+([\w.]+)\s*\('
|
|
||||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
||||||
functions.update(matches)
|
|
||||||
|
|
||||||
# Agregar funciones de prerequisites
|
|
||||||
prereq_file = BASE_PATH / "00-prerequisites.sql"
|
|
||||||
if prereq_file.exists():
|
|
||||||
content = prereq_file.read_text()
|
|
||||||
pattern = r'CREATE\s+(?:OR REPLACE\s+)?FUNCTION\s+([\w.]+)\s*\('
|
|
||||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
||||||
functions.update(matches)
|
|
||||||
|
|
||||||
# Validar triggers
|
|
||||||
for trigger_file in SCHEMAS_PATH.rglob("triggers/*.sql"):
|
|
||||||
content = trigger_file.read_text()
|
|
||||||
|
|
||||||
# Buscar EXECUTE FUNCTION
|
|
||||||
pattern = r'EXECUTE\s+(?:FUNCTION|PROCEDURE)\s+([\w.]+)\s*\('
|
|
||||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
||||||
|
|
||||||
for func_ref in matches:
|
|
||||||
if func_ref not in functions:
|
|
||||||
issues.append({
|
|
||||||
"severity": "CRÍTICO",
|
|
||||||
"type": "TRIGGER_BROKEN_REF",
|
|
||||||
"file": str(trigger_file),
|
|
||||||
"message": f"Trigger llama función inexistente: {func_ref}"
|
|
||||||
})
|
|
||||||
|
|
||||||
if not issues:
|
|
||||||
print_ok("Todos los triggers llaman funciones válidas")
|
|
||||||
else:
|
|
||||||
for issue in issues:
|
|
||||||
print_error(issue["severity"], f"{issue['message']}\n Archivo: {issue['file']}")
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
# 8. BUSCAR ENUMS DUPLICADOS
|
|
||||||
def check_duplicate_enums(enums):
|
|
||||||
"""Busca ENUMs duplicados en múltiples schemas"""
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
print_section("VALIDACIÓN 6: ENUMs DUPLICADOS")
|
|
||||||
|
|
||||||
enum_names = defaultdict(list)
|
|
||||||
for full_name, info in enums.items():
|
|
||||||
name = info["name"]
|
|
||||||
enum_names[name].append(full_name)
|
|
||||||
|
|
||||||
for name, locations in enum_names.items():
|
|
||||||
if len(locations) > 1:
|
|
||||||
issues.append({
|
|
||||||
"severity": "ALTO",
|
|
||||||
"type": "ENUM_DUPLICATE",
|
|
||||||
"file": "N/A",
|
|
||||||
"message": f"ENUM '{name}' duplicado en: {', '.join(locations)}"
|
|
||||||
})
|
|
||||||
|
|
||||||
if not issues:
|
|
||||||
print_ok("No hay ENUMs duplicados")
|
|
||||||
else:
|
|
||||||
for issue in issues:
|
|
||||||
print_error(issue["severity"], issue["message"])
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
# MAIN
|
|
||||||
def main():
|
|
||||||
print(f"\n{Colors.BOLD}VALIDACIÓN EXHAUSTIVA DE INTEGRIDAD - BASE DE DATOS GAMILIT{Colors.ENDC}")
|
|
||||||
print(f"{Colors.BOLD}Fecha: 2025-11-07{Colors.ENDC}")
|
|
||||||
print(f"{Colors.BOLD}Post-correcciones: 9/142 completadas{Colors.ENDC}\n")
|
|
||||||
|
|
||||||
# Extraer información
|
|
||||||
print("Extrayendo información de la base de datos...")
|
|
||||||
enums = extract_enums()
|
|
||||||
tables = extract_tables()
|
|
||||||
|
|
||||||
print(f"✓ {len(enums)} ENUMs encontrados")
|
|
||||||
print(f"✓ {len(tables)} tablas encontradas")
|
|
||||||
|
|
||||||
# Ejecutar validaciones
|
|
||||||
all_issues = []
|
|
||||||
|
|
||||||
all_issues.extend(validate_foreign_keys(tables))
|
|
||||||
all_issues.extend(validate_enum_references(enums, tables))
|
|
||||||
all_issues.extend(validate_corrections())
|
|
||||||
all_issues.extend(validate_functions(tables))
|
|
||||||
all_issues.extend(validate_triggers())
|
|
||||||
all_issues.extend(check_duplicate_enums(enums))
|
|
||||||
|
|
||||||
# RESUMEN FINAL
|
|
||||||
print_section("RESUMEN DE VALIDACIÓN")
|
|
||||||
|
|
||||||
critical = [i for i in all_issues if i["severity"] == "CRÍTICO"]
|
|
||||||
high = [i for i in all_issues if i["severity"] == "ALTO"]
|
|
||||||
medium = [i for i in all_issues if i["severity"] == "MEDIO"]
|
|
||||||
low = [i for i in all_issues if i["severity"] == "BAJO"]
|
|
||||||
|
|
||||||
print(f"\n{Colors.FAIL}CRÍTICO: {len(critical)} problemas{Colors.ENDC}")
|
|
||||||
print(f"{Colors.WARNING}ALTO: {len(high)} problemas{Colors.ENDC}")
|
|
||||||
print(f"{Colors.OKCYAN}MEDIO: {len(medium)} problemas{Colors.ENDC}")
|
|
||||||
print(f"{Colors.OKBLUE}BAJO: {len(low)} problemas{Colors.ENDC}")
|
|
||||||
print(f"\n{Colors.BOLD}TOTAL: {len(all_issues)} problemas encontrados{Colors.ENDC}\n")
|
|
||||||
|
|
||||||
if len(all_issues) == 0:
|
|
||||||
print(f"{Colors.OKGREEN}{Colors.BOLD}✓✓✓ BASE DE DATOS VALIDADA EXITOSAMENTE ✓✓✓{Colors.ENDC}\n")
|
|
||||||
else:
|
|
||||||
print(f"{Colors.FAIL}{Colors.BOLD}⚠ SE REQUIERE ATENCIÓN ⚠{Colors.ENDC}\n")
|
|
||||||
|
|
||||||
return all_issues
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
issues = main()
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# =====================================================
|
|
||||||
# Script: verify-missions-status.sh
|
|
||||||
# Description: Verifica el estado de las misiones en la base de datos
|
|
||||||
# Author: Database-Agent
|
|
||||||
# Created: 2025-11-24
|
|
||||||
# =====================================================
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colores para output
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${BLUE}========================================${NC}"
|
|
||||||
echo -e "${BLUE}VERIFICACIÓN DE MISIONES - GAMILIT${NC}"
|
|
||||||
echo -e "${BLUE}========================================${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Leer credenciales
|
|
||||||
DB_USER="gamilit_user"
|
|
||||||
DB_NAME="gamilit_platform"
|
|
||||||
DB_HOST="localhost"
|
|
||||||
|
|
||||||
# Verificar si existe el archivo de credenciales
|
|
||||||
if [ -f "database-credentials-dev.txt" ]; then
|
|
||||||
DB_PASSWORD=$(grep "Password:" database-credentials-dev.txt | awk '{print $2}')
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}⚠️ No se encontró archivo de credenciales${NC}"
|
|
||||||
echo "Por favor ingresa la contraseña de la base de datos:"
|
|
||||||
read -s DB_PASSWORD
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PGPASSWORD="$DB_PASSWORD"
|
|
||||||
|
|
||||||
echo -e "${GREEN}1. RESUMEN GENERAL${NC}"
|
|
||||||
echo "-------------------------------------------"
|
|
||||||
psql -U "$DB_USER" -d "$DB_NAME" -h "$DB_HOST" -c "
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total_missions,
|
|
||||||
COUNT(DISTINCT user_id) as unique_users,
|
|
||||||
SUM(CASE WHEN mission_type = 'daily' THEN 1 ELSE 0 END) as daily_missions,
|
|
||||||
SUM(CASE WHEN mission_type = 'weekly' THEN 1 ELSE 0 END) as weekly_missions
|
|
||||||
FROM gamification_system.missions;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}2. USUARIOS CON Y SIN MISIONES${NC}"
|
|
||||||
echo "-------------------------------------------"
|
|
||||||
psql -U "$DB_USER" -d "$DB_NAME" -h "$DB_HOST" -c "
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN email LIKE '%@gamilit.com' THEN 'Test Users'
|
|
||||||
ELSE 'Production Users'
|
|
||||||
END as user_type,
|
|
||||||
COUNT(*) as total_users,
|
|
||||||
SUM(CASE WHEN has_missions THEN 1 ELSE 0 END) as with_missions,
|
|
||||||
SUM(CASE WHEN NOT has_missions THEN 1 ELSE 0 END) as without_missions
|
|
||||||
FROM (
|
|
||||||
SELECT
|
|
||||||
p.email,
|
|
||||||
EXISTS (SELECT 1 FROM gamification_system.missions m WHERE m.user_id = p.id) as has_missions
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
) subq
|
|
||||||
GROUP BY user_type
|
|
||||||
ORDER BY user_type;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}3. DISTRIBUCIÓN DE MISIONES POR USUARIO${NC}"
|
|
||||||
echo "-------------------------------------------"
|
|
||||||
psql -U "$DB_USER" -d "$DB_NAME" -h "$DB_HOST" -c "
|
|
||||||
SELECT
|
|
||||||
p.email,
|
|
||||||
p.role,
|
|
||||||
COUNT(m.id) as total_missions,
|
|
||||||
SUM(CASE WHEN m.mission_type = 'daily' THEN 1 ELSE 0 END) as daily,
|
|
||||||
SUM(CASE WHEN m.mission_type = 'weekly' THEN 1 ELSE 0 END) as weekly,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(m.id) = 0 THEN '❌ Sin misiones'
|
|
||||||
WHEN COUNT(m.id) = 8 THEN '✅ OK (8)'
|
|
||||||
WHEN COUNT(m.id) > 8 THEN '⚠️ Duplicadas (' || COUNT(m.id) || ')'
|
|
||||||
ELSE '⚠️ Incompletas (' || COUNT(m.id) || ')'
|
|
||||||
END as status
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
LEFT JOIN gamification_system.missions m ON p.id = m.user_id
|
|
||||||
GROUP BY p.email, p.role
|
|
||||||
ORDER BY
|
|
||||||
CASE WHEN p.email LIKE '%@gamilit.com' THEN 0 ELSE 1 END,
|
|
||||||
p.email;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}4. MISIONES POR TEMPLATE${NC}"
|
|
||||||
echo "-------------------------------------------"
|
|
||||||
psql -U "$DB_USER" -d "$DB_NAME" -h "$DB_HOST" -c "
|
|
||||||
SELECT
|
|
||||||
template_id,
|
|
||||||
mission_type,
|
|
||||||
COUNT(DISTINCT user_id) as users_with_mission,
|
|
||||||
COUNT(*) as total_instances,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(*) = COUNT(DISTINCT user_id) THEN '✅ No duplicadas'
|
|
||||||
ELSE '⚠️ ' || (COUNT(*) - COUNT(DISTINCT user_id)) || ' duplicadas'
|
|
||||||
END as status
|
|
||||||
FROM gamification_system.missions
|
|
||||||
GROUP BY template_id, mission_type
|
|
||||||
ORDER BY mission_type, template_id;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}5. USUARIOS SIN MISIONES (si existen)${NC}"
|
|
||||||
echo "-------------------------------------------"
|
|
||||||
psql -U "$DB_USER" -d "$DB_NAME" -h "$DB_HOST" -c "
|
|
||||||
SELECT
|
|
||||||
p.email,
|
|
||||||
p.role,
|
|
||||||
p.created_at::date as created_date
|
|
||||||
FROM auth_management.profiles p
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM gamification_system.missions m WHERE m.user_id = p.id
|
|
||||||
)
|
|
||||||
ORDER BY p.created_at;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}========================================${NC}"
|
|
||||||
echo -e "${BLUE}VERIFICACIÓN COMPLETADA${NC}"
|
|
||||||
echo -e "${BLUE}========================================${NC}"
|
|
||||||
|
|
||||||
# Limpiar variable de entorno
|
|
||||||
unset PGPASSWORD
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ============================================================================
|
|
||||||
# Script: verify-users.sh
|
|
||||||
# Descripción: Verifica que usuarios y perfiles estén correctamente cargados
|
|
||||||
# Fecha: 2025-11-09
|
|
||||||
# Autor: Claude Code (AI Assistant)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
DB_DIR="$(dirname "$SCRIPT_DIR")"
|
|
||||||
|
|
||||||
cd "$DB_DIR"
|
|
||||||
|
|
||||||
# Cargar credenciales
|
|
||||||
if [ ! -f "database-credentials-dev.txt" ]; then
|
|
||||||
echo "❌ Error: database-credentials-dev.txt no encontrado"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DB_PASSWORD=$(grep "^Password:" database-credentials-dev.txt | awk '{print $2}')
|
|
||||||
export PGPASSWORD="$DB_PASSWORD"
|
|
||||||
|
|
||||||
PSQL="psql -h localhost -p 5432 -U gamilit_user -d gamilit_platform"
|
|
||||||
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo " VERIFICACIÓN DE USUARIOS Y PERFILES"
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "📊 USUARIOS EN auth.users:"
|
|
||||||
echo ""
|
|
||||||
$PSQL -c "
|
|
||||||
SELECT
|
|
||||||
email,
|
|
||||||
role,
|
|
||||||
email_confirmed_at IS NOT NULL as confirmed,
|
|
||||||
TO_CHAR(created_at, 'YYYY-MM-DD') as created
|
|
||||||
FROM auth.users
|
|
||||||
WHERE email LIKE '%@glit.edu.mx'
|
|
||||||
OR email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR email LIKE '%@gamilit.com'
|
|
||||||
ORDER BY role, email;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo "📝 PERFILES EN auth_management.profiles:"
|
|
||||||
echo ""
|
|
||||||
$PSQL -c "
|
|
||||||
SELECT
|
|
||||||
email,
|
|
||||||
role,
|
|
||||||
full_name,
|
|
||||||
status,
|
|
||||||
email_verified
|
|
||||||
FROM auth_management.profiles
|
|
||||||
WHERE email LIKE '%@glit.edu.mx'
|
|
||||||
OR email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR email LIKE '%@gamilit.com'
|
|
||||||
ORDER BY role, email;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo "🔗 VINCULACIÓN users <-> profiles:"
|
|
||||||
echo ""
|
|
||||||
$PSQL -c "
|
|
||||||
SELECT
|
|
||||||
u.email,
|
|
||||||
CASE
|
|
||||||
WHEN p.user_id IS NOT NULL THEN '✅ Vinculado'
|
|
||||||
ELSE '❌ Sin Profile'
|
|
||||||
END as vinculacion,
|
|
||||||
u.role as user_role,
|
|
||||||
p.role as profile_role
|
|
||||||
FROM auth.users u
|
|
||||||
LEFT JOIN auth_management.profiles p ON u.id = p.user_id
|
|
||||||
WHERE u.email LIKE '%@glit.edu.mx'
|
|
||||||
OR u.email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR u.email LIKE '%@gamilit.com'
|
|
||||||
ORDER BY u.role, u.email;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo "📈 RESUMEN:"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Contar totales
|
|
||||||
TOTAL_USERS=$($PSQL -t -c "
|
|
||||||
SELECT COUNT(*) FROM auth.users
|
|
||||||
WHERE email LIKE '%@glit.edu.mx'
|
|
||||||
OR email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR email LIKE '%@gamilit.com';
|
|
||||||
" | tr -d ' ')
|
|
||||||
|
|
||||||
TOTAL_PROFILES=$($PSQL -t -c "
|
|
||||||
SELECT COUNT(*) FROM auth_management.profiles
|
|
||||||
WHERE email LIKE '%@glit.edu.mx'
|
|
||||||
OR email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR email LIKE '%@gamilit.com';
|
|
||||||
" | tr -d ' ')
|
|
||||||
|
|
||||||
LINKED=$($PSQL -t -c "
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM auth.users u
|
|
||||||
INNER JOIN auth_management.profiles p ON u.id = p.user_id
|
|
||||||
WHERE u.email LIKE '%@glit.edu.mx'
|
|
||||||
OR u.email LIKE '%@demo.glit.edu.mx'
|
|
||||||
OR u.email LIKE '%@gamilit.com';
|
|
||||||
" | tr -d ' ')
|
|
||||||
|
|
||||||
UNLINKED=$((TOTAL_USERS - LINKED))
|
|
||||||
|
|
||||||
echo " Total usuarios: $TOTAL_USERS"
|
|
||||||
echo " Total profiles: $TOTAL_PROFILES"
|
|
||||||
echo " Vinculados: $LINKED"
|
|
||||||
echo " Sin vincular: $UNLINKED"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ "$TOTAL_USERS" -eq "$TOTAL_PROFILES" ] && [ "$UNLINKED" -eq 0 ]; then
|
|
||||||
echo "✅ Estado: CORRECTO - Todos los usuarios tienen perfil"
|
|
||||||
else
|
|
||||||
echo "⚠️ Estado: REVISAR - Hay usuarios sin perfil o desvinculados"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
@ -114,34 +114,14 @@ case $ENV in
|
|||||||
log_info "Cargando seeds de DEVELOPMENT (todos los datos)..."
|
log_info "Cargando seeds de DEVELOPMENT (todos los datos)..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Base config
|
|
||||||
execute_seed "$SEED_DIR/01-achievement_categories.sql" || exit 1
|
execute_seed "$SEED_DIR/01-achievement_categories.sql" || exit 1
|
||||||
execute_seed "$SEED_DIR/02-achievements.sql" || exit 1
|
execute_seed "$SEED_DIR/02-achievements.sql" || exit 1
|
||||||
execute_seed "$SEED_DIR/03-leaderboard_metadata.sql" || exit 1
|
execute_seed "$SEED_DIR/03-leaderboard_metadata.sql" || exit 1
|
||||||
|
execute_seed "$SEED_DIR/04-initialize_user_gamification.sql" || exit 1
|
||||||
# Maya ranks (si existe)
|
|
||||||
if [ -f "$SEED_DIR/03-maya_ranks.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/03-maya_ranks.sql" || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
execute_seed "$SEED_DIR/04-achievements.sql" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Shop system
|
|
||||||
if [ -f "$SEED_DIR/12-shop_categories.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/12-shop_categories.sql" || exit 1
|
|
||||||
fi
|
|
||||||
if [ -f "$SEED_DIR/13-shop_items.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/13-shop_items.sql" || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# User gamification (si existen)
|
|
||||||
if [ -f "$SEED_DIR/05-user_stats.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/05-user_stats.sql" || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
log_success "Seeds de DEV cargados exitosamente"
|
log_success "Seeds de DEV cargados exitosamente"
|
||||||
log_info "Total de archivos: 8+ (todos disponibles)"
|
log_info "Total de archivos: 4"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
staging)
|
staging)
|
||||||
@ -158,43 +138,16 @@ case $ENV in
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
production)
|
production)
|
||||||
log_info "Cargando seeds de PRODUCTION (configuración completa)..."
|
log_info "Cargando seeds de PRODUCTION (solo configuración esencial)..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Base config
|
|
||||||
execute_seed "$SEED_DIR/01-achievement_categories.sql" || exit 1
|
execute_seed "$SEED_DIR/01-achievement_categories.sql" || exit 1
|
||||||
execute_seed "$SEED_DIR/02-leaderboard_metadata.sql" || exit 1
|
execute_seed "$SEED_DIR/02-leaderboard_metadata.sql" || exit 1
|
||||||
execute_seed "$SEED_DIR/03-maya_ranks.sql" || exit 1
|
|
||||||
execute_seed "$SEED_DIR/04-achievements.sql" || exit 1
|
|
||||||
|
|
||||||
# Mission templates (antes de missions de usuarios)
|
|
||||||
execute_seed "$SEED_DIR/10-mission_templates.sql" || exit 1
|
|
||||||
execute_seed "$SEED_DIR/11-missions-production-users.sql" || exit 1
|
|
||||||
|
|
||||||
# Shop system (categorías e items)
|
|
||||||
execute_seed "$SEED_DIR/12-shop_categories.sql" || exit 1
|
|
||||||
execute_seed "$SEED_DIR/13-shop_items.sql" || exit 1
|
|
||||||
|
|
||||||
# User gamification data (si existen usuarios)
|
|
||||||
if [ -f "$SEED_DIR/05-user_stats.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/05-user_stats.sql" || exit 1
|
|
||||||
fi
|
|
||||||
if [ -f "$SEED_DIR/06-user_ranks.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/06-user_ranks.sql" || exit 1
|
|
||||||
fi
|
|
||||||
if [ -f "$SEED_DIR/07-ml_coins_transactions.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/07-ml_coins_transactions.sql" || exit 1
|
|
||||||
fi
|
|
||||||
if [ -f "$SEED_DIR/08-user_achievements.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/08-user_achievements.sql" || exit 1
|
|
||||||
fi
|
|
||||||
if [ -f "$SEED_DIR/09-comodines_inventory.sql" ]; then
|
|
||||||
execute_seed "$SEED_DIR/09-comodines_inventory.sql" || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
log_success "Seeds de PRODUCTION cargados exitosamente"
|
log_success "Seeds de PRODUCTION cargados exitosamente"
|
||||||
log_info "Total de archivos: 13 (base + shop + user data)"
|
log_info "Total de archivos: 2"
|
||||||
|
log_warning "NOTA: No se cargaron achievements demo ni datos de prueba"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ SET search_path TO auth, public;
|
|||||||
-- PASSWORDS ENCRYPTED WITH BCRYPT
|
-- PASSWORDS ENCRYPTED WITH BCRYPT
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- Password: "Test1234" (todos los usuarios)
|
-- Password: "Test1234" (todos los usuarios)
|
||||||
-- Se genera dinámicamente con: crypt('Test1234', gen_salt('bf', 10))
|
-- Hash estático (bcrypt cost=10): $2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
@ -62,7 +62,7 @@ INSERT INTO auth.users (
|
|||||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid,
|
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid,
|
||||||
'00000000-0000-0000-0000-000000000000'::uuid,
|
'00000000-0000-0000-0000-000000000000'::uuid,
|
||||||
'admin@gamilit.com',
|
'admin@gamilit.com',
|
||||||
crypt('Test1234', gen_salt('bf', 10)),
|
'$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga',
|
||||||
gamilit.now_mexico(),
|
gamilit.now_mexico(),
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'provider', 'email',
|
'provider', 'email',
|
||||||
@ -90,7 +90,7 @@ INSERT INTO auth.users (
|
|||||||
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
|
||||||
'00000000-0000-0000-0000-000000000000'::uuid,
|
'00000000-0000-0000-0000-000000000000'::uuid,
|
||||||
'teacher@gamilit.com',
|
'teacher@gamilit.com',
|
||||||
crypt('Test1234', gen_salt('bf', 10)),
|
'$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga',
|
||||||
gamilit.now_mexico(),
|
gamilit.now_mexico(),
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'provider', 'email',
|
'provider', 'email',
|
||||||
@ -118,7 +118,7 @@ INSERT INTO auth.users (
|
|||||||
'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid,
|
'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid,
|
||||||
'00000000-0000-0000-0000-000000000000'::uuid,
|
'00000000-0000-0000-0000-000000000000'::uuid,
|
||||||
'student@gamilit.com',
|
'student@gamilit.com',
|
||||||
crypt('Test1234', gen_salt('bf', 10)),
|
'$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga',
|
||||||
gamilit.now_mexico(),
|
gamilit.now_mexico(),
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'provider', 'email',
|
'provider', 'email',
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
-- - Lote 3 (2025-12-08 y 2025-12-17): 2 usuarios
|
-- - Lote 3 (2025-12-08 y 2025-12-17): 2 usuarios
|
||||||
--
|
--
|
||||||
-- TOTAL: 44 usuarios estudiantes
|
-- TOTAL: 44 usuarios estudiantes
|
||||||
|
-- EXCLUIDO: rckrdmrd@gmail.com (usuario de pruebas del owner)
|
||||||
--
|
--
|
||||||
-- POLÍTICA DE CARGA LIMPIA:
|
-- POLÍTICA DE CARGA LIMPIA:
|
||||||
-- ✅ UUIDs originales del servidor preservados
|
-- ✅ UUIDs originales del servidor preservados
|
||||||
@ -832,6 +833,9 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
RAISE NOTICE '========================================';
|
RAISE NOTICE '========================================';
|
||||||
|
RAISE NOTICE 'NOTA: Usuario rckrdmrd@gmail.com EXCLUIDO';
|
||||||
|
RAISE NOTICE '(Usuario de pruebas del owner)';
|
||||||
|
RAISE NOTICE '========================================';
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
@ -851,7 +855,7 @@ END $$;
|
|||||||
-- CHANGELOG
|
-- CHANGELOG
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- v2.0 (2025-12-18): Actualización completa desde backup producción
|
-- v2.0 (2025-12-18): Actualización completa desde backup producción
|
||||||
-- - 44 usuarios totales
|
-- - 44 usuarios totales (excluyendo rckrdmrd@gmail.com)
|
||||||
-- - Lote 1: 13 usuarios (2025-11-18)
|
-- - Lote 1: 13 usuarios (2025-11-18)
|
||||||
-- - Lote 2: 23 usuarios (2025-11-24)
|
-- - Lote 2: 23 usuarios (2025-11-24)
|
||||||
-- - Lote 3: 6 usuarios (2025-11-25)
|
-- - Lote 3: 6 usuarios (2025-11-25)
|
||||||
|
|||||||
@ -1,223 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Seed Data: Test Users (DEV + STAGING)
|
|
||||||
-- =====================================================
|
|
||||||
-- Description: Usuarios de prueba con dominio @gamilit.com
|
|
||||||
-- Environment: DEVELOPMENT + STAGING (NO production)
|
|
||||||
-- Records: 3 usuarios (admin, teacher, student)
|
|
||||||
-- Date: 2025-11-04 (Updated)
|
|
||||||
-- Based on: ANALISIS-PRE-CORRECCIONES-BD-ORIGEN.md
|
|
||||||
-- Migration from: /home/isem/workspace/projects/glit/database
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
SET search_path TO auth, auth_management, public;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Passwords Reference (Plain Text - DO NOT COMMIT TO PROD)
|
|
||||||
-- =====================================================
|
|
||||||
-- ALL USERS: "Test1234"
|
|
||||||
-- Hash bcrypt (cost=10): $2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- STEP 1: Create users in auth.users
|
|
||||||
-- =====================================================
|
|
||||||
-- IMPORTANTE: UUIDs predecibles para consistencia con seeds PROD
|
|
||||||
-- Password: "Test1234" (bcrypt hasheado dinámicamente)
|
|
||||||
-- =====================================================
|
|
||||||
INSERT INTO auth.users (
|
|
||||||
id, -- ✅ UUID predecible explícito
|
|
||||||
email,
|
|
||||||
encrypted_password,
|
|
||||||
role,
|
|
||||||
email_confirmed_at,
|
|
||||||
raw_user_meta_data,
|
|
||||||
status,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES
|
|
||||||
-- Admin de Prueba
|
|
||||||
(
|
|
||||||
'dddddddd-dddd-dddd-dddd-dddddddddddd'::uuid, -- ✅ UUID predecible
|
|
||||||
'admin@gamilit.com',
|
|
||||||
'$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga', -- Password: Test1234
|
|
||||||
'super_admin',
|
|
||||||
NOW(),
|
|
||||||
'{"name": "Admin Gamilit", "description": "Usuario administrador de testing"}'::jsonb,
|
|
||||||
'active',
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
|
|
||||||
-- Maestro de Prueba
|
|
||||||
(
|
|
||||||
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'::uuid, -- ✅ UUID predecible
|
|
||||||
'teacher@gamilit.com',
|
|
||||||
'$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga', -- Password: Test1234
|
|
||||||
'admin_teacher',
|
|
||||||
NOW(),
|
|
||||||
'{"name": "Teacher Gamilit", "description": "Usuario maestro de testing"}'::jsonb,
|
|
||||||
'active',
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
),
|
|
||||||
|
|
||||||
-- Estudiante de Prueba
|
|
||||||
(
|
|
||||||
'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid, -- ✅ UUID predecible
|
|
||||||
'student@gamilit.com',
|
|
||||||
'$2b$10$pkqX0/v7H3F5TBTuDTaoYeBjH581pXpjlcNcYmMtXofd/2HjfTuga', -- Password: Test1234
|
|
||||||
'student',
|
|
||||||
NOW(),
|
|
||||||
'{"name": "Student Gamilit", "description": "Usuario estudiante de testing"}'::jsonb,
|
|
||||||
'active',
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
|
|
||||||
ON CONFLICT (email) DO UPDATE SET
|
|
||||||
encrypted_password = EXCLUDED.encrypted_password,
|
|
||||||
role = EXCLUDED.role,
|
|
||||||
email_confirmed_at = EXCLUDED.email_confirmed_at,
|
|
||||||
raw_user_meta_data = EXCLUDED.raw_user_meta_data,
|
|
||||||
status = EXCLUDED.status,
|
|
||||||
updated_at = NOW();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- STEP 2: Create profiles in auth_management.profiles
|
|
||||||
-- =====================================================
|
|
||||||
-- IMPORTANTE: profiles.id = auth.users.id (unificación de IDs)
|
|
||||||
-- El trigger initialize_user_stats() se ejecutará automáticamente
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO auth_management.profiles (
|
|
||||||
id, -- ✅ profiles.id = auth.users.id (consistente)
|
|
||||||
tenant_id,
|
|
||||||
user_id, -- ✅ FK a auth.users.id
|
|
||||||
email,
|
|
||||||
display_name,
|
|
||||||
full_name,
|
|
||||||
role,
|
|
||||||
status,
|
|
||||||
email_verified,
|
|
||||||
preferences,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
u.id as id, -- ✅ profiles.id = auth.users.id
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid as tenant_id,
|
|
||||||
u.id as user_id, -- ✅ user_id = auth.users.id
|
|
||||||
u.email,
|
|
||||||
CASE
|
|
||||||
WHEN u.email = 'admin@gamilit.com' THEN 'Admin Gamilit'
|
|
||||||
WHEN u.email = 'teacher@gamilit.com' THEN 'Teacher Gamilit'
|
|
||||||
WHEN u.email = 'student@gamilit.com' THEN 'Student Gamilit'
|
|
||||||
END as display_name,
|
|
||||||
CASE
|
|
||||||
WHEN u.email = 'admin@gamilit.com' THEN 'Administrator Gamilit'
|
|
||||||
WHEN u.email = 'teacher@gamilit.com' THEN 'Teacher Gamilit'
|
|
||||||
WHEN u.email = 'student@gamilit.com' THEN 'Student Gamilit'
|
|
||||||
END as full_name,
|
|
||||||
u.role::auth_management.gamilit_role,
|
|
||||||
'active'::auth_management.user_status as status,
|
|
||||||
true as email_verified,
|
|
||||||
jsonb_build_object(
|
|
||||||
'theme', 'detective',
|
|
||||||
'language', 'es',
|
|
||||||
'timezone', 'America/Mexico_City',
|
|
||||||
'sound_enabled', true,
|
|
||||||
'notifications_enabled', true
|
|
||||||
) as preferences,
|
|
||||||
NOW() as created_at,
|
|
||||||
NOW() as updated_at
|
|
||||||
FROM auth.users u
|
|
||||||
WHERE u.email IN ('admin@gamilit.com', 'teacher@gamilit.com', 'student@gamilit.com')
|
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
|
||||||
status = 'active'::auth_management.user_status,
|
|
||||||
email_verified = true,
|
|
||||||
display_name = EXCLUDED.display_name,
|
|
||||||
full_name = EXCLUDED.full_name,
|
|
||||||
role = EXCLUDED.role::auth_management.gamilit_role,
|
|
||||||
preferences = EXCLUDED.preferences,
|
|
||||||
updated_at = NOW();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Verification
|
|
||||||
-- =====================================================
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
test_users_count INT;
|
|
||||||
test_profiles_count INT;
|
|
||||||
active_profiles_count INT;
|
|
||||||
BEGIN
|
|
||||||
-- Count users
|
|
||||||
SELECT COUNT(*) INTO test_users_count
|
|
||||||
FROM auth.users
|
|
||||||
WHERE email LIKE '%@gamilit.com';
|
|
||||||
|
|
||||||
-- Count profiles
|
|
||||||
SELECT COUNT(*) INTO test_profiles_count
|
|
||||||
FROM auth_management.profiles
|
|
||||||
WHERE email LIKE '%@gamilit.com';
|
|
||||||
|
|
||||||
-- Count active profiles
|
|
||||||
SELECT COUNT(*) INTO active_profiles_count
|
|
||||||
FROM auth_management.profiles
|
|
||||||
WHERE email LIKE '%@gamilit.com' AND status = 'active';
|
|
||||||
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE ' Test Users & Profiles Created';
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE 'Test users count: %', test_users_count;
|
|
||||||
RAISE NOTICE 'Test profiles count: %', test_profiles_count;
|
|
||||||
RAISE NOTICE 'Active profiles: %', active_profiles_count;
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE 'Credentials:';
|
|
||||||
RAISE NOTICE ' admin@gamilit.com | Test1234 | super_admin';
|
|
||||||
RAISE NOTICE ' teacher@gamilit.com | Test1234 | admin_teacher';
|
|
||||||
RAISE NOTICE ' student@gamilit.com | Test1234 | student';
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE 'All users:';
|
|
||||||
RAISE NOTICE ' ✓ Email confirmed (email_confirmed_at = NOW())';
|
|
||||||
RAISE NOTICE ' ✓ Profile active (status = ''active'')';
|
|
||||||
RAISE NOTICE ' ✓ Email verified (email_verified = true)';
|
|
||||||
RAISE NOTICE ' ✓ Ready for immediate login';
|
|
||||||
RAISE NOTICE '';
|
|
||||||
RAISE NOTICE 'Tenant: Gamilit Test Organization';
|
|
||||||
RAISE NOTICE ' ID: 00000000-0000-0000-0000-000000000001';
|
|
||||||
RAISE NOTICE '========================================';
|
|
||||||
RAISE NOTICE '';
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- MIGRATION NOTES
|
|
||||||
-- =====================================================
|
|
||||||
-- Source: /home/isem/workspace/projects/glit/database/seed_data/04_demo_users_and_data_seed.sql
|
|
||||||
-- Changes from source:
|
|
||||||
-- 1. Domain changed: @glit.com → @gamilit.com (per user requirement)
|
|
||||||
-- 2. Password changed: Glit2024! → Test1234 (per user requirement)
|
|
||||||
-- 3. User count reduced: 10 → 3 (admin, teacher, student only)
|
|
||||||
-- 4. Email format simplified: student1@... → student@...
|
|
||||||
-- 5. All users have email_confirmed_at = NOW() for immediate testing
|
|
||||||
-- 6. Added profiles creation in auth_management.profiles (2025-11-04)
|
|
||||||
-- 7. Set status = 'active' to enable login (2025-11-04)
|
|
||||||
-- 8. Set email_verified = true (2025-11-04)
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- IMPORTANT NOTES
|
|
||||||
-- =====================================================
|
|
||||||
-- 1. ✅ El trigger trg_initialize_user_stats funciona correctamente
|
|
||||||
-- porque usamos profiles.id = auth.users.id (unificación de IDs)
|
|
||||||
-- NO es necesario deshabilitar el trigger.
|
|
||||||
--
|
|
||||||
-- 2. ✅ Este seed es para DEV/STAGING únicamente (NO producción).
|
|
||||||
--
|
|
||||||
-- 3. ✅ Todos los usuarios comparten password "Test1234" (testing).
|
|
||||||
--
|
|
||||||
-- 4. ✅ UUIDs predecibles para consistencia con ambiente PROD:
|
|
||||||
-- - admin@gamilit.com: dddddddd-dddd-dddd-dddd-dddddddddddd
|
|
||||||
-- - teacher@gamilit.com: eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee
|
|
||||||
-- - student@gamilit.com: ffffffff-ffff-ffff-ffff-ffffffffffff
|
|
||||||
-- =====================================================
|
|
||||||
@ -1,17 +1,30 @@
|
|||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- Seed: auth_management.tenants (DEV)
|
-- Seed: auth_management.tenants (PROD)
|
||||||
-- Description: Tenants de desarrollo para testing y demos
|
-- Description: Tenant principal de producción
|
||||||
-- Environment: DEVELOPMENT
|
-- Environment: PRODUCTION
|
||||||
-- Dependencies: None
|
-- Dependencies: None
|
||||||
-- Order: 01
|
-- Order: 01
|
||||||
-- Validated: 2025-11-02
|
-- Created: 2025-11-11
|
||||||
-- Score: 100/100
|
-- Version: 2.0 (reescrito para carga limpia)
|
||||||
|
-- =====================================================
|
||||||
|
--
|
||||||
|
-- CAMBIOS v2.0:
|
||||||
|
-- - Convertido de STRING a UUID
|
||||||
|
-- - Agregada columna 'slug' (requerida NOT NULL)
|
||||||
|
-- - Agregadas 7 columnas faltantes del schema
|
||||||
|
-- - Cambiado NOW() → gamilit.now_mexico()
|
||||||
|
-- - Estructura alineada 100% con DDL
|
||||||
|
--
|
||||||
|
-- VALIDADO CONTRA:
|
||||||
|
-- - DDL: ddl/schemas/auth_management/tables/01-tenants.sql
|
||||||
|
-- - Template: seeds/dev/auth_management/01-tenants.sql
|
||||||
|
--
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
SET search_path TO auth_management, public;
|
SET search_path TO auth_management, public;
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- INSERT: Default Test Tenant
|
-- INSERT: Tenant Principal de Producción
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
INSERT INTO auth_management.tenants (
|
INSERT INTO auth_management.tenants (
|
||||||
@ -29,100 +42,58 @@ INSERT INTO auth_management.tenants (
|
|||||||
metadata,
|
metadata,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES
|
) VALUES (
|
||||||
-- Tenant 1: Gamilit Test Organization
|
-- UUID real en lugar de STRING
|
||||||
(
|
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid,
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
'GAMILIT Platform',
|
||||||
'Gamilit Test Organization',
|
'gamilit-prod', -- NUEVO: slug requerido NOT NULL
|
||||||
'gamilit-test',
|
'gamilit.com',
|
||||||
'test.gamilit.com',
|
'/assets/logo-gamilit.png', -- NUEVO: logo_url
|
||||||
NULL,
|
'enterprise', -- NUEVO: subscription_tier
|
||||||
'enterprise',
|
10000, -- NUEVO: max_users
|
||||||
1000,
|
100, -- NUEVO: max_storage_gb
|
||||||
100,
|
true, -- NUEVO: is_active
|
||||||
true,
|
NULL, -- NUEVO: trial_ends_at (sin trial en producción)
|
||||||
NULL,
|
jsonb_build_object(
|
||||||
'{
|
'theme', 'detective',
|
||||||
"theme": "detective",
|
'language', 'es',
|
||||||
"language": "es",
|
'timezone', 'America/Mexico_City',
|
||||||
"timezone": "America/Mexico_City",
|
'features', jsonb_build_object(
|
||||||
"features": {
|
'analytics_enabled', true,
|
||||||
"analytics_enabled": true,
|
'gamification_enabled', true,
|
||||||
"gamification_enabled": true,
|
'social_features_enabled', true,
|
||||||
"social_features_enabled": true
|
'assessments', true,
|
||||||
}
|
'progress_tracking', true
|
||||||
}'::jsonb,
|
),
|
||||||
'{
|
'limits', jsonb_build_object(
|
||||||
"description": "Default tenant for test users",
|
'daily_api_calls', 100000,
|
||||||
"environment": "development",
|
'storage_gb', 100,
|
||||||
"created_by": "seed_script"
|
'max_file_size_mb', 50
|
||||||
}'::jsonb,
|
),
|
||||||
gamilit.now_mexico(),
|
'contact', jsonb_build_object(
|
||||||
gamilit.now_mexico()
|
'support_email', 'soporte@gamilit.com',
|
||||||
),
|
'admin_email', 'admin@gamilit.com'
|
||||||
-- Tenant 2: Demo School
|
),
|
||||||
(
|
'branding', jsonb_build_object(
|
||||||
'00000000-0000-0000-0000-000000000002'::uuid,
|
'logo_url', '/assets/logo-gamilit.png',
|
||||||
'Demo School - Escuela Primaria',
|
'primary_color', '#4F46E5',
|
||||||
'demo-school-primary',
|
'secondary_color', '#10B981'
|
||||||
'demo-primary.gamilit.com',
|
)
|
||||||
NULL,
|
),
|
||||||
'professional',
|
jsonb_build_object( -- NUEVO: metadata
|
||||||
500,
|
'description', 'Tenant principal de producción',
|
||||||
50,
|
'environment', 'production',
|
||||||
true,
|
'created_by', 'seed_script_v2',
|
||||||
(gamilit.now_mexico() + INTERVAL '90 days'),
|
'version', '2.0'
|
||||||
'{
|
),
|
||||||
"theme": "detective",
|
gamilit.now_mexico(), -- CORREGIDO: gamilit.now_mexico() en lugar de NOW()
|
||||||
"language": "es",
|
gamilit.now_mexico() -- CORREGIDO: gamilit.now_mexico() en lugar de NOW()
|
||||||
"timezone": "America/Mexico_City",
|
|
||||||
"features": {
|
|
||||||
"analytics_enabled": true,
|
|
||||||
"gamification_enabled": true,
|
|
||||||
"social_features_enabled": true
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
'{
|
|
||||||
"description": "Demo tenant for primary school",
|
|
||||||
"environment": "development",
|
|
||||||
"school_level": "primary"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Tenant 3: Demo School Secondary
|
|
||||||
(
|
|
||||||
'00000000-0000-0000-0000-000000000003'::uuid,
|
|
||||||
'Demo School - Escuela Secundaria',
|
|
||||||
'demo-school-secondary',
|
|
||||||
'demo-secondary.gamilit.com',
|
|
||||||
NULL,
|
|
||||||
'basic',
|
|
||||||
200,
|
|
||||||
20,
|
|
||||||
true,
|
|
||||||
(gamilit.now_mexico() + INTERVAL '30 days'),
|
|
||||||
'{
|
|
||||||
"theme": "detective",
|
|
||||||
"language": "es",
|
|
||||||
"timezone": "America/Mexico_City",
|
|
||||||
"features": {
|
|
||||||
"analytics_enabled": true,
|
|
||||||
"gamification_enabled": false,
|
|
||||||
"social_features_enabled": true
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
'{
|
|
||||||
"description": "Demo tenant for secondary school",
|
|
||||||
"environment": "development",
|
|
||||||
"school_level": "secondary"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
)
|
)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
name = EXCLUDED.name,
|
name = EXCLUDED.name,
|
||||||
|
slug = EXCLUDED.slug,
|
||||||
domain = EXCLUDED.domain,
|
domain = EXCLUDED.domain,
|
||||||
|
logo_url = EXCLUDED.logo_url,
|
||||||
subscription_tier = EXCLUDED.subscription_tier,
|
subscription_tier = EXCLUDED.subscription_tier,
|
||||||
max_users = EXCLUDED.max_users,
|
max_users = EXCLUDED.max_users,
|
||||||
max_storage_gb = EXCLUDED.max_storage_gb,
|
max_storage_gb = EXCLUDED.max_storage_gb,
|
||||||
@ -139,7 +110,52 @@ ON CONFLICT (id) DO UPDATE SET
|
|||||||
DO $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
tenant_count INTEGER;
|
tenant_count INTEGER;
|
||||||
|
tenant_name TEXT;
|
||||||
|
tenant_slug TEXT;
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT COUNT(*) INTO tenant_count FROM auth_management.tenants;
|
SELECT COUNT(*), MAX(name), MAX(slug)
|
||||||
RAISE NOTICE '✓ Tenants insertados correctamente: % registros', tenant_count;
|
INTO tenant_count, tenant_name, tenant_slug
|
||||||
|
FROM auth_management.tenants
|
||||||
|
WHERE id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid;
|
||||||
|
|
||||||
|
IF tenant_count = 1 THEN
|
||||||
|
RAISE NOTICE '✓ Tenant de producción creado correctamente';
|
||||||
|
RAISE NOTICE ' ID: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11';
|
||||||
|
RAISE NOTICE ' Name: %', tenant_name;
|
||||||
|
RAISE NOTICE ' Slug: %', tenant_slug;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING '⚠ Tenant de producción NO fue creado';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Validación de Estructura
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Verificar que todas las columnas existan
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
missing_columns TEXT[];
|
||||||
|
BEGIN
|
||||||
|
SELECT ARRAY_AGG(column_name) INTO missing_columns
|
||||||
|
FROM (
|
||||||
|
SELECT unnest(ARRAY[
|
||||||
|
'id', 'name', 'slug', 'domain', 'logo_url',
|
||||||
|
'subscription_tier', 'max_users', 'max_storage_gb',
|
||||||
|
'is_active', 'trial_ends_at', 'settings', 'metadata',
|
||||||
|
'created_at', 'updated_at'
|
||||||
|
]) AS column_name
|
||||||
|
) expected
|
||||||
|
WHERE column_name NOT IN (
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'auth_management'
|
||||||
|
AND table_name = 'tenants'
|
||||||
|
);
|
||||||
|
|
||||||
|
IF missing_columns IS NOT NULL THEN
|
||||||
|
RAISE WARNING '⚠ Columnas faltantes en tabla tenants: %', missing_columns;
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '✓ Todas las columnas del seed están presentes en la tabla';
|
||||||
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
|
|||||||
@ -1,17 +1,34 @@
|
|||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- Seed: auth_management.auth_providers (DEV)
|
-- Seed: auth_management.auth_providers (PROD)
|
||||||
-- Description: Configuración de proveedores de autenticación
|
-- Description: Configuración de proveedores de autenticación para producción
|
||||||
-- Environment: DEVELOPMENT
|
-- Environment: PRODUCTION
|
||||||
-- Dependencies: None
|
-- Dependencies: None
|
||||||
-- Order: 02
|
-- Order: 02
|
||||||
-- Validated: 2025-11-02
|
-- Created: 2025-11-11
|
||||||
-- Score: 100/100
|
-- Version: 2.0 (reescrito para carga limpia)
|
||||||
|
-- =====================================================
|
||||||
|
--
|
||||||
|
-- CAMBIOS v2.0:
|
||||||
|
-- - Convertido de STRING a ENUM auth_provider
|
||||||
|
-- - Estructura alineada 100% con DDL
|
||||||
|
-- - Cambiado NOW() → gamilit.now_mexico()
|
||||||
|
-- - Configuración de producción (credentials pendientes)
|
||||||
|
--
|
||||||
|
-- VALIDADO CONTRA:
|
||||||
|
-- - DDL: ddl/schemas/auth_management/tables/05-auth_providers.sql
|
||||||
|
-- - Template: seeds/dev/auth_management/02-auth_providers.sql
|
||||||
|
--
|
||||||
|
-- IMPORTANTE:
|
||||||
|
-- - Los client_id y client_secret deben ser configurados con valores reales
|
||||||
|
-- - Los valores actuales son PLACEHOLDERS que deben ser reemplazados
|
||||||
|
-- - En producción, considerar usar variables de entorno o secretos encriptados
|
||||||
|
--
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
SET search_path TO auth_management, public;
|
SET search_path TO auth_management, public;
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- INSERT: Auth Providers Configuration
|
-- INSERT: Auth Providers Configuration (PRODUCTION)
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
INSERT INTO auth_management.auth_providers (
|
INSERT INTO auth_management.auth_providers (
|
||||||
@ -31,9 +48,9 @@ INSERT INTO auth_management.auth_providers (
|
|||||||
config,
|
config,
|
||||||
metadata
|
metadata
|
||||||
) VALUES
|
) VALUES
|
||||||
-- Local Auth (email/password)
|
-- Local Auth (email/password) - ENABLED
|
||||||
(
|
(
|
||||||
'local',
|
'local'::auth_management.auth_provider,
|
||||||
'Email y Contraseña',
|
'Email y Contraseña',
|
||||||
true,
|
true,
|
||||||
NULL,
|
NULL,
|
||||||
@ -46,47 +63,53 @@ INSERT INTO auth_management.auth_providers (
|
|||||||
NULL,
|
NULL,
|
||||||
'#4F46E5',
|
'#4F46E5',
|
||||||
1,
|
1,
|
||||||
'{
|
jsonb_build_object(
|
||||||
"requires_email_verification": false,
|
'requires_email_verification', true, -- PROD: email verification required
|
||||||
"password_min_length": 8,
|
'password_min_length', 12, -- PROD: stronger password (12 vs 8)
|
||||||
"password_requires_uppercase": true,
|
'password_requires_uppercase', true,
|
||||||
"password_requires_number": true,
|
'password_requires_number', true,
|
||||||
"password_requires_special": true
|
'password_requires_special', true,
|
||||||
}'::jsonb,
|
'password_max_age_days', 90, -- PROD: password expiration
|
||||||
'{
|
'failed_login_attempts_max', 5, -- PROD: rate limiting
|
||||||
"description": "Local authentication using email and password",
|
'account_lockout_duration_minutes', 30
|
||||||
"environment": "development"
|
),
|
||||||
}'::jsonb
|
jsonb_build_object(
|
||||||
|
'description', 'Local authentication using email and password',
|
||||||
|
'environment', 'production',
|
||||||
|
'security_level', 'high'
|
||||||
|
)
|
||||||
),
|
),
|
||||||
-- Google OAuth (ENABLED for dev)
|
-- Google OAuth - ENABLED
|
||||||
(
|
(
|
||||||
'google',
|
'google'::auth_management.auth_provider,
|
||||||
'Continuar con Google',
|
'Continuar con Google',
|
||||||
true,
|
true,
|
||||||
'dev-google-client-id.apps.googleusercontent.com',
|
'GOOGLE_CLIENT_ID_PLACEHOLDER', -- ⚠️ REEMPLAZAR con valor real
|
||||||
'dev-google-client-secret',
|
'GOOGLE_CLIENT_SECRET_PLACEHOLDER', -- ⚠️ REEMPLAZAR con valor real
|
||||||
'https://accounts.google.com/o/oauth2/v2/auth',
|
'https://accounts.google.com/o/oauth2/v2/auth',
|
||||||
'https://oauth2.googleapis.com/token',
|
'https://oauth2.googleapis.com/token',
|
||||||
'https://www.googleapis.com/oauth2/v2/userinfo',
|
'https://www.googleapis.com/oauth2/v2/userinfo',
|
||||||
ARRAY['openid', 'profile', 'email'],
|
ARRAY['openid', 'profile', 'email'],
|
||||||
'http://localhost:3000/auth/callback/google',
|
'https://gamilit.com/auth/callback/google',
|
||||||
'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg',
|
'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg',
|
||||||
'#4285F4',
|
'#4285F4',
|
||||||
10,
|
10,
|
||||||
'{
|
jsonb_build_object(
|
||||||
"prompt": "select_account",
|
'prompt', 'select_account',
|
||||||
"access_type": "offline"
|
'access_type', 'offline',
|
||||||
}'::jsonb,
|
'include_granted_scopes', true
|
||||||
'{
|
),
|
||||||
"description": "Google OAuth authentication for development",
|
jsonb_build_object(
|
||||||
"environment": "development"
|
'description', 'Google OAuth authentication for production',
|
||||||
}'::jsonb
|
'environment', 'production',
|
||||||
|
'status', 'credentials_pending'
|
||||||
|
)
|
||||||
),
|
),
|
||||||
-- Facebook OAuth (DISABLED for dev)
|
-- Facebook OAuth - DISABLED (pending configuration)
|
||||||
(
|
(
|
||||||
'facebook',
|
'facebook'::auth_management.auth_provider,
|
||||||
'Continuar con Facebook',
|
'Continuar con Facebook',
|
||||||
false,
|
false, -- DISABLED hasta configurar credentials
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
'https://www.facebook.com/v12.0/dialog/oauth',
|
'https://www.facebook.com/v12.0/dialog/oauth',
|
||||||
@ -97,19 +120,20 @@ INSERT INTO auth_management.auth_providers (
|
|||||||
'https://www.facebook.com/images/fb_icon_325x325.png',
|
'https://www.facebook.com/images/fb_icon_325x325.png',
|
||||||
'#1877F2',
|
'#1877F2',
|
||||||
20,
|
20,
|
||||||
'{
|
jsonb_build_object(
|
||||||
"fields": "id,name,email,picture"
|
'fields', 'id,name,email,picture'
|
||||||
}'::jsonb,
|
),
|
||||||
'{
|
jsonb_build_object(
|
||||||
"description": "Facebook OAuth authentication (disabled in development)",
|
'description', 'Facebook OAuth authentication (disabled - pending configuration)',
|
||||||
"environment": "development"
|
'environment', 'production',
|
||||||
}'::jsonb
|
'status', 'pending_configuration'
|
||||||
|
)
|
||||||
),
|
),
|
||||||
-- Apple Sign In (DISABLED for dev)
|
-- Apple Sign In - DISABLED (pending configuration)
|
||||||
(
|
(
|
||||||
'apple',
|
'apple'::auth_management.auth_provider,
|
||||||
'Continuar con Apple',
|
'Continuar con Apple',
|
||||||
false,
|
false, -- DISABLED hasta configurar credentials
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
'https://appleid.apple.com/auth/authorize',
|
'https://appleid.apple.com/auth/authorize',
|
||||||
@ -120,20 +144,21 @@ INSERT INTO auth_management.auth_providers (
|
|||||||
'https://appleid.cdn-apple.com/appleid/button',
|
'https://appleid.cdn-apple.com/appleid/button',
|
||||||
'#000000',
|
'#000000',
|
||||||
15,
|
15,
|
||||||
'{
|
jsonb_build_object(
|
||||||
"response_mode": "form_post",
|
'response_mode', 'form_post',
|
||||||
"response_type": "code id_token"
|
'response_type', 'code id_token'
|
||||||
}'::jsonb,
|
),
|
||||||
'{
|
jsonb_build_object(
|
||||||
"description": "Apple Sign In (disabled in development)",
|
'description', 'Apple Sign In (disabled - pending configuration)',
|
||||||
"environment": "development"
|
'environment', 'production',
|
||||||
}'::jsonb
|
'status', 'pending_configuration'
|
||||||
|
)
|
||||||
),
|
),
|
||||||
-- Microsoft OAuth (DISABLED for dev)
|
-- Microsoft OAuth - DISABLED (pending configuration)
|
||||||
(
|
(
|
||||||
'microsoft',
|
'microsoft'::auth_management.auth_provider,
|
||||||
'Continuar con Microsoft',
|
'Continuar con Microsoft',
|
||||||
false,
|
false, -- DISABLED hasta configurar credentials
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||||
@ -144,36 +169,38 @@ INSERT INTO auth_management.auth_providers (
|
|||||||
'https://docs.microsoft.com/en-us/azure/active-directory/develop/media/howto-add-branding-in-azure-ad-apps/ms-symbollockup_mssymbol_19.png',
|
'https://docs.microsoft.com/en-us/azure/active-directory/develop/media/howto-add-branding-in-azure-ad-apps/ms-symbollockup_mssymbol_19.png',
|
||||||
'#00A4EF',
|
'#00A4EF',
|
||||||
30,
|
30,
|
||||||
'{
|
jsonb_build_object(
|
||||||
"tenant": "common"
|
'tenant', 'common'
|
||||||
}'::jsonb,
|
),
|
||||||
'{
|
jsonb_build_object(
|
||||||
"description": "Microsoft OAuth authentication (disabled in development)",
|
'description', 'Microsoft OAuth authentication (disabled - pending configuration)',
|
||||||
"environment": "development"
|
'environment', 'production',
|
||||||
}'::jsonb
|
'status', 'pending_configuration'
|
||||||
|
)
|
||||||
),
|
),
|
||||||
-- GitHub OAuth (ENABLED for dev)
|
-- GitHub OAuth - DISABLED (not needed in production)
|
||||||
(
|
(
|
||||||
'github',
|
'github'::auth_management.auth_provider,
|
||||||
'Continuar con GitHub',
|
'Continuar con GitHub',
|
||||||
true,
|
false, -- DISABLED in production (developer-focused)
|
||||||
'dev-github-client-id',
|
NULL,
|
||||||
'dev-github-client-secret',
|
NULL,
|
||||||
'https://github.com/login/oauth/authorize',
|
'https://github.com/login/oauth/authorize',
|
||||||
'https://github.com/login/oauth/access_token',
|
'https://github.com/login/oauth/access_token',
|
||||||
'https://api.github.com/user',
|
'https://api.github.com/user',
|
||||||
ARRAY['user:email', 'read:user'],
|
ARRAY['user:email', 'read:user'],
|
||||||
'http://localhost:3000/auth/callback/github',
|
NULL,
|
||||||
'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png',
|
'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png',
|
||||||
'#24292e',
|
'#24292e',
|
||||||
40,
|
40,
|
||||||
'{
|
jsonb_build_object(
|
||||||
"allow_signup": "true"
|
'allow_signup', 'true'
|
||||||
}'::jsonb,
|
),
|
||||||
'{
|
jsonb_build_object(
|
||||||
"description": "GitHub OAuth authentication for development",
|
'description', 'GitHub OAuth authentication (disabled in production - developer use only)',
|
||||||
"environment": "development"
|
'environment', 'production',
|
||||||
}'::jsonb
|
'status', 'not_needed'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
ON CONFLICT (provider_name) DO UPDATE SET
|
ON CONFLICT (provider_name) DO UPDATE SET
|
||||||
display_name = EXCLUDED.display_name,
|
display_name = EXCLUDED.display_name,
|
||||||
@ -200,8 +227,53 @@ DO $$
|
|||||||
DECLARE
|
DECLARE
|
||||||
provider_count INTEGER;
|
provider_count INTEGER;
|
||||||
enabled_count INTEGER;
|
enabled_count INTEGER;
|
||||||
|
pending_credentials_count INTEGER;
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT COUNT(*) INTO provider_count FROM auth_management.auth_providers;
|
SELECT COUNT(*) INTO provider_count FROM auth_management.auth_providers;
|
||||||
SELECT COUNT(*) INTO enabled_count FROM auth_management.auth_providers WHERE is_enabled = true;
|
SELECT COUNT(*) INTO enabled_count FROM auth_management.auth_providers WHERE is_enabled = true;
|
||||||
RAISE NOTICE '✓ Auth providers insertados: % total (% habilitados)', provider_count, enabled_count;
|
SELECT COUNT(*) INTO pending_credentials_count
|
||||||
|
FROM auth_management.auth_providers
|
||||||
|
WHERE metadata->>'status' = 'credentials_pending';
|
||||||
|
|
||||||
|
RAISE NOTICE '✓ Auth providers insertados: % total', provider_count;
|
||||||
|
RAISE NOTICE ' - Habilitados: %', enabled_count;
|
||||||
|
RAISE NOTICE ' - Pendientes de credenciales: %', pending_credentials_count;
|
||||||
|
|
||||||
|
IF pending_credentials_count > 0 THEN
|
||||||
|
RAISE WARNING '⚠ IMPORTANTE: % proveedores tienen credenciales PLACEHOLDER que deben ser configuradas', pending_credentials_count;
|
||||||
|
RAISE WARNING ' Actualizar client_id y client_secret para Google OAuth antes de habilitar en producción';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Validación de Estructura
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Verificar que todas las columnas existan
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
missing_columns TEXT[];
|
||||||
|
BEGIN
|
||||||
|
SELECT ARRAY_AGG(column_name) INTO missing_columns
|
||||||
|
FROM (
|
||||||
|
SELECT unnest(ARRAY[
|
||||||
|
'id', 'provider_name', 'display_name', 'is_enabled',
|
||||||
|
'client_id', 'client_secret', 'authorization_url', 'token_url',
|
||||||
|
'user_info_url', 'scope', 'redirect_uri', 'icon_url',
|
||||||
|
'button_color', 'priority', 'config', 'metadata',
|
||||||
|
'created_at', 'updated_at'
|
||||||
|
]) AS column_name
|
||||||
|
) expected
|
||||||
|
WHERE column_name NOT IN (
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'auth_management'
|
||||||
|
AND table_name = 'auth_providers'
|
||||||
|
);
|
||||||
|
|
||||||
|
IF missing_columns IS NOT NULL THEN
|
||||||
|
RAISE WARNING '⚠ Columnas faltantes en tabla auth_providers: %', missing_columns;
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '✓ Todas las columnas del seed están presentes en la tabla';
|
||||||
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
|
|||||||
@ -1,119 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Seed: auth_management.profiles (DEV)
|
|
||||||
-- Description: Perfiles de usuarios de prueba para desarrollo
|
|
||||||
-- Environment: DEVELOPMENT
|
|
||||||
-- Dependencies: auth.users (01-demo-users.sql), auth_management.tenants (01-tenants.sql)
|
|
||||||
-- Order: 03
|
|
||||||
-- Updated: 2025-11-02
|
|
||||||
-- Agent: ATLAS-DATABASE
|
|
||||||
-- Note: Este seed SOLO crea profiles. Los usuarios deben existir previamente.
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
SET search_path TO auth_management, auth, public;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Crear profiles para usuarios existentes
|
|
||||||
-- =====================================================
|
|
||||||
-- Este seed lee los usuarios de auth.users y crea sus profiles
|
|
||||||
-- Si el usuario ya tiene profile, se actualiza
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO auth_management.profiles (
|
|
||||||
user_id,
|
|
||||||
tenant_id,
|
|
||||||
email,
|
|
||||||
first_name,
|
|
||||||
last_name,
|
|
||||||
display_name,
|
|
||||||
full_name,
|
|
||||||
role
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
u.id as user_id,
|
|
||||||
(SELECT id FROM auth_management.tenants
|
|
||||||
WHERE name LIKE '%Test%' OR name LIKE '%Gamilit%'
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
LIMIT 1) as tenant_id,
|
|
||||||
u.email,
|
|
||||||
-- Extraer first_name del email o raw_user_meta_data
|
|
||||||
CASE
|
|
||||||
WHEN u.email LIKE '%admin%' THEN 'Admin'
|
|
||||||
WHEN u.email LIKE '%instructor%' OR u.email LIKE '%teacher%' THEN 'Instructor'
|
|
||||||
WHEN u.email LIKE '%estudiante1%' OR u.email LIKE '%student1%' THEN 'Ana'
|
|
||||||
WHEN u.email LIKE '%estudiante2%' OR u.email LIKE '%student2%' THEN 'María'
|
|
||||||
WHEN u.email LIKE '%estudiante3%' OR u.email LIKE '%student3%' THEN 'Carlos'
|
|
||||||
ELSE COALESCE(
|
|
||||||
u.raw_user_meta_data->>'firstName',
|
|
||||||
SPLIT_PART(u.email, '@', 1)
|
|
||||||
)
|
|
||||||
END as first_name,
|
|
||||||
-- Extraer last_name
|
|
||||||
CASE
|
|
||||||
WHEN u.email LIKE '%admin%' THEN 'Sistema'
|
|
||||||
WHEN u.email LIKE '%instructor%' OR u.email LIKE '%teacher%' THEN 'Demo'
|
|
||||||
WHEN u.email LIKE '%estudiante1%' OR u.email LIKE '%student1%' THEN 'García'
|
|
||||||
WHEN u.email LIKE '%estudiante2%' OR u.email LIKE '%student2%' THEN 'Curie'
|
|
||||||
WHEN u.email LIKE '%estudiante3%' OR u.email LIKE '%student3%' THEN 'Einstein'
|
|
||||||
ELSE COALESCE(
|
|
||||||
u.raw_user_meta_data->>'lastName',
|
|
||||||
'Demo'
|
|
||||||
)
|
|
||||||
END as last_name,
|
|
||||||
-- Display name (identificador corto para UI)
|
|
||||||
COALESCE(
|
|
||||||
u.raw_user_meta_data->>'displayName',
|
|
||||||
SPLIT_PART(u.email, '@', 1)
|
|
||||||
) as display_name,
|
|
||||||
-- Full name (nombre completo)
|
|
||||||
CASE
|
|
||||||
WHEN u.email LIKE '%admin%' THEN 'Admin Sistema'
|
|
||||||
WHEN u.email LIKE '%instructor%' OR u.email LIKE '%teacher%' THEN 'Instructor Demo'
|
|
||||||
WHEN u.email LIKE '%estudiante1%' OR u.email LIKE '%student1%' THEN 'Ana García'
|
|
||||||
WHEN u.email LIKE '%estudiante2%' OR u.email LIKE '%student2%' THEN 'María Curie'
|
|
||||||
WHEN u.email LIKE '%estudiante3%' OR u.email LIKE '%student3%' THEN 'Carlos Einstein'
|
|
||||||
ELSE COALESCE(
|
|
||||||
u.raw_user_meta_data->>'fullName',
|
|
||||||
CONCAT(
|
|
||||||
COALESCE(u.raw_user_meta_data->>'firstName', SPLIT_PART(u.email, '@', 1)),
|
|
||||||
' ',
|
|
||||||
COALESCE(u.raw_user_meta_data->>'lastName', 'Demo')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
END as full_name,
|
|
||||||
-- Rol (copiado de auth.users)
|
|
||||||
u.role
|
|
||||||
FROM auth.users u
|
|
||||||
WHERE u.deleted_at IS NULL
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
email = EXCLUDED.email,
|
|
||||||
first_name = EXCLUDED.first_name,
|
|
||||||
last_name = EXCLUDED.last_name,
|
|
||||||
display_name = EXCLUDED.display_name,
|
|
||||||
full_name = EXCLUDED.full_name,
|
|
||||||
role = EXCLUDED.role,
|
|
||||||
updated_at = NOW();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Validación y Mensaje de Confirmación
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
profile_count INTEGER;
|
|
||||||
student_count INTEGER;
|
|
||||||
teacher_count INTEGER;
|
|
||||||
admin_count INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO profile_count FROM auth_management.profiles;
|
|
||||||
SELECT COUNT(*) INTO student_count FROM auth_management.profiles WHERE role = 'student';
|
|
||||||
SELECT COUNT(*) INTO teacher_count FROM auth_management.profiles WHERE role IN ('admin_teacher', 'teacher');
|
|
||||||
SELECT COUNT(*) INTO admin_count FROM auth_management.profiles WHERE role = 'super_admin';
|
|
||||||
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
RAISE NOTICE '✓ Profiles insertados correctamente';
|
|
||||||
RAISE NOTICE ' Total: % perfiles', profile_count;
|
|
||||||
RAISE NOTICE ' Estudiantes: %', student_count;
|
|
||||||
RAISE NOTICE ' Profesores: %', teacher_count;
|
|
||||||
RAISE NOTICE ' Admins: %', admin_count;
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
END $$;
|
|
||||||
@ -1,214 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Seed: auth_management.user_roles (DEV)
|
|
||||||
-- Description: Asignación de roles a usuarios de prueba
|
|
||||||
-- Environment: DEVELOPMENT
|
|
||||||
-- Dependencies: auth_management.profiles, auth_management.tenants
|
|
||||||
-- Order: 04
|
|
||||||
-- Validated: 2025-11-02
|
|
||||||
-- Score: 100/100
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
SET search_path TO auth_management, public;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- INSERT: User Role Assignments
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO auth_management.user_roles (
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
tenant_id,
|
|
||||||
role,
|
|
||||||
permissions,
|
|
||||||
assigned_by,
|
|
||||||
assigned_at,
|
|
||||||
expires_at,
|
|
||||||
revoked_by,
|
|
||||||
revoked_at,
|
|
||||||
is_active,
|
|
||||||
metadata,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES
|
|
||||||
-- Student 1 Role
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'estudiante1@demo.glit.edu.mx'),
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'student'::public.gamilit_role,
|
|
||||||
'{
|
|
||||||
"read": true,
|
|
||||||
"write": false,
|
|
||||||
"admin": false,
|
|
||||||
"analytics": false,
|
|
||||||
"can_view_own_progress": true,
|
|
||||||
"can_submit_assignments": true,
|
|
||||||
"can_participate_challenges": true
|
|
||||||
}'::jsonb,
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'admin@glit.edu.mx'),
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"test_role": true,
|
|
||||||
"environment": "development",
|
|
||||||
"assigned_by_name": "Admin Gamilit"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Student 2 Role
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'estudiante2@demo.glit.edu.mx'),
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'student'::public.gamilit_role,
|
|
||||||
'{
|
|
||||||
"read": true,
|
|
||||||
"write": false,
|
|
||||||
"admin": false,
|
|
||||||
"analytics": false,
|
|
||||||
"can_view_own_progress": true,
|
|
||||||
"can_submit_assignments": true,
|
|
||||||
"can_participate_challenges": true
|
|
||||||
}'::jsonb,
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'admin@glit.edu.mx'),
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"test_role": true,
|
|
||||||
"environment": "development"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Student 3 Role
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'estudiante3@demo.glit.edu.mx'),
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'student'::public.gamilit_role,
|
|
||||||
'{
|
|
||||||
"read": true,
|
|
||||||
"write": false,
|
|
||||||
"admin": false,
|
|
||||||
"analytics": false,
|
|
||||||
"can_view_own_progress": true,
|
|
||||||
"can_submit_assignments": true,
|
|
||||||
"can_participate_challenges": true
|
|
||||||
}'::jsonb,
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'admin@glit.edu.mx'),
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"test_role": true,
|
|
||||||
"environment": "development"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Teacher Role
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'instructor@demo.glit.edu.mx'),
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'admin_teacher'::public.gamilit_role,
|
|
||||||
'{
|
|
||||||
"read": true,
|
|
||||||
"write": true,
|
|
||||||
"admin": false,
|
|
||||||
"analytics": true,
|
|
||||||
"can_manage_students": true,
|
|
||||||
"can_create_assignments": true,
|
|
||||||
"can_grade_submissions": true,
|
|
||||||
"can_view_class_analytics": true,
|
|
||||||
"can_manage_content": true
|
|
||||||
}'::jsonb,
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'admin@glit.edu.mx'),
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"test_role": true,
|
|
||||||
"environment": "development",
|
|
||||||
"assigned_by_name": "Admin Gamilit"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Admin Role
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'admin@glit.edu.mx'),
|
|
||||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'super_admin'::public.gamilit_role,
|
|
||||||
'{
|
|
||||||
"read": true,
|
|
||||||
"write": true,
|
|
||||||
"admin": true,
|
|
||||||
"analytics": true,
|
|
||||||
"can_manage_all": true,
|
|
||||||
"can_manage_users": true,
|
|
||||||
"can_manage_tenants": true,
|
|
||||||
"can_manage_system_settings": true,
|
|
||||||
"can_view_all_analytics": true,
|
|
||||||
"can_manage_roles": true
|
|
||||||
}'::jsonb,
|
|
||||||
NULL,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"test_role": true,
|
|
||||||
"environment": "development",
|
|
||||||
"note": "Self-assigned admin role"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
)
|
|
||||||
ON CONFLICT (user_id, tenant_id, role) DO UPDATE SET
|
|
||||||
permissions = EXCLUDED.permissions,
|
|
||||||
assigned_by = EXCLUDED.assigned_by,
|
|
||||||
is_active = EXCLUDED.is_active,
|
|
||||||
metadata = EXCLUDED.metadata,
|
|
||||||
updated_at = gamilit.now_mexico();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Verification Query
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
role_count INTEGER;
|
|
||||||
active_count INTEGER;
|
|
||||||
student_roles INTEGER;
|
|
||||||
teacher_roles INTEGER;
|
|
||||||
admin_roles INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO role_count FROM auth_management.user_roles;
|
|
||||||
SELECT COUNT(*) INTO active_count FROM auth_management.user_roles WHERE is_active = true;
|
|
||||||
SELECT COUNT(*) INTO student_roles FROM auth_management.user_roles WHERE role = 'student';
|
|
||||||
SELECT COUNT(*) INTO teacher_roles FROM auth_management.user_roles WHERE role = 'admin_teacher';
|
|
||||||
SELECT COUNT(*) INTO admin_roles FROM auth_management.user_roles WHERE role = 'super_admin';
|
|
||||||
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
RAISE NOTICE '✓ User roles asignados correctamente';
|
|
||||||
RAISE NOTICE ' Total: % roles', role_count;
|
|
||||||
RAISE NOTICE ' Activos: %', active_count;
|
|
||||||
RAISE NOTICE ' Estudiantes: %', student_roles;
|
|
||||||
RAISE NOTICE ' Profesores: %', teacher_roles;
|
|
||||||
RAISE NOTICE ' Admins: %', admin_roles;
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
END $$;
|
|
||||||
@ -1,208 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Seed: auth_management.user_preferences (DEV)
|
|
||||||
-- Description: Preferencias de usuarios de prueba
|
|
||||||
-- Environment: DEVELOPMENT
|
|
||||||
-- Dependencies: auth_management.profiles
|
|
||||||
-- Order: 05
|
|
||||||
-- Validated: 2025-11-02
|
|
||||||
-- Score: 100/100
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
SET search_path TO auth_management, public;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- INSERT: User Preferences
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO auth_management.user_preferences (
|
|
||||||
user_id,
|
|
||||||
theme,
|
|
||||||
language,
|
|
||||||
notifications_enabled,
|
|
||||||
email_notifications,
|
|
||||||
sound_enabled,
|
|
||||||
tutorial_completed,
|
|
||||||
preferences,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES
|
|
||||||
-- Student 1 Preferences
|
|
||||||
(
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'estudiante1@demo.glit.edu.mx'),
|
|
||||||
'light',
|
|
||||||
'es',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
'{
|
|
||||||
"detective_theme_variant": "classic",
|
|
||||||
"accessibility": {
|
|
||||||
"high_contrast": false,
|
|
||||||
"large_text": false,
|
|
||||||
"reduce_motion": false
|
|
||||||
},
|
|
||||||
"game_settings": {
|
|
||||||
"difficulty": "medium",
|
|
||||||
"show_hints": true,
|
|
||||||
"timer_visible": true
|
|
||||||
},
|
|
||||||
"privacy": {
|
|
||||||
"show_profile": true,
|
|
||||||
"show_achievements": true,
|
|
||||||
"allow_friend_requests": true
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Student 2 Preferences
|
|
||||||
(
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'estudiante2@demo.glit.edu.mx'),
|
|
||||||
'dark',
|
|
||||||
'es',
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"detective_theme_variant": "noir",
|
|
||||||
"accessibility": {
|
|
||||||
"high_contrast": true,
|
|
||||||
"large_text": false,
|
|
||||||
"reduce_motion": false
|
|
||||||
},
|
|
||||||
"game_settings": {
|
|
||||||
"difficulty": "hard",
|
|
||||||
"show_hints": false,
|
|
||||||
"timer_visible": true
|
|
||||||
},
|
|
||||||
"privacy": {
|
|
||||||
"show_profile": true,
|
|
||||||
"show_achievements": true,
|
|
||||||
"allow_friend_requests": true
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Student 3 Preferences
|
|
||||||
(
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'estudiante3@demo.glit.edu.mx'),
|
|
||||||
'auto',
|
|
||||||
'es',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"detective_theme_variant": "modern",
|
|
||||||
"accessibility": {
|
|
||||||
"high_contrast": false,
|
|
||||||
"large_text": true,
|
|
||||||
"reduce_motion": false
|
|
||||||
},
|
|
||||||
"game_settings": {
|
|
||||||
"difficulty": "easy",
|
|
||||||
"show_hints": true,
|
|
||||||
"timer_visible": false
|
|
||||||
},
|
|
||||||
"privacy": {
|
|
||||||
"show_profile": false,
|
|
||||||
"show_achievements": false,
|
|
||||||
"allow_friend_requests": false
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Teacher Preferences
|
|
||||||
(
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'instructor@demo.glit.edu.mx'),
|
|
||||||
'light',
|
|
||||||
'es',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"detective_theme_variant": "classic",
|
|
||||||
"accessibility": {
|
|
||||||
"high_contrast": false,
|
|
||||||
"large_text": false,
|
|
||||||
"reduce_motion": false
|
|
||||||
},
|
|
||||||
"dashboard_settings": {
|
|
||||||
"default_view": "overview",
|
|
||||||
"show_quick_stats": true,
|
|
||||||
"charts_enabled": true
|
|
||||||
},
|
|
||||||
"teacher_tools": {
|
|
||||||
"auto_save_grades": true,
|
|
||||||
"show_student_progress": true,
|
|
||||||
"enable_bulk_actions": true
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
),
|
|
||||||
-- Admin Preferences
|
|
||||||
(
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'admin@glit.edu.mx'),
|
|
||||||
'dark',
|
|
||||||
'es',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
'{
|
|
||||||
"detective_theme_variant": "admin",
|
|
||||||
"accessibility": {
|
|
||||||
"high_contrast": false,
|
|
||||||
"large_text": false,
|
|
||||||
"reduce_motion": false
|
|
||||||
},
|
|
||||||
"admin_settings": {
|
|
||||||
"show_system_stats": true,
|
|
||||||
"enable_debug_mode": true,
|
|
||||||
"show_all_logs": true
|
|
||||||
},
|
|
||||||
"dashboard_layout": {
|
|
||||||
"widgets": ["users", "activity", "performance", "security"],
|
|
||||||
"refresh_interval": 30
|
|
||||||
}
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico(),
|
|
||||||
gamilit.now_mexico()
|
|
||||||
)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
theme = EXCLUDED.theme,
|
|
||||||
language = EXCLUDED.language,
|
|
||||||
notifications_enabled = EXCLUDED.notifications_enabled,
|
|
||||||
email_notifications = EXCLUDED.email_notifications,
|
|
||||||
sound_enabled = EXCLUDED.sound_enabled,
|
|
||||||
tutorial_completed = EXCLUDED.tutorial_completed,
|
|
||||||
preferences = EXCLUDED.preferences,
|
|
||||||
updated_at = gamilit.now_mexico();
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Verification Query
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
pref_count INTEGER;
|
|
||||||
tutorial_completed INTEGER;
|
|
||||||
dark_theme INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO pref_count FROM auth_management.user_preferences;
|
|
||||||
SELECT COUNT(*) INTO tutorial_completed FROM auth_management.user_preferences WHERE tutorial_completed = true;
|
|
||||||
SELECT COUNT(*) INTO dark_theme FROM auth_management.user_preferences WHERE theme = 'dark';
|
|
||||||
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
RAISE NOTICE '✓ User preferences insertadas correctamente';
|
|
||||||
RAISE NOTICE ' Total: % preferencias', pref_count;
|
|
||||||
RAISE NOTICE ' Tutorial completado: %', tutorial_completed;
|
|
||||||
RAISE NOTICE ' Tema oscuro: %', dark_theme;
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
END $$;
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Seed: auth_management.auth_attempts (DEV)
|
|
||||||
-- Description: Ejemplos de intentos de autenticación para pruebas de auditoría
|
|
||||||
-- Environment: DEVELOPMENT
|
|
||||||
-- Dependencies: None (tabla de auditoría independiente)
|
|
||||||
-- Order: 06
|
|
||||||
-- Validated: 2025-11-02
|
|
||||||
-- Score: 100/100
|
|
||||||
-- Note: Seed opcional - datos de ejemplo para testing de auditoría
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
SET search_path TO auth_management, public;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- INSERT: Sample Auth Attempts
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO auth_management.auth_attempts (
|
|
||||||
id,
|
|
||||||
email,
|
|
||||||
ip_address,
|
|
||||||
user_agent,
|
|
||||||
success,
|
|
||||||
failure_reason,
|
|
||||||
tenant_slug,
|
|
||||||
attempted_at,
|
|
||||||
metadata
|
|
||||||
) VALUES
|
|
||||||
-- Successful login - Student
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'student@test.gamilit.com',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
true,
|
|
||||||
NULL,
|
|
||||||
'gamilit-test',
|
|
||||||
gamilit.now_mexico() - INTERVAL '2 hours',
|
|
||||||
'{"device": "desktop", "os": "Windows 10", "browser": "Chrome"}'::jsonb
|
|
||||||
),
|
|
||||||
-- Successful login - Teacher
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'teacher@test.gamilit.com',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
||||||
true,
|
|
||||||
NULL,
|
|
||||||
'gamilit-test',
|
|
||||||
gamilit.now_mexico() - INTERVAL '1 hour',
|
|
||||||
'{"device": "desktop", "os": "macOS", "browser": "Chrome"}'::jsonb
|
|
||||||
),
|
|
||||||
-- Failed login - Wrong password
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'student@test.gamilit.com',
|
|
||||||
'192.168.1.100'::inet,
|
|
||||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X)',
|
|
||||||
false,
|
|
||||||
'invalid_password',
|
|
||||||
'gamilit-test',
|
|
||||||
gamilit.now_mexico() - INTERVAL '3 hours',
|
|
||||||
'{"device": "mobile", "os": "iOS", "browser": "Safari", "attempts": 1}'::jsonb
|
|
||||||
),
|
|
||||||
-- Failed login - User not found
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'nonexistent@test.gamilit.com',
|
|
||||||
'192.168.1.200'::inet,
|
|
||||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
|
|
||||||
false,
|
|
||||||
'user_not_found',
|
|
||||||
'gamilit-test',
|
|
||||||
gamilit.now_mexico() - INTERVAL '5 hours',
|
|
||||||
'{"device": "desktop", "os": "Linux", "browser": "Firefox"}'::jsonb
|
|
||||||
),
|
|
||||||
-- Successful login - Admin
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'admin@test.gamilit.com',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
true,
|
|
||||||
NULL,
|
|
||||||
'gamilit-test',
|
|
||||||
gamilit.now_mexico() - INTERVAL '30 minutes',
|
|
||||||
'{"device": "desktop", "os": "Windows 10", "browser": "Edge"}'::jsonb
|
|
||||||
),
|
|
||||||
-- Failed login - Multiple attempts (suspicious)
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'admin@test.gamilit.com',
|
|
||||||
'203.0.113.45'::inet,
|
|
||||||
'curl/7.68.0',
|
|
||||||
false,
|
|
||||||
'invalid_password',
|
|
||||||
'gamilit-test',
|
|
||||||
gamilit.now_mexico() - INTERVAL '6 hours',
|
|
||||||
'{"device": "bot", "suspicious": true, "attempts": 5, "blocked": true}'::jsonb
|
|
||||||
);
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Verification Query
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
attempt_count INTEGER;
|
|
||||||
success_count INTEGER;
|
|
||||||
failed_count INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO attempt_count FROM auth_management.auth_attempts;
|
|
||||||
SELECT COUNT(*) INTO success_count FROM auth_management.auth_attempts WHERE success = true;
|
|
||||||
SELECT COUNT(*) INTO failed_count FROM auth_management.auth_attempts WHERE success = false;
|
|
||||||
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
RAISE NOTICE '✓ Auth attempts insertados (datos de ejemplo)';
|
|
||||||
RAISE NOTICE ' Total: %', attempt_count;
|
|
||||||
RAISE NOTICE ' Exitosos: %', success_count;
|
|
||||||
RAISE NOTICE ' Fallidos: %', failed_count;
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
END $$;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,186 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Seed: auth_management.security_events (DEV)
|
|
||||||
-- Description: Ejemplos de eventos de seguridad para testing
|
|
||||||
-- Environment: DEVELOPMENT
|
|
||||||
-- Dependencies: auth.users (opcional)
|
|
||||||
-- Order: 07
|
|
||||||
-- Validated: 2025-11-02
|
|
||||||
-- Score: 100/100
|
|
||||||
-- Note: Seed opcional - datos de ejemplo para testing de auditoría de seguridad
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
SET search_path TO auth_management, public;
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- INSERT: Sample Security Events
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
INSERT INTO auth_management.security_events (
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
event_type,
|
|
||||||
severity,
|
|
||||||
description,
|
|
||||||
ip_address,
|
|
||||||
user_agent,
|
|
||||||
metadata,
|
|
||||||
created_at
|
|
||||||
) VALUES
|
|
||||||
-- Low severity - Successful login
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'login_success',
|
|
||||||
'low',
|
|
||||||
'Usuario inició sesión exitosamente',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
'{
|
|
||||||
"method": "email_password",
|
|
||||||
"device": "desktop",
|
|
||||||
"location": "Mexico City, MX"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico() - INTERVAL '2 hours'
|
|
||||||
),
|
|
||||||
-- Medium severity - Password change
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'20000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'password_change',
|
|
||||||
'medium',
|
|
||||||
'Usuario cambió su contraseña',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
||||||
'{
|
|
||||||
"initiated_by": "user",
|
|
||||||
"verified": true,
|
|
||||||
"old_password_hash_prefix": "$2a$10$jcD1M4"
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico() - INTERVAL '1 day'
|
|
||||||
),
|
|
||||||
-- High severity - Multiple failed login attempts
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
NULL,
|
|
||||||
'multiple_failed_logins',
|
|
||||||
'high',
|
|
||||||
'Múltiples intentos fallidos de inicio de sesión desde la misma IP',
|
|
||||||
'203.0.113.45'::inet,
|
|
||||||
'curl/7.68.0',
|
|
||||||
'{
|
|
||||||
"attempts": 5,
|
|
||||||
"timespan_minutes": 10,
|
|
||||||
"targeted_email": "admin@test.gamilit.com",
|
|
||||||
"blocked": true
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico() - INTERVAL '3 hours'
|
|
||||||
),
|
|
||||||
-- Medium severity - Email verification sent
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
|
||||||
'email_verification_sent',
|
|
||||||
'low',
|
|
||||||
'Token de verificación de email enviado',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X)',
|
|
||||||
'{
|
|
||||||
"email": "student2@test.gamilit.com",
|
|
||||||
"token_expires_in_hours": 24
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico() - INTERVAL '5 hours'
|
|
||||||
),
|
|
||||||
-- Low severity - Logout
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'30000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'logout',
|
|
||||||
'low',
|
|
||||||
'Usuario cerró sesión',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
'{
|
|
||||||
"session_duration_minutes": 45,
|
|
||||||
"manual_logout": true
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico() - INTERVAL '30 minutes'
|
|
||||||
),
|
|
||||||
-- Critical severity - Unauthorized access attempt
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
NULL,
|
|
||||||
'unauthorized_access_attempt',
|
|
||||||
'critical',
|
|
||||||
'Intento de acceso a recursos sin autorización',
|
|
||||||
'198.51.100.23'::inet,
|
|
||||||
'Python/3.9 requests/2.26.0',
|
|
||||||
'{
|
|
||||||
"endpoint": "/api/admin/users",
|
|
||||||
"method": "GET",
|
|
||||||
"attempted_user": "unknown",
|
|
||||||
"blocked": true,
|
|
||||||
"firewall_triggered": true
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico() - INTERVAL '6 hours'
|
|
||||||
),
|
|
||||||
-- Medium severity - Permission elevation
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'20000000-0000-0000-0000-000000000001'::uuid,
|
|
||||||
'permission_elevation',
|
|
||||||
'medium',
|
|
||||||
'Permisos de usuario elevados temporalmente',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
||||||
'{
|
|
||||||
"elevated_by": "30000000-0000-0000-0000-000000000001",
|
|
||||||
"from_role": "admin_teacher",
|
|
||||||
"to_role": "admin_teacher",
|
|
||||||
"additional_permissions": ["can_manage_system_settings"],
|
|
||||||
"duration_hours": 2
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico() - INTERVAL '4 hours'
|
|
||||||
),
|
|
||||||
-- Low severity - Profile update
|
|
||||||
(
|
|
||||||
gen_random_uuid(),
|
|
||||||
'10000000-0000-0000-0000-000000000003'::uuid,
|
|
||||||
'profile_update',
|
|
||||||
'low',
|
|
||||||
'Usuario actualizó su perfil',
|
|
||||||
'127.0.0.1'::inet,
|
|
||||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
|
|
||||||
'{
|
|
||||||
"fields_updated": ["display_name", "avatar_url", "bio"],
|
|
||||||
"verified": true
|
|
||||||
}'::jsonb,
|
|
||||||
gamilit.now_mexico() - INTERVAL '12 hours'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- =====================================================
|
|
||||||
-- Verification Query
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
event_count INTEGER;
|
|
||||||
critical_count INTEGER;
|
|
||||||
high_count INTEGER;
|
|
||||||
medium_count INTEGER;
|
|
||||||
low_count INTEGER;
|
|
||||||
BEGIN
|
|
||||||
SELECT COUNT(*) INTO event_count FROM auth_management.security_events;
|
|
||||||
SELECT COUNT(*) INTO critical_count FROM auth_management.security_events WHERE severity = 'critical';
|
|
||||||
SELECT COUNT(*) INTO high_count FROM auth_management.security_events WHERE severity = 'high';
|
|
||||||
SELECT COUNT(*) INTO medium_count FROM auth_management.security_events WHERE severity = 'medium';
|
|
||||||
SELECT COUNT(*) INTO low_count FROM auth_management.security_events WHERE severity = 'low';
|
|
||||||
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
RAISE NOTICE '✓ Security events insertados (datos de ejemplo)';
|
|
||||||
RAISE NOTICE ' Total: %', event_count;
|
|
||||||
RAISE NOTICE ' Críticos: %', critical_count;
|
|
||||||
RAISE NOTICE ' Altos: %', high_count;
|
|
||||||
RAISE NOTICE ' Medios: %', medium_count;
|
|
||||||
RAISE NOTICE ' Bajos: %', low_count;
|
|
||||||
RAISE NOTICE '==============================================';
|
|
||||||
END $$;
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
# ============================================================================
|
|
||||||
# GAMILIT Frontend - Production Environment EXAMPLE
|
|
||||||
# ============================================================================
|
|
||||||
# COPIAR A .env.production
|
|
||||||
# Fecha: 2025-12-18
|
|
||||||
# Server: 74.208.126.102
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# ==================== APPLICATION ====================
|
|
||||||
VITE_APP_NAME=GAMILIT Platform
|
|
||||||
VITE_APP_VERSION=1.0.0
|
|
||||||
VITE_APP_ENV=production
|
|
||||||
VITE_ENV=production
|
|
||||||
|
|
||||||
# ==================== API CONFIGURATION ====================
|
|
||||||
# IMPORTANTE: Usar HTTPS/WSS si el servidor tiene SSL configurado
|
|
||||||
# ============================================================================
|
|
||||||
# SSL CONFIGURADO - Usar HTTPS/WSS
|
|
||||||
# ============================================================================
|
|
||||||
VITE_API_HOST=74.208.126.102:3006
|
|
||||||
VITE_API_PROTOCOL=https
|
|
||||||
VITE_API_VERSION=v1
|
|
||||||
VITE_API_TIMEOUT=30000
|
|
||||||
|
|
||||||
# WebSocket configuration (WSS para conexiones seguras)
|
|
||||||
VITE_WS_HOST=74.208.126.102:3006
|
|
||||||
VITE_WS_PROTOCOL=wss
|
|
||||||
|
|
||||||
# ==================== AUTHENTICATION ====================
|
|
||||||
VITE_JWT_EXPIRATION=7d
|
|
||||||
|
|
||||||
# ==================== FEATURE FLAGS ====================
|
|
||||||
VITE_ENABLE_GAMIFICATION=true
|
|
||||||
VITE_ENABLE_SOCIAL_FEATURES=true
|
|
||||||
VITE_ENABLE_ANALYTICS=true
|
|
||||||
VITE_ENABLE_DEBUG=false
|
|
||||||
VITE_ENABLE_STORYBOOK=false
|
|
||||||
VITE_MOCK_API=false
|
|
||||||
|
|
||||||
# ==================== EXTERNAL SERVICES ====================
|
|
||||||
# Configurar en produccion si se usan
|
|
||||||
VITE_GOOGLE_ANALYTICS_ID=
|
|
||||||
VITE_SENTRY_DSN=
|
|
||||||
VITE_AI_SERVICE_URL=
|
|
||||||
|
|
||||||
# ==================== PRODUCTION ====================
|
|
||||||
VITE_ENABLE_DEBUG=false
|
|
||||||
VITE_LOG_LEVEL=error
|
|
||||||
|
|
||||||
# ==================== TESTING ====================
|
|
||||||
# NO usar credenciales en produccion
|
|
||||||
VITE_TEST_USER_EMAIL=
|
|
||||||
VITE_TEST_USER_PASSWORD=
|
|
||||||
@ -1,733 +0,0 @@
|
|||||||
/**
|
|
||||||
* RubricEvaluator Component
|
|
||||||
*
|
|
||||||
* P2-02: Created 2025-12-18
|
|
||||||
* Standardized rubric-based evaluation component for manual grading mechanics.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Configurable rubric criteria with weight support
|
|
||||||
* - Visual scoring interface with level descriptors
|
|
||||||
* - Automatic weighted score calculation
|
|
||||||
* - Feedback templates per criterion
|
|
||||||
* - Support for 10 manual grading mechanics
|
|
||||||
*
|
|
||||||
* @component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useMemo, useCallback } from 'react';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import {
|
|
||||||
Star,
|
|
||||||
Info,
|
|
||||||
CheckCircle,
|
|
||||||
AlertCircle,
|
|
||||||
MessageSquare,
|
|
||||||
Save,
|
|
||||||
RotateCcw,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TYPES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface RubricLevel {
|
|
||||||
score: number;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RubricCriterion {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
weight: number; // Percentage weight (0-100)
|
|
||||||
levels: RubricLevel[];
|
|
||||||
feedbackTemplates?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RubricConfig {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
mechanicType: string;
|
|
||||||
maxScore: number;
|
|
||||||
criteria: RubricCriterion[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RubricScore {
|
|
||||||
criterionId: string;
|
|
||||||
selectedLevel: number;
|
|
||||||
feedback?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RubricEvaluatorProps {
|
|
||||||
rubric: RubricConfig;
|
|
||||||
initialScores?: RubricScore[];
|
|
||||||
onScoreChange?: (scores: RubricScore[], totalScore: number, percentage: number) => void;
|
|
||||||
onSubmit?: (scores: RubricScore[], totalScore: number, feedback: string) => Promise<void>;
|
|
||||||
readOnly?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RubricEvaluatorResult {
|
|
||||||
scores: RubricScore[];
|
|
||||||
totalScore: number;
|
|
||||||
percentage: number;
|
|
||||||
feedback: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// DEFAULT RUBRICS BY MECHANIC TYPE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export const DEFAULT_RUBRICS: Record<string, RubricConfig> = {
|
|
||||||
prediccion_narrativa: {
|
|
||||||
id: 'rubric-prediccion',
|
|
||||||
name: 'Evaluaci\u00f3n de Predicci\u00f3n Narrativa',
|
|
||||||
description: 'R\u00fabrica para evaluar predicciones narrativas basadas en el texto',
|
|
||||||
mechanicType: 'prediccion_narrativa',
|
|
||||||
maxScore: 100,
|
|
||||||
criteria: [
|
|
||||||
{
|
|
||||||
id: 'coherencia',
|
|
||||||
name: 'Coherencia con el texto',
|
|
||||||
description: '\u00bfLa predicci\u00f3n es coherente con la informaci\u00f3n presentada en el texto?',
|
|
||||||
weight: 30,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Sin relaci\u00f3n con el texto' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Relaci\u00f3n m\u00ednima' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Relaci\u00f3n adecuada' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Relaci\u00f3n s\u00f3lida con evidencia' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Fundamentaci\u00f3n excepcional' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'creatividad',
|
|
||||||
name: 'Creatividad',
|
|
||||||
description: '\u00bfLa predicci\u00f3n muestra pensamiento original?',
|
|
||||||
weight: 25,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Respuesta literal/copiada' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Poca originalidad' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Algo de creatividad' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Ideas originales' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Muy creativo e innovador' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'justificacion',
|
|
||||||
name: 'Justificaci\u00f3n',
|
|
||||||
description: '\u00bfEl estudiante justifica su predicci\u00f3n adecuadamente?',
|
|
||||||
weight: 30,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Sin justificaci\u00f3n' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Justificaci\u00f3n vaga' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Justificaci\u00f3n aceptable' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Bien justificado con ejemplos' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Argumentaci\u00f3n excepcional' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'expresion',
|
|
||||||
name: 'Expresi\u00f3n escrita',
|
|
||||||
description: 'Claridad, gram\u00e1tica y ortograf\u00eda',
|
|
||||||
weight: 15,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Muchos errores' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Errores frecuentes' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Algunos errores' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Pocos errores' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Escritura impecable' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
tribunal_opiniones: {
|
|
||||||
id: 'rubric-tribunal',
|
|
||||||
name: 'Evaluaci\u00f3n de Tribunal de Opiniones',
|
|
||||||
description: 'R\u00fabrica para evaluar argumentos en debates',
|
|
||||||
mechanicType: 'tribunal_opiniones',
|
|
||||||
maxScore: 100,
|
|
||||||
criteria: [
|
|
||||||
{
|
|
||||||
id: 'argumentacion',
|
|
||||||
name: 'Calidad argumentativa',
|
|
||||||
description: 'Solidez y l\u00f3gica de los argumentos',
|
|
||||||
weight: 35,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Sin argumentos claros' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Argumentos d\u00e9biles' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Argumentos aceptables' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Argumentos s\u00f3lidos' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Argumentaci\u00f3n excepcional' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'evidencia',
|
|
||||||
name: 'Uso de evidencia',
|
|
||||||
description: 'Respaldo con datos o ejemplos del texto',
|
|
||||||
weight: 30,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Sin evidencia' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Evidencia irrelevante' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Alguna evidencia' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Buena evidencia' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Evidencia excepcional' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'contraargumentos',
|
|
||||||
name: 'Manejo de contraargumentos',
|
|
||||||
description: 'Capacidad de anticipar y responder objeciones',
|
|
||||||
weight: 20,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'No considera otras perspectivas' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Reconocimiento superficial' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Considera algunas objeciones' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Buen manejo de objeciones' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Anticipaci\u00f3n magistral' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'respeto',
|
|
||||||
name: 'Respeto y \u00e9tica',
|
|
||||||
description: 'Tono respetuoso y \u00e9tico en el debate',
|
|
||||||
weight: 15,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Irrespetuoso' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Poco respetuoso' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Generalmente respetuoso' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Respetuoso' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Ejemplar' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
comic_digital: {
|
|
||||||
id: 'rubric-comic',
|
|
||||||
name: 'Evaluaci\u00f3n de C\u00f3mic Digital',
|
|
||||||
description: 'R\u00fabrica para evaluar creaciones de c\u00f3mic digital',
|
|
||||||
mechanicType: 'comic_digital',
|
|
||||||
maxScore: 100,
|
|
||||||
criteria: [
|
|
||||||
{
|
|
||||||
id: 'narrativa',
|
|
||||||
name: 'Narrativa visual',
|
|
||||||
description: 'Secuencia l\u00f3gica y fluidez de la historia',
|
|
||||||
weight: 30,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Sin secuencia clara' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Secuencia confusa' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Secuencia aceptable' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Buena secuencia' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Narrativa excepcional' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'creatividad',
|
|
||||||
name: 'Creatividad visual',
|
|
||||||
description: 'Originalidad en dise\u00f1o y presentaci\u00f3n',
|
|
||||||
weight: 25,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Sin esfuerzo creativo' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Poca creatividad' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Algo de creatividad' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Creativo' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Muy creativo' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'comprension',
|
|
||||||
name: 'Comprensi\u00f3n del tema',
|
|
||||||
description: 'Demuestra entendimiento del contenido',
|
|
||||||
weight: 30,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'No demuestra comprensi\u00f3n' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Comprensi\u00f3n limitada' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Comprensi\u00f3n adecuada' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Buena comprensi\u00f3n' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Comprensi\u00f3n profunda' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'presentacion',
|
|
||||||
name: 'Presentaci\u00f3n t\u00e9cnica',
|
|
||||||
description: 'Calidad t\u00e9cnica del c\u00f3mic',
|
|
||||||
weight: 15,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Ilegible/incompleto' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Dif\u00edcil de leer' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Legible' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Bien presentado' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Presentaci\u00f3n profesional' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// Generic rubric for other manual mechanics
|
|
||||||
generic_creative: {
|
|
||||||
id: 'rubric-generic',
|
|
||||||
name: 'Evaluaci\u00f3n de Trabajo Creativo',
|
|
||||||
description: 'R\u00fabrica general para trabajos creativos',
|
|
||||||
mechanicType: 'generic',
|
|
||||||
maxScore: 100,
|
|
||||||
criteria: [
|
|
||||||
{
|
|
||||||
id: 'contenido',
|
|
||||||
name: 'Contenido',
|
|
||||||
description: 'Calidad y relevancia del contenido',
|
|
||||||
weight: 35,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Contenido inadecuado' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Contenido m\u00ednimo' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Contenido adecuado' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Buen contenido' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Contenido excepcional' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'creatividad',
|
|
||||||
name: 'Creatividad',
|
|
||||||
description: 'Originalidad y esfuerzo creativo',
|
|
||||||
weight: 25,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Sin creatividad' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Poca creatividad' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Algo de creatividad' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Creativo' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Muy creativo' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'esfuerzo',
|
|
||||||
name: 'Esfuerzo',
|
|
||||||
description: 'Dedicaci\u00f3n y completitud del trabajo',
|
|
||||||
weight: 25,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Trabajo incompleto' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Esfuerzo m\u00ednimo' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Esfuerzo aceptable' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Buen esfuerzo' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Esfuerzo sobresaliente' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'presentacion',
|
|
||||||
name: 'Presentaci\u00f3n',
|
|
||||||
description: 'Claridad y organizaci\u00f3n',
|
|
||||||
weight: 15,
|
|
||||||
levels: [
|
|
||||||
{ score: 1, label: 'Insuficiente', description: 'Desorganizado' },
|
|
||||||
{ score: 2, label: 'B\u00e1sico', description: 'Poco organizado' },
|
|
||||||
{ score: 3, label: 'Competente', description: 'Organizado' },
|
|
||||||
{ score: 4, label: 'Avanzado', description: 'Bien organizado' },
|
|
||||||
{ score: 5, label: 'Excelente', description: 'Excelente presentaci\u00f3n' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get rubric for a specific mechanic type
|
|
||||||
*/
|
|
||||||
export const getRubricForMechanic = (mechanicType: string): RubricConfig => {
|
|
||||||
return DEFAULT_RUBRICS[mechanicType] || DEFAULT_RUBRICS.generic_creative;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SUB-COMPONENTS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
interface CriterionCardProps {
|
|
||||||
criterion: RubricCriterion;
|
|
||||||
selectedLevel: number | undefined;
|
|
||||||
feedback: string;
|
|
||||||
onLevelSelect: (level: number) => void;
|
|
||||||
onFeedbackChange: (feedback: string) => void;
|
|
||||||
readOnly?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CriterionCard: React.FC<CriterionCardProps> = ({
|
|
||||||
criterion,
|
|
||||||
selectedLevel,
|
|
||||||
feedback,
|
|
||||||
onLevelSelect,
|
|
||||||
onFeedbackChange,
|
|
||||||
readOnly = false,
|
|
||||||
}) => {
|
|
||||||
const [showFeedback, setShowFeedback] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-3 flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h4 className="font-bold text-gray-800">{criterion.name}</h4>
|
|
||||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
|
|
||||||
{criterion.weight}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-sm text-gray-600">{criterion.description}</p>
|
|
||||||
</div>
|
|
||||||
{selectedLevel !== undefined && (
|
|
||||||
<div className="ml-4 flex items-center gap-1">
|
|
||||||
{[1, 2, 3, 4, 5].map((star) => (
|
|
||||||
<Star
|
|
||||||
key={star}
|
|
||||||
className={`h-4 w-4 ${
|
|
||||||
star <= selectedLevel
|
|
||||||
? 'fill-yellow-400 text-yellow-400'
|
|
||||||
: 'text-gray-300'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Level Selection */}
|
|
||||||
<div className="mb-3 grid grid-cols-5 gap-2">
|
|
||||||
{criterion.levels.map((level) => (
|
|
||||||
<button
|
|
||||||
key={level.score}
|
|
||||||
onClick={() => !readOnly && onLevelSelect(level.score)}
|
|
||||||
disabled={readOnly}
|
|
||||||
className={`group relative rounded-lg border-2 p-2 transition-all ${
|
|
||||||
selectedLevel === level.score
|
|
||||||
? 'border-blue-500 bg-blue-50'
|
|
||||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
|
||||||
} ${readOnly ? 'cursor-default' : 'cursor-pointer'}`}
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<span
|
|
||||||
className={`block text-lg font-bold ${
|
|
||||||
selectedLevel === level.score ? 'text-blue-600' : 'text-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{level.score}
|
|
||||||
</span>
|
|
||||||
<span className="block text-xs text-gray-600">{level.label}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tooltip */}
|
|
||||||
<div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 hidden w-48 -translate-x-1/2 rounded-lg bg-gray-800 p-2 text-xs text-white group-hover:block">
|
|
||||||
{level.description}
|
|
||||||
<div className="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-800" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feedback Toggle */}
|
|
||||||
{!readOnly && (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowFeedback(!showFeedback)}
|
|
||||||
className="flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800"
|
|
||||||
>
|
|
||||||
<MessageSquare className="h-4 w-4" />
|
|
||||||
{showFeedback ? 'Ocultar comentario' : 'Agregar comentario'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showFeedback && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
|
||||||
className="mt-2"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
value={feedback}
|
|
||||||
onChange={(e) => onFeedbackChange(e.target.value)}
|
|
||||||
placeholder="Comentario sobre este criterio..."
|
|
||||||
className="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Show feedback in readOnly mode */}
|
|
||||||
{readOnly && feedback && (
|
|
||||||
<div className="mt-2 rounded-lg bg-gray-50 p-2 text-sm text-gray-700">
|
|
||||||
<span className="font-medium">Comentario:</span> {feedback}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MAIN COMPONENT
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export const RubricEvaluator: React.FC<RubricEvaluatorProps> = ({
|
|
||||||
rubric,
|
|
||||||
initialScores = [],
|
|
||||||
onScoreChange,
|
|
||||||
onSubmit,
|
|
||||||
readOnly = false,
|
|
||||||
}) => {
|
|
||||||
// Initialize scores from props or empty
|
|
||||||
const [scores, setScores] = useState<Record<string, RubricScore>>(() => {
|
|
||||||
const initial: Record<string, RubricScore> = {};
|
|
||||||
initialScores.forEach((score) => {
|
|
||||||
initial[score.criterionId] = score;
|
|
||||||
});
|
|
||||||
return initial;
|
|
||||||
});
|
|
||||||
const [generalFeedback, setGeneralFeedback] = useState('');
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
||||||
|
|
||||||
// Calculate total score
|
|
||||||
const { totalScore, percentage, isComplete } = useMemo(() => {
|
|
||||||
let weightedSum = 0;
|
|
||||||
let totalWeight = 0;
|
|
||||||
let complete = true;
|
|
||||||
|
|
||||||
rubric.criteria.forEach((criterion) => {
|
|
||||||
const score = scores[criterion.id];
|
|
||||||
if (score?.selectedLevel !== undefined) {
|
|
||||||
// Normalize to percentage: (score/5) * weight
|
|
||||||
weightedSum += (score.selectedLevel / 5) * criterion.weight;
|
|
||||||
totalWeight += criterion.weight;
|
|
||||||
} else {
|
|
||||||
complete = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const pct = totalWeight > 0 ? (weightedSum / totalWeight) * 100 : 0;
|
|
||||||
const total = Math.round((pct / 100) * rubric.maxScore);
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalScore: total,
|
|
||||||
percentage: Math.round(pct),
|
|
||||||
isComplete: complete,
|
|
||||||
};
|
|
||||||
}, [scores, rubric]);
|
|
||||||
|
|
||||||
// Notify parent of changes
|
|
||||||
const notifyChange = useCallback(
|
|
||||||
(newScores: Record<string, RubricScore>) => {
|
|
||||||
if (onScoreChange) {
|
|
||||||
const scoresArray = Object.values(newScores);
|
|
||||||
let weightedSum = 0;
|
|
||||||
|
|
||||||
rubric.criteria.forEach((criterion) => {
|
|
||||||
const score = newScores[criterion.id];
|
|
||||||
if (score?.selectedLevel !== undefined) {
|
|
||||||
weightedSum += (score.selectedLevel / 5) * criterion.weight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const pct = Math.round(weightedSum);
|
|
||||||
const total = Math.round((weightedSum / 100) * rubric.maxScore);
|
|
||||||
onScoreChange(scoresArray, total, pct);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onScoreChange, rubric],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle level selection
|
|
||||||
const handleLevelSelect = useCallback(
|
|
||||||
(criterionId: string, level: number) => {
|
|
||||||
const newScores = {
|
|
||||||
...scores,
|
|
||||||
[criterionId]: {
|
|
||||||
...scores[criterionId],
|
|
||||||
criterionId,
|
|
||||||
selectedLevel: level,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setScores(newScores);
|
|
||||||
notifyChange(newScores);
|
|
||||||
},
|
|
||||||
[scores, notifyChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle criterion feedback
|
|
||||||
const handleFeedbackChange = useCallback(
|
|
||||||
(criterionId: string, feedback: string) => {
|
|
||||||
const newScores = {
|
|
||||||
...scores,
|
|
||||||
[criterionId]: {
|
|
||||||
...scores[criterionId],
|
|
||||||
criterionId,
|
|
||||||
feedback,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setScores(newScores);
|
|
||||||
notifyChange(newScores);
|
|
||||||
},
|
|
||||||
[scores, notifyChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset all scores
|
|
||||||
const handleReset = () => {
|
|
||||||
setScores({});
|
|
||||||
setGeneralFeedback('');
|
|
||||||
setSubmitStatus('idle');
|
|
||||||
if (onScoreChange) {
|
|
||||||
onScoreChange([], 0, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Submit evaluation
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!onSubmit || !isComplete) return;
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
setSubmitStatus('idle');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await onSubmit(Object.values(scores), totalScore, generalFeedback);
|
|
||||||
setSubmitStatus('success');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[RubricEvaluator] Submit error:', error);
|
|
||||||
setSubmitStatus('error');
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get grade color
|
|
||||||
const getGradeColor = (pct: number): string => {
|
|
||||||
if (pct >= 90) return 'text-green-600 bg-green-100';
|
|
||||||
if (pct >= 80) return 'text-blue-600 bg-blue-100';
|
|
||||||
if (pct >= 70) return 'text-yellow-600 bg-yellow-100';
|
|
||||||
if (pct >= 60) return 'text-orange-600 bg-orange-100';
|
|
||||||
return 'text-red-600 bg-red-100';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="rounded-xl bg-gradient-to-r from-purple-50 to-blue-50 p-4">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-bold text-gray-800">{rubric.name}</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-600">{rubric.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Info className="h-5 w-5 text-gray-400" />
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
{rubric.criteria.length} criterios
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Score Summary */}
|
|
||||||
<div className="flex items-center justify-between rounded-xl border-2 border-gray-200 p-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Puntaje calculado</p>
|
|
||||||
<div className="flex items-baseline gap-2">
|
|
||||||
<span className="text-3xl font-bold text-gray-800">{totalScore}</span>
|
|
||||||
<span className="text-lg text-gray-500">/ {rubric.maxScore}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={`rounded-xl px-6 py-3 ${getGradeColor(percentage)}`}>
|
|
||||||
<p className="text-3xl font-bold">{percentage}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Criteria */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
{rubric.criteria.map((criterion) => (
|
|
||||||
<CriterionCard
|
|
||||||
key={criterion.id}
|
|
||||||
criterion={criterion}
|
|
||||||
selectedLevel={scores[criterion.id]?.selectedLevel}
|
|
||||||
feedback={scores[criterion.id]?.feedback || ''}
|
|
||||||
onLevelSelect={(level) => handleLevelSelect(criterion.id, level)}
|
|
||||||
onFeedbackChange={(feedback) => handleFeedbackChange(criterion.id, feedback)}
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* General Feedback */}
|
|
||||||
{!readOnly && (
|
|
||||||
<div>
|
|
||||||
<label className="mb-2 block font-semibold text-gray-700">
|
|
||||||
Retroalimentaci\u00f3n general
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={generalFeedback}
|
|
||||||
onChange={(e) => setGeneralFeedback(e.target.value)}
|
|
||||||
placeholder="Escribe comentarios generales para el estudiante..."
|
|
||||||
className="w-full rounded-lg border border-gray-300 p-3 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Status Messages */}
|
|
||||||
{submitStatus === 'success' && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="flex items-center gap-3 rounded-lg border border-green-200 bg-green-50 p-4"
|
|
||||||
>
|
|
||||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
|
||||||
<p className="font-medium text-green-800">Evaluaci\u00f3n guardada exitosamente</p>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{submitStatus === 'error' && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-4"
|
|
||||||
>
|
|
||||||
<AlertCircle className="h-5 w-5 text-red-600" />
|
|
||||||
<p className="font-medium text-red-800">Error al guardar la evaluaci\u00f3n</p>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
{!readOnly && (
|
|
||||||
<div className="flex items-center justify-between border-t border-gray-200 pt-4">
|
|
||||||
<button
|
|
||||||
onClick={handleReset}
|
|
||||||
className="flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2 font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<RotateCcw className="h-4 w-4" />
|
|
||||||
Reiniciar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{!isComplete && (
|
|
||||||
<p className="text-sm text-orange-600">
|
|
||||||
Faltan {rubric.criteria.length - Object.keys(scores).length} criterios
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{onSubmit && (
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={!isComplete || isSubmitting}
|
|
||||||
className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-purple-500 to-indigo-500 px-6 py-2 font-semibold text-white transition-all hover:shadow-lg disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
||||||
Guardando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Save className="h-4 w-4" />
|
|
||||||
Guardar evaluaci\u00f3n
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RubricEvaluator;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Grading Components - Central export point
|
|
||||||
*
|
|
||||||
* P2-02: Created 2025-12-18
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {
|
|
||||||
RubricEvaluator,
|
|
||||||
DEFAULT_RUBRICS,
|
|
||||||
getRubricForMechanic,
|
|
||||||
} from './RubricEvaluator';
|
|
||||||
|
|
||||||
export type {
|
|
||||||
RubricLevel,
|
|
||||||
RubricCriterion,
|
|
||||||
RubricConfig,
|
|
||||||
RubricScore,
|
|
||||||
RubricEvaluatorProps,
|
|
||||||
RubricEvaluatorResult,
|
|
||||||
} from './RubricEvaluator';
|
|
||||||
@ -14,7 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useState, useRef } from 'react';
|
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
User,
|
User,
|
||||||
@ -29,15 +28,6 @@ import {
|
|||||||
TrendingUp,
|
TrendingUp,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
Play,
|
|
||||||
Pause,
|
|
||||||
Volume2,
|
|
||||||
VolumeX,
|
|
||||||
Maximize2,
|
|
||||||
Image as ImageIcon,
|
|
||||||
Video,
|
|
||||||
Music,
|
|
||||||
Download,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAttemptDetail } from '@apps/teacher/hooks/useExerciseResponses';
|
import { useAttemptDetail } from '@apps/teacher/hooks/useExerciseResponses';
|
||||||
@ -76,510 +66,26 @@ const formatDate = (dateString: string): string => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if an exercise requires manual grading
|
* Determines if an exercise requires manual grading
|
||||||
* P0-03: Updated 2025-12-18 - Complete list of manual mechanics
|
* Modules 3, 4, 5 (creative exercises) require manual review
|
||||||
*
|
|
||||||
* 10 manual mechanics identified in analysis:
|
|
||||||
* - Predicción Narrativa, Tribunal de Opiniones, Podcast Argumentativo
|
|
||||||
* - Debate Digital, Cómic Digital, Video Carta, Diario Multimedia
|
|
||||||
* - Collage Prensa, Call to Action, Texto en Movimiento
|
|
||||||
*/
|
*/
|
||||||
const requiresManualGrading = (exerciseType: string): boolean => {
|
const requiresManualGrading = (exerciseType: string): boolean => {
|
||||||
const manualGradingTypes = [
|
const manualGradingTypes = [
|
||||||
// Módulo 2 - Manual
|
// Módulo 3
|
||||||
'prediccion_narrativa',
|
|
||||||
// Módulo 3 - Críticos/Argumentativos
|
|
||||||
'tribunal_opiniones',
|
|
||||||
'podcast_argumentativo',
|
'podcast_argumentativo',
|
||||||
'debate_digital',
|
// Módulo 4
|
||||||
// Módulo 4 - Alfabetización Mediática (creativos)
|
'verificador_fake_news',
|
||||||
'analisis_memes', // Semi-auto but needs review
|
'quiz_tiktok',
|
||||||
// Módulo 5 - Creación de Contenido
|
'analisis_memes',
|
||||||
|
'infografia_interactiva',
|
||||||
|
'navegacion_hipertextual',
|
||||||
|
// Módulo 5
|
||||||
|
'diario_multimedia',
|
||||||
'comic_digital',
|
'comic_digital',
|
||||||
'video_carta',
|
'video_carta',
|
||||||
'diario_multimedia',
|
|
||||||
// Auxiliares
|
|
||||||
'collage_prensa',
|
|
||||||
'call_to_action',
|
|
||||||
'texto_en_movimiento',
|
|
||||||
];
|
];
|
||||||
return manualGradingTypes.includes(exerciseType);
|
return manualGradingTypes.includes(exerciseType);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* P2-03: Multimedia content type detection
|
|
||||||
* Identifies multimedia content types for creative exercises
|
|
||||||
*/
|
|
||||||
type MediaType = 'video' | 'audio' | 'image' | 'text' | 'unknown';
|
|
||||||
|
|
||||||
interface MediaContent {
|
|
||||||
type: MediaType;
|
|
||||||
url?: string;
|
|
||||||
urls?: string[];
|
|
||||||
text?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
thumbnail?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const detectMediaType = (url: string): MediaType => {
|
|
||||||
const extension = url.split('.').pop()?.toLowerCase() || '';
|
|
||||||
const videoExts = ['mp4', 'webm', 'ogg', 'mov', 'avi'];
|
|
||||||
const audioExts = ['mp3', 'wav', 'ogg', 'aac', 'm4a'];
|
|
||||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
|
|
||||||
|
|
||||||
if (videoExts.includes(extension)) return 'video';
|
|
||||||
if (audioExts.includes(extension)) return 'audio';
|
|
||||||
if (imageExts.includes(extension)) return 'image';
|
|
||||||
return 'unknown';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts multimedia content from answer data
|
|
||||||
*/
|
|
||||||
const extractMediaContent = (answerData: Record<string, unknown>): MediaContent[] => {
|
|
||||||
const media: MediaContent[] = [];
|
|
||||||
|
|
||||||
// Check for direct media URLs
|
|
||||||
const mediaFields = ['videoUrl', 'audioUrl', 'imageUrl', 'mediaUrl', 'url', 'file'];
|
|
||||||
for (const field of mediaFields) {
|
|
||||||
if (typeof answerData[field] === 'string' && answerData[field]) {
|
|
||||||
const url = answerData[field] as string;
|
|
||||||
media.push({
|
|
||||||
type: detectMediaType(url),
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for arrays of media
|
|
||||||
const arrayFields = ['images', 'videos', 'audios', 'files', 'media'];
|
|
||||||
for (const field of arrayFields) {
|
|
||||||
if (Array.isArray(answerData[field])) {
|
|
||||||
for (const item of answerData[field] as (string | { url: string })[]) {
|
|
||||||
const url = typeof item === 'string' ? item : item?.url;
|
|
||||||
if (url) {
|
|
||||||
media.push({
|
|
||||||
type: detectMediaType(url),
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for podcast/video specific fields
|
|
||||||
if (answerData.podcast_url && typeof answerData.podcast_url === 'string') {
|
|
||||||
media.push({ type: 'audio', url: answerData.podcast_url });
|
|
||||||
}
|
|
||||||
if (answerData.video_url && typeof answerData.video_url === 'string') {
|
|
||||||
media.push({ type: 'video', url: answerData.video_url });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for comic panels (images)
|
|
||||||
if (Array.isArray(answerData.panels)) {
|
|
||||||
for (const panel of answerData.panels as { imageUrl?: string }[]) {
|
|
||||||
if (panel.imageUrl) {
|
|
||||||
media.push({ type: 'image', url: panel.imageUrl });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for collage images
|
|
||||||
if (Array.isArray(answerData.collage_items)) {
|
|
||||||
for (const item of answerData.collage_items as { url?: string }[]) {
|
|
||||||
if (item.url) {
|
|
||||||
media.push({ type: 'image', url: item.url });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return media;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if exercise type has multimedia content
|
|
||||||
*/
|
|
||||||
const hasMultimediaContent = (exerciseType: string): boolean => {
|
|
||||||
const multimediaTypes = [
|
|
||||||
'video_carta',
|
|
||||||
'podcast_argumentativo',
|
|
||||||
'comic_digital',
|
|
||||||
'diario_multimedia',
|
|
||||||
'collage_prensa',
|
|
||||||
'infografia_interactiva',
|
|
||||||
'creacion_storyboard',
|
|
||||||
];
|
|
||||||
return multimediaTypes.includes(exerciseType);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MULTIMEDIA PLAYER COMPONENTS (P2-03)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Video Player Component
|
|
||||||
*/
|
|
||||||
const VideoPlayer: React.FC<{ url: string; title?: string }> = ({ url, title }) => {
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
|
||||||
const [duration, setDuration] = useState(0);
|
|
||||||
|
|
||||||
const togglePlay = () => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
if (isPlaying) {
|
|
||||||
videoRef.current.pause();
|
|
||||||
} else {
|
|
||||||
videoRef.current.play();
|
|
||||||
}
|
|
||||||
setIsPlaying(!isPlaying);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleMute = () => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
videoRef.current.muted = !isMuted;
|
|
||||||
setIsMuted(!isMuted);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeUpdate = () => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
setCurrentTime(videoRef.current.currentTime);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadedMetadata = () => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
setDuration(videoRef.current.duration);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const time = parseFloat(e.target.value);
|
|
||||||
if (videoRef.current) {
|
|
||||||
videoRef.current.currentTime = time;
|
|
||||||
setCurrentTime(time);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatVideoTime = (time: number): string => {
|
|
||||||
const mins = Math.floor(time / 60);
|
|
||||||
const secs = Math.floor(time % 60);
|
|
||||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFullscreen = () => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
videoRef.current.requestFullscreen?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-black">
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
src={url}
|
|
||||||
className="aspect-video w-full"
|
|
||||||
onTimeUpdate={handleTimeUpdate}
|
|
||||||
onLoadedMetadata={handleLoadedMetadata}
|
|
||||||
onEnded={() => setIsPlaying(false)}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-3 bg-gray-900 px-4 py-2">
|
|
||||||
<button
|
|
||||||
onClick={togglePlay}
|
|
||||||
className="rounded-full p-2 text-white transition-colors hover:bg-white/20"
|
|
||||||
>
|
|
||||||
{isPlaying ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5" />}
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={duration}
|
|
||||||
value={currentTime}
|
|
||||||
onChange={handleSeek}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-white">
|
|
||||||
{formatVideoTime(currentTime)} / {formatVideoTime(duration)}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={toggleMute}
|
|
||||||
className="rounded-full p-2 text-white transition-colors hover:bg-white/20"
|
|
||||||
>
|
|
||||||
{isMuted ? <VolumeX className="h-5 w-5" /> : <Volume2 className="h-5 w-5" />}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleFullscreen}
|
|
||||||
className="rounded-full p-2 text-white transition-colors hover:bg-white/20"
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{title && <p className="bg-gray-800 px-4 py-2 text-sm text-white">{title}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Audio Player Component
|
|
||||||
*/
|
|
||||||
const AudioPlayer: React.FC<{ url: string; title?: string }> = ({ url, title }) => {
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
|
||||||
const [duration, setDuration] = useState(0);
|
|
||||||
|
|
||||||
const togglePlay = () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
if (isPlaying) {
|
|
||||||
audioRef.current.pause();
|
|
||||||
} else {
|
|
||||||
audioRef.current.play();
|
|
||||||
}
|
|
||||||
setIsPlaying(!isPlaying);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeUpdate = () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
setCurrentTime(audioRef.current.currentTime);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadedMetadata = () => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
setDuration(audioRef.current.duration);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const time = parseFloat(e.target.value);
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.currentTime = time;
|
|
||||||
setCurrentTime(time);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAudioTime = (time: number): string => {
|
|
||||||
const mins = Math.floor(time / 60);
|
|
||||||
const secs = Math.floor(time % 60);
|
|
||||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-purple-200 bg-gradient-to-r from-purple-50 to-indigo-50 p-4">
|
|
||||||
<audio
|
|
||||||
ref={audioRef}
|
|
||||||
src={url}
|
|
||||||
onTimeUpdate={handleTimeUpdate}
|
|
||||||
onLoadedMetadata={handleLoadedMetadata}
|
|
||||||
onEnded={() => setIsPlaying(false)}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-purple-600 text-white">
|
|
||||||
<Music className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
{title && <p className="mb-1 font-medium text-gray-800">{title}</p>}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
onClick={togglePlay}
|
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-purple-600 text-white transition-colors hover:bg-purple-700"
|
|
||||||
>
|
|
||||||
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={duration}
|
|
||||||
value={currentTime}
|
|
||||||
onChange={handleSeek}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-gray-600">
|
|
||||||
{formatAudioTime(currentTime)} / {formatAudioTime(duration)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Image Gallery Component
|
|
||||||
*/
|
|
||||||
const ImageGallery: React.FC<{ images: string[]; title?: string }> = ({ images, title }) => {
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
||||||
const [isLightboxOpen, setIsLightboxOpen] = useState(false);
|
|
||||||
|
|
||||||
if (images.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{title && <p className="font-medium text-gray-800">{title}</p>}
|
|
||||||
{/* Main Image */}
|
|
||||||
<div
|
|
||||||
className="group relative cursor-pointer overflow-hidden rounded-xl border border-gray-200"
|
|
||||||
onClick={() => setIsLightboxOpen(true)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={images[selectedIndex]}
|
|
||||||
alt={`Imagen ${selectedIndex + 1}`}
|
|
||||||
className="h-64 w-full object-contain bg-gray-100"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/0 transition-colors group-hover:bg-black/30">
|
|
||||||
<Maximize2 className="h-8 w-8 text-white opacity-0 transition-opacity group-hover:opacity-100" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Thumbnails */}
|
|
||||||
{images.length > 1 && (
|
|
||||||
<div className="flex gap-2 overflow-x-auto py-2">
|
|
||||||
{images.map((img, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
onClick={() => setSelectedIndex(index)}
|
|
||||||
className={`h-16 w-16 flex-shrink-0 overflow-hidden rounded-lg border-2 transition-all ${
|
|
||||||
index === selectedIndex ? 'border-orange-500' : 'border-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<img src={img} alt={`Miniatura ${index + 1}`} className="h-full w-full object-cover" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Lightbox */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{isLightboxOpen && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/90 p-4"
|
|
||||||
onClick={() => setIsLightboxOpen(false)}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsLightboxOpen(false)}
|
|
||||||
className="absolute right-4 top-4 rounded-full p-2 text-white hover:bg-white/20"
|
|
||||||
>
|
|
||||||
<X className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
<img
|
|
||||||
src={images[selectedIndex]}
|
|
||||||
alt={`Imagen ${selectedIndex + 1}`}
|
|
||||||
className="max-h-[90vh] max-w-[90vw] object-contain"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
{images.length > 1 && (
|
|
||||||
<div className="absolute bottom-4 flex gap-2">
|
|
||||||
{images.map((_, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setSelectedIndex(index);
|
|
||||||
}}
|
|
||||||
className={`h-2 w-2 rounded-full ${
|
|
||||||
index === selectedIndex ? 'bg-white' : 'bg-white/50'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multimedia Content Section
|
|
||||||
* Renders all multimedia content found in the answer
|
|
||||||
*/
|
|
||||||
const MultimediaContent: React.FC<{
|
|
||||||
answerData: Record<string, unknown>;
|
|
||||||
exerciseType: string;
|
|
||||||
}> = ({ answerData, exerciseType }) => {
|
|
||||||
const mediaContent = extractMediaContent(answerData);
|
|
||||||
|
|
||||||
if (mediaContent.length === 0 && !hasMultimediaContent(exerciseType)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const videos = mediaContent.filter((m) => m.type === 'video');
|
|
||||||
const audios = mediaContent.filter((m) => m.type === 'audio');
|
|
||||||
const images = mediaContent.filter((m) => m.type === 'image');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 rounded-xl border border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50 p-4">
|
|
||||||
<h3 className="flex items-center gap-2 font-bold text-gray-800">
|
|
||||||
{videos.length > 0 && <Video className="h-5 w-5 text-blue-600" />}
|
|
||||||
{audios.length > 0 && <Music className="h-5 w-5 text-purple-600" />}
|
|
||||||
{images.length > 0 && <ImageIcon className="h-5 w-5 text-green-600" />}
|
|
||||||
Contenido Multimedia
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Videos */}
|
|
||||||
{videos.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{videos.map((video, index) => (
|
|
||||||
<VideoPlayer
|
|
||||||
key={index}
|
|
||||||
url={video.url!}
|
|
||||||
title={videos.length > 1 ? `Video ${index + 1}` : undefined}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Audios */}
|
|
||||||
{audios.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{audios.map((audio, index) => (
|
|
||||||
<AudioPlayer
|
|
||||||
key={index}
|
|
||||||
url={audio.url!}
|
|
||||||
title={audios.length > 1 ? `Audio ${index + 1}` : exerciseType === 'podcast_argumentativo' ? 'Podcast' : undefined}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Images */}
|
|
||||||
{images.length > 0 && (
|
|
||||||
<ImageGallery
|
|
||||||
images={images.map((img) => img.url!)}
|
|
||||||
title={exerciseType === 'comic_digital' ? 'Paneles del C\u00f3mic' : exerciseType === 'collage_prensa' ? 'Collage' : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Download Links */}
|
|
||||||
{mediaContent.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2 border-t border-blue-200 pt-3">
|
|
||||||
{mediaContent.map((media, index) => (
|
|
||||||
<a
|
|
||||||
key={index}
|
|
||||||
href={media.url}
|
|
||||||
download
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 rounded-lg bg-white px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4" />
|
|
||||||
Descargar {media.type === 'video' ? 'Video' : media.type === 'audio' ? 'Audio' : 'Imagen'}{' '}
|
|
||||||
{mediaContent.length > 1 ? index + 1 : ''}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// SUB-COMPONENTS
|
// SUB-COMPONENTS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -873,7 +379,7 @@ export const ResponseDetailModal: React.FC<ResponseDetailModalProps> = ({
|
|||||||
{/* Answer Comparison */}
|
{/* Answer Comparison */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-3 text-lg font-bold text-gray-800">
|
<h3 className="mb-3 text-lg font-bold text-gray-800">
|
||||||
Comparaci\u00f3n de Respuestas
|
Comparación de Respuestas
|
||||||
</h3>
|
</h3>
|
||||||
<AnswerComparison
|
<AnswerComparison
|
||||||
studentAnswer={attempt.submitted_answers}
|
studentAnswer={attempt.submitted_answers}
|
||||||
@ -882,14 +388,6 @@ export const ResponseDetailModal: React.FC<ResponseDetailModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* P2-03: Multimedia Content Section */}
|
|
||||||
{hasMultimediaContent(attempt.exercise_type) && (
|
|
||||||
<MultimediaContent
|
|
||||||
answerData={attempt.submitted_answers}
|
|
||||||
exerciseType={attempt.exercise_type}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Metadata */}
|
{/* Metadata */}
|
||||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
|
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
|
|||||||
@ -36,22 +36,3 @@ export type { UseAssignmentsReturn } from './useAssignments';
|
|||||||
export type { UseInterventionAlertsReturn, AlertFilters } from './useInterventionAlerts';
|
export type { UseInterventionAlertsReturn, AlertFilters } from './useInterventionAlerts';
|
||||||
export type { UseTeacherMessagesReturn, MessageFilters } from './useTeacherMessages';
|
export type { UseTeacherMessagesReturn, MessageFilters } from './useTeacherMessages';
|
||||||
export type { UseGrantBonusReturn } from './useGrantBonus';
|
export type { UseGrantBonusReturn } from './useGrantBonus';
|
||||||
|
|
||||||
// P1-06/P1-07: Mission and Mastery tracking hooks (2025-12-18)
|
|
||||||
export { useMissionStats, useMissionStatsMultiple } from './useMissionStats';
|
|
||||||
export { useMasteryTracking } from './useMasteryTracking';
|
|
||||||
export type { UseMissionStatsReturn, MissionStats, ClassroomMission } from './useMissionStats';
|
|
||||||
export type { UseMasteryTrackingReturn, MasteryData, SkillMastery } from './useMasteryTracking';
|
|
||||||
|
|
||||||
// P2-01: Real-time classroom monitoring (2025-12-18)
|
|
||||||
export { useClassroomRealtime } from './useClassroomRealtime';
|
|
||||||
export type {
|
|
||||||
UseClassroomRealtimeReturn,
|
|
||||||
StudentActivity,
|
|
||||||
ClassroomUpdate,
|
|
||||||
NewSubmission,
|
|
||||||
AlertTriggered,
|
|
||||||
StudentOnlineStatus,
|
|
||||||
ProgressUpdate,
|
|
||||||
RealtimeEvent,
|
|
||||||
} from './useClassroomRealtime';
|
|
||||||
|
|||||||
@ -1,383 +0,0 @@
|
|||||||
/**
|
|
||||||
* useClassroomRealtime Hook
|
|
||||||
*
|
|
||||||
* P2-01: Created 2025-12-18
|
|
||||||
* Real-time classroom monitoring via WebSocket for Teacher Portal.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Subscribe to classroom activity updates
|
|
||||||
* - Receive real-time student activity notifications
|
|
||||||
* - Handle new submissions and alerts
|
|
||||||
* - Track student online/offline status
|
|
||||||
* - Auto-reconnect and connection status
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
||||||
import { io, Socket } from 'socket.io-client';
|
|
||||||
import { useAuth } from '@/features/auth/hooks/useAuth';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TYPES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface StudentActivity {
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
classroomId: string;
|
|
||||||
activityType: 'exercise_start' | 'exercise_complete' | 'hint_used' | 'comodin_used' | 'module_start';
|
|
||||||
exerciseId?: string;
|
|
||||||
exerciseTitle?: string;
|
|
||||||
moduleId?: string;
|
|
||||||
moduleTitle?: string;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassroomUpdate {
|
|
||||||
classroomId: string;
|
|
||||||
classroomName: string;
|
|
||||||
updateType: 'student_joined' | 'student_left' | 'stats_changed';
|
|
||||||
data: Record<string, unknown>;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NewSubmission {
|
|
||||||
submissionId: string;
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
exerciseId: string;
|
|
||||||
exerciseTitle: string;
|
|
||||||
classroomId: string;
|
|
||||||
score: number;
|
|
||||||
maxScore: number;
|
|
||||||
requiresReview: boolean;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlertTriggered {
|
|
||||||
alertId: string;
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
classroomId: string;
|
|
||||||
alertType: 'at_risk' | 'low_performance' | 'inactive' | 'struggling';
|
|
||||||
severity: 'low' | 'medium' | 'high';
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StudentOnlineStatus {
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
classroomId: string;
|
|
||||||
isOnline: boolean;
|
|
||||||
lastActivity?: string;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProgressUpdate {
|
|
||||||
studentId: string;
|
|
||||||
studentName: string;
|
|
||||||
classroomId: string;
|
|
||||||
progressType: 'module_complete' | 'exercise_complete' | 'level_up' | 'achievement';
|
|
||||||
details: Record<string, unknown>;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RealtimeEvent =
|
|
||||||
| { type: 'activity'; data: StudentActivity }
|
|
||||||
| { type: 'classroom_update'; data: ClassroomUpdate }
|
|
||||||
| { type: 'submission'; data: NewSubmission }
|
|
||||||
| { type: 'alert'; data: AlertTriggered }
|
|
||||||
| { type: 'online_status'; data: StudentOnlineStatus }
|
|
||||||
| { type: 'progress'; data: ProgressUpdate };
|
|
||||||
|
|
||||||
export interface UseClassroomRealtimeOptions {
|
|
||||||
classroomIds: string[];
|
|
||||||
onActivity?: (data: StudentActivity) => void;
|
|
||||||
onSubmission?: (data: NewSubmission) => void;
|
|
||||||
onAlert?: (data: AlertTriggered) => void;
|
|
||||||
onStudentOnline?: (data: StudentOnlineStatus) => void;
|
|
||||||
onStudentOffline?: (data: StudentOnlineStatus) => void;
|
|
||||||
onProgressUpdate?: (data: ProgressUpdate) => void;
|
|
||||||
onClassroomUpdate?: (data: ClassroomUpdate) => void;
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseClassroomRealtimeReturn {
|
|
||||||
isConnected: boolean;
|
|
||||||
isConnecting: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
events: RealtimeEvent[];
|
|
||||||
onlineStudents: Map<string, StudentOnlineStatus>;
|
|
||||||
clearEvents: () => void;
|
|
||||||
reconnect: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// SOCKET EVENTS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const SocketEvents = {
|
|
||||||
// Client -> Server
|
|
||||||
SUBSCRIBE_CLASSROOM: 'teacher:subscribe_classroom',
|
|
||||||
UNSUBSCRIBE_CLASSROOM: 'teacher:unsubscribe_classroom',
|
|
||||||
// Server -> Client
|
|
||||||
STUDENT_ACTIVITY: 'teacher:student_activity',
|
|
||||||
CLASSROOM_UPDATE: 'teacher:classroom_update',
|
|
||||||
NEW_SUBMISSION: 'teacher:new_submission',
|
|
||||||
ALERT_TRIGGERED: 'teacher:alert_triggered',
|
|
||||||
STUDENT_ONLINE: 'teacher:student_online',
|
|
||||||
STUDENT_OFFLINE: 'teacher:student_offline',
|
|
||||||
PROGRESS_UPDATE: 'teacher:progress_update',
|
|
||||||
AUTHENTICATED: 'authenticated',
|
|
||||||
ERROR: 'error',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HOOK IMPLEMENTATION
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export function useClassroomRealtime(
|
|
||||||
options: UseClassroomRealtimeOptions,
|
|
||||||
): UseClassroomRealtimeReturn {
|
|
||||||
const {
|
|
||||||
classroomIds,
|
|
||||||
onActivity,
|
|
||||||
onSubmission,
|
|
||||||
onAlert,
|
|
||||||
onStudentOnline,
|
|
||||||
onStudentOffline,
|
|
||||||
onProgressUpdate,
|
|
||||||
onClassroomUpdate,
|
|
||||||
enabled = true,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const { user, token } = useAuth();
|
|
||||||
const socketRef = useRef<Socket | null>(null);
|
|
||||||
const subscribedRoomsRef = useRef<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
const [events, setEvents] = useState<RealtimeEvent[]>([]);
|
|
||||||
const [onlineStudents, setOnlineStudents] = useState<Map<string, StudentOnlineStatus>>(
|
|
||||||
new Map(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add event to history
|
|
||||||
const addEvent = useCallback((event: RealtimeEvent) => {
|
|
||||||
setEvents((prev) => {
|
|
||||||
const newEvents = [event, ...prev];
|
|
||||||
// Keep only last 100 events
|
|
||||||
return newEvents.slice(0, 100);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Clear events
|
|
||||||
const clearEvents = useCallback(() => {
|
|
||||||
setEvents([]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Subscribe to a classroom
|
|
||||||
const subscribeToClassroom = useCallback((socket: Socket, classroomId: string) => {
|
|
||||||
if (subscribedRoomsRef.current.has(classroomId)) return;
|
|
||||||
|
|
||||||
socket.emit(SocketEvents.SUBSCRIBE_CLASSROOM, { classroomId }, (response: { success: boolean }) => {
|
|
||||||
if (response?.success) {
|
|
||||||
subscribedRoomsRef.current.add(classroomId);
|
|
||||||
console.log(`[useClassroomRealtime] Subscribed to classroom ${classroomId}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Unsubscribe from a classroom
|
|
||||||
const unsubscribeFromClassroom = useCallback((socket: Socket, classroomId: string) => {
|
|
||||||
if (!subscribedRoomsRef.current.has(classroomId)) return;
|
|
||||||
|
|
||||||
socket.emit(SocketEvents.UNSUBSCRIBE_CLASSROOM, { classroomId }, (response: { success: boolean }) => {
|
|
||||||
if (response?.success) {
|
|
||||||
subscribedRoomsRef.current.delete(classroomId);
|
|
||||||
console.log(`[useClassroomRealtime] Unsubscribed from classroom ${classroomId}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Connect to WebSocket
|
|
||||||
const connect = useCallback(() => {
|
|
||||||
if (!enabled || !user || !token) return;
|
|
||||||
|
|
||||||
const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:3000';
|
|
||||||
|
|
||||||
setIsConnecting(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const socket = io(wsUrl, {
|
|
||||||
path: '/socket.io/',
|
|
||||||
transports: ['websocket', 'polling'],
|
|
||||||
auth: { token },
|
|
||||||
reconnection: true,
|
|
||||||
reconnectionAttempts: 5,
|
|
||||||
reconnectionDelay: 1000,
|
|
||||||
reconnectionDelayMax: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('connect', () => {
|
|
||||||
console.log('[useClassroomRealtime] Connected');
|
|
||||||
setIsConnecting(false);
|
|
||||||
setIsConnected(true);
|
|
||||||
setError(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on(SocketEvents.AUTHENTICATED, () => {
|
|
||||||
console.log('[useClassroomRealtime] Authenticated');
|
|
||||||
// Subscribe to all classrooms
|
|
||||||
classroomIds.forEach((id) => subscribeToClassroom(socket, id));
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
|
||||||
console.log('[useClassroomRealtime] Disconnected:', reason);
|
|
||||||
setIsConnected(false);
|
|
||||||
subscribedRoomsRef.current.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('connect_error', (err) => {
|
|
||||||
console.error('[useClassroomRealtime] Connection error:', err);
|
|
||||||
setIsConnecting(false);
|
|
||||||
setError(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on(SocketEvents.ERROR, (data: { message: string }) => {
|
|
||||||
console.error('[useClassroomRealtime] Socket error:', data.message);
|
|
||||||
setError(new Error(data.message));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Activity event
|
|
||||||
socket.on(SocketEvents.STUDENT_ACTIVITY, (data: StudentActivity) => {
|
|
||||||
addEvent({ type: 'activity', data });
|
|
||||||
onActivity?.(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Classroom update event
|
|
||||||
socket.on(SocketEvents.CLASSROOM_UPDATE, (data: ClassroomUpdate) => {
|
|
||||||
addEvent({ type: 'classroom_update', data });
|
|
||||||
onClassroomUpdate?.(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// New submission event
|
|
||||||
socket.on(SocketEvents.NEW_SUBMISSION, (data: NewSubmission) => {
|
|
||||||
addEvent({ type: 'submission', data });
|
|
||||||
onSubmission?.(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Alert triggered event
|
|
||||||
socket.on(SocketEvents.ALERT_TRIGGERED, (data: AlertTriggered) => {
|
|
||||||
addEvent({ type: 'alert', data });
|
|
||||||
onAlert?.(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Student online event
|
|
||||||
socket.on(SocketEvents.STUDENT_ONLINE, (data: StudentOnlineStatus) => {
|
|
||||||
setOnlineStudents((prev) => {
|
|
||||||
const updated = new Map(prev);
|
|
||||||
updated.set(data.studentId, data);
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
addEvent({ type: 'online_status', data });
|
|
||||||
onStudentOnline?.(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Student offline event
|
|
||||||
socket.on(SocketEvents.STUDENT_OFFLINE, (data: StudentOnlineStatus) => {
|
|
||||||
setOnlineStudents((prev) => {
|
|
||||||
const updated = new Map(prev);
|
|
||||||
updated.delete(data.studentId);
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
addEvent({ type: 'online_status', data });
|
|
||||||
onStudentOffline?.(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Progress update event
|
|
||||||
socket.on(SocketEvents.PROGRESS_UPDATE, (data: ProgressUpdate) => {
|
|
||||||
addEvent({ type: 'progress', data });
|
|
||||||
onProgressUpdate?.(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socketRef.current = socket;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.disconnect();
|
|
||||||
socketRef.current = null;
|
|
||||||
subscribedRoomsRef.current.clear();
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
enabled,
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
classroomIds,
|
|
||||||
subscribeToClassroom,
|
|
||||||
addEvent,
|
|
||||||
onActivity,
|
|
||||||
onSubmission,
|
|
||||||
onAlert,
|
|
||||||
onStudentOnline,
|
|
||||||
onStudentOffline,
|
|
||||||
onProgressUpdate,
|
|
||||||
onClassroomUpdate,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Reconnect function
|
|
||||||
const reconnect = useCallback(() => {
|
|
||||||
if (socketRef.current) {
|
|
||||||
socketRef.current.disconnect();
|
|
||||||
socketRef.current = null;
|
|
||||||
}
|
|
||||||
subscribedRoomsRef.current.clear();
|
|
||||||
connect();
|
|
||||||
}, [connect]);
|
|
||||||
|
|
||||||
// Effect to connect on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const cleanup = connect();
|
|
||||||
return () => {
|
|
||||||
cleanup?.();
|
|
||||||
};
|
|
||||||
}, [connect]);
|
|
||||||
|
|
||||||
// Effect to handle classroom subscription changes
|
|
||||||
useEffect(() => {
|
|
||||||
const socket = socketRef.current;
|
|
||||||
if (!socket || !isConnected) return;
|
|
||||||
|
|
||||||
// Get current subscriptions
|
|
||||||
const currentSubs = subscribedRoomsRef.current;
|
|
||||||
const newClassroomIds = new Set(classroomIds);
|
|
||||||
|
|
||||||
// Subscribe to new classrooms
|
|
||||||
classroomIds.forEach((id) => {
|
|
||||||
if (!currentSubs.has(id)) {
|
|
||||||
subscribeToClassroom(socket, id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unsubscribe from removed classrooms
|
|
||||||
currentSubs.forEach((id) => {
|
|
||||||
if (!newClassroomIds.has(id)) {
|
|
||||||
unsubscribeFromClassroom(socket, id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [classroomIds, isConnected, subscribeToClassroom, unsubscribeFromClassroom]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isConnected,
|
|
||||||
isConnecting,
|
|
||||||
error,
|
|
||||||
events,
|
|
||||||
onlineStudents,
|
|
||||||
clearEvents,
|
|
||||||
reconnect,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useClassroomRealtime;
|
|
||||||
@ -1,353 +0,0 @@
|
|||||||
/**
|
|
||||||
* useMasteryTracking Hook
|
|
||||||
*
|
|
||||||
* P1-07: Created 2025-12-18
|
|
||||||
* Tracks student mastery of skills and competencies for Teacher Portal.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Individual student mastery tracking
|
|
||||||
* - Classroom mastery overview
|
|
||||||
* - Skill-level progression
|
|
||||||
* - Competency assessment
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { apiClient } from '@/services/api/apiClient';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TYPES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface SkillMastery {
|
|
||||||
skill_id: string;
|
|
||||||
skill_name: string;
|
|
||||||
category: 'comprehension' | 'analysis' | 'synthesis' | 'evaluation' | 'creation';
|
|
||||||
mastery_level: 'novice' | 'developing' | 'proficient' | 'advanced' | 'expert';
|
|
||||||
mastery_percentage: number;
|
|
||||||
exercises_completed: number;
|
|
||||||
exercises_total: number;
|
|
||||||
average_score: number;
|
|
||||||
last_practiced: string | null;
|
|
||||||
trend: 'improving' | 'stable' | 'declining';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CompetencyProgress {
|
|
||||||
competency_id: string;
|
|
||||||
competency_name: string;
|
|
||||||
description: string;
|
|
||||||
skills: SkillMastery[];
|
|
||||||
overall_mastery: number;
|
|
||||||
status: 'not_started' | 'in_progress' | 'mastered';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MasteryData {
|
|
||||||
student_id: string;
|
|
||||||
student_name: string;
|
|
||||||
overall_mastery: number;
|
|
||||||
mastery_level: 'novice' | 'developing' | 'proficient' | 'advanced' | 'expert';
|
|
||||||
competencies: CompetencyProgress[];
|
|
||||||
strengths: SkillMastery[];
|
|
||||||
areas_for_improvement: SkillMastery[];
|
|
||||||
learning_velocity: number; // Skills mastered per week
|
|
||||||
time_to_mastery_estimate?: number; // Days to complete current module
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClassroomMasteryOverview {
|
|
||||||
classroom_id: string;
|
|
||||||
classroom_name: string;
|
|
||||||
average_mastery: number;
|
|
||||||
students_by_level: {
|
|
||||||
novice: number;
|
|
||||||
developing: number;
|
|
||||||
proficient: number;
|
|
||||||
advanced: number;
|
|
||||||
expert: number;
|
|
||||||
};
|
|
||||||
top_skills: SkillMastery[];
|
|
||||||
struggling_skills: SkillMastery[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseMasteryTrackingReturn {
|
|
||||||
data: MasteryData | null;
|
|
||||||
loading: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
refresh: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseClassroomMasteryReturn {
|
|
||||||
overview: ClassroomMasteryOverview | null;
|
|
||||||
students: MasteryData[];
|
|
||||||
loading: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
refresh: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPER FUNCTIONS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate mastery level from percentage
|
|
||||||
*/
|
|
||||||
const getMasteryLevel = (percentage: number): SkillMastery['mastery_level'] => {
|
|
||||||
if (percentage >= 90) return 'expert';
|
|
||||||
if (percentage >= 75) return 'advanced';
|
|
||||||
if (percentage >= 60) return 'proficient';
|
|
||||||
if (percentage >= 40) return 'developing';
|
|
||||||
return 'novice';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map module progress to skill mastery
|
|
||||||
*/
|
|
||||||
const mapModuleToSkills = (moduleProgress: any): SkillMastery[] => {
|
|
||||||
// Define skills per module based on GAMILIT's 5 reading comprehension levels
|
|
||||||
const skillMappings: Record<number, { name: string; category: SkillMastery['category'] }[]> = {
|
|
||||||
1: [
|
|
||||||
{ name: 'Identificación de Ideas Principales', category: 'comprehension' },
|
|
||||||
{ name: 'Vocabulario Contextual', category: 'comprehension' },
|
|
||||||
],
|
|
||||||
2: [
|
|
||||||
{ name: 'Inferencia Textual', category: 'analysis' },
|
|
||||||
{ name: 'Predicción Narrativa', category: 'analysis' },
|
|
||||||
],
|
|
||||||
3: [
|
|
||||||
{ name: 'Análisis Crítico', category: 'evaluation' },
|
|
||||||
{ name: 'Evaluación de Argumentos', category: 'evaluation' },
|
|
||||||
],
|
|
||||||
4: [
|
|
||||||
{ name: 'Alfabetización Mediática', category: 'synthesis' },
|
|
||||||
{ name: 'Verificación de Fuentes', category: 'synthesis' },
|
|
||||||
],
|
|
||||||
5: [
|
|
||||||
{ name: 'Creación de Contenido', category: 'creation' },
|
|
||||||
{ name: 'Expresión Multimedia', category: 'creation' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const moduleOrder = moduleProgress.module_order || 1;
|
|
||||||
const skills = skillMappings[moduleOrder] || skillMappings[1];
|
|
||||||
|
|
||||||
return skills.map((skill, index) => ({
|
|
||||||
skill_id: `skill-${moduleOrder}-${index}`,
|
|
||||||
skill_name: skill.name,
|
|
||||||
category: skill.category,
|
|
||||||
mastery_level: getMasteryLevel(moduleProgress.average_score || 0),
|
|
||||||
mastery_percentage: moduleProgress.average_score || 0,
|
|
||||||
exercises_completed: moduleProgress.completed_activities || 0,
|
|
||||||
exercises_total: moduleProgress.total_activities || 15,
|
|
||||||
average_score: moduleProgress.average_score || 0,
|
|
||||||
last_practiced: moduleProgress.last_activity_date || null,
|
|
||||||
trend: 'stable' as const,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HOOKS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useMasteryTracking
|
|
||||||
*
|
|
||||||
* Tracks mastery progress for an individual student
|
|
||||||
*
|
|
||||||
* @param studentId - ID of the student to track
|
|
||||||
* @returns Mastery data, loading state, error, and refresh function
|
|
||||||
*/
|
|
||||||
export function useMasteryTracking(studentId: string): UseMasteryTrackingReturn {
|
|
||||||
const [data, setData] = useState<MasteryData | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
const fetchMastery = useCallback(async () => {
|
|
||||||
if (!studentId) {
|
|
||||||
setData(null);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Fetch student progress data
|
|
||||||
const response = await apiClient.get(`/teacher/students/${studentId}/progress`);
|
|
||||||
const progressData = response.data.data || response.data;
|
|
||||||
|
|
||||||
// Extract module progress
|
|
||||||
const moduleProgress = progressData.moduleProgress || [];
|
|
||||||
|
|
||||||
// Map to skills
|
|
||||||
const allSkills: SkillMastery[] = moduleProgress.flatMap(mapModuleToSkills);
|
|
||||||
|
|
||||||
// Calculate overall mastery
|
|
||||||
const overallMastery = allSkills.length > 0
|
|
||||||
? Math.round(allSkills.reduce((sum, s) => sum + s.mastery_percentage, 0) / allSkills.length)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Identify strengths and areas for improvement
|
|
||||||
const sortedSkills = [...allSkills].sort((a, b) => b.mastery_percentage - a.mastery_percentage);
|
|
||||||
const strengths = sortedSkills.slice(0, 3);
|
|
||||||
const areasForImprovement = sortedSkills.slice(-3).reverse();
|
|
||||||
|
|
||||||
// Group skills into competencies
|
|
||||||
const competencyMap = new Map<string, SkillMastery[]>();
|
|
||||||
allSkills.forEach(skill => {
|
|
||||||
const key = skill.category;
|
|
||||||
if (!competencyMap.has(key)) {
|
|
||||||
competencyMap.set(key, []);
|
|
||||||
}
|
|
||||||
competencyMap.get(key)!.push(skill);
|
|
||||||
});
|
|
||||||
|
|
||||||
const competencies: CompetencyProgress[] = Array.from(competencyMap.entries()).map(
|
|
||||||
([category, skills]) => {
|
|
||||||
const avgMastery = skills.reduce((sum, s) => sum + s.mastery_percentage, 0) / skills.length;
|
|
||||||
return {
|
|
||||||
competency_id: `comp-${category}`,
|
|
||||||
competency_name: getCategoryName(category as SkillMastery['category']),
|
|
||||||
description: getCategoryDescription(category as SkillMastery['category']),
|
|
||||||
skills,
|
|
||||||
overall_mastery: Math.round(avgMastery),
|
|
||||||
status: avgMastery >= 80 ? 'mastered' : avgMastery > 0 ? 'in_progress' : 'not_started',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
setData({
|
|
||||||
student_id: studentId,
|
|
||||||
student_name: progressData.student?.full_name || 'Estudiante',
|
|
||||||
overall_mastery: overallMastery,
|
|
||||||
mastery_level: getMasteryLevel(overallMastery),
|
|
||||||
competencies,
|
|
||||||
strengths,
|
|
||||||
areas_for_improvement: areasForImprovement,
|
|
||||||
learning_velocity: 2, // Placeholder - would need historical data
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[useMasteryTracking] Error:', err);
|
|
||||||
setError(err as Error);
|
|
||||||
setData(null);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [studentId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchMastery();
|
|
||||||
}, [fetchMastery]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: fetchMastery,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useClassroomMastery
|
|
||||||
*
|
|
||||||
* Tracks mastery overview for an entire classroom
|
|
||||||
*
|
|
||||||
* @param classroomId - ID of the classroom to track
|
|
||||||
* @returns Classroom mastery overview and individual student data
|
|
||||||
*/
|
|
||||||
export function useClassroomMastery(classroomId: string): UseClassroomMasteryReturn {
|
|
||||||
const [overview, setOverview] = useState<ClassroomMasteryOverview | null>(null);
|
|
||||||
const [students, setStudents] = useState<MasteryData[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
const fetchMastery = useCallback(async () => {
|
|
||||||
if (!classroomId) {
|
|
||||||
setOverview(null);
|
|
||||||
setStudents([]);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Fetch classroom students
|
|
||||||
const studentsResponse = await apiClient.get(`/teacher/classrooms/${classroomId}/students`);
|
|
||||||
const studentsData = studentsResponse.data.data || studentsResponse.data || [];
|
|
||||||
|
|
||||||
// Placeholder for aggregated data
|
|
||||||
// In a real implementation, this would come from a dedicated endpoint
|
|
||||||
const studentLevels = {
|
|
||||||
novice: 0,
|
|
||||||
developing: 0,
|
|
||||||
proficient: 0,
|
|
||||||
advanced: 0,
|
|
||||||
expert: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Count students per level (placeholder logic)
|
|
||||||
studentsData.forEach((student: any) => {
|
|
||||||
const level = getMasteryLevel(student.progress_percentage || 50);
|
|
||||||
studentLevels[level]++;
|
|
||||||
});
|
|
||||||
|
|
||||||
setOverview({
|
|
||||||
classroom_id: classroomId,
|
|
||||||
classroom_name: 'Classroom',
|
|
||||||
average_mastery: 65, // Placeholder
|
|
||||||
students_by_level: studentLevels,
|
|
||||||
top_skills: [],
|
|
||||||
struggling_skills: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
setStudents([]);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[useClassroomMastery] Error:', err);
|
|
||||||
setError(err as Error);
|
|
||||||
setOverview(null);
|
|
||||||
setStudents([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [classroomId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchMastery();
|
|
||||||
}, [fetchMastery]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
overview,
|
|
||||||
students,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: fetchMastery,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPER FUNCTIONS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function getCategoryName(category: SkillMastery['category']): string {
|
|
||||||
const names: Record<SkillMastery['category'], string> = {
|
|
||||||
comprehension: 'Comprensión Literal',
|
|
||||||
analysis: 'Comprensión Inferencial',
|
|
||||||
evaluation: 'Lectura Crítica',
|
|
||||||
synthesis: 'Alfabetización Mediática',
|
|
||||||
creation: 'Producción Textual',
|
|
||||||
};
|
|
||||||
return names[category];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCategoryDescription(category: SkillMastery['category']): string {
|
|
||||||
const descriptions: Record<SkillMastery['category'], string> = {
|
|
||||||
comprehension: 'Identificación y extracción de información explícita del texto',
|
|
||||||
analysis: 'Conexión de ideas y generación de inferencias a partir del texto',
|
|
||||||
evaluation: 'Análisis crítico y evaluación de argumentos y fuentes',
|
|
||||||
synthesis: 'Integración de múltiples fuentes y formatos de información',
|
|
||||||
creation: 'Producción de contenido original basado en la comprensión lectora',
|
|
||||||
};
|
|
||||||
return descriptions[category];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useMasteryTracking;
|
|
||||||
@ -1,234 +0,0 @@
|
|||||||
/**
|
|
||||||
* useMissionStats Hook
|
|
||||||
*
|
|
||||||
* P1-06: Created 2025-12-18
|
|
||||||
* Fetches and manages mission statistics for Teacher Portal.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Classroom missions overview
|
|
||||||
* - Completion rates and participation metrics
|
|
||||||
* - Top participants tracking
|
|
||||||
* - Active missions monitoring
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { apiClient } from '@/services/api/apiClient';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TYPES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface ClassroomMission {
|
|
||||||
id: string;
|
|
||||||
classroom_id: string;
|
|
||||||
mission_template_id: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
mission_type: 'daily' | 'weekly' | 'special';
|
|
||||||
objectives: Array<{
|
|
||||||
type: string;
|
|
||||||
target: number;
|
|
||||||
description?: string;
|
|
||||||
}>;
|
|
||||||
base_rewards: {
|
|
||||||
ml_coins: number;
|
|
||||||
xp: number;
|
|
||||||
};
|
|
||||||
total_rewards: {
|
|
||||||
ml_coins: number;
|
|
||||||
xp: number;
|
|
||||||
};
|
|
||||||
bonus_xp: number;
|
|
||||||
bonus_coins: number;
|
|
||||||
is_mandatory: boolean;
|
|
||||||
is_active: boolean;
|
|
||||||
due_date?: string;
|
|
||||||
assigned_at: string;
|
|
||||||
assigned_by: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MissionParticipant {
|
|
||||||
student_id: string;
|
|
||||||
student_name: string;
|
|
||||||
avatar_url?: string;
|
|
||||||
missions_completed: number;
|
|
||||||
total_xp_earned: number;
|
|
||||||
total_coins_earned: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MissionStats {
|
|
||||||
activeMissions: ClassroomMission[];
|
|
||||||
completionRate: number;
|
|
||||||
participationRate: number;
|
|
||||||
topParticipants: MissionParticipant[];
|
|
||||||
totalMissionsAssigned: number;
|
|
||||||
totalMissionsCompleted: number;
|
|
||||||
averageCompletionTime?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseMissionStatsReturn {
|
|
||||||
stats: MissionStats | null;
|
|
||||||
loading: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
refresh: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// API FUNCTIONS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const fetchClassroomMissions = async (classroomId: string): Promise<ClassroomMission[]> => {
|
|
||||||
const response = await apiClient.get(`/gamification/classrooms/${classroomId}/missions`);
|
|
||||||
return response.data.data || response.data || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HOOK
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useMissionStats
|
|
||||||
*
|
|
||||||
* @param classroomId - ID of the classroom to get mission stats for
|
|
||||||
* @returns Mission statistics, loading state, error, and refresh function
|
|
||||||
*/
|
|
||||||
export function useMissionStats(classroomId: string): UseMissionStatsReturn {
|
|
||||||
const [stats, setStats] = useState<MissionStats | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
const fetchStats = useCallback(async () => {
|
|
||||||
if (!classroomId) {
|
|
||||||
setStats(null);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Fetch missions for the classroom
|
|
||||||
const missions = await fetchClassroomMissions(classroomId);
|
|
||||||
|
|
||||||
// Filter active missions
|
|
||||||
const activeMissions = missions.filter(m => m.is_active);
|
|
||||||
|
|
||||||
// Calculate stats (in a real implementation, this would come from backend)
|
|
||||||
// For now, we'll calculate basic metrics from the missions data
|
|
||||||
const totalMissionsAssigned = missions.length;
|
|
||||||
const mandatoryMissions = missions.filter(m => m.is_mandatory);
|
|
||||||
|
|
||||||
// These would ideally come from a dedicated stats endpoint
|
|
||||||
// For now, we estimate based on available data
|
|
||||||
const completionRate = totalMissionsAssigned > 0
|
|
||||||
? Math.round((missions.filter(m => !m.is_active).length / totalMissionsAssigned) * 100)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Participation rate would require student progress data
|
|
||||||
// This is a placeholder - actual implementation would query student mission progress
|
|
||||||
const participationRate = activeMissions.length > 0 ? 75 : 0;
|
|
||||||
|
|
||||||
// Top participants would also require additional queries
|
|
||||||
// Placeholder for now
|
|
||||||
const topParticipants: MissionParticipant[] = [];
|
|
||||||
|
|
||||||
setStats({
|
|
||||||
activeMissions,
|
|
||||||
completionRate,
|
|
||||||
participationRate,
|
|
||||||
topParticipants,
|
|
||||||
totalMissionsAssigned,
|
|
||||||
totalMissionsCompleted: missions.filter(m => !m.is_active).length,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[useMissionStats] Error:', err);
|
|
||||||
setError(err as Error);
|
|
||||||
setStats(null);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [classroomId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchStats();
|
|
||||||
}, [fetchStats]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
stats,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: fetchStats,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useMissionStatsMultiple
|
|
||||||
*
|
|
||||||
* Fetches mission stats for multiple classrooms (useful for teacher dashboard)
|
|
||||||
*
|
|
||||||
* @param classroomIds - Array of classroom IDs
|
|
||||||
* @returns Aggregated mission statistics across all classrooms
|
|
||||||
*/
|
|
||||||
export function useMissionStatsMultiple(classroomIds: string[]): UseMissionStatsReturn {
|
|
||||||
const [stats, setStats] = useState<MissionStats | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
const fetchStats = useCallback(async () => {
|
|
||||||
if (!classroomIds.length) {
|
|
||||||
setStats(null);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Fetch missions for all classrooms in parallel
|
|
||||||
const allMissionsArrays = await Promise.all(
|
|
||||||
classroomIds.map(id => fetchClassroomMissions(id).catch(() => [])),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Flatten and aggregate
|
|
||||||
const allMissions = allMissionsArrays.flat();
|
|
||||||
const activeMissions = allMissions.filter(m => m.is_active);
|
|
||||||
|
|
||||||
const totalMissionsAssigned = allMissions.length;
|
|
||||||
const totalMissionsCompleted = allMissions.filter(m => !m.is_active).length;
|
|
||||||
|
|
||||||
const completionRate = totalMissionsAssigned > 0
|
|
||||||
? Math.round((totalMissionsCompleted / totalMissionsAssigned) * 100)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
setStats({
|
|
||||||
activeMissions,
|
|
||||||
completionRate,
|
|
||||||
participationRate: activeMissions.length > 0 ? 75 : 0,
|
|
||||||
topParticipants: [],
|
|
||||||
totalMissionsAssigned,
|
|
||||||
totalMissionsCompleted,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[useMissionStatsMultiple] Error:', err);
|
|
||||||
setError(err as Error);
|
|
||||||
setStats(null);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [classroomIds]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchStats();
|
|
||||||
}, [fetchStats]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
stats,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: fetchStats,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useMissionStats;
|
|
||||||
@ -9,8 +9,9 @@
|
|||||||
* - Filtros y búsqueda
|
* - Filtros y búsqueda
|
||||||
* - Paginación
|
* - Paginación
|
||||||
*
|
*
|
||||||
* ESTADO: HABILITADO (2025-12-18)
|
* ESTADO: Descartada para Fase 2 (no depende de actividad del estudiante)
|
||||||
* Funcionalidad completa disponible.
|
* La funcionalidad está implementada pero deshabilitada temporalmente.
|
||||||
|
* Cambiar SHOW_UNDER_CONSTRUCTION a false para habilitar.
|
||||||
*
|
*
|
||||||
* @module apps/teacher/pages/TeacherCommunicationPage
|
* @module apps/teacher/pages/TeacherCommunicationPage
|
||||||
*/
|
*/
|
||||||
@ -33,9 +34,9 @@ import { Message } from '../../../services/api/teacher/teacherMessagesApi';
|
|||||||
import { classroomsApi } from '../../../services/api/teacher/classroomsApi';
|
import { classroomsApi } from '../../../services/api/teacher/classroomsApi';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FEATURE FLAG - Habilitado 2025-12-18
|
// FEATURE FLAG - Cambiar a false para habilitar la funcionalidad completa
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const SHOW_UNDER_CONSTRUCTION = false;
|
const SHOW_UNDER_CONSTRUCTION = true;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TYPES
|
// TYPES
|
||||||
|
|||||||
@ -5,15 +5,16 @@ import TeacherContentManagement from './TeacherContentManagement';
|
|||||||
import { UnderConstruction } from '@shared/components/UnderConstruction';
|
import { UnderConstruction } from '@shared/components/UnderConstruction';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FEATURE FLAG - Habilitado 2025-12-18
|
// FEATURE FLAG - Cambiar a false para habilitar la funcionalidad completa
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const SHOW_UNDER_CONSTRUCTION = false;
|
const SHOW_UNDER_CONSTRUCTION = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TeacherContentPage - Página de gestión de contenido educativo
|
* TeacherContentPage - Página de gestión de contenido educativo
|
||||||
*
|
*
|
||||||
* ESTADO: HABILITADO (2025-12-18)
|
* ESTADO: Descartada para Fase 2 (no depende de actividad del estudiante)
|
||||||
* Funcionalidad completa disponible.
|
* La funcionalidad está implementada pero deshabilitada temporalmente.
|
||||||
|
* Cambiar SHOW_UNDER_CONSTRUCTION a false para habilitar.
|
||||||
*/
|
*/
|
||||||
export default function TeacherContentPage() {
|
export default function TeacherContentPage() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|||||||
@ -4,10 +4,6 @@ import { FeedbackModal } from '@shared/components/mechanics/FeedbackModal';
|
|||||||
import { MatchingCard } from './MatchingCard';
|
import { MatchingCard } from './MatchingCard';
|
||||||
import { EmparejamientoExerciseProps } from './emparejamientoTypes';
|
import { EmparejamientoExerciseProps } from './emparejamientoTypes';
|
||||||
import { calculateScore, FeedbackData } from '@shared/components/mechanics/mechanicsTypes';
|
import { calculateScore, FeedbackData } from '@shared/components/mechanics/mechanicsTypes';
|
||||||
// P0-02: Added 2025-12-18 - Backend submission for progress persistence
|
|
||||||
import { submitExercise } from '@/features/progress/api/progressAPI';
|
|
||||||
import { useAuth } from '@/features/auth/hooks/useAuth';
|
|
||||||
import { useInvalidateDashboard } from '@/shared/hooks';
|
|
||||||
|
|
||||||
export const EmparejamientoExercise: React.FC<EmparejamientoExerciseProps> = ({
|
export const EmparejamientoExercise: React.FC<EmparejamientoExerciseProps> = ({
|
||||||
exercise,
|
exercise,
|
||||||
@ -22,10 +18,6 @@ export const EmparejamientoExercise: React.FC<EmparejamientoExerciseProps> = ({
|
|||||||
const [startTime] = useState(new Date());
|
const [startTime] = useState(new Date());
|
||||||
const [hintsUsed] = useState(0);
|
const [hintsUsed] = useState(0);
|
||||||
|
|
||||||
// P0-02: Added 2025-12-18 - Hooks for backend submission
|
|
||||||
const { user } = useAuth();
|
|
||||||
const invalidateDashboard = useInvalidateDashboard();
|
|
||||||
|
|
||||||
// FE-055: Notify parent of progress updates WITH user answers
|
// FE-055: Notify parent of progress updates WITH user answers
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (onProgressUpdate) {
|
if (onProgressUpdate) {
|
||||||
@ -99,70 +91,15 @@ export const EmparejamientoExercise: React.FC<EmparejamientoExerciseProps> = ({
|
|||||||
const isComplete = matched === total;
|
const isComplete = matched === total;
|
||||||
const score = calculateScore(matched / 2, total / 2);
|
const score = calculateScore(matched / 2, total / 2);
|
||||||
|
|
||||||
// P0-02: Submit to backend when complete
|
setFeedback({
|
||||||
if (isComplete && user?.id) {
|
type: isComplete ? 'success' : 'error',
|
||||||
try {
|
title: isComplete ? '¡Completado!' : 'Faltan parejas',
|
||||||
// Prepare matched pairs for submission
|
message: isComplete
|
||||||
const matchedCards = cards.filter((c) => c.isMatched);
|
? '¡Emparejaste todas las tarjetas correctamente!'
|
||||||
const matchGroups: Record<string, typeof cards> = {};
|
: `Emparejaste ${matched / 2} de ${total / 2} parejas.`,
|
||||||
matchedCards.forEach((card) => {
|
score: isComplete ? score : undefined,
|
||||||
if (!matchGroups[card.matchId]) {
|
showConfetti: isComplete,
|
||||||
matchGroups[card.matchId] = [];
|
});
|
||||||
}
|
|
||||||
matchGroups[card.matchId].push(card);
|
|
||||||
});
|
|
||||||
|
|
||||||
const matches = Object.values(matchGroups).map((group) => {
|
|
||||||
const left = group.find((c) => c.type === 'question');
|
|
||||||
const right = group.find((c) => c.type === 'answer');
|
|
||||||
return { leftId: left?.id, rightId: right?.id, matchId: left?.matchId };
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await submitExercise(exercise.id, user.id, { matches });
|
|
||||||
|
|
||||||
// Invalidate dashboard to reflect new progress
|
|
||||||
invalidateDashboard();
|
|
||||||
|
|
||||||
console.log('✅ [Emparejamiento] Submitted to backend:', response);
|
|
||||||
|
|
||||||
setFeedback({
|
|
||||||
type: response.isPerfect ? 'success' : response.score >= 70 ? 'partial' : 'error',
|
|
||||||
title: response.isPerfect
|
|
||||||
? '¡Perfecto!'
|
|
||||||
: response.score >= 70
|
|
||||||
? '¡Buen trabajo!'
|
|
||||||
: 'Sigue practicando',
|
|
||||||
message: response.isPerfect
|
|
||||||
? '¡Emparejaste todas las tarjetas correctamente!'
|
|
||||||
: `Obtuviste ${response.score}% de aciertos.`,
|
|
||||||
score: response.score,
|
|
||||||
xpEarned: response.xp_earned,
|
|
||||||
coinsEarned: response.coins_earned,
|
|
||||||
showConfetti: response.isPerfect,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ [Emparejamiento] Submit error:', error);
|
|
||||||
// Fallback to local feedback on error
|
|
||||||
setFeedback({
|
|
||||||
type: 'success',
|
|
||||||
title: '¡Completado!',
|
|
||||||
message: '¡Emparejaste todas las tarjetas correctamente!',
|
|
||||||
score,
|
|
||||||
showConfetti: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not complete or no user - show local feedback
|
|
||||||
setFeedback({
|
|
||||||
type: isComplete ? 'success' : 'error',
|
|
||||||
title: isComplete ? '¡Completado!' : 'Faltan parejas',
|
|
||||||
message: isComplete
|
|
||||||
? '¡Emparejaste todas las tarjetas correctamente!'
|
|
||||||
: `Emparejaste ${matched / 2} de ${total / 2} parejas.`,
|
|
||||||
score: isComplete ? score : undefined,
|
|
||||||
showConfetti: isComplete,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setShowFeedback(true);
|
setShowFeedback(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,10 +12,6 @@ import { MatchingDragDrop, MatchingPair } from './MatchingDragDrop';
|
|||||||
import { calculateScore, saveProgress } from '@shared/components/mechanics/mechanicsTypes';
|
import { calculateScore, saveProgress } from '@shared/components/mechanics/mechanicsTypes';
|
||||||
import { Check, RotateCcw } from 'lucide-react';
|
import { Check, RotateCcw } from 'lucide-react';
|
||||||
import type { FeedbackData } from '@shared/components/mechanics/mechanicsTypes';
|
import type { FeedbackData } from '@shared/components/mechanics/mechanicsTypes';
|
||||||
// P0-02-B: Added 2025-12-18 - Backend submission for progress persistence
|
|
||||||
import { submitExercise } from '@/features/progress/api/progressAPI';
|
|
||||||
import { useAuth } from '@/features/auth/hooks/useAuth';
|
|
||||||
import { useInvalidateDashboard } from '@/shared/hooks';
|
|
||||||
|
|
||||||
export interface EmparejamientoDragDropData {
|
export interface EmparejamientoDragDropData {
|
||||||
id: string;
|
id: string;
|
||||||
@ -61,10 +57,6 @@ export const EmparejamientoExerciseDragDrop: React.FC<EmparejamientoDragDropProp
|
|||||||
const [currentScore, setCurrentScore] = useState(0);
|
const [currentScore, setCurrentScore] = useState(0);
|
||||||
const [checkClicked, setCheckClicked] = useState(false);
|
const [checkClicked, setCheckClicked] = useState(false);
|
||||||
|
|
||||||
// P0-02-B: Added 2025-12-18 - Hooks for backend submission
|
|
||||||
const { user } = useAuth();
|
|
||||||
const invalidateDashboard = useInvalidateDashboard();
|
|
||||||
|
|
||||||
const handleConnect = (itemAId: string, itemBId: string) => {
|
const handleConnect = (itemAId: string, itemBId: string) => {
|
||||||
const newConnections = new Map(connections);
|
const newConnections = new Map(connections);
|
||||||
newConnections.set(itemBId, itemAId);
|
newConnections.set(itemBId, itemAId);
|
||||||
@ -86,7 +78,7 @@ export const EmparejamientoExerciseDragDrop: React.FC<EmparejamientoDragDropProp
|
|||||||
alert(`Pista: ${hint.text}`);
|
alert(`Pista: ${hint.text}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheck = async () => {
|
const handleCheck = () => {
|
||||||
setCheckClicked(true);
|
setCheckClicked(true);
|
||||||
const allConnected = connections.size === exercise.pairs.length;
|
const allConnected = connections.size === exercise.pairs.length;
|
||||||
|
|
||||||
@ -114,61 +106,15 @@ export const EmparejamientoExerciseDragDrop: React.FC<EmparejamientoDragDropProp
|
|||||||
|
|
||||||
const isSuccess = correctCount === exercise.pairs.length;
|
const isSuccess = correctCount === exercise.pairs.length;
|
||||||
|
|
||||||
// P0-02-B: Submit to backend when complete
|
setFeedback({
|
||||||
if (isSuccess && user?.id) {
|
type: isSuccess ? 'success' : 'error',
|
||||||
try {
|
title: isSuccess ? '¡Emparejamiento Completado!' : 'Revisa tus Parejas',
|
||||||
// Prepare connections for submission
|
message: isSuccess
|
||||||
const matchesData = Array.from(connections.entries()).map(([itemBId, itemAId]) => ({
|
? '¡Excelente trabajo! Has emparejado todos los elementos correctamente.'
|
||||||
itemBId,
|
: `Has emparejado ${correctCount} de ${exercise.pairs.length} elementos correctamente. Revisa los marcados en rojo.`,
|
||||||
itemAId,
|
score: isSuccess ? score : undefined,
|
||||||
pairId: exercise.pairs.find(p => p.id === itemBId)?.id,
|
showConfetti: isSuccess,
|
||||||
}));
|
});
|
||||||
|
|
||||||
const response = await submitExercise(exercise.id, user.id, { connections: matchesData });
|
|
||||||
|
|
||||||
// Invalidate dashboard to reflect new progress
|
|
||||||
invalidateDashboard();
|
|
||||||
|
|
||||||
console.log('✅ [EmparejamientoDragDrop] Submitted to backend:', response);
|
|
||||||
|
|
||||||
setFeedback({
|
|
||||||
type: response.isPerfect ? 'success' : response.score >= 70 ? 'partial' : 'error',
|
|
||||||
title: response.isPerfect
|
|
||||||
? '¡Perfecto!'
|
|
||||||
: response.score >= 70
|
|
||||||
? '¡Buen trabajo!'
|
|
||||||
: 'Sigue practicando',
|
|
||||||
message: response.isPerfect
|
|
||||||
? '¡Excelente trabajo! Has emparejado todos los elementos correctamente.'
|
|
||||||
: `Obtuviste ${response.score}% de aciertos.`,
|
|
||||||
score: response.score,
|
|
||||||
xpEarned: response.xp_earned,
|
|
||||||
coinsEarned: response.coins_earned,
|
|
||||||
showConfetti: response.isPerfect,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ [EmparejamientoDragDrop] Submit error:', error);
|
|
||||||
// Fallback to local feedback on error
|
|
||||||
setFeedback({
|
|
||||||
type: 'success',
|
|
||||||
title: '¡Emparejamiento Completado!',
|
|
||||||
message: '¡Excelente trabajo! Has emparejado todos los elementos correctamente.',
|
|
||||||
score,
|
|
||||||
showConfetti: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not success or no user - show local feedback
|
|
||||||
setFeedback({
|
|
||||||
type: isSuccess ? 'success' : 'error',
|
|
||||||
title: isSuccess ? '¡Emparejamiento Completado!' : 'Revisa tus Parejas',
|
|
||||||
message: isSuccess
|
|
||||||
? '¡Excelente trabajo! Has emparejado todos los elementos correctamente.'
|
|
||||||
: `Has emparejado ${correctCount} de ${exercise.pairs.length} elementos correctamente. Revisa los marcados en rojo.`,
|
|
||||||
score: isSuccess ? score : undefined,
|
|
||||||
showConfetti: isSuccess,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setShowFeedback(true);
|
setShowFeedback(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiClient } from '@/services/api/apiClient';
|
import { apiClient } from '@/services/api/apiClient';
|
||||||
import { handleAPIError } from '@/services/api/apiErrorHandler';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Usar Mission de @/features/gamification/missions/types/missionsTypes.ts
|
* @deprecated Usar Mission de @/features/gamification/missions/types/missionsTypes.ts
|
||||||
@ -57,71 +56,47 @@ export const missionsAPI = {
|
|||||||
* Get 3 daily missions (auto-generates if needed)
|
* Get 3 daily missions (auto-generates if needed)
|
||||||
*/
|
*/
|
||||||
getDailyMissions: async (): Promise<Mission[]> => {
|
getDailyMissions: async (): Promise<Mission[]> => {
|
||||||
try {
|
const response = await apiClient.get('/gamification/missions/daily');
|
||||||
const response = await apiClient.get('/gamification/missions/daily');
|
return response.data.data.missions;
|
||||||
return response.data.data.missions;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get 5 weekly missions (auto-generates if needed)
|
* Get 5 weekly missions (auto-generates if needed)
|
||||||
*/
|
*/
|
||||||
getWeeklyMissions: async (): Promise<Mission[]> => {
|
getWeeklyMissions: async (): Promise<Mission[]> => {
|
||||||
try {
|
const response = await apiClient.get('/gamification/missions/weekly');
|
||||||
const response = await apiClient.get('/gamification/missions/weekly');
|
return response.data.data.missions;
|
||||||
return response.data.data.missions;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get active special missions (events)
|
* Get active special missions (events)
|
||||||
*/
|
*/
|
||||||
getSpecialMissions: async (): Promise<Mission[]> => {
|
getSpecialMissions: async (): Promise<Mission[]> => {
|
||||||
try {
|
const response = await apiClient.get('/gamification/missions/special');
|
||||||
const response = await apiClient.get('/gamification/missions/special');
|
return response.data.data.missions;
|
||||||
return response.data.data.missions;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claim mission rewards
|
* Claim mission rewards
|
||||||
*/
|
*/
|
||||||
claimRewards: async (missionId: string) => {
|
claimRewards: async (missionId: string) => {
|
||||||
try {
|
const response = await apiClient.post(`/gamification/missions/${missionId}/claim`);
|
||||||
const response = await apiClient.post(`/gamification/missions/${missionId}/claim`);
|
return response.data.data;
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get mission progress
|
* Get mission progress
|
||||||
*/
|
*/
|
||||||
getMissionProgress: async (missionId: string) => {
|
getMissionProgress: async (missionId: string) => {
|
||||||
try {
|
const response = await apiClient.get(`/gamification/missions/${missionId}/progress`);
|
||||||
const response = await apiClient.get(`/gamification/missions/${missionId}/progress`);
|
return response.data.data;
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user mission statistics
|
* Get user mission statistics
|
||||||
*/
|
*/
|
||||||
getMissionStats: async (userId: string) => {
|
getMissionStats: async (userId: string) => {
|
||||||
try {
|
const response = await apiClient.get(`/gamification/missions/stats/${userId}`);
|
||||||
const response = await apiClient.get(`/gamification/missions/stats/${userId}`);
|
return response.data.data;
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiClient } from './apiClient';
|
import { apiClient } from './apiClient';
|
||||||
import { handleAPIError } from './apiErrorHandler';
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TYPES
|
// TYPES
|
||||||
@ -72,12 +71,8 @@ export const passwordAPI = {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
requestPasswordReset: async (email: string): Promise<PasswordResetRequestResponse> => {
|
requestPasswordReset: async (email: string): Promise<PasswordResetRequestResponse> => {
|
||||||
try {
|
const response = await apiClient.post('/auth/reset-password/request', { email });
|
||||||
const response = await apiClient.post('/auth/reset-password/request', { email });
|
return response.data;
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -95,15 +90,11 @@ export const passwordAPI = {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
resetPassword: async (token: string, newPassword: string): Promise<PasswordResetResponse> => {
|
resetPassword: async (token: string, newPassword: string): Promise<PasswordResetResponse> => {
|
||||||
try {
|
const response = await apiClient.post('/auth/reset-password', {
|
||||||
const response = await apiClient.post('/auth/reset-password', {
|
token,
|
||||||
token,
|
new_password: newPassword,
|
||||||
new_password: newPassword,
|
});
|
||||||
});
|
return response.data;
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiClient } from './apiClient';
|
import { apiClient } from './apiClient';
|
||||||
import { handleAPIError } from './apiErrorHandler';
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TYPES
|
// TYPES
|
||||||
@ -102,12 +101,8 @@ export const profileAPI = {
|
|||||||
* @returns Updated profile data
|
* @returns Updated profile data
|
||||||
*/
|
*/
|
||||||
updateProfile: async (userId: string, data: UpdateProfileDto): Promise<ProfileUpdateResponse> => {
|
updateProfile: async (userId: string, data: UpdateProfileDto): Promise<ProfileUpdateResponse> => {
|
||||||
try {
|
const response = await apiClient.put(`/users/${userId}/profile`, data);
|
||||||
const response = await apiClient.put(`/users/${userId}/profile`, data);
|
return response.data;
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,12 +111,8 @@ export const profileAPI = {
|
|||||||
* @returns User preferences data
|
* @returns User preferences data
|
||||||
*/
|
*/
|
||||||
getPreferences: async (): Promise<{ preferences: Record<string, unknown> }> => {
|
getPreferences: async (): Promise<{ preferences: Record<string, unknown> }> => {
|
||||||
try {
|
const response = await apiClient.get('/users/preferences');
|
||||||
const response = await apiClient.get('/users/preferences');
|
return response.data;
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -135,12 +126,8 @@ export const profileAPI = {
|
|||||||
userId: string,
|
userId: string,
|
||||||
preferences: UpdatePreferencesDto,
|
preferences: UpdatePreferencesDto,
|
||||||
): Promise<PreferencesUpdateResponse> => {
|
): Promise<PreferencesUpdateResponse> => {
|
||||||
try {
|
const response = await apiClient.put(`/users/${userId}/preferences`, { preferences });
|
||||||
const response = await apiClient.put(`/users/${userId}/preferences`, { preferences });
|
return response.data;
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,18 +138,14 @@ export const profileAPI = {
|
|||||||
* @returns Avatar URL
|
* @returns Avatar URL
|
||||||
*/
|
*/
|
||||||
uploadAvatar: async (userId: string, file: File): Promise<AvatarUploadResponse> => {
|
uploadAvatar: async (userId: string, file: File): Promise<AvatarUploadResponse> => {
|
||||||
try {
|
const formData = new FormData();
|
||||||
const formData = new FormData();
|
formData.append('avatar', file);
|
||||||
formData.append('avatar', file);
|
|
||||||
|
|
||||||
const response = await apiClient.post(`/users/${userId}/avatar`, formData, {
|
const response = await apiClient.post(`/users/${userId}/avatar`, formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -176,12 +159,8 @@ export const profileAPI = {
|
|||||||
userId: string,
|
userId: string,
|
||||||
passwords: UpdatePasswordDto,
|
passwords: UpdatePasswordDto,
|
||||||
): Promise<PasswordUpdateResponse> => {
|
): Promise<PasswordUpdateResponse> => {
|
||||||
try {
|
const response = await apiClient.put(`/users/${userId}/password`, passwords);
|
||||||
const response = await apiClient.put(`/users/${userId}/password`, passwords);
|
return response.data;
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw handleAPIError(error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FileText, CheckCircle, XCircle, Music, Type, Grid3X3, ListChecks, Link2 } from 'lucide-react';
|
import { FileText, CheckCircle, XCircle, Music, Type, Grid3X3, ListChecks } from 'lucide-react';
|
||||||
|
|
||||||
interface ExerciseContentRendererProps {
|
interface ExerciseContentRendererProps {
|
||||||
exerciseType: string;
|
exerciseType: string;
|
||||||
@ -35,22 +35,10 @@ export const ExerciseContentRenderer: React.FC<ExerciseContentRendererProps> = (
|
|||||||
return <PodcastRenderer data={answerData} />;
|
return <PodcastRenderer data={answerData} />;
|
||||||
|
|
||||||
case 'verdadero_falso':
|
case 'verdadero_falso':
|
||||||
return (
|
return <VerdaderoFalsoRenderer data={answerData} correct={correctAnswer} showComparison={showComparison} />;
|
||||||
<VerdaderoFalsoRenderer
|
|
||||||
data={answerData}
|
|
||||||
correct={correctAnswer}
|
|
||||||
showComparison={showComparison}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'completar_espacios':
|
case 'completar_espacios':
|
||||||
return (
|
return <CompletarEspaciosRenderer data={answerData} correct={correctAnswer} showComparison={showComparison} />;
|
||||||
<CompletarEspaciosRenderer
|
|
||||||
data={answerData}
|
|
||||||
correct={correctAnswer}
|
|
||||||
showComparison={showComparison}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'crucigrama':
|
case 'crucigrama':
|
||||||
return <CrucigramaRenderer data={answerData} />;
|
return <CrucigramaRenderer data={answerData} />;
|
||||||
@ -64,47 +52,22 @@ export const ExerciseContentRenderer: React.FC<ExerciseContentRendererProps> = (
|
|||||||
case 'timeline':
|
case 'timeline':
|
||||||
return <TimelineRenderer data={answerData} />;
|
return <TimelineRenderer data={answerData} />;
|
||||||
|
|
||||||
case 'emparejamiento':
|
// Módulo 2
|
||||||
return (
|
|
||||||
<EmparejamientoRenderer
|
|
||||||
data={answerData}
|
|
||||||
correct={correctAnswer}
|
|
||||||
showComparison={showComparison}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Módulo 2 - Automáticos (opción múltiple)
|
|
||||||
case 'lectura_inferencial':
|
case 'lectura_inferencial':
|
||||||
|
case 'prediccion_narrativa':
|
||||||
case 'puzzle_contexto':
|
case 'puzzle_contexto':
|
||||||
case 'detective_textual':
|
case 'detective_textual':
|
||||||
case 'rueda_inferencias':
|
case 'rueda_inferencias':
|
||||||
case 'causa_efecto':
|
case 'causa_efecto':
|
||||||
return (
|
return <MultipleChoiceRenderer data={answerData} correct={correctAnswer} showComparison={showComparison} />;
|
||||||
<MultipleChoiceRenderer
|
|
||||||
data={answerData}
|
|
||||||
correct={correctAnswer}
|
|
||||||
showComparison={showComparison}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Módulo 2 - Manuales (texto abierto)
|
// Módulo 3
|
||||||
// P0-03: Moved prediccion_narrativa to TextResponseRenderer (2025-12-18)
|
|
||||||
case 'prediccion_narrativa':
|
|
||||||
return <TextResponseRenderer data={answerData} />;
|
|
||||||
|
|
||||||
// Módulo 3 - Manuales (texto/análisis)
|
|
||||||
case 'analisis_fuentes':
|
case 'analisis_fuentes':
|
||||||
case 'debate_digital':
|
case 'debate_digital':
|
||||||
case 'matriz_perspectivas':
|
case 'matriz_perspectivas':
|
||||||
case 'tribunal_opiniones':
|
case 'tribunal_opiniones':
|
||||||
return <TextResponseRenderer data={answerData} />;
|
return <TextResponseRenderer data={answerData} />;
|
||||||
|
|
||||||
// P0-03: Added missing auxiliary mechanics (2025-12-18)
|
|
||||||
case 'collage_prensa':
|
|
||||||
case 'call_to_action':
|
|
||||||
case 'texto_en_movimiento':
|
|
||||||
return <TextResponseRenderer data={answerData} />;
|
|
||||||
|
|
||||||
// Módulo 4 y 5 (creativos con multimedia)
|
// Módulo 4 y 5 (creativos con multimedia)
|
||||||
case 'verificador_fake_news':
|
case 'verificador_fake_news':
|
||||||
case 'quiz_tiktok':
|
case 'quiz_tiktok':
|
||||||
@ -141,7 +104,7 @@ const PodcastRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data })
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-lg bg-purple-50 p-4">
|
<div className="rounded-lg bg-purple-50 p-4">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<FileText className="h-5 w-5 text-purple-600" />
|
<FileText className="h-5 w-5 text-purple-600" />
|
||||||
<span className="font-semibold text-purple-800">Tema seleccionado</span>
|
<span className="font-semibold text-purple-800">Tema seleccionado</span>
|
||||||
</div>
|
</div>
|
||||||
@ -149,16 +112,16 @@ const PodcastRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg bg-blue-50 p-4">
|
<div className="rounded-lg bg-blue-50 p-4">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Type className="h-5 w-5 text-blue-600" />
|
<Type className="h-5 w-5 text-blue-600" />
|
||||||
<span className="font-semibold text-blue-800">Guión del Podcast</span>
|
<span className="font-semibold text-blue-800">Guión del Podcast</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="whitespace-pre-wrap text-gray-700">{script}</p>
|
<p className="text-gray-700 whitespace-pre-wrap">{script}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{audioUrl && (
|
{audioUrl && (
|
||||||
<div className="rounded-lg bg-green-50 p-4">
|
<div className="rounded-lg bg-green-50 p-4">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Music className="h-5 w-5 text-green-600" />
|
<Music className="h-5 w-5 text-green-600" />
|
||||||
<span className="font-semibold text-green-800">Audio del Podcast</span>
|
<span className="font-semibold text-green-800">Audio del Podcast</span>
|
||||||
</div>
|
</div>
|
||||||
@ -215,19 +178,16 @@ const VerdaderoFalsoRenderer: React.FC<{
|
|||||||
|
|
||||||
const rawCorrectAnswers = correct?.statements || correct?.answers || correct;
|
const rawCorrectAnswers = correct?.statements || correct?.answers || correct;
|
||||||
const correctAnswers: Record<string, boolean> | undefined = rawCorrectAnswers
|
const correctAnswers: Record<string, boolean> | undefined = rawCorrectAnswers
|
||||||
? Object.entries(rawCorrectAnswers as Record<string, unknown>).reduce(
|
? Object.entries(rawCorrectAnswers as Record<string, unknown>).reduce((acc, [key, val]) => {
|
||||||
(acc, [key, val]) => {
|
if (typeof val === 'string') {
|
||||||
if (typeof val === 'string') {
|
acc[key] = val.toLowerCase() === 'true';
|
||||||
acc[key] = val.toLowerCase() === 'true';
|
} else if (typeof val === 'boolean') {
|
||||||
} else if (typeof val === 'boolean') {
|
acc[key] = val;
|
||||||
acc[key] = val;
|
} else {
|
||||||
} else {
|
acc[key] = Boolean(val);
|
||||||
acc[key] = Boolean(val);
|
}
|
||||||
}
|
return acc;
|
||||||
return acc;
|
}, {} as Record<string, boolean>)
|
||||||
},
|
|
||||||
{} as Record<string, boolean>,
|
|
||||||
)
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
console.log('[VerdaderoFalsoRenderer] Normalized:', { answers, correctAnswers });
|
console.log('[VerdaderoFalsoRenderer] Normalized:', { answers, correctAnswers });
|
||||||
@ -242,8 +202,8 @@ const VerdaderoFalsoRenderer: React.FC<{
|
|||||||
className={`flex items-center gap-3 rounded-lg p-3 ${
|
className={`flex items-center gap-3 rounded-lg p-3 ${
|
||||||
showComparison && isCorrect !== undefined
|
showComparison && isCorrect !== undefined
|
||||||
? isCorrect
|
? isCorrect
|
||||||
? 'border border-green-200 bg-green-50'
|
? 'bg-green-50 border border-green-200'
|
||||||
: 'border border-red-200 bg-red-50'
|
: 'bg-red-50 border border-red-200'
|
||||||
: 'bg-gray-50'
|
: 'bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -255,7 +215,7 @@ const VerdaderoFalsoRenderer: React.FC<{
|
|||||||
<span className="font-medium">Pregunta {key}:</span>
|
<span className="font-medium">Pregunta {key}:</span>
|
||||||
<span>{value ? 'Verdadero' : 'Falso'}</span>
|
<span>{value ? 'Verdadero' : 'Falso'}</span>
|
||||||
{showComparison && isCorrect === false && correctAnswers && (
|
{showComparison && isCorrect === false && correctAnswers && (
|
||||||
<span className="ml-2 text-sm text-red-600">
|
<span className="text-sm text-red-600 ml-2">
|
||||||
(Correcto: {correctAnswers[key] ? 'Verdadero' : 'Falso'})
|
(Correcto: {correctAnswers[key] ? 'Verdadero' : 'Falso'})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -290,15 +250,17 @@ const CompletarEspaciosRenderer: React.FC<{
|
|||||||
className={`flex items-center gap-3 rounded-lg p-3 ${
|
className={`flex items-center gap-3 rounded-lg p-3 ${
|
||||||
showComparison && isCorrect !== undefined
|
showComparison && isCorrect !== undefined
|
||||||
? isCorrect
|
? isCorrect
|
||||||
? 'border border-green-200 bg-green-50'
|
? 'bg-green-50 border border-green-200'
|
||||||
: 'border border-red-200 bg-red-50'
|
: 'bg-red-50 border border-red-200'
|
||||||
: 'bg-gray-50'
|
: 'bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="font-medium text-gray-600">Espacio {key}:</span>
|
<span className="font-medium text-gray-600">Espacio {key}:</span>
|
||||||
<span className="rounded bg-yellow-100 px-2 py-1 font-mono">{value || '(vacío)'}</span>
|
<span className="px-2 py-1 bg-yellow-100 rounded font-mono">{value || '(vacío)'}</span>
|
||||||
{showComparison && isCorrect === false && correctBlanks && (
|
{showComparison && isCorrect === false && correctBlanks && (
|
||||||
<span className="ml-2 text-sm text-green-600">→ {correctBlanks[key]}</span>
|
<span className="text-sm text-green-600 ml-2">
|
||||||
|
→ {correctBlanks[key]}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -316,13 +278,13 @@ const CrucigramaRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-gray-50 p-4">
|
<div className="rounded-lg bg-gray-50 p-4">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Grid3X3 className="h-5 w-5 text-gray-600" />
|
<Grid3X3 className="h-5 w-5 text-gray-600" />
|
||||||
<span className="font-semibold">Palabras del Crucigrama</span>
|
<span className="font-semibold">Palabras del Crucigrama</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{Object.entries(words).map(([key, value]) => (
|
{Object.entries(words).map(([key, value]) => (
|
||||||
<div key={key} className="flex items-center gap-2 rounded bg-white p-2">
|
<div key={key} className="flex items-center gap-2 bg-white p-2 rounded">
|
||||||
<span className="text-sm text-gray-500">{key}:</span>
|
<span className="text-sm text-gray-500">{key}:</span>
|
||||||
<span className="font-mono font-medium">{value}</span>
|
<span className="font-mono font-medium">{value}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -341,13 +303,13 @@ const SopaLetrasRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-gray-50 p-4">
|
<div className="rounded-lg bg-gray-50 p-4">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<ListChecks className="h-5 w-5 text-gray-600" />
|
<ListChecks className="h-5 w-5 text-gray-600" />
|
||||||
<span className="font-semibold">Palabras Encontradas ({foundWords.length})</span>
|
<span className="font-semibold">Palabras Encontradas ({foundWords.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{foundWords.map((word, idx) => (
|
{foundWords.map((word, idx) => (
|
||||||
<span key={idx} className="rounded-full bg-green-100 px-3 py-1 text-sm text-green-800">
|
<span key={idx} className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm">
|
||||||
{word}
|
{word}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -361,27 +323,19 @@ const SopaLetrasRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data
|
|||||||
* Muestra las conexiones entre nodos
|
* Muestra las conexiones entre nodos
|
||||||
*/
|
*/
|
||||||
const MapaConceptualRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
|
const MapaConceptualRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
|
||||||
const connections = (data.connections || data.nodes || []) as Array<{
|
const connections = (data.connections || data.nodes || []) as Array<{from?: string; to?: string; label?: string}>;
|
||||||
from?: string;
|
|
||||||
to?: string;
|
|
||||||
label?: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-gray-50 p-4">
|
<div className="rounded-lg bg-gray-50 p-4">
|
||||||
<span className="mb-3 block font-semibold">Conexiones del Mapa Conceptual</span>
|
<span className="font-semibold mb-3 block">Conexiones del Mapa Conceptual</span>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Array.isArray(connections) ? (
|
{Array.isArray(connections) ? connections.map((conn, idx) => (
|
||||||
connections.map((conn, idx) => (
|
<div key={idx} className="flex items-center gap-2 text-sm">
|
||||||
<div key={idx} className="flex items-center gap-2 text-sm">
|
<span className="bg-blue-100 px-2 py-1 rounded">{conn.from || `Nodo ${idx}`}</span>
|
||||||
<span className="rounded bg-blue-100 px-2 py-1">{conn.from || `Nodo ${idx}`}</span>
|
<span className="text-gray-400">→</span>
|
||||||
<span className="text-gray-400">→</span>
|
<span className="bg-green-100 px-2 py-1 rounded">{conn.to || conn.label || 'conecta'}</span>
|
||||||
<span className="rounded bg-green-100 px-2 py-1">
|
</div>
|
||||||
{conn.to || conn.label || 'conecta'}
|
)) : (
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<pre className="text-sm">{JSON.stringify(data, null, 2)}</pre>
|
<pre className="text-sm">{JSON.stringify(data, null, 2)}</pre>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -394,26 +348,20 @@ const MapaConceptualRenderer: React.FC<{ data: Record<string, unknown> }> = ({ d
|
|||||||
* Muestra los eventos en orden cronológico
|
* Muestra los eventos en orden cronológico
|
||||||
*/
|
*/
|
||||||
const TimelineRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
|
const TimelineRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
|
||||||
const events = (data.events || data.order || []) as Array<{
|
const events = (data.events || data.order || []) as Array<{id?: string; position?: number; text?: string}>;
|
||||||
id?: string;
|
|
||||||
position?: number;
|
|
||||||
text?: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-gray-50 p-4">
|
<div className="rounded-lg bg-gray-50 p-4">
|
||||||
<span className="mb-3 block font-semibold">Orden de Eventos</span>
|
<span className="font-semibold mb-3 block">Orden de Eventos</span>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Array.isArray(events) ? (
|
{Array.isArray(events) ? events.map((event, idx) => (
|
||||||
events.map((event, idx) => (
|
<div key={idx} className="flex items-center gap-3">
|
||||||
<div key={idx} className="flex items-center gap-3">
|
<span className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm font-bold">
|
||||||
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-sm font-bold text-white">
|
{event.position || idx + 1}
|
||||||
{event.position || idx + 1}
|
</span>
|
||||||
</span>
|
<span>{event.text || event.id || `Evento ${idx + 1}`}</span>
|
||||||
<span>{event.text || event.id || `Evento ${idx + 1}`}</span>
|
</div>
|
||||||
</div>
|
)) : (
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<pre className="text-sm">{JSON.stringify(data, null, 2)}</pre>
|
<pre className="text-sm">{JSON.stringify(data, null, 2)}</pre>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -421,70 +369,6 @@ const TimelineRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data })
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Renderiza respuestas del ejercicio Emparejamiento
|
|
||||||
* Muestra los pares que el estudiante conectó
|
|
||||||
*/
|
|
||||||
const EmparejamientoRenderer: React.FC<{
|
|
||||||
data: Record<string, unknown>;
|
|
||||||
correct?: Record<string, unknown>;
|
|
||||||
showComparison: boolean;
|
|
||||||
}> = ({ data, correct, showComparison }) => {
|
|
||||||
// El formato de respuesta es { matches: { questionId: answerId } }
|
|
||||||
const matches = (data.matches || data) as Record<string, string>;
|
|
||||||
const correctMatches = (correct?.matches || correct) as Record<string, string> | undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg bg-gray-50 p-4">
|
|
||||||
<div className="mb-3 flex items-center gap-2">
|
|
||||||
<Link2 className="h-5 w-5 text-gray-600" />
|
|
||||||
<span className="font-semibold">Emparejamientos Realizados</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{Object.entries(matches).map(([questionId, answerId]) => {
|
|
||||||
const isCorrect = correctMatches
|
|
||||||
? correctMatches[questionId] === answerId
|
|
||||||
: undefined;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={questionId}
|
|
||||||
className={`flex items-center gap-3 rounded-lg p-3 ${
|
|
||||||
showComparison && isCorrect !== undefined
|
|
||||||
? isCorrect
|
|
||||||
? 'border border-green-200 bg-green-50'
|
|
||||||
: 'border border-red-200 bg-red-50'
|
|
||||||
: 'bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="rounded bg-blue-100 px-2 py-1 text-sm font-medium text-blue-800">
|
|
||||||
{questionId}
|
|
||||||
</span>
|
|
||||||
<span className="text-gray-400">↔</span>
|
|
||||||
<span className="rounded bg-purple-100 px-2 py-1 text-sm font-medium text-purple-800">
|
|
||||||
{answerId}
|
|
||||||
</span>
|
|
||||||
{showComparison && isCorrect !== undefined && (
|
|
||||||
isCorrect ? (
|
|
||||||
<CheckCircle className="ml-auto h-5 w-5 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<XCircle className="ml-auto h-5 w-5 text-red-600" />
|
|
||||||
{correctMatches && (
|
|
||||||
<span className="text-sm text-green-600">
|
|
||||||
→ {correctMatches[questionId]}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renderiza respuestas de ejercicios de opción múltiple
|
* Renderiza respuestas de ejercicios de opción múltiple
|
||||||
* Usado para ejercicios del Módulo 2 (inferenciales)
|
* Usado para ejercicios del Módulo 2 (inferenciales)
|
||||||
@ -507,14 +391,14 @@ const MultipleChoiceRenderer: React.FC<{
|
|||||||
className={`rounded-lg p-3 ${
|
className={`rounded-lg p-3 ${
|
||||||
showComparison && isCorrect !== undefined
|
showComparison && isCorrect !== undefined
|
||||||
? isCorrect
|
? isCorrect
|
||||||
? 'border border-green-200 bg-green-50'
|
? 'bg-green-50 border border-green-200'
|
||||||
: 'border border-red-200 bg-red-50'
|
: 'bg-red-50 border border-red-200'
|
||||||
: 'bg-gray-50'
|
: 'bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="font-medium">{key}:</span> {String(value)}
|
<span className="font-medium">{key}:</span> {String(value)}
|
||||||
{showComparison && isCorrect === false && correctAnswers && (
|
{showComparison && isCorrect === false && correctAnswers && (
|
||||||
<span className="ml-2 text-sm text-green-600">
|
<span className="text-sm text-green-600 ml-2">
|
||||||
(Correcto: {String(correctAnswers[key])})
|
(Correcto: {String(correctAnswers[key])})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -534,10 +418,10 @@ const TextResponseRenderer: React.FC<{ data: Record<string, unknown> }> = ({ dat
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{Object.entries(data).map(([key, value]) => (
|
{Object.entries(data).map(([key, value]) => (
|
||||||
<div key={key} className="rounded-lg bg-gray-50 p-4">
|
<div key={key} className="rounded-lg bg-gray-50 p-4">
|
||||||
<span className="mb-2 block font-semibold capitalize text-gray-700">
|
<span className="font-semibold text-gray-700 block mb-2 capitalize">
|
||||||
{key.replace(/_/g, ' ')}
|
{key.replace(/_/g, ' ')}
|
||||||
</span>
|
</span>
|
||||||
<p className="whitespace-pre-wrap text-gray-800">
|
<p className="text-gray-800 whitespace-pre-wrap">
|
||||||
{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
|
{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -551,10 +435,7 @@ const TextResponseRenderer: React.FC<{ data: Record<string, unknown> }> = ({ dat
|
|||||||
* Usado para ejercicios de Módulos 4 y 5 (creativos)
|
* Usado para ejercicios de Módulos 4 y 5 (creativos)
|
||||||
* Detecta y renderiza imágenes, videos y audio inline
|
* Detecta y renderiza imágenes, videos y audio inline
|
||||||
*/
|
*/
|
||||||
const MultimediaRenderer: React.FC<{ data: Record<string, unknown>; type: string }> = ({
|
const MultimediaRenderer: React.FC<{ data: Record<string, unknown>; type: string }> = ({ data, type: _type }) => {
|
||||||
data,
|
|
||||||
type: _type,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{Object.entries(data).map(([key, value]) => {
|
{Object.entries(data).map(([key, value]) => {
|
||||||
@ -566,12 +447,12 @@ const MultimediaRenderer: React.FC<{ data: Record<string, unknown>; type: string
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="rounded-lg bg-gray-50 p-4">
|
<div key={key} className="rounded-lg bg-gray-50 p-4">
|
||||||
<span className="mb-2 block font-semibold capitalize text-gray-700">
|
<span className="font-semibold text-gray-700 block mb-2 capitalize">
|
||||||
{key.replace(/_/g, ' ')}
|
{key.replace(/_/g, ' ')}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{isImageUrl && typeof value === 'string' ? (
|
{isImageUrl && typeof value === 'string' ? (
|
||||||
<img src={value} alt={key} className="h-auto max-w-full rounded-lg" />
|
<img src={value} alt={key} className="max-w-full h-auto rounded-lg" />
|
||||||
) : isVideoUrl && typeof value === 'string' ? (
|
) : isVideoUrl && typeof value === 'string' ? (
|
||||||
<video controls className="max-w-full rounded-lg">
|
<video controls className="max-w-full rounded-lg">
|
||||||
<source src={value} />
|
<source src={value} />
|
||||||
@ -581,9 +462,9 @@ const MultimediaRenderer: React.FC<{ data: Record<string, unknown>; type: string
|
|||||||
<source src={value} />
|
<source src={value} />
|
||||||
</audio>
|
</audio>
|
||||||
) : typeof value === 'string' ? (
|
) : typeof value === 'string' ? (
|
||||||
<p className="whitespace-pre-wrap text-gray-800">{value}</p>
|
<p className="text-gray-800 whitespace-pre-wrap">{value}</p>
|
||||||
) : (
|
) : (
|
||||||
<pre className="overflow-x-auto rounded bg-white p-2 text-sm">
|
<pre className="text-sm bg-white p-2 rounded overflow-x-auto">
|
||||||
{JSON.stringify(value, null, 2)}
|
{JSON.stringify(value, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@ -601,7 +482,7 @@ const MultimediaRenderer: React.FC<{ data: Record<string, unknown>; type: string
|
|||||||
const FallbackRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
|
const FallbackRenderer: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-gray-100 p-4">
|
<div className="rounded-lg bg-gray-100 p-4">
|
||||||
<pre className="overflow-x-auto whitespace-pre-wrap text-sm">
|
<pre className="text-sm overflow-x-auto whitespace-pre-wrap">
|
||||||
{JSON.stringify(data, null, 2)}
|
{JSON.stringify(data, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
| **Título** | Sistema de Rangos Maya - Especificación Técnica |
|
| **Título** | Sistema de Rangos Maya - Especificación Técnica |
|
||||||
| **Prioridad** | Alta |
|
| **Prioridad** | Alta |
|
||||||
| **Estado** | ✅ Implementado |
|
| **Estado** | ✅ Implementado |
|
||||||
| **Versión** | 2.4.0 |
|
| **Versión** | 2.3.0 |
|
||||||
| **Fecha Creación** | 2025-11-07 |
|
| **Fecha Creación** | 2025-11-07 |
|
||||||
| **Última Actualización** | 2025-12-18 |
|
| **Última Actualización** | 2025-11-28 |
|
||||||
| **Sistema Actual** | [docs/sistema-recompensas/](../../../sistema-recompensas/) v2.3.0 |
|
| **Sistema Actual** | [docs/sistema-recompensas/](../../../sistema-recompensas/) v2.3.0 |
|
||||||
| **Autor** | Backend Team |
|
| **Autor** | Backend Team |
|
||||||
| **Stakeholders** | Backend Team, Frontend Team, Database Team |
|
| **Stakeholders** | Backend Team, Frontend Team, Database Team |
|
||||||
@ -93,8 +93,6 @@ El **Sistema de Rangos Maya** implementa una progresión jerárquica basada en X
|
|||||||
5. **K'uk'ulkan** (1,900+ XP) - Serpiente emplumada (máximo)
|
5. **K'uk'ulkan** (1,900+ XP) - Serpiente emplumada (máximo)
|
||||||
|
|
||||||
> **Nota v2.3.0:** Umbral K'uk'ulkan ajustado de 2,250 a 1,900 XP para ser alcanzable completando Módulos 1-3 (1,950 XP disponibles). Ver [DocumentoDeDiseño v6.5](../../../00-vision-general/DocumentoDeDiseño_Mecanicas_GAMILIT_v6_1.md).
|
> **Nota v2.3.0:** Umbral K'uk'ulkan ajustado de 2,250 a 1,900 XP para ser alcanzable completando Módulos 1-3 (1,950 XP disponibles). Ver [DocumentoDeDiseño v6.5](../../../00-vision-general/DocumentoDeDiseño_Mecanicas_GAMILIT_v6_1.md).
|
||||||
>
|
|
||||||
> **Migracion:** Para detalles tecnicos de la migracion v2.0 → v2.1, ver [MIGRACION-MAYA-RANKS-v2.1.md](../../../../90-transversal/migraciones/MIGRACION-MAYA-RANKS-v2.1.md).
|
|
||||||
|
|
||||||
### Características Técnicas
|
### Características Técnicas
|
||||||
|
|
||||||
@ -202,11 +200,10 @@ COMMENT ON TABLE gamification_system.rank_history IS
|
|||||||
Cada registro representa una promoción exitosa de un rango a otro.';
|
Cada registro representa una promoción exitosa de un rango a otro.';
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Función: check_rank_promotion (v2.1 - Lectura dinámica)
|
### 3. Función: check_rank_promotion
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- apps/database/ddl/schemas/gamification_system/functions/check_rank_promotion.sql
|
-- apps/database/ddl/schemas/gamification_system/functions/check_rank_promotion.sql
|
||||||
-- v2.1: Lee umbrales dinámicamente desde tabla maya_ranks
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION gamification_system.check_rank_promotion(
|
CREATE OR REPLACE FUNCTION gamification_system.check_rank_promotion(
|
||||||
p_user_id UUID
|
p_user_id UUID
|
||||||
@ -217,9 +214,7 @@ SECURITY DEFINER -- Ejecuta con permisos del owner
|
|||||||
AS $$
|
AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_current_rank gamification_system.maya_rank;
|
v_current_rank gamification_system.maya_rank;
|
||||||
v_total_xp BIGINT;
|
v_total_xp INTEGER;
|
||||||
v_next_rank gamification_system.maya_rank;
|
|
||||||
v_next_rank_min_xp BIGINT;
|
|
||||||
v_promoted BOOLEAN := false;
|
v_promoted BOOLEAN := false;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Obtener datos actuales del usuario
|
-- Obtener datos actuales del usuario
|
||||||
@ -234,164 +229,47 @@ BEGIN
|
|||||||
RETURN false;
|
RETURN false;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- v2.1: Leer siguiente rango y umbral dinámicamente desde maya_ranks
|
-- Verificar promociones según rango actual
|
||||||
SELECT mr.next_rank, next_mr.min_xp_required
|
CASE v_current_rank
|
||||||
INTO v_next_rank, v_next_rank_min_xp
|
WHEN 'Ajaw' THEN
|
||||||
FROM gamification_system.maya_ranks mr
|
IF v_total_xp >= 500 THEN
|
||||||
LEFT JOIN gamification_system.maya_ranks next_mr
|
PERFORM gamification_system.promote_to_next_rank(p_user_id, 'Nacom');
|
||||||
ON next_mr.rank_name = mr.next_rank
|
v_promoted := true;
|
||||||
WHERE mr.rank_name = v_current_rank
|
END IF;
|
||||||
AND mr.is_active = true;
|
|
||||||
|
|
||||||
-- Si no hay siguiente rango (ya está en máximo), no promocionar
|
WHEN 'Nacom' THEN
|
||||||
IF v_next_rank IS NULL THEN
|
IF v_total_xp >= 1000 THEN
|
||||||
RETURN false;
|
PERFORM gamification_system.promote_to_next_rank(p_user_id, 'Ah K''in');
|
||||||
END IF;
|
v_promoted := true;
|
||||||
|
END IF;
|
||||||
|
|
||||||
-- Verificar si el usuario tiene suficiente XP para el siguiente rango
|
WHEN 'Ah K''in' THEN
|
||||||
IF v_total_xp >= v_next_rank_min_xp THEN
|
IF v_total_xp >= 1500 THEN
|
||||||
PERFORM gamification_system.promote_to_next_rank(p_user_id, v_next_rank);
|
PERFORM gamification_system.promote_to_next_rank(p_user_id, 'Halach Uinic');
|
||||||
v_promoted := true;
|
v_promoted := true;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
WHEN 'Halach Uinic' THEN
|
||||||
|
IF v_total_xp >= 2250 THEN
|
||||||
|
PERFORM gamification_system.promote_to_next_rank(p_user_id, 'K''uk''ulkan');
|
||||||
|
v_promoted := true;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
WHEN 'K''uk''ulkan' THEN
|
||||||
|
-- Rango máximo alcanzado, no hay más promociones
|
||||||
|
v_promoted := false;
|
||||||
|
END CASE;
|
||||||
|
|
||||||
RETURN v_promoted;
|
RETURN v_promoted;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
COMMENT ON FUNCTION gamification_system.check_rank_promotion(UUID) IS
|
COMMENT ON FUNCTION gamification_system.check_rank_promotion IS
|
||||||
'Verifica si un usuario califica para promoción de rango según su total_xp actual.
|
'Verifica si un usuario califica para promoción de rango según su total_xp actual.
|
||||||
Lee configuración dinámica desde maya_ranks table (next_rank y min_xp_required).
|
|
||||||
Retorna true si el usuario fue promovido, false en caso contrario.
|
Retorna true si el usuario fue promovido, false en caso contrario.
|
||||||
Se ejecuta automáticamente mediante trigger después de actualizar total_xp.';
|
Se ejecuta automáticamente mediante trigger después de actualizar total_xp.';
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.1 Funciones Helper v2.1: Cálculo de Rangos
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- apps/database/ddl/schemas/gamification_system/functions/calculate_maya_rank_helpers.sql
|
|
||||||
-- v2.1: Funciones puras IMMUTABLE para cálculo de rangos sin queries a BD
|
|
||||||
|
|
||||||
-- Función: calculate_maya_rank_from_xp
|
|
||||||
-- Calcula el rango correcto basado en XP total (función pura, sin queries)
|
|
||||||
CREATE OR REPLACE FUNCTION gamification_system.calculate_maya_rank_from_xp(xp INTEGER)
|
|
||||||
RETURNS TEXT AS $$
|
|
||||||
BEGIN
|
|
||||||
-- v2.1 thresholds (sincronizado con 03-maya_ranks.sql seeds)
|
|
||||||
IF xp < 500 THEN
|
|
||||||
RETURN 'Ajaw'; -- 0-499 XP
|
|
||||||
ELSIF xp < 1000 THEN
|
|
||||||
RETURN 'Nacom'; -- 500-999 XP
|
|
||||||
ELSIF xp < 1500 THEN
|
|
||||||
RETURN 'Ah K''in'; -- 1,000-1,499 XP
|
|
||||||
ELSIF xp < 1900 THEN
|
|
||||||
RETURN 'Halach Uinic'; -- 1,500-1,899 XP
|
|
||||||
ELSE
|
|
||||||
RETURN 'K''uk''ulkan'; -- 1,900+ XP (máximo)
|
|
||||||
END IF;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
||||||
|
|
||||||
-- Función: calculate_rank_progress_percentage
|
|
||||||
-- Calcula porcentaje de progreso dentro de un rango (0-100)
|
|
||||||
CREATE OR REPLACE FUNCTION gamification_system.calculate_rank_progress_percentage(
|
|
||||||
xp INTEGER,
|
|
||||||
rank TEXT
|
|
||||||
)
|
|
||||||
RETURNS NUMERIC(5,2) AS $$
|
|
||||||
DECLARE
|
|
||||||
xp_in_rank INTEGER;
|
|
||||||
rank_size INTEGER;
|
|
||||||
BEGIN
|
|
||||||
CASE rank
|
|
||||||
WHEN 'Ajaw' THEN
|
|
||||||
xp_in_rank := xp; -- 0-499 XP
|
|
||||||
rank_size := 500;
|
|
||||||
WHEN 'Nacom' THEN
|
|
||||||
xp_in_rank := xp - 500; -- 500-999 XP
|
|
||||||
rank_size := 500;
|
|
||||||
WHEN 'Ah K''in' THEN
|
|
||||||
xp_in_rank := xp - 1000; -- 1,000-1,499 XP
|
|
||||||
rank_size := 500;
|
|
||||||
WHEN 'Halach Uinic' THEN
|
|
||||||
xp_in_rank := xp - 1500; -- 1,500-1,899 XP
|
|
||||||
rank_size := 400; -- v2.1: reducido de 750 a 400
|
|
||||||
WHEN 'K''uk''ulkan' THEN
|
|
||||||
RETURN 100.00; -- Rango máximo siempre 100%
|
|
||||||
ELSE
|
|
||||||
RETURN 0.00;
|
|
||||||
END CASE;
|
|
||||||
|
|
||||||
IF rank_size > 0 THEN
|
|
||||||
RETURN LEAST(100.00, (xp_in_rank::NUMERIC / rank_size::NUMERIC) * 100);
|
|
||||||
ELSE
|
|
||||||
RETURN 0.00;
|
|
||||||
END IF;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Función: calculate_user_rank (CORR-P0-001)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- apps/database/ddl/schemas/gamification_system/functions/calculate_user_rank.sql
|
|
||||||
-- CORR-P0-001: Corregido missions_completed → modules_completed
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION gamification_system.calculate_user_rank(p_user_id UUID)
|
|
||||||
RETURNS TABLE (
|
|
||||||
user_id UUID,
|
|
||||||
current_rank VARCHAR,
|
|
||||||
next_rank VARCHAR,
|
|
||||||
xp_to_next_rank BIGINT,
|
|
||||||
modules_to_next_rank INTEGER, -- CORR-P0-001: Renombrado missions → modules
|
|
||||||
rank_percentage NUMERIC(5,2)
|
|
||||||
) AS $$
|
|
||||||
DECLARE
|
|
||||||
v_total_xp BIGINT;
|
|
||||||
v_modules_completed INTEGER; -- CORR-P0-001: missions_completed no existe
|
|
||||||
v_current_rank VARCHAR;
|
|
||||||
v_next_rank VARCHAR;
|
|
||||||
v_next_rank_xp BIGINT;
|
|
||||||
v_next_rank_modules INTEGER;
|
|
||||||
BEGIN
|
|
||||||
-- CORR-P0-001: Usar modules_completed (missions_completed no existe)
|
|
||||||
SELECT us.total_xp, us.modules_completed INTO v_total_xp, v_modules_completed
|
|
||||||
FROM gamification_system.user_stats us
|
|
||||||
WHERE us.user_id = p_user_id;
|
|
||||||
|
|
||||||
IF NOT FOUND THEN
|
|
||||||
RETURN;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Determinar rango actual
|
|
||||||
SELECT ur.current_rank INTO v_current_rank
|
|
||||||
FROM gamification_system.user_ranks ur
|
|
||||||
WHERE ur.user_id = p_user_id AND ur.is_current = true;
|
|
||||||
|
|
||||||
-- Obtener siguiente rango desde maya_ranks
|
|
||||||
SELECT rank_name::VARCHAR, min_xp_required, COALESCE(modules_required, 0)
|
|
||||||
INTO v_next_rank, v_next_rank_xp, v_next_rank_modules
|
|
||||||
FROM gamification_system.maya_ranks
|
|
||||||
WHERE rank_name::VARCHAR > COALESCE(v_current_rank, 'Ajaw')
|
|
||||||
ORDER BY min_xp_required ASC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
IF v_next_rank IS NULL THEN
|
|
||||||
v_next_rank := v_current_rank;
|
|
||||||
v_next_rank_xp := v_total_xp;
|
|
||||||
v_next_rank_modules := v_modules_completed;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN QUERY SELECT
|
|
||||||
p_user_id,
|
|
||||||
COALESCE(v_current_rank, 'Ajaw'::VARCHAR),
|
|
||||||
v_next_rank,
|
|
||||||
GREATEST(0, v_next_rank_xp - v_total_xp),
|
|
||||||
GREATEST(0, COALESCE(v_next_rank_modules, 0) - v_modules_completed),
|
|
||||||
LEAST(100.0::NUMERIC, (v_total_xp::NUMERIC / NULLIF(v_next_rank_xp, 0)) * 100);
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql STABLE;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Función: promote_to_next_rank
|
### 4. Función: promote_to_next_rank
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@ -724,13 +602,12 @@ export const RANK_ORDER = [
|
|||||||
MayaRankEnum.KUKULKAN,
|
MayaRankEnum.KUKULKAN,
|
||||||
];
|
];
|
||||||
|
|
||||||
// v2.1 Thresholds - K'uk'ulkan ajustado a 1900 XP para ser alcanzable en Módulos 1-3
|
|
||||||
export const RANK_THRESHOLDS: Record<MayaRankEnum, { min: number; max: number | null }> = {
|
export const RANK_THRESHOLDS: Record<MayaRankEnum, { min: number; max: number | null }> = {
|
||||||
[MayaRankEnum.AJAW]: { min: 0, max: 499 },
|
[MayaRankEnum.AJAW]: { min: 0, max: 499 },
|
||||||
[MayaRankEnum.NACOM]: { min: 500, max: 999 },
|
[MayaRankEnum.NACOM]: { min: 500, max: 999 },
|
||||||
[MayaRankEnum.AH_KIN]: { min: 1000, max: 1499 },
|
[MayaRankEnum.AH_KIN]: { min: 1000, max: 1499 },
|
||||||
[MayaRankEnum.HALACH_UINIC]: { min: 1500, max: 1899 }, // v2.1: max reducido de 2249 a 1899
|
[MayaRankEnum.HALACH_UINIC]: { min: 1500, max: 2249 },
|
||||||
[MayaRankEnum.KUKULKAN]: { min: 1900, max: null }, // v2.1: min reducido de 2250 a 1900
|
[MayaRankEnum.KUKULKAN]: { min: 2250, max: null }, // Sin límite superior
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RANK_MULTIPLIERS: Record<MayaRankEnum, number> = {
|
export const RANK_MULTIPLIERS: Record<MayaRankEnum, number> = {
|
||||||
|
|||||||
@ -1,131 +0,0 @@
|
|||||||
# ET-AUD-001: Sistema de Auditoría
|
|
||||||
|
|
||||||
**Versión:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Estado:** Implementado
|
|
||||||
**Módulo Backend:** `apps/backend/src/modules/audit/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. DESCRIPCIÓN
|
|
||||||
|
|
||||||
El módulo de auditoría proporciona capacidades de logging y seguimiento de acciones en el sistema GAMILIT. Permite registrar eventos importantes para compliance, debugging y análisis de comportamiento.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. COMPONENTES
|
|
||||||
|
|
||||||
### 2.1 AuditService
|
|
||||||
|
|
||||||
**Ubicación:** `audit/audit.service.ts`
|
|
||||||
|
|
||||||
**Responsabilidades:**
|
|
||||||
- Crear registros de auditoría
|
|
||||||
- Consultar logs por usuario, fecha, tipo
|
|
||||||
- Limpiar logs antiguos (cron job)
|
|
||||||
|
|
||||||
**Métodos:**
|
|
||||||
| Método | Descripción | Parámetros |
|
|
||||||
|--------|-------------|------------|
|
|
||||||
| `create(dto)` | Registrar evento de auditoría | CreateAuditLogDto |
|
|
||||||
| `findAll(filters)` | Listar logs con filtros | Pagination, userId, dateRange |
|
|
||||||
| `findByUser(userId)` | Logs de un usuario específico | userId: string |
|
|
||||||
| `deleteOldLogs(days)` | Limpiar logs antiguos | days: number |
|
|
||||||
|
|
||||||
### 2.2 AuditLog Entity
|
|
||||||
|
|
||||||
**Ubicación:** `audit/entities/audit-log.entity.ts`
|
|
||||||
**Schema:** `audit_logging`
|
|
||||||
|
|
||||||
**Campos:**
|
|
||||||
| Campo | Tipo | Descripción |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| id | UUID | Identificador único |
|
|
||||||
| user_id | UUID | Usuario que realizó la acción |
|
|
||||||
| action | string | Tipo de acción (CREATE, UPDATE, DELETE, etc.) |
|
|
||||||
| resource_type | string | Tipo de recurso afectado |
|
|
||||||
| resource_id | string | ID del recurso |
|
|
||||||
| old_value | jsonb | Valor anterior (opcional) |
|
|
||||||
| new_value | jsonb | Valor nuevo (opcional) |
|
|
||||||
| ip_address | string | IP del cliente |
|
|
||||||
| user_agent | string | User agent del cliente |
|
|
||||||
| created_at | timestamp | Fecha del evento |
|
|
||||||
|
|
||||||
### 2.3 AuditInterceptor
|
|
||||||
|
|
||||||
**Ubicación:** `audit/interceptors/audit.interceptor.ts`
|
|
||||||
|
|
||||||
**Funcionalidad:**
|
|
||||||
- Interceptor global que captura eventos de mutación
|
|
||||||
- Registra automáticamente CREATE/UPDATE/DELETE
|
|
||||||
- Configurable por decorador `@Auditable()`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. CONFIGURACIÓN
|
|
||||||
|
|
||||||
### 3.1 Módulo
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([AuditLog], 'audit'),
|
|
||||||
],
|
|
||||||
providers: [AuditService, AuditInterceptor],
|
|
||||||
exports: [AuditService],
|
|
||||||
})
|
|
||||||
export class AuditModule {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Uso del Interceptor
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Aplicar a un controlador completo
|
|
||||||
@UseInterceptors(AuditInterceptor)
|
|
||||||
@Controller('users')
|
|
||||||
export class UsersController {}
|
|
||||||
|
|
||||||
// O a métodos específicos
|
|
||||||
@Post()
|
|
||||||
@Auditable('CREATE_USER')
|
|
||||||
async create(@Body() dto: CreateUserDto) {}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. EVENTOS AUDITADOS
|
|
||||||
|
|
||||||
| Categoría | Eventos |
|
|
||||||
|-----------|---------|
|
|
||||||
| **Autenticación** | LOGIN, LOGOUT, FAILED_LOGIN, PASSWORD_RESET |
|
|
||||||
| **Usuarios** | CREATE_USER, UPDATE_USER, DELETE_USER, ROLE_CHANGE |
|
|
||||||
| **Contenido** | CREATE_EXERCISE, GRADE_SUBMISSION, APPROVE_CONTENT |
|
|
||||||
| **Administración** | SYSTEM_CONFIG_CHANGE, BULK_OPERATION |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. RETENCIÓN DE DATOS
|
|
||||||
|
|
||||||
- **Logs de seguridad:** 1 año
|
|
||||||
- **Logs de operaciones:** 90 días
|
|
||||||
- **Logs de debugging:** 30 días
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. DEPENDENCIAS
|
|
||||||
|
|
||||||
- **Ninguna** (módulo independiente)
|
|
||||||
- **Es usado por:** AdminModule (para panel de logs)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. SEGURIDAD
|
|
||||||
|
|
||||||
- Logs inmutables (solo inserción)
|
|
||||||
- Acceso restringido a roles Admin
|
|
||||||
- No almacenar datos sensibles (passwords, tokens)
|
|
||||||
- Sanitización de inputs antes de logging
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Especificación generada: 2025-12-18*
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
# ET-HLT-001: Health Checks
|
|
||||||
|
|
||||||
**Versión:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Estado:** Implementado
|
|
||||||
**Módulo Backend:** `apps/backend/src/modules/health/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. DESCRIPCIÓN
|
|
||||||
|
|
||||||
El módulo de health checks proporciona endpoints para verificar el estado del servicio backend. Es utilizado por sistemas de monitoreo, load balancers y kubernetes para determinar la disponibilidad del servicio.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. COMPONENTES
|
|
||||||
|
|
||||||
### 2.1 HealthController
|
|
||||||
|
|
||||||
**Ubicación:** `health/health.controller.ts`
|
|
||||||
|
|
||||||
**Endpoints:**
|
|
||||||
|
|
||||||
| Método | Ruta | Descripción | Respuesta |
|
|
||||||
|--------|------|-------------|-----------|
|
|
||||||
| GET | `/health` | Estado general | `{ status: "ok", timestamp }` |
|
|
||||||
| GET | `/health/ready` | Readiness check | `{ ready: true, checks: [...] }` |
|
|
||||||
| GET | `/health/live` | Liveness check | `{ live: true }` |
|
|
||||||
|
|
||||||
### 2.2 HealthService
|
|
||||||
|
|
||||||
**Ubicación:** `health/health.service.ts`
|
|
||||||
|
|
||||||
**Métodos:**
|
|
||||||
| Método | Descripción | Retorno |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| `check()` | Estado general del servicio | HealthStatus |
|
|
||||||
| `checkDatabase()` | Conectividad a PostgreSQL | boolean |
|
|
||||||
| `checkRedis()` | Conectividad a Redis (si aplica) | boolean |
|
|
||||||
| `checkDiskSpace()` | Espacio en disco | DiskStatus |
|
|
||||||
| `checkMemory()` | Uso de memoria | MemoryStatus |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. RESPUESTAS
|
|
||||||
|
|
||||||
### 3.1 Health Check Exitoso (200 OK)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"timestamp": "2025-12-18T12:00:00Z",
|
|
||||||
"uptime": 86400,
|
|
||||||
"version": "1.0.0",
|
|
||||||
"checks": {
|
|
||||||
"database": { "status": "up", "responseTime": 5 },
|
|
||||||
"memory": { "status": "ok", "usage": "45%" },
|
|
||||||
"disk": { "status": "ok", "usage": "60%" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Health Check Fallido (503 Service Unavailable)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "error",
|
|
||||||
"timestamp": "2025-12-18T12:00:00Z",
|
|
||||||
"checks": {
|
|
||||||
"database": { "status": "down", "error": "Connection refused" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. CONFIGURACIÓN
|
|
||||||
|
|
||||||
### 4.1 Módulo
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Module({
|
|
||||||
controllers: [HealthController],
|
|
||||||
providers: [HealthService],
|
|
||||||
})
|
|
||||||
export class HealthModule {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Thresholds
|
|
||||||
|
|
||||||
| Métrica | Warning | Critical |
|
|
||||||
|---------|---------|----------|
|
|
||||||
| Memory Usage | 70% | 90% |
|
|
||||||
| Disk Usage | 80% | 95% |
|
|
||||||
| DB Response Time | 100ms | 500ms |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. USO EN KUBERNETES
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health/live
|
|
||||||
port: 3000
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 10
|
|
||||||
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health/ready
|
|
||||||
port: 3000
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. DEPENDENCIAS
|
|
||||||
|
|
||||||
- **Ninguna** (módulo independiente)
|
|
||||||
- **No es usado por otros módulos**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. SEGURIDAD
|
|
||||||
|
|
||||||
- Endpoint `/health` es público (sin autenticación)
|
|
||||||
- Endpoints detallados (`/health/ready`) pueden requerir auth en producción
|
|
||||||
- No exponer información sensible en respuestas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Especificación generada: 2025-12-18*
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
# ET-TSK-001: Sistema de Tareas Programadas (Cron Jobs)
|
|
||||||
|
|
||||||
**Versión:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Estado:** Implementado
|
|
||||||
**Módulo Backend:** `apps/backend/src/modules/tasks/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. DESCRIPCIÓN
|
|
||||||
|
|
||||||
El módulo de tareas programadas gestiona los cron jobs del sistema GAMILIT. Ejecuta procesos automáticos como reset de misiones diarias, limpieza de notificaciones y mantenimiento del sistema.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. COMPONENTES
|
|
||||||
|
|
||||||
### 2.1 MissionsCronService
|
|
||||||
|
|
||||||
**Ubicación:** `tasks/missions-cron.service.ts`
|
|
||||||
|
|
||||||
**Responsabilidades:**
|
|
||||||
- Resetear misiones diarias a medianoche
|
|
||||||
- Generar nuevas misiones para usuarios activos
|
|
||||||
- Calcular expiración de misiones semanales
|
|
||||||
|
|
||||||
**Cron Jobs:**
|
|
||||||
|
|
||||||
| Job | Schedule | Descripción |
|
|
||||||
|-----|----------|-------------|
|
|
||||||
| `resetDailyMissions` | `0 0 * * *` (00:00 UTC) | Reset misiones diarias |
|
|
||||||
| `generateWeeklyMissions` | `0 0 * * 1` (Lunes 00:00) | Generar misiones semanales |
|
|
||||||
| `cleanExpiredMissions` | `0 2 * * *` (02:00 UTC) | Limpiar misiones expiradas |
|
|
||||||
|
|
||||||
**Dependencias:**
|
|
||||||
- `GamificationModule.MissionsService`
|
|
||||||
|
|
||||||
### 2.2 NotificationsCronService
|
|
||||||
|
|
||||||
**Ubicación:** `tasks/notifications-cron.service.ts`
|
|
||||||
|
|
||||||
**Responsabilidades:**
|
|
||||||
- Enviar notificaciones en batch
|
|
||||||
- Limpiar notificaciones leídas antiguas
|
|
||||||
- Procesar cola de emails
|
|
||||||
|
|
||||||
**Cron Jobs:**
|
|
||||||
|
|
||||||
| Job | Schedule | Descripción |
|
|
||||||
|-----|----------|-------------|
|
|
||||||
| `processNotificationQueue` | `*/5 * * * *` (cada 5 min) | Procesar cola pendiente |
|
|
||||||
| `cleanOldNotifications` | `0 3 * * *` (03:00 UTC) | Limpiar >30 días |
|
|
||||||
| `sendDigestEmails` | `0 8 * * *` (08:00 UTC) | Enviar resúmenes diarios |
|
|
||||||
|
|
||||||
**Dependencias:**
|
|
||||||
- `NotificationsModule.NotificationQueueService`
|
|
||||||
- `MailModule.MailService`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. CONFIGURACIÓN
|
|
||||||
|
|
||||||
### 3.1 Módulo
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ScheduleModule.forRoot(),
|
|
||||||
GamificationModule,
|
|
||||||
NotificationsModule,
|
|
||||||
MailModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
MissionsCronService,
|
|
||||||
NotificationsCronService,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class TasksModule {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Decoradores de Schedule
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class MissionsCronService {
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
|
||||||
async resetDailyMissions() {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. MONITOREO
|
|
||||||
|
|
||||||
### 4.1 Logs de Ejecución
|
|
||||||
|
|
||||||
Cada job registra:
|
|
||||||
- Inicio de ejecución
|
|
||||||
- Duración
|
|
||||||
- Registros procesados
|
|
||||||
- Errores encontrados
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
this.logger.log(`[CRON] resetDailyMissions started`);
|
|
||||||
// ... proceso
|
|
||||||
this.logger.log(`[CRON] resetDailyMissions completed: ${count} missions reset in ${duration}ms`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Alertas
|
|
||||||
|
|
||||||
| Condición | Alerta |
|
|
||||||
|-----------|--------|
|
|
||||||
| Job falla 3 veces consecutivas | CRITICAL |
|
|
||||||
| Job toma >5 minutos | WARNING |
|
|
||||||
| Job no se ejecuta en 24h | CRITICAL |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. DEPENDENCIAS
|
|
||||||
|
|
||||||
### 5.1 Módulos Requeridos
|
|
||||||
|
|
||||||
```
|
|
||||||
TasksModule
|
|
||||||
├── GamificationModule (MissionsService)
|
|
||||||
├── NotificationsModule (NotificationQueueService)
|
|
||||||
└── MailModule (MailService)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Es Usado Por
|
|
||||||
|
|
||||||
- **AdminModule** (para monitoreo de jobs)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. CONSIDERACIONES DE PRODUCCIÓN
|
|
||||||
|
|
||||||
### 6.1 Timezone
|
|
||||||
|
|
||||||
- Todos los cron jobs usan **UTC**
|
|
||||||
- Frontend convierte a timezone del usuario
|
|
||||||
|
|
||||||
### 6.2 Concurrencia
|
|
||||||
|
|
||||||
- Solo una instancia ejecuta cron jobs (usar distributed lock)
|
|
||||||
- Implementar idempotencia en cada job
|
|
||||||
|
|
||||||
### 6.3 Retry Logic
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Cron('0 0 * * *')
|
|
||||||
async resetDailyMissions() {
|
|
||||||
const MAX_RETRIES = 3;
|
|
||||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
||||||
try {
|
|
||||||
await this.executeMissionReset();
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
|
||||||
if (attempt === MAX_RETRIES) throw error;
|
|
||||||
await this.delay(attempt * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. TESTING
|
|
||||||
|
|
||||||
### 7.1 Unit Tests
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
describe('MissionsCronService', () => {
|
|
||||||
it('should reset daily missions', async () => {
|
|
||||||
// Mock MissionsService
|
|
||||||
// Execute cron handler
|
|
||||||
// Verify missions were reset
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 Integration Tests
|
|
||||||
|
|
||||||
- Verificar que jobs se registran correctamente
|
|
||||||
- Simular ejecución manual de jobs
|
|
||||||
- Verificar side effects en BD
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Especificación generada: 2025-12-18*
|
|
||||||
@ -1,223 +0,0 @@
|
|||||||
# ET-WS-001: WebSocket y Comunicación en Tiempo Real
|
|
||||||
|
|
||||||
**Versión:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Estado:** Implementado
|
|
||||||
**Módulo Backend:** `apps/backend/src/modules/websocket/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. DESCRIPCIÓN
|
|
||||||
|
|
||||||
El módulo WebSocket proporciona comunicación bidireccional en tiempo real entre el backend y los clientes. Utiliza Socket.IO para gestionar conexiones, salas y eventos.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. COMPONENTES
|
|
||||||
|
|
||||||
### 2.1 WebSocketService
|
|
||||||
|
|
||||||
**Ubicación:** `websocket/websocket.service.ts`
|
|
||||||
|
|
||||||
**Responsabilidades:**
|
|
||||||
- Gestionar conexiones de clientes
|
|
||||||
- Broadcast de eventos a usuarios/salas
|
|
||||||
- Mantener registro de usuarios conectados
|
|
||||||
|
|
||||||
**Métodos:**
|
|
||||||
|
|
||||||
| Método | Descripción | Parámetros |
|
|
||||||
|--------|-------------|------------|
|
|
||||||
| `sendToUser(userId, event, data)` | Enviar a usuario específico | userId, event, payload |
|
|
||||||
| `sendToRoom(room, event, data)` | Broadcast a una sala | roomId, event, payload |
|
|
||||||
| `broadcastToAll(event, data)` | Broadcast global | event, payload |
|
|
||||||
| `joinRoom(socketId, room)` | Unir socket a sala | socketId, roomId |
|
|
||||||
| `leaveRoom(socketId, room)` | Salir de sala | socketId, roomId |
|
|
||||||
|
|
||||||
### 2.2 NotificationsGateway
|
|
||||||
|
|
||||||
**Ubicación:** `websocket/gateways/notifications.gateway.ts`
|
|
||||||
|
|
||||||
**Eventos Soportados:**
|
|
||||||
|
|
||||||
| Evento | Dirección | Descripción |
|
|
||||||
|--------|-----------|-------------|
|
|
||||||
| `connection` | Client→Server | Cliente se conecta |
|
|
||||||
| `disconnect` | Client→Server | Cliente se desconecta |
|
|
||||||
| `subscribe:notifications` | Client→Server | Suscribirse a notificaciones |
|
|
||||||
| `notification:new` | Server→Client | Nueva notificación |
|
|
||||||
| `notification:read` | Server→Client | Notificación marcada leída |
|
|
||||||
| `achievement:unlocked` | Server→Client | Logro desbloqueado |
|
|
||||||
| `rank:promoted` | Server→Client | Promoción de rango |
|
|
||||||
| `coins:earned` | Server→Client | ML Coins ganadas |
|
|
||||||
|
|
||||||
### 2.3 WsJwtGuard
|
|
||||||
|
|
||||||
**Ubicación:** `websocket/guards/ws-jwt.guard.ts`
|
|
||||||
|
|
||||||
**Funcionalidad:**
|
|
||||||
- Valida JWT en handshake de conexión
|
|
||||||
- Extrae userId del token
|
|
||||||
- Rechaza conexiones no autorizadas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. FLUJO DE CONEXIÓN
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Cliente inicia conexión WebSocket
|
|
||||||
└─> ws://api.gamilit.com/socket.io
|
|
||||||
|
|
||||||
2. Handshake con token JWT
|
|
||||||
└─> { auth: { token: "Bearer xxx" } }
|
|
||||||
|
|
||||||
3. WsJwtGuard valida token
|
|
||||||
└─> Si válido: conexión aceptada
|
|
||||||
└─> Si inválido: conexión rechazada
|
|
||||||
|
|
||||||
4. Cliente se une a sala personal
|
|
||||||
└─> room: "user:{userId}"
|
|
||||||
|
|
||||||
5. Cliente recibe eventos en tiempo real
|
|
||||||
└─> notification:new, achievement:unlocked, etc.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. CONFIGURACIÓN
|
|
||||||
|
|
||||||
### 4.1 Módulo
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Module({
|
|
||||||
imports: [JwtModule],
|
|
||||||
providers: [
|
|
||||||
WebSocketService,
|
|
||||||
NotificationsGateway,
|
|
||||||
WsJwtGuard,
|
|
||||||
],
|
|
||||||
exports: [WebSocketService],
|
|
||||||
})
|
|
||||||
export class WebSocketModule {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Gateway
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@WebSocketGateway({
|
|
||||||
cors: {
|
|
||||||
origin: process.env.FRONTEND_URL,
|
|
||||||
credentials: true,
|
|
||||||
},
|
|
||||||
namespace: '/notifications',
|
|
||||||
})
|
|
||||||
export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. SALAS (ROOMS)
|
|
||||||
|
|
||||||
| Sala | Formato | Propósito |
|
|
||||||
|------|---------|-----------|
|
|
||||||
| Usuario | `user:{userId}` | Notificaciones personales |
|
|
||||||
| Classroom | `classroom:{classroomId}` | Eventos del aula |
|
|
||||||
| Teacher | `teacher:{teacherId}` | Alertas para maestros |
|
|
||||||
| Admin | `admin:all` | Alertas administrativas |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. EVENTOS DE GAMIFICACIÓN
|
|
||||||
|
|
||||||
### 6.1 Achievement Unlocked
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Server envía
|
|
||||||
socket.emit('achievement:unlocked', {
|
|
||||||
achievementId: 'ach-123',
|
|
||||||
name: 'Primer Ejercicio',
|
|
||||||
description: 'Completaste tu primer ejercicio',
|
|
||||||
icon: 'trophy',
|
|
||||||
xpReward: 100,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Rank Promoted
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
socket.emit('rank:promoted', {
|
|
||||||
oldRank: { name: 'Ajaw', level: 1 },
|
|
||||||
newRank: { name: 'Nacom', level: 2 },
|
|
||||||
xpRequired: 500,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 Coins Earned
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
socket.emit('coins:earned', {
|
|
||||||
amount: 50,
|
|
||||||
reason: 'exercise_completed',
|
|
||||||
newBalance: 1250,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. INTEGRACIÓN CON NOTIFICACIONES
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// NotificationsService usa WebSocketService
|
|
||||||
@Injectable()
|
|
||||||
export class NotificationsService {
|
|
||||||
constructor(private readonly wsService: WebSocketService) {}
|
|
||||||
|
|
||||||
async sendNotification(userId: string, notification: Notification) {
|
|
||||||
// Guardar en BD
|
|
||||||
await this.notificationRepo.save(notification);
|
|
||||||
|
|
||||||
// Enviar por WebSocket si usuario conectado
|
|
||||||
this.wsService.sendToUser(userId, 'notification:new', notification);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. DEPENDENCIAS
|
|
||||||
|
|
||||||
### 8.1 Módulos Requeridos
|
|
||||||
|
|
||||||
- `JwtModule` (para autenticación)
|
|
||||||
|
|
||||||
### 8.2 Es Usado Por
|
|
||||||
|
|
||||||
- `NotificationsModule` (envío de notificaciones real-time)
|
|
||||||
- `GamificationModule` (eventos de logros, rangos)
|
|
||||||
- `ProgressModule` (eventos de ejercicios)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. SEGURIDAD
|
|
||||||
|
|
||||||
- Autenticación JWT obligatoria
|
|
||||||
- Validación de pertenencia a salas
|
|
||||||
- Rate limiting por conexión
|
|
||||||
- Sanitización de payloads
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. MONITOREO
|
|
||||||
|
|
||||||
| Métrica | Descripción |
|
|
||||||
|---------|-------------|
|
|
||||||
| `ws_connections_active` | Conexiones activas |
|
|
||||||
| `ws_messages_sent` | Mensajes enviados/min |
|
|
||||||
| `ws_errors` | Errores de conexión |
|
|
||||||
| `ws_latency` | Latencia promedio |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Especificación generada: 2025-12-18*
|
|
||||||
@ -1,304 +0,0 @@
|
|||||||
# Inventario de Funciones de Base de Datos
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Autor:** Requirements-Analyst (SIMCO)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RESUMEN
|
|
||||||
|
|
||||||
| Schema | Total Funciones | Documentadas |
|
|
||||||
|--------|-----------------|--------------|
|
|
||||||
| gamification_system | 12 | 12 |
|
|
||||||
| educational_content | 15 | 15 |
|
|
||||||
| auth_management | 5 | 5 |
|
|
||||||
| **TOTAL** | **32** | **32** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SCHEMA: gamification_system
|
|
||||||
|
|
||||||
### Funciones de Rangos Maya
|
|
||||||
|
|
||||||
#### calculate_maya_rank_from_xp(xp INTEGER)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/calculate_maya_rank_helpers.sql:1-50`
|
|
||||||
**Version:** v2.1 (Dec 14, 2025)
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Determina el rango Maya basado en XP total.
|
|
||||||
|
|
||||||
**Thresholds v2.1:**
|
|
||||||
| XP | Rango |
|
|
||||||
|----|-------|
|
|
||||||
| 0-499 | Ajaw |
|
|
||||||
| 500-999 | Nacom |
|
|
||||||
| 1000-1499 | Ah K'in |
|
|
||||||
| 1500-1899 | Halach Uinic |
|
|
||||||
| 1900+ | K'uk'ulkan |
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT gamification_system.calculate_maya_rank_from_xp(500);
|
|
||||||
-- Retorna: 'Nacom'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### calculate_rank_progress_percentage(xp INTEGER)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/calculate_maya_rank_helpers.sql:51-119`
|
|
||||||
**Version:** v2.1 (Dec 14, 2025)
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Calcula el porcentaje de progreso dentro del rango actual.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT gamification_system.calculate_rank_progress_percentage(750);
|
|
||||||
-- Retorna: 50 (50% dentro de Nacom)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### calculate_user_rank(p_user_id UUID)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/calculate_user_rank.sql:1-75`
|
|
||||||
**Version:** v2.1 (Dec 15, 2025)
|
|
||||||
**Status:** Activo
|
|
||||||
**Correccion:** CORR-P0-001 (missions_completed -> modules_completed)
|
|
||||||
|
|
||||||
**Proposito:** Calcula y actualiza el rango de un usuario basado en su XP.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT gamification_system.calculate_user_rank('user-uuid');
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### get_user_rank_progress(p_user_id UUID)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/get_user_rank_progress.sql:1-86`
|
|
||||||
**Version:** v2.1 (Dec 18, 2025)
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Obtiene informacion completa de progreso de rango.
|
|
||||||
|
|
||||||
**Retorna:** Record con current_rank, xp_total, progress_percentage, next_rank, xp_to_next
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Funciones de Leaderboard
|
|
||||||
|
|
||||||
#### update_leaderboard_global()
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/update_leaderboard_global.sql:1-79`
|
|
||||||
**Version:** v2.1 (Dec 18, 2025)
|
|
||||||
**Status:** Activo
|
|
||||||
**Correccion:** CORR-P0-001
|
|
||||||
|
|
||||||
**Proposito:** Actualiza el leaderboard global con posiciones actuales.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### update_leaderboard_coins()
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/update_leaderboard_coins.sql:1-60`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Actualiza leaderboard ordenado por ML Coins.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### update_leaderboard_streaks()
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/update_leaderboard_streaks.sql:1-94`
|
|
||||||
**Version:** v2.1 (Dec 15, 2025)
|
|
||||||
**Status:** Activo
|
|
||||||
**Correccion:** CORR-001 (last_activity_date -> last_activity_at, longest_streak -> max_streak)
|
|
||||||
|
|
||||||
**Proposito:** Actualiza leaderboard de rachas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Funciones de Promocion
|
|
||||||
|
|
||||||
#### check_rank_promotion(p_user_id UUID)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/check_rank_promotion.sql:1-80`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Verifica si un usuario califica para promocion de rango.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### promote_to_next_rank(p_user_id UUID)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/promote_to_next_rank.sql:1-120`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Ejecuta la promocion de rango y registra en historial.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Funciones de Beneficios
|
|
||||||
|
|
||||||
#### get_rank_benefits(p_rank maya_rank)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/get_rank_benefits.sql:1-45`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Obtiene los beneficios/perks de un rango.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### get_rank_multiplier(p_rank maya_rank)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/gamification_system/functions/get_rank_multiplier.sql:1-25`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Obtiene el multiplicador de XP para un rango.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SCHEMA: educational_content
|
|
||||||
|
|
||||||
### Funciones de Validacion
|
|
||||||
|
|
||||||
#### validate_rueda_inferencias(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/educational_content/functions/14-validate_rueda_inferencias.sql:176-410`
|
|
||||||
**Version:** v1.0 (Dec 15, 2025)
|
|
||||||
**Status:** NUEVO
|
|
||||||
|
|
||||||
**Proposito:** Valida respuestas abiertas para ejercicios de "Rueda de Inferencias".
|
|
||||||
|
|
||||||
**Caracteristicas:**
|
|
||||||
- Soporte para estructura `categoryExpectations` (nueva)
|
|
||||||
- Soporte para estructura `flat` (legacy)
|
|
||||||
- Validacion de keywords con normalizacion
|
|
||||||
- Puntuacion parcial
|
|
||||||
|
|
||||||
**Retorna:**
|
|
||||||
```sql
|
|
||||||
RECORD (
|
|
||||||
is_correct BOOLEAN,
|
|
||||||
score INTEGER,
|
|
||||||
feedback TEXT,
|
|
||||||
details JSONB
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### validate_multiple_choice(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/educational_content/functions/01-validate_multiple_choice.sql`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Valida respuestas de opcion multiple.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### validate_drag_and_drop(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/educational_content/functions/02-validate_drag_and_drop.sql`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Valida ejercicios de arrastrar y soltar.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### validate_fill_blanks(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/educational_content/functions/03-validate_fill_blanks.sql`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Valida ejercicios de llenar espacios.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### validate_ordering(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/educational_content/functions/04-validate_ordering.sql`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Valida ejercicios de ordenamiento.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### validate_matching(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/educational_content/functions/05-validate_matching.sql`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Valida ejercicios de emparejamiento.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SCHEMA: auth_management
|
|
||||||
|
|
||||||
### Funciones de Autenticacion
|
|
||||||
|
|
||||||
#### validate_user_credentials(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/auth_management/functions/validate_user_credentials.sql`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Valida credenciales de usuario.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### create_session(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/auth_management/functions/create_session.sql`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Crea sesion de usuario.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### invalidate_session(...)
|
|
||||||
|
|
||||||
**Ubicacion:** `ddl/schemas/auth_management/functions/invalidate_session.sql`
|
|
||||||
**Status:** Activo
|
|
||||||
|
|
||||||
**Proposito:** Invalida sesion de usuario.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## HISTORIAL DE CORRECCIONES
|
|
||||||
|
|
||||||
### CORR-P0-001 (Dec 15, 2025)
|
|
||||||
|
|
||||||
**Problema:** Funciones referenciaban `missions_completed` que no existe en `user_stats`.
|
|
||||||
|
|
||||||
**Solucion:** Cambiar a `modules_completed`.
|
|
||||||
|
|
||||||
**Funciones afectadas:**
|
|
||||||
- `calculate_user_rank()`
|
|
||||||
- `update_leaderboard_global()`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### CORR-001 (Dec 15, 2025)
|
|
||||||
|
|
||||||
**Problema:** Funciones referenciaban columnas incorrectas en `user_stats`.
|
|
||||||
|
|
||||||
**Cambios:**
|
|
||||||
- `last_activity_date` -> `last_activity_at::DATE`
|
|
||||||
- `longest_streak` -> `max_streak`
|
|
||||||
|
|
||||||
**Funciones afectadas:**
|
|
||||||
- `update_leaderboard_streaks()`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## REFERENCIAS
|
|
||||||
|
|
||||||
- [MIGRACION-MAYA-RANKS-v2.1.md](../../migraciones/MIGRACION-MAYA-RANKS-v2.1.md)
|
|
||||||
- [01-SCHEMAS-INVENTORY.md](./01-SCHEMAS-INVENTORY.md)
|
|
||||||
- [02-TABLES-INVENTORY.md](./02-TABLES-INVENTORY.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
@ -1,363 +0,0 @@
|
|||||||
# Migracion: Sistema de Rangos Maya v2.0 a v2.1
|
|
||||||
|
|
||||||
**Version:** 2.1.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Autor:** Requirements-Analyst (SIMCO)
|
|
||||||
**Tipo:** Migracion de Datos y Logica
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RESUMEN EJECUTIVO
|
|
||||||
|
|
||||||
Migracion de umbrales de XP para hacer **todos los 5 rangos Maya alcanzables** con el contenido educativo actual (1,950 XP disponibles en Modulos 1-3).
|
|
||||||
|
|
||||||
### Problema Resuelto
|
|
||||||
|
|
||||||
Los umbrales v2.0 (hasta 10,000 XP para K'uk'ulkan) eran **inalcanzables** con el contenido educativo disponible, causando frustracion en estudiantes y falta de motivacion.
|
|
||||||
|
|
||||||
### Solucion Implementada
|
|
||||||
|
|
||||||
Reduccion de umbrales para que la progresion sea:
|
|
||||||
- **Justa:** Todos los rangos alcanzables completando M1-M3 con excelencia
|
|
||||||
- **Motivante:** Progresion visible y constante
|
|
||||||
- **Pedagogica:** Recompensas alineadas con esfuerzo real
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CAMBIOS TECNICOS
|
|
||||||
|
|
||||||
### Tabla Comparativa de Umbrales
|
|
||||||
|
|
||||||
| Rango | v2.0 XP | v2.1 XP | Reduccion |
|
|
||||||
|-------|---------|---------|-----------|
|
|
||||||
| Ajaw | 0-999 | 0-499 | -500 XP |
|
|
||||||
| Nacom | 1,000-2,999 | 500-999 | Reorganizado |
|
|
||||||
| Ah K'in | 3,000-5,999 | 1,000-1,499 | -1,500 XP |
|
|
||||||
| Halach Uinic | 6,000-9,999 | 1,500-1,899 | -3,100 XP |
|
|
||||||
| K'uk'ulkan | 10,000+ | 1,900+ | **-8,100 XP** |
|
|
||||||
|
|
||||||
### XP Disponible por Modulo
|
|
||||||
|
|
||||||
| Modulo | XP Aproximado | Rango Alcanzable |
|
|
||||||
|--------|---------------|------------------|
|
|
||||||
| M1 | 0-500 | Ajaw -> Nacom |
|
|
||||||
| M2 | 500-1,000 | Nacom -> Ah K'in |
|
|
||||||
| M3 | 1,000-1,950 | Ah K'in -> K'uk'ulkan |
|
|
||||||
|
|
||||||
**Total disponible M1-M3:** 1,950 XP
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FUNCIONES MODIFICADAS
|
|
||||||
|
|
||||||
### 1. calculate_maya_rank_helpers.sql
|
|
||||||
|
|
||||||
**Ubicacion:** `apps/database/ddl/schemas/gamification_system/functions/calculate_maya_rank_helpers.sql`
|
|
||||||
|
|
||||||
#### Funcion: `calculate_maya_rank_from_xp(xp INTEGER)`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- v2.0 (ANTES)
|
|
||||||
IF xp < 1000 THEN RETURN 'Ajaw';
|
|
||||||
ELSIF xp < 3000 THEN RETURN 'Nacom';
|
|
||||||
ELSIF xp < 6000 THEN RETURN 'Ah K''in';
|
|
||||||
ELSIF xp < 10000 THEN RETURN 'Halach Uinic';
|
|
||||||
ELSE RETURN 'K''uk''ulkan';
|
|
||||||
|
|
||||||
-- v2.1 (DESPUES)
|
|
||||||
IF xp < 500 THEN RETURN 'Ajaw';
|
|
||||||
ELSIF xp < 1000 THEN RETURN 'Nacom';
|
|
||||||
ELSIF xp < 1500 THEN RETURN 'Ah K''in';
|
|
||||||
ELSIF xp < 1900 THEN RETURN 'Halach Uinic';
|
|
||||||
ELSE RETURN 'K''uk''ulkan';
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Funcion: `calculate_rank_progress_percentage(xp INTEGER)`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- v2.0 (ANTES)
|
|
||||||
WHEN 'Ajaw' THEN xp_in_rank := xp; rank_size := 1000;
|
|
||||||
WHEN 'Nacom' THEN xp_in_rank := xp - 1000; rank_size := 2000;
|
|
||||||
WHEN 'Ah K''in' THEN xp_in_rank := xp - 3000; rank_size := 3000;
|
|
||||||
WHEN 'Halach Uinic' THEN xp_in_rank := xp - 6000; rank_size := 4000;
|
|
||||||
|
|
||||||
-- v2.1 (DESPUES)
|
|
||||||
WHEN 'Ajaw' THEN xp_in_rank := xp; rank_size := 500;
|
|
||||||
WHEN 'Nacom' THEN xp_in_rank := xp - 500; rank_size := 500;
|
|
||||||
WHEN 'Ah K''in' THEN xp_in_rank := xp - 1000; rank_size := 500;
|
|
||||||
WHEN 'Halach Uinic' THEN xp_in_rank := xp - 1500; rank_size := 400;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. calculate_user_rank.sql
|
|
||||||
|
|
||||||
**Ubicacion:** `apps/database/ddl/schemas/gamification_system/functions/calculate_user_rank.sql`
|
|
||||||
|
|
||||||
**Correccion aplicada (CORR-P0-001):**
|
|
||||||
```sql
|
|
||||||
-- ANTES (ERROR - columna no existe)
|
|
||||||
SELECT us.total_xp, us.missions_completed INTO v_total_xp, v_missions_completed
|
|
||||||
|
|
||||||
-- DESPUES (CORRECTO)
|
|
||||||
SELECT us.total_xp, us.modules_completed INTO v_total_xp, v_modules_completed
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. update_leaderboard_streaks.sql
|
|
||||||
|
|
||||||
**Ubicacion:** `apps/database/ddl/schemas/gamification_system/functions/update_leaderboard_streaks.sql`
|
|
||||||
|
|
||||||
**Correcciones aplicadas (CORR-001):**
|
|
||||||
```sql
|
|
||||||
-- ANTES (ERROR)
|
|
||||||
last_activity_date -> last_activity_at::DATE
|
|
||||||
longest_streak -> max_streak
|
|
||||||
|
|
||||||
-- DESPUES (CORRECTO - alineado con user_stats)
|
|
||||||
v_last_activity, v_current_streak, v_longest_streak
|
|
||||||
FROM gamification_system.user_stats us
|
|
||||||
WHERE us.user_id = p_user_id;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. update_leaderboard_global.sql
|
|
||||||
|
|
||||||
**Ubicacion:** `apps/database/ddl/schemas/gamification_system/functions/update_leaderboard_global.sql`
|
|
||||||
|
|
||||||
**Correccion aplicada (CORR-P0-001):**
|
|
||||||
- Cambio de `missions_completed` a `modules_completed`
|
|
||||||
- Alineacion con columnas reales de `user_stats`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SEEDS ACTUALIZADOS
|
|
||||||
|
|
||||||
### 03-maya_ranks.sql
|
|
||||||
|
|
||||||
**Ubicacion:** `apps/database/seeds/dev/gamification_system/03-maya_ranks.sql`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO gamification_system.maya_ranks (
|
|
||||||
rank_name, min_xp_required, max_xp_threshold,
|
|
||||||
ml_coins_bonus, xp_multiplier, perks
|
|
||||||
) VALUES
|
|
||||||
('Ajaw', 0, 499, 0, 1.00,
|
|
||||||
'["basic_access", "forum_access"]'),
|
|
||||||
('Nacom', 500, 999, 100, 1.10,
|
|
||||||
'["xp_boost_10", "daily_bonus", "avatar_customization"]'),
|
|
||||||
('Ah K''in', 1000, 1499, 250, 1.15,
|
|
||||||
'["xp_boost_15", "coin_bonus", "exclusive_content"]'),
|
|
||||||
('Halach Uinic', 1500, 1899, 500, 1.20,
|
|
||||||
'["xp_boost_20", "priority_support", "mentor_badge"]'),
|
|
||||||
('K''uk''ulkan', 1900, NULL, 1000, 1.25,
|
|
||||||
'["xp_boost_25", "all_perks", "hall_of_fame", "certificate"]')
|
|
||||||
ON CONFLICT (rank_name) DO UPDATE SET
|
|
||||||
min_xp_required = EXCLUDED.min_xp_required,
|
|
||||||
max_xp_threshold = EXCLUDED.max_xp_threshold,
|
|
||||||
ml_coins_bonus = EXCLUDED.ml_coins_bonus,
|
|
||||||
xp_multiplier = EXCLUDED.xp_multiplier,
|
|
||||||
perks = EXCLUDED.perks;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## IMPACTO EN EL SISTEMA
|
|
||||||
|
|
||||||
### Funciones Dependientes
|
|
||||||
|
|
||||||
| Funcion | Status | Notas |
|
|
||||||
|---------|--------|-------|
|
|
||||||
| `calculate_user_rank()` | ✅ Actualizada | Usa v2.1 thresholds |
|
|
||||||
| `get_user_rank_progress()` | ✅ Actualizada | Calculo de porcentaje correcto |
|
|
||||||
| `update_leaderboard_global()` | ✅ Corregida | Alineacion con user_stats |
|
|
||||||
| `update_leaderboard_streaks()` | ✅ Corregida | Columnas correctas |
|
|
||||||
|
|
||||||
### Tablas Afectadas
|
|
||||||
|
|
||||||
| Tabla | Cambio |
|
|
||||||
|-------|--------|
|
|
||||||
| `gamification_system.maya_ranks` | Datos actualizados via seed |
|
|
||||||
| `gamification_system.user_stats` | Sin cambios de schema |
|
|
||||||
| `gamification_system.user_ranks` | Sin cambios de schema |
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
| Componente | Impacto |
|
|
||||||
|------------|---------|
|
|
||||||
| RankProgressBar | Muestra porcentaje correcto |
|
|
||||||
| Leaderboard | Posiciones calculadas con v2.1 |
|
|
||||||
| UserProfile | Rango mostrado correctamente |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CALCULO DE PROGRESION
|
|
||||||
|
|
||||||
### Distribucion por Modulo
|
|
||||||
|
|
||||||
```
|
|
||||||
Modulo 1: 0-500 XP
|
|
||||||
├── Ejercicios basicos: ~300 XP
|
|
||||||
├── Bonuses: ~100 XP
|
|
||||||
└── Completitud: ~100 XP
|
|
||||||
→ Rango: Ajaw → Nacom
|
|
||||||
|
|
||||||
Modulo 2: 500-1,000 XP
|
|
||||||
├── Ejercicios intermedios: ~350 XP
|
|
||||||
├── Bonuses: ~100 XP
|
|
||||||
└── Completitud: ~100 XP
|
|
||||||
→ Rango: Nacom → Ah K'in
|
|
||||||
|
|
||||||
Modulo 3: 1,000-1,950 XP
|
|
||||||
├── Ejercicios avanzados: ~600 XP
|
|
||||||
├── Bonuses: ~200 XP
|
|
||||||
└── Completitud: ~150 XP
|
|
||||||
→ Rango: Ah K'in → K'uk'ulkan
|
|
||||||
```
|
|
||||||
|
|
||||||
### XP Multipliers por Rango
|
|
||||||
|
|
||||||
| Rango | Multiplicador | Efecto |
|
|
||||||
|-------|---------------|--------|
|
|
||||||
| Ajaw | 1.00x | Base |
|
|
||||||
| Nacom | 1.10x | +10% XP |
|
|
||||||
| Ah K'in | 1.15x | +15% XP |
|
|
||||||
| Halach Uinic | 1.20x | +20% XP |
|
|
||||||
| K'uk'ulkan | 1.25x | +25% XP |
|
|
||||||
|
|
||||||
### ML Coins Bonus por Rango
|
|
||||||
|
|
||||||
| Rango | Bonus | Acumulado |
|
|
||||||
|-------|-------|-----------|
|
|
||||||
| Ajaw | 0 | 0 |
|
|
||||||
| Nacom | 100 | 100 |
|
|
||||||
| Ah K'in | 250 | 350 |
|
|
||||||
| Halach Uinic | 500 | 850 |
|
|
||||||
| K'uk'ulkan | 1,000 | 1,850 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PERKS DESBLOQUEABLES
|
|
||||||
|
|
||||||
### Por Rango
|
|
||||||
|
|
||||||
| Rango | Perks |
|
|
||||||
|-------|-------|
|
|
||||||
| **Ajaw** | Acceso basico, Foro |
|
|
||||||
| **Nacom** | +10% XP, Bonus diario, Avatar custom |
|
|
||||||
| **Ah K'in** | +15% XP, Bonus coins, Contenido exclusivo |
|
|
||||||
| **Halach Uinic** | +20% XP, Soporte prioritario, Badge mentor |
|
|
||||||
| **K'uk'ulkan** | +25% XP, Todos los perks, Hall of Fame, Certificado |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MIGRACION DE DATOS
|
|
||||||
|
|
||||||
### Para Nuevas Instalaciones
|
|
||||||
|
|
||||||
Los seeds ya incluyen los valores v2.1. No se requiere accion adicional.
|
|
||||||
|
|
||||||
### Para Instalaciones Existentes
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Ejecutar seed actualizado
|
|
||||||
\i apps/database/seeds/dev/gamification_system/03-maya_ranks.sql
|
|
||||||
|
|
||||||
-- O manualmente:
|
|
||||||
UPDATE gamification_system.maya_ranks SET
|
|
||||||
min_xp_required = 0, max_xp_threshold = 499
|
|
||||||
WHERE rank_name = 'Ajaw';
|
|
||||||
|
|
||||||
UPDATE gamification_system.maya_ranks SET
|
|
||||||
min_xp_required = 500, max_xp_threshold = 999
|
|
||||||
WHERE rank_name = 'Nacom';
|
|
||||||
|
|
||||||
UPDATE gamification_system.maya_ranks SET
|
|
||||||
min_xp_required = 1000, max_xp_threshold = 1499
|
|
||||||
WHERE rank_name = 'Ah K''in';
|
|
||||||
|
|
||||||
UPDATE gamification_system.maya_ranks SET
|
|
||||||
min_xp_required = 1500, max_xp_threshold = 1899
|
|
||||||
WHERE rank_name = 'Halach Uinic';
|
|
||||||
|
|
||||||
UPDATE gamification_system.maya_ranks SET
|
|
||||||
min_xp_required = 1900, max_xp_threshold = NULL
|
|
||||||
WHERE rank_name = 'K''uk''ulkan';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Recalculo de Rangos de Usuarios
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Recalcular rango de todos los usuarios
|
|
||||||
SELECT gamification_system.calculate_user_rank(user_id)
|
|
||||||
FROM gamification_system.user_stats;
|
|
||||||
|
|
||||||
-- Actualizar leaderboards
|
|
||||||
SELECT gamification_system.update_leaderboard_global();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## VERIFICACION
|
|
||||||
|
|
||||||
### Queries de Validacion
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Verificar umbrales
|
|
||||||
SELECT rank_name, min_xp_required, max_xp_threshold, xp_multiplier
|
|
||||||
FROM gamification_system.maya_ranks
|
|
||||||
ORDER BY min_xp_required;
|
|
||||||
|
|
||||||
-- Verificar distribucion de usuarios por rango
|
|
||||||
SELECT
|
|
||||||
mr.rank_name,
|
|
||||||
COUNT(us.user_id) as usuarios
|
|
||||||
FROM gamification_system.maya_ranks mr
|
|
||||||
LEFT JOIN gamification_system.user_stats us
|
|
||||||
ON us.current_rank = mr.rank_name
|
|
||||||
GROUP BY mr.rank_name
|
|
||||||
ORDER BY mr.min_xp_required;
|
|
||||||
|
|
||||||
-- Verificar funcion de calculo
|
|
||||||
SELECT gamification_system.calculate_maya_rank_from_xp(0); -- Ajaw
|
|
||||||
SELECT gamification_system.calculate_maya_rank_from_xp(500); -- Nacom
|
|
||||||
SELECT gamification_system.calculate_maya_rank_from_xp(1000); -- Ah K'in
|
|
||||||
SELECT gamification_system.calculate_maya_rank_from_xp(1500); -- Halach Uinic
|
|
||||||
SELECT gamification_system.calculate_maya_rank_from_xp(1900); -- K'uk'ulkan
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## REFERENCIAS
|
|
||||||
|
|
||||||
### Documentacion Relacionada
|
|
||||||
|
|
||||||
- `ET-GAM-003-rangos-maya.md` - Especificacion tecnica de rangos
|
|
||||||
- `ET-GAM-005-hook-user-gamification.md` - Hook de gamificacion
|
|
||||||
- `DocumentoDiseño_Mecanicas_GAMILIT_v6.2.md` - Diseño de mecanicas
|
|
||||||
|
|
||||||
### Archivos Modificados
|
|
||||||
|
|
||||||
```
|
|
||||||
apps/database/ddl/schemas/gamification_system/functions/
|
|
||||||
├── calculate_maya_rank_helpers.sql (v2.1 thresholds)
|
|
||||||
├── calculate_user_rank.sql (CORR-P0-001)
|
|
||||||
├── update_leaderboard_streaks.sql (CORR-001)
|
|
||||||
├── update_leaderboard_global.sql (CORR-P0-001)
|
|
||||||
└── get_user_rank_progress.sql (actualizado)
|
|
||||||
|
|
||||||
apps/database/seeds/dev/gamification_system/
|
|
||||||
└── 03-maya_ranks.sql (v2.1 data)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## HISTORIAL DE CAMBIOS
|
|
||||||
|
|
||||||
| Fecha | Version | Cambio |
|
|
||||||
|-------|---------|--------|
|
|
||||||
| 2025-11-24 | v2.0 | Implementacion inicial |
|
|
||||||
| 2025-12-14 | v2.1 | Reduccion de umbrales |
|
|
||||||
| 2025-12-15 | v2.1.1 | Correcciones CORR-P0-001, CORR-001 |
|
|
||||||
| 2025-12-18 | v2.1.2 | Homologacion DEV → PROD |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status:** IMPLEMENTADO
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
@ -1,208 +0,0 @@
|
|||||||
# DIRECTIVA: Deployment en Producción
|
|
||||||
|
|
||||||
**Versión:** 1.0
|
|
||||||
**Servidor:** 74.208.126.102
|
|
||||||
**Ejecutar después de:** Backup configs + Pull + Restaurar configs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PREREQUISITOS
|
|
||||||
|
|
||||||
Antes de ejecutar esta directiva, debes haber completado:
|
|
||||||
- [x] Backup de configuraciones en ../backups/TIMESTAMP/
|
|
||||||
- [x] Pull del repositorio (git reset --hard origin/master)
|
|
||||||
- [x] Restauración de .env.production desde backup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PROCESO DE DEPLOYMENT
|
|
||||||
|
|
||||||
### PASO 1: Verificar SSL/Nginx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar si Nginx está instalado y corriendo
|
|
||||||
if ! command -v nginx &> /dev/null; then
|
|
||||||
echo "Nginx no instalado. Ejecutar GUIA-SSL-AUTOFIRMADO.md primero"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! systemctl is-active --quiet nginx; then
|
|
||||||
echo "Nginx no está corriendo. Iniciando..."
|
|
||||||
sudo systemctl start nginx
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verificar certificado SSL existe
|
|
||||||
if [ ! -f /etc/nginx/ssl/gamilit.crt ]; then
|
|
||||||
echo "Certificado SSL no encontrado. Ejecutar GUIA-SSL-AUTOFIRMADO.md"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✓ SSL/Nginx configurado"
|
|
||||||
```
|
|
||||||
|
|
||||||
### PASO 2: Recrear Base de Datos
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/database
|
|
||||||
chmod +x create-database.sh
|
|
||||||
./create-database.sh
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
echo "✓ Base de datos recreada"
|
|
||||||
```
|
|
||||||
|
|
||||||
### PASO 3: Instalar Dependencias
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend
|
|
||||||
cd apps/backend
|
|
||||||
npm install --production=false
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
cd apps/frontend
|
|
||||||
npm install --production=false
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
echo "✓ Dependencias instaladas"
|
|
||||||
```
|
|
||||||
|
|
||||||
### PASO 4: Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend
|
|
||||||
cd apps/backend
|
|
||||||
npm run build
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
cd apps/frontend
|
|
||||||
npm run build
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
echo "✓ Build completado"
|
|
||||||
```
|
|
||||||
|
|
||||||
### PASO 5: Iniciar Servicios con PM2
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 start ecosystem.config.js --env production
|
|
||||||
pm2 save
|
|
||||||
pm2 list
|
|
||||||
|
|
||||||
echo "✓ Servicios iniciados"
|
|
||||||
```
|
|
||||||
|
|
||||||
### PASO 6: Validación
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Esperar a que los servicios inicien
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# Health check backend (interno)
|
|
||||||
echo "=== Health Check Backend (interno) ==="
|
|
||||||
curl -s http://localhost:3006/api/v1/health | head -10
|
|
||||||
|
|
||||||
# Health check frontend (interno)
|
|
||||||
echo "=== Health Check Frontend (interno) ==="
|
|
||||||
curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" http://localhost:3005
|
|
||||||
|
|
||||||
# Health check via Nginx (HTTPS)
|
|
||||||
echo "=== Health Check HTTPS ==="
|
|
||||||
curl -sk https://74.208.126.102/api/v1/health | head -10
|
|
||||||
|
|
||||||
echo "=== Frontend HTTPS ==="
|
|
||||||
curl -sk -o /dev/null -w "HTTP Status: %{http_code}\n" https://74.208.126.102
|
|
||||||
|
|
||||||
# PM2 status
|
|
||||||
echo "=== PM2 Status ==="
|
|
||||||
pm2 list
|
|
||||||
|
|
||||||
# Logs recientes
|
|
||||||
echo "=== Logs Recientes ==="
|
|
||||||
pm2 logs --lines 10 --nostream
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CONFIGURACIÓN ESPERADA
|
|
||||||
|
|
||||||
### Puertos
|
|
||||||
|
|
||||||
| Servicio | Puerto | Protocolo |
|
|
||||||
|----------|--------|-----------|
|
|
||||||
| Backend | 3006 | HTTP (interno) |
|
|
||||||
| Frontend | 3005 | HTTP (interno) |
|
|
||||||
| Nginx | 443 | HTTPS (externo) |
|
|
||||||
|
|
||||||
### URLs de Acceso
|
|
||||||
|
|
||||||
| Servicio | URL |
|
|
||||||
|----------|-----|
|
|
||||||
| Frontend | https://74.208.126.102 |
|
|
||||||
| Backend API | https://74.208.126.102/api/v1 |
|
|
||||||
| Health Check | https://74.208.126.102/api/v1/health |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SI FALLA ALGO
|
|
||||||
|
|
||||||
### Error en Base de Datos
|
|
||||||
```bash
|
|
||||||
# Verificar PostgreSQL
|
|
||||||
sudo systemctl status postgresql
|
|
||||||
PGPASSWORD="$DB_PASSWORD" psql -h localhost -U gamilit_user -d gamilit_platform -c "SELECT 1"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error en Build
|
|
||||||
```bash
|
|
||||||
# Ver logs de error
|
|
||||||
cd apps/backend && npm run build 2>&1 | tail -50
|
|
||||||
cd apps/frontend && npm run build 2>&1 | tail -50
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error en PM2
|
|
||||||
```bash
|
|
||||||
pm2 logs gamilit-backend --err --lines 50
|
|
||||||
pm2 logs gamilit-frontend --err --lines 50
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error en Nginx/SSL
|
|
||||||
```bash
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl status nginx
|
|
||||||
curl -vk https://74.208.126.102:3006/api/v1/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rollback Completo
|
|
||||||
```bash
|
|
||||||
pm2 stop all
|
|
||||||
|
|
||||||
# Restaurar configs
|
|
||||||
cp "../backups/latest/config/backend.env.production" apps/backend/.env.production
|
|
||||||
cp "../backups/latest/config/frontend.env.production" apps/frontend/.env.production
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
cd apps/backend && npm run build && cd ../..
|
|
||||||
cd apps/frontend && npm run build && cd ../..
|
|
||||||
|
|
||||||
# Reiniciar
|
|
||||||
pm2 start ecosystem.config.js --env production
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CHECKLIST FINAL
|
|
||||||
|
|
||||||
- [ ] Nginx corriendo con SSL
|
|
||||||
- [ ] Base de datos recreada
|
|
||||||
- [ ] Backend build exitoso
|
|
||||||
- [ ] Frontend build exitoso
|
|
||||||
- [ ] PM2 servicios online
|
|
||||||
- [ ] Health check backend OK
|
|
||||||
- [ ] Health check frontend OK
|
|
||||||
- [ ] HTTPS funcionando en :3005 y :3006
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Directiva creada: 2025-12-18*
|
|
||||||
@ -1,622 +0,0 @@
|
|||||||
# GUIA-ACTUALIZACION-PRODUCCION.md
|
|
||||||
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Version:** 1.0
|
|
||||||
**Proposito:** Guia paso a paso para actualizar el servidor de produccion desde el repositorio remoto
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## IMPORTANTE - LEER PRIMERO
|
|
||||||
|
|
||||||
Este documento es la **guia maestra** que el agente en produccion debe seguir cuando:
|
|
||||||
1. Se notifica que hay un commit en remoto
|
|
||||||
2. Se necesita actualizar el servidor con los cambios del repositorio
|
|
||||||
|
|
||||||
**Principio:** Respaldar TODO antes de actualizar, dar preferencia a remoto, reintegrar configuraciones locales.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FLUJO COMPLETO DE ACTUALIZACION
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ FLUJO DE ACTUALIZACION │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ 1. DETENER SERVICIOS │
|
|
||||||
│ └─> pm2 stop all │
|
|
||||||
│ │
|
|
||||||
│ 2. RESPALDAR CONFIGURACIONES │
|
|
||||||
│ └─> Copiar .env files a /backup/config/ │
|
|
||||||
│ │
|
|
||||||
│ 3. RESPALDAR BASE DE DATOS │
|
|
||||||
│ └─> pg_dump completo a /backup/database/ │
|
|
||||||
│ │
|
|
||||||
│ 4. PULL DEL REPOSITORIO │
|
|
||||||
│ └─> git fetch && git reset --hard origin/main │
|
|
||||||
│ │
|
|
||||||
│ 5. RESTAURAR CONFIGURACIONES │
|
|
||||||
│ └─> Copiar .env files de vuelta │
|
|
||||||
│ │
|
|
||||||
│ 6. RECREAR BASE DE DATOS (limpia) │
|
|
||||||
│ └─> drop + create + seeds │
|
|
||||||
│ │
|
|
||||||
│ 7. INSTALAR DEPENDENCIAS │
|
|
||||||
│ └─> npm install en backend y frontend │
|
|
||||||
│ │
|
|
||||||
│ 8. BUILD DE APLICACIONES │
|
|
||||||
│ └─> npm run build │
|
|
||||||
│ │
|
|
||||||
│ 9. INICIAR SERVICIOS │
|
|
||||||
│ └─> pm2 start ecosystem.config.js │
|
|
||||||
│ │
|
|
||||||
│ 10. VALIDAR DEPLOYMENT │
|
|
||||||
│ └─> ./scripts/diagnose-production.sh │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 1: DETENER SERVICIOS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Detener todos los procesos PM2
|
|
||||||
pm2 stop all
|
|
||||||
|
|
||||||
# Verificar que estan detenidos
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 2: RESPALDAR CONFIGURACIONES
|
|
||||||
|
|
||||||
### 2.1 Crear directorio de backup (si no existe)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Directorio de backups fuera del workspace
|
|
||||||
BACKUP_DIR="/home/gamilit/backups/$(date +%Y%m%d_%H%M%S)"
|
|
||||||
mkdir -p "$BACKUP_DIR/config"
|
|
||||||
mkdir -p "$BACKUP_DIR/database"
|
|
||||||
|
|
||||||
echo "Directorio de backup: $BACKUP_DIR"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Respaldar archivos de configuracion
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Respaldar .env de produccion
|
|
||||||
cp apps/backend/.env.production "$BACKUP_DIR/config/backend.env.production"
|
|
||||||
cp apps/frontend/.env.production "$BACKUP_DIR/config/frontend.env.production"
|
|
||||||
|
|
||||||
# Respaldar ecosystem.config.js si tiene modificaciones locales
|
|
||||||
cp ecosystem.config.js "$BACKUP_DIR/config/ecosystem.config.js"
|
|
||||||
|
|
||||||
# Respaldar cualquier otro archivo de configuracion local
|
|
||||||
if [ -f "apps/backend/.env" ]; then
|
|
||||||
cp apps/backend/.env "$BACKUP_DIR/config/backend.env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "apps/frontend/.env" ]; then
|
|
||||||
cp apps/frontend/.env "$BACKUP_DIR/config/frontend.env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Listar archivos respaldados
|
|
||||||
echo "=== Archivos de configuracion respaldados ==="
|
|
||||||
ls -la "$BACKUP_DIR/config/"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 Archivos de configuracion criticos
|
|
||||||
|
|
||||||
| Archivo | Ubicacion | Contenido critico |
|
|
||||||
|---------|-----------|-------------------|
|
|
||||||
| `.env.production` | `apps/backend/` | DATABASE_URL, JWT_SECRET, CORS |
|
|
||||||
| `.env.production` | `apps/frontend/` | VITE_API_HOST, VITE_API_PORT |
|
|
||||||
| `ecosystem.config.js` | raiz | Configuracion PM2 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 3: RESPALDAR BASE DE DATOS
|
|
||||||
|
|
||||||
### 3.1 Backup completo con pg_dump
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Variables de conexion (ajustar segun el servidor)
|
|
||||||
DB_NAME="gamilit_platform"
|
|
||||||
DB_USER="gamilit_user"
|
|
||||||
DB_HOST="localhost"
|
|
||||||
DB_PORT="5432"
|
|
||||||
|
|
||||||
# Archivo de backup
|
|
||||||
BACKUP_FILE="$BACKUP_DIR/database/gamilit_backup_$(date +%Y%m%d_%H%M%S).sql"
|
|
||||||
|
|
||||||
# Ejecutar backup completo
|
|
||||||
PGPASSWORD="$DB_PASSWORD" pg_dump \
|
|
||||||
-h "$DB_HOST" \
|
|
||||||
-p "$DB_PORT" \
|
|
||||||
-U "$DB_USER" \
|
|
||||||
-d "$DB_NAME" \
|
|
||||||
--format=plain \
|
|
||||||
--no-owner \
|
|
||||||
--no-acl \
|
|
||||||
> "$BACKUP_FILE"
|
|
||||||
|
|
||||||
# Verificar tamano del backup
|
|
||||||
ls -lh "$BACKUP_FILE"
|
|
||||||
|
|
||||||
# Comprimir backup
|
|
||||||
gzip "$BACKUP_FILE"
|
|
||||||
echo "Backup creado: ${BACKUP_FILE}.gz"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Verificar backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar que el backup tiene contenido
|
|
||||||
gunzip -c "${BACKUP_FILE}.gz" | head -50
|
|
||||||
|
|
||||||
# Contar tablas en el backup
|
|
||||||
echo "Tablas en backup:"
|
|
||||||
gunzip -c "${BACKUP_FILE}.gz" | grep "CREATE TABLE" | wc -l
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 4: PULL DEL REPOSITORIO
|
|
||||||
|
|
||||||
### 4.1 Verificar estado actual
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver estado actual
|
|
||||||
git status
|
|
||||||
|
|
||||||
# Ver rama actual
|
|
||||||
git branch
|
|
||||||
|
|
||||||
# Ver diferencias con remoto
|
|
||||||
git fetch origin
|
|
||||||
git log HEAD..origin/main --oneline
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Hacer pull dando preferencia a remoto
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# OPCION A: Reset completo a remoto (RECOMENDADO)
|
|
||||||
# Descarta TODOS los cambios locales y usa exactamente lo que hay en remoto
|
|
||||||
git fetch origin
|
|
||||||
git reset --hard origin/main
|
|
||||||
|
|
||||||
# OPCION B: Pull con estrategia de merge (si hay cambios locales importantes)
|
|
||||||
# git pull origin main --strategy-option theirs
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Verificar el pull
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar que estamos en el commit correcto
|
|
||||||
git log --oneline -5
|
|
||||||
|
|
||||||
# Verificar que no hay cambios pendientes
|
|
||||||
git status
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 5: RESTAURAR CONFIGURACIONES
|
|
||||||
|
|
||||||
### 5.1 Restaurar archivos .env
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Restaurar configuraciones de produccion
|
|
||||||
cp "$BACKUP_DIR/config/backend.env.production" apps/backend/.env.production
|
|
||||||
cp "$BACKUP_DIR/config/frontend.env.production" apps/frontend/.env.production
|
|
||||||
|
|
||||||
# Restaurar ecosystem.config.js si fue modificado
|
|
||||||
# cp "$BACKUP_DIR/config/ecosystem.config.js" ecosystem.config.js
|
|
||||||
|
|
||||||
# Crear enlaces simbolicos a .env si el backend los requiere
|
|
||||||
cd apps/backend
|
|
||||||
ln -sf .env.production .env
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
cd apps/frontend
|
|
||||||
ln -sf .env.production .env
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Verificar configuraciones restauradas
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar que los archivos existen
|
|
||||||
ls -la apps/backend/.env*
|
|
||||||
ls -la apps/frontend/.env*
|
|
||||||
|
|
||||||
# Verificar contenido critico (sin mostrar secrets)
|
|
||||||
echo "=== Backend Config ==="
|
|
||||||
grep -E "^(APP_|NODE_ENV|CORS)" apps/backend/.env.production
|
|
||||||
|
|
||||||
echo "=== Frontend Config ==="
|
|
||||||
grep -E "^VITE_" apps/frontend/.env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 6: RECREAR BASE DE DATOS (LIMPIA)
|
|
||||||
|
|
||||||
### 6.1 Ejecutar script de creacion
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/database
|
|
||||||
|
|
||||||
# Configurar DATABASE_URL
|
|
||||||
export DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
|
|
||||||
|
|
||||||
# Ejecutar script de creacion limpia
|
|
||||||
# IMPORTANTE: Este script hace DROP y CREATE de la base de datos
|
|
||||||
./create-database.sh
|
|
||||||
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Proceso del script create-database.sh
|
|
||||||
|
|
||||||
El script ejecuta en orden:
|
|
||||||
1. DROP DATABASE (si existe)
|
|
||||||
2. CREATE DATABASE
|
|
||||||
3. Cargar DDL (16 fases - schemas, tablas, funciones, triggers)
|
|
||||||
4. Cargar Seeds de produccion (57 archivos)
|
|
||||||
|
|
||||||
### 6.3 Verificar carga de datos
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ejecutar script de diagnostico
|
|
||||||
./scripts/diagnose-production.sh
|
|
||||||
|
|
||||||
# O verificar manualmente las tablas criticas
|
|
||||||
psql "$DATABASE_URL" -c "
|
|
||||||
SELECT 'Tenants' as tabla, COUNT(*) as total FROM auth_management.tenants
|
|
||||||
UNION ALL SELECT 'Users', COUNT(*) FROM auth.users
|
|
||||||
UNION ALL SELECT 'Profiles', COUNT(*) FROM auth_management.profiles
|
|
||||||
UNION ALL SELECT 'Modules', COUNT(*) FROM educational_content.modules
|
|
||||||
UNION ALL SELECT 'Exercises', COUNT(*) FROM educational_content.exercises
|
|
||||||
UNION ALL SELECT 'Maya Ranks', COUNT(*) FROM gamification_system.maya_ranks
|
|
||||||
UNION ALL SELECT 'Achievements', COUNT(*) FROM gamification_system.achievements;
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.4 Si hay datos faltantes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ejecutar script de reparacion
|
|
||||||
./scripts/repair-missing-data.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 7: INSTALAR DEPENDENCIAS
|
|
||||||
|
|
||||||
### 7.1 Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/backend
|
|
||||||
|
|
||||||
# Limpiar node_modules si hay problemas
|
|
||||||
# rm -rf node_modules package-lock.json
|
|
||||||
|
|
||||||
# Instalar dependencias
|
|
||||||
npm install
|
|
||||||
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 Frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/frontend
|
|
||||||
|
|
||||||
# Limpiar node_modules si hay problemas
|
|
||||||
# rm -rf node_modules package-lock.json
|
|
||||||
|
|
||||||
# Instalar dependencias
|
|
||||||
npm install
|
|
||||||
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 8: BUILD DE APLICACIONES
|
|
||||||
|
|
||||||
### 8.1 Build Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/backend
|
|
||||||
|
|
||||||
# Build de produccion
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Verificar que el build fue exitoso
|
|
||||||
ls -la dist/
|
|
||||||
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 Build Frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/frontend
|
|
||||||
|
|
||||||
# Build de produccion
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Verificar que el build fue exitoso
|
|
||||||
ls -la dist/
|
|
||||||
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 9: INICIAR SERVICIOS
|
|
||||||
|
|
||||||
### 9.1 Iniciar con PM2
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Iniciar usando ecosystem.config.js
|
|
||||||
pm2 start ecosystem.config.js
|
|
||||||
|
|
||||||
# Verificar estado
|
|
||||||
pm2 list
|
|
||||||
|
|
||||||
# Guardar configuracion de PM2
|
|
||||||
pm2 save
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.2 Verificar logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver logs de todos los procesos
|
|
||||||
pm2 logs --lines 50
|
|
||||||
|
|
||||||
# Ver logs solo del backend
|
|
||||||
pm2 logs gamilit-backend --lines 30
|
|
||||||
|
|
||||||
# Ver logs solo del frontend
|
|
||||||
pm2 logs gamilit-frontend --lines 30
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 10: VALIDAR DEPLOYMENT
|
|
||||||
|
|
||||||
### 10.1 Ejecutar script de diagnostico
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/diagnose-production.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10.2 Verificaciones manuales
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Health check del backend
|
|
||||||
curl -s http://localhost:3006/api/health | jq .
|
|
||||||
|
|
||||||
# Verificar frontend
|
|
||||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:3005
|
|
||||||
|
|
||||||
# Verificar tenant principal
|
|
||||||
psql "$DATABASE_URL" -c "SELECT slug, is_active FROM auth_management.tenants WHERE slug = 'gamilit-prod';"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10.3 Prueba de registro (opcional)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Probar endpoint de registro
|
|
||||||
curl -X POST http://localhost:3006/api/v1/auth/register \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"email": "test-deploy@example.com",
|
|
||||||
"password": "TestPassword123!",
|
|
||||||
"first_name": "Test",
|
|
||||||
"last_name": "Deploy"
|
|
||||||
}'
|
|
||||||
|
|
||||||
# Si funciona, eliminar usuario de prueba
|
|
||||||
# psql "$DATABASE_URL" -c "DELETE FROM auth.users WHERE email = 'test-deploy@example.com';"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SCRIPT AUTOMATIZADO COMPLETO
|
|
||||||
|
|
||||||
Para automatizar todo el proceso, usar el siguiente script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# update-production.sh
|
|
||||||
# Uso: ./scripts/update-production.sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colores
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
# Configuracion
|
|
||||||
PROJECT_DIR="/ruta/al/proyecto/gamilit"
|
|
||||||
BACKUP_BASE="/home/gamilit/backups"
|
|
||||||
DB_NAME="gamilit_platform"
|
|
||||||
DB_USER="gamilit_user"
|
|
||||||
DB_HOST="localhost"
|
|
||||||
DB_PORT="5432"
|
|
||||||
# DB_PASSWORD debe estar en variable de entorno
|
|
||||||
|
|
||||||
# Crear timestamp
|
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
||||||
BACKUP_DIR="$BACKUP_BASE/$TIMESTAMP"
|
|
||||||
|
|
||||||
cd "$PROJECT_DIR"
|
|
||||||
|
|
||||||
echo -e "${BLUE}"
|
|
||||||
echo "=============================================="
|
|
||||||
echo " ACTUALIZACION PRODUCCION GAMILIT"
|
|
||||||
echo " Timestamp: $TIMESTAMP"
|
|
||||||
echo "=============================================="
|
|
||||||
echo -e "${NC}"
|
|
||||||
|
|
||||||
# 1. Detener servicios
|
|
||||||
echo -e "${YELLOW}[1/10] Deteniendo servicios...${NC}"
|
|
||||||
pm2 stop all
|
|
||||||
|
|
||||||
# 2. Crear directorios de backup
|
|
||||||
echo -e "${YELLOW}[2/10] Creando backup de configuraciones...${NC}"
|
|
||||||
mkdir -p "$BACKUP_DIR/config" "$BACKUP_DIR/database"
|
|
||||||
|
|
||||||
cp apps/backend/.env.production "$BACKUP_DIR/config/" 2>/dev/null || true
|
|
||||||
cp apps/backend/.env "$BACKUP_DIR/config/backend.env" 2>/dev/null || true
|
|
||||||
cp apps/frontend/.env.production "$BACKUP_DIR/config/" 2>/dev/null || true
|
|
||||||
cp apps/frontend/.env "$BACKUP_DIR/config/frontend.env" 2>/dev/null || true
|
|
||||||
cp ecosystem.config.js "$BACKUP_DIR/config/" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo -e "${GREEN}Configuraciones respaldadas en $BACKUP_DIR/config/${NC}"
|
|
||||||
|
|
||||||
# 3. Backup de base de datos
|
|
||||||
echo -e "${YELLOW}[3/10] Creando backup de base de datos...${NC}"
|
|
||||||
BACKUP_FILE="$BACKUP_DIR/database/gamilit_$TIMESTAMP.sql"
|
|
||||||
PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" > "$BACKUP_FILE"
|
|
||||||
gzip "$BACKUP_FILE"
|
|
||||||
echo -e "${GREEN}Base de datos respaldada: ${BACKUP_FILE}.gz${NC}"
|
|
||||||
|
|
||||||
# 4. Pull del repositorio
|
|
||||||
echo -e "${YELLOW}[4/10] Actualizando desde repositorio remoto...${NC}"
|
|
||||||
git fetch origin
|
|
||||||
git reset --hard origin/main
|
|
||||||
echo -e "${GREEN}Repositorio actualizado${NC}"
|
|
||||||
|
|
||||||
# 5. Restaurar configuraciones
|
|
||||||
echo -e "${YELLOW}[5/10] Restaurando configuraciones...${NC}"
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/backend/ 2>/dev/null || true
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/frontend/ 2>/dev/null || true
|
|
||||||
|
|
||||||
# 6. Recrear base de datos
|
|
||||||
echo -e "${YELLOW}[6/10] Recreando base de datos...${NC}"
|
|
||||||
cd apps/database
|
|
||||||
export DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
|
|
||||||
./create-database.sh
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
# 7. Instalar dependencias
|
|
||||||
echo -e "${YELLOW}[7/10] Instalando dependencias backend...${NC}"
|
|
||||||
cd apps/backend && npm install && cd ../..
|
|
||||||
|
|
||||||
echo -e "${YELLOW}[8/10] Instalando dependencias frontend...${NC}"
|
|
||||||
cd apps/frontend && npm install && cd ../..
|
|
||||||
|
|
||||||
# 8. Build
|
|
||||||
echo -e "${YELLOW}[9/10] Construyendo aplicaciones...${NC}"
|
|
||||||
cd apps/backend && npm run build && cd ../..
|
|
||||||
cd apps/frontend && npm run build && cd ../..
|
|
||||||
|
|
||||||
# 9. Iniciar servicios
|
|
||||||
echo -e "${YELLOW}[10/10] Iniciando servicios...${NC}"
|
|
||||||
pm2 start ecosystem.config.js
|
|
||||||
pm2 save
|
|
||||||
|
|
||||||
# 10. Validar
|
|
||||||
echo -e "${BLUE}"
|
|
||||||
echo "=============================================="
|
|
||||||
echo " VALIDACION"
|
|
||||||
echo "=============================================="
|
|
||||||
echo -e "${NC}"
|
|
||||||
|
|
||||||
./scripts/diagnose-production.sh
|
|
||||||
|
|
||||||
echo -e "${GREEN}"
|
|
||||||
echo "=============================================="
|
|
||||||
echo " ACTUALIZACION COMPLETADA"
|
|
||||||
echo "=============================================="
|
|
||||||
echo -e "${NC}"
|
|
||||||
echo "Backup disponible en: $BACKUP_DIR"
|
|
||||||
echo ""
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ROLLBACK (Si algo falla)
|
|
||||||
|
|
||||||
### Restaurar configuraciones
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copiar archivos de configuracion del backup
|
|
||||||
cp /home/gamilit/backups/YYYYMMDD_HHMMSS/config/* apps/backend/
|
|
||||||
cp /home/gamilit/backups/YYYYMMDD_HHMMSS/config/* apps/frontend/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restaurar base de datos
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Restaurar desde backup
|
|
||||||
BACKUP_FILE="/home/gamilit/backups/YYYYMMDD_HHMMSS/database/gamilit_*.sql.gz"
|
|
||||||
|
|
||||||
# Descomprimir
|
|
||||||
gunzip -c "$BACKUP_FILE" > /tmp/restore.sql
|
|
||||||
|
|
||||||
# Restaurar
|
|
||||||
psql "$DATABASE_URL" < /tmp/restore.sql
|
|
||||||
|
|
||||||
# Limpiar
|
|
||||||
rm /tmp/restore.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Volver a commit anterior
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver commits anteriores
|
|
||||||
git log --oneline -10
|
|
||||||
|
|
||||||
# Volver a commit especifico
|
|
||||||
git reset --hard <commit_hash>
|
|
||||||
|
|
||||||
# Reinstalar y reconstruir
|
|
||||||
cd apps/backend && npm install && npm run build && cd ../..
|
|
||||||
cd apps/frontend && npm install && npm run build && cd ../..
|
|
||||||
|
|
||||||
# Reiniciar
|
|
||||||
pm2 restart all
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DOCUMENTACION RELACIONADA
|
|
||||||
|
|
||||||
Despues del pull, el agente debe leer estas guias si necesita mas detalle:
|
|
||||||
|
|
||||||
| Guia | Proposito |
|
|
||||||
|------|-----------|
|
|
||||||
| `GUIA-DESPLIEGUE-PRODUCCION-COMPLETA.md` | Configuracion completa del servidor |
|
|
||||||
| `GUIA-VALIDACION-PRODUCCION.md` | Validaciones y troubleshooting |
|
|
||||||
| `GUIA-CREAR-BASE-DATOS.md` | Detalle del proceso de creacion de BD |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CHECKLIST RAPIDO
|
|
||||||
|
|
||||||
```
|
|
||||||
[ ] 1. pm2 stop all
|
|
||||||
[ ] 2. Backup configuraciones a /home/gamilit/backups/
|
|
||||||
[ ] 3. Backup base de datos (pg_dump)
|
|
||||||
[ ] 4. git fetch && git reset --hard origin/main
|
|
||||||
[ ] 5. Restaurar .env files
|
|
||||||
[ ] 6. cd apps/database && ./create-database.sh
|
|
||||||
[ ] 7. npm install (backend y frontend)
|
|
||||||
[ ] 8. npm run build (backend y frontend)
|
|
||||||
[ ] 9. pm2 start ecosystem.config.js
|
|
||||||
[ ] 10. ./scripts/diagnose-production.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
**Autor:** Sistema de documentacion GAMILIT
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
# GUIA-CORS-PRODUCCION.md
|
|
||||||
|
|
||||||
## Configuración CORS para Producción - GAMILIT
|
|
||||||
|
|
||||||
**Fecha**: 2025-12-18
|
|
||||||
**Problema resuelto**: Error `Access-Control-Allow-Origin contains multiple values`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Descripción del Problema
|
|
||||||
|
|
||||||
Al hacer requests desde el frontend (puerto 3005) al backend (puerto 3006) en producción con HTTPS, se recibe el error:
|
|
||||||
|
|
||||||
```
|
|
||||||
Access to XMLHttpRequest at 'https://74.208.126.102:3006/api/v1/auth/register'
|
|
||||||
from origin 'https://74.208.126.102:3005' has been blocked by CORS policy:
|
|
||||||
The 'Access-Control-Allow-Origin' header contains multiple values
|
|
||||||
'https://74.208.126.102:3005, https://74.208.126.102:3005', but only one is allowed.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Causa Raíz
|
|
||||||
|
|
||||||
El header `Access-Control-Allow-Origin` se está enviando **DOS VECES**:
|
|
||||||
|
|
||||||
1. **Nginx** (proxy SSL) agrega headers CORS
|
|
||||||
2. **NestJS** (backend) también agrega headers CORS via `app.enableCors()`
|
|
||||||
|
|
||||||
Cuando ambos envían el header, el navegador ve valores duplicados y rechaza la respuesta.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Solución Definitiva
|
|
||||||
|
|
||||||
### Regla de Oro: **Solo NestJS maneja CORS**
|
|
||||||
|
|
||||||
Nginx debe actuar únicamente como proxy SSL sin agregar headers CORS.
|
|
||||||
|
|
||||||
### 3.1 Configuración Backend (.env.production)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# CORS - CONFIGURACIÓN CRÍTICA
|
|
||||||
# ============================================================================
|
|
||||||
# ADVERTENCIA: HEADERS CORS DUPLICADOS
|
|
||||||
# ============================================================================
|
|
||||||
# Si usas Nginx como proxy SSL, NO agregar headers CORS en Nginx.
|
|
||||||
# NestJS maneja CORS internamente en main.ts.
|
|
||||||
# Headers duplicados causan error:
|
|
||||||
# "The 'Access-Control-Allow-Origin' header contains multiple values"
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Incluye HTTPS y HTTP para compatibilidad durante transición
|
|
||||||
CORS_ORIGIN=https://74.208.126.102:3005,https://74.208.126.102,http://74.208.126.102:3005,http://74.208.126.102
|
|
||||||
ENABLE_CORS=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Configuración Frontend (.env.production)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# SSL CONFIGURADO - Usar HTTPS/WSS
|
|
||||||
VITE_API_HOST=74.208.126.102:3006
|
|
||||||
VITE_API_PROTOCOL=https
|
|
||||||
VITE_WS_HOST=74.208.126.102:3006
|
|
||||||
VITE_WS_PROTOCOL=wss
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 Configuración Nginx (SIN CORS)
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# /etc/nginx/sites-available/gamilit-backend
|
|
||||||
server {
|
|
||||||
listen 3006 ssl;
|
|
||||||
server_name 74.208.126.102;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/certs/gamilit.crt;
|
|
||||||
ssl_certificate_key /etc/ssl/private/gamilit.key;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:3007; # PM2 cluster interno
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
|
|
||||||
# ⚠️ NO AGREGAR headers CORS aquí
|
|
||||||
# NestJS los maneja internamente
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Verificación
|
|
||||||
|
|
||||||
### 4.1 Verificar headers con curl
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar que solo hay UN header Access-Control-Allow-Origin
|
|
||||||
curl -I -X OPTIONS \
|
|
||||||
-H "Origin: https://74.208.126.102:3005" \
|
|
||||||
-H "Access-Control-Request-Method: POST" \
|
|
||||||
https://74.208.126.102:3006/api/v1/auth/register
|
|
||||||
|
|
||||||
# Salida esperada (UN solo header):
|
|
||||||
# Access-Control-Allow-Origin: https://74.208.126.102:3005
|
|
||||||
# Access-Control-Allow-Credentials: true
|
|
||||||
# Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Verificar configuración Nginx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Buscar headers CORS duplicados en configuración
|
|
||||||
grep -r "Access-Control" /etc/nginx/
|
|
||||||
```
|
|
||||||
|
|
||||||
Si encuentra líneas como `add_header Access-Control-Allow-Origin`, elimínelas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Pasos de Deploy
|
|
||||||
|
|
||||||
1. **Backend**: Actualizar `.env.production` con HTTPS origins
|
|
||||||
2. **Frontend**: Actualizar `.env.production` con HTTPS/WSS
|
|
||||||
3. **Nginx**: Remover cualquier header CORS
|
|
||||||
4. **Reiniciar servicios**:
|
|
||||||
```bash
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
pm2 restart gamilit-backend
|
|
||||||
cd /path/to/frontend && npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Troubleshooting
|
|
||||||
|
|
||||||
### Error persiste después de la configuración
|
|
||||||
1. Verificar que Nginx no tenga headers CORS en ningún include
|
|
||||||
2. Revisar si hay otro proxy (CloudFlare, etc.) agregando headers
|
|
||||||
3. Limpiar cache del navegador (F12 > Network > Disable cache)
|
|
||||||
|
|
||||||
### Error "CORS blocked request" en logs
|
|
||||||
- El origen que hace la petición no está en CORS_ORIGIN
|
|
||||||
- Verificar que el protocolo coincide (https vs http)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Referencias
|
|
||||||
|
|
||||||
- **Backend CORS**: `apps/backend/src/main.ts` líneas 27-46
|
|
||||||
- **Config CORS**: `apps/backend/src/config/app.config.ts`
|
|
||||||
- **Ejemplo .env**: `apps/backend/.env.production.example`
|
|
||||||
@ -1,483 +0,0 @@
|
|||||||
# GUIA DE DEPLOYMENT PARA AGENTE EN PRODUCCION - GAMILIT
|
|
||||||
|
|
||||||
**Version:** 1.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Servidor:** 74.208.126.102
|
|
||||||
**Proposito:** Guia estandarizada para el agente que ejecuta deployments en produccion
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## INFORMACION DEL SERVIDOR
|
|
||||||
|
|
||||||
| Aspecto | Valor |
|
|
||||||
|---------|-------|
|
|
||||||
| **IP** | 74.208.126.102 |
|
|
||||||
| **Usuario** | gamilit (o el usuario configurado) |
|
|
||||||
| **Backend** | Puerto 3006 (PM2 cluster, 2 instancias) |
|
|
||||||
| **Frontend** | Puerto 3005 (PM2 fork, 1 instancia) |
|
|
||||||
| **Database** | PostgreSQL puerto 5432, database `gamilit_platform` |
|
|
||||||
| **Repositorio** | git@github.com:rckrdmrd/gamilit-workspace.git |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ESTRUCTURA DE BACKUPS ESTANDAR
|
|
||||||
|
|
||||||
### Directorio Base
|
|
||||||
```
|
|
||||||
/home/gamilit/backups/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Estructura por Deployment
|
|
||||||
```
|
|
||||||
/home/gamilit/backups/
|
|
||||||
├── YYYYMMDD_HHMMSS/ # Timestamp del deployment
|
|
||||||
│ ├── database/
|
|
||||||
│ │ └── gamilit_YYYYMMDD_HHMMSS.sql.gz # Backup comprimido de BD
|
|
||||||
│ ├── config/
|
|
||||||
│ │ ├── backend.env.production # .env.production del backend
|
|
||||||
│ │ ├── backend.env # .env del backend (si existe)
|
|
||||||
│ │ ├── frontend.env.production # .env.production del frontend
|
|
||||||
│ │ ├── frontend.env # .env del frontend (si existe)
|
|
||||||
│ │ └── ecosystem.config.js # Configuracion PM2
|
|
||||||
│ └── logs/
|
|
||||||
│ ├── backend-error.log # Logs de error pre-deployment
|
|
||||||
│ ├── backend-out.log # Logs de salida pre-deployment
|
|
||||||
│ ├── frontend-error.log
|
|
||||||
│ └── frontend-out.log
|
|
||||||
├── latest -> YYYYMMDD_HHMMSS/ # Symlink al ultimo backup
|
|
||||||
└── README.md # Documentacion de backups
|
|
||||||
```
|
|
||||||
|
|
||||||
### Crear Estructura Inicial
|
|
||||||
```bash
|
|
||||||
# Ejecutar UNA VEZ para crear la estructura base
|
|
||||||
mkdir -p /home/gamilit/backups
|
|
||||||
chmod 700 /home/gamilit/backups
|
|
||||||
|
|
||||||
# Crear README
|
|
||||||
cat > /home/gamilit/backups/README.md << 'EOF'
|
|
||||||
# Backups de GAMILIT
|
|
||||||
|
|
||||||
Este directorio contiene los backups automaticos generados durante deployments.
|
|
||||||
|
|
||||||
## Estructura
|
|
||||||
- Cada subdirectorio tiene formato YYYYMMDD_HHMMSS
|
|
||||||
- `latest` es un symlink al backup mas reciente
|
|
||||||
- Los backups de BD estan comprimidos con gzip
|
|
||||||
|
|
||||||
## Restaurar Base de Datos
|
|
||||||
```bash
|
|
||||||
gunzip -c /home/gamilit/backups/YYYYMMDD_HHMMSS/database/gamilit_*.sql.gz | psql "$DATABASE_URL"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Restaurar Configuraciones
|
|
||||||
```bash
|
|
||||||
cp /home/gamilit/backups/YYYYMMDD_HHMMSS/config/backend.env.production apps/backend/.env.production
|
|
||||||
cp /home/gamilit/backups/YYYYMMDD_HHMMSS/config/frontend.env.production apps/frontend/.env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
## Retencion
|
|
||||||
Se recomienda mantener los ultimos 10 backups y eliminar los antiguos.
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## VARIABLES DE ENTORNO REQUERIDAS
|
|
||||||
|
|
||||||
Antes de cualquier deployment, verificar que estas variables esten configuradas:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# En ~/.bashrc o /etc/environment del servidor
|
|
||||||
|
|
||||||
# Database
|
|
||||||
export DB_HOST=localhost
|
|
||||||
export DB_PORT=5432
|
|
||||||
export DB_NAME=gamilit_platform
|
|
||||||
export DB_USER=gamilit_user
|
|
||||||
export DB_PASSWORD="[PASSWORD_SEGURO]"
|
|
||||||
export DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
|
|
||||||
|
|
||||||
# Seguridad (GENERAR VALORES UNICOS)
|
|
||||||
export JWT_SECRET="[VALOR_GENERADO_CON_openssl_rand_-base64_32]"
|
|
||||||
export SESSION_SECRET="[OTRO_VALOR_GENERADO]"
|
|
||||||
|
|
||||||
# CORS
|
|
||||||
export CORS_ORIGIN="https://gamilit.com,https://www.gamilit.com,http://74.208.126.102:3005"
|
|
||||||
|
|
||||||
# URLs
|
|
||||||
export FRONTEND_URL="https://gamilit.com"
|
|
||||||
export BACKEND_URL="https://gamilit.com/api"
|
|
||||||
|
|
||||||
# Backups
|
|
||||||
export BACKUP_BASE="/home/gamilit/backups"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Generar secretos seguros:**
|
|
||||||
```bash
|
|
||||||
openssl rand -base64 32 # Para JWT_SECRET
|
|
||||||
openssl rand -base64 32 # Para SESSION_SECRET
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PROCEDIMIENTO ESTANDAR DE DEPLOYMENT
|
|
||||||
|
|
||||||
### FASE 1: BACKUP (Antes de tocar nada)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1.1 Crear timestamp y directorio de backup
|
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
||||||
BACKUP_DIR="${BACKUP_BASE:-/home/gamilit/backups}/$TIMESTAMP"
|
|
||||||
mkdir -p "$BACKUP_DIR"/{database,config,logs}
|
|
||||||
|
|
||||||
# 1.2 Backup de base de datos
|
|
||||||
echo "=== BACKUP DE BASE DE DATOS ==="
|
|
||||||
PGPASSWORD="$DB_PASSWORD" pg_dump \
|
|
||||||
-h "$DB_HOST" \
|
|
||||||
-p "$DB_PORT" \
|
|
||||||
-U "$DB_USER" \
|
|
||||||
-d "$DB_NAME" \
|
|
||||||
--format=plain \
|
|
||||||
--no-owner \
|
|
||||||
--no-acl \
|
|
||||||
| gzip > "$BACKUP_DIR/database/gamilit_$TIMESTAMP.sql.gz"
|
|
||||||
|
|
||||||
echo "Backup creado: $BACKUP_DIR/database/gamilit_$TIMESTAMP.sql.gz"
|
|
||||||
|
|
||||||
# 1.3 Backup de configuraciones
|
|
||||||
echo "=== BACKUP DE CONFIGURACIONES ==="
|
|
||||||
cp apps/backend/.env.production "$BACKUP_DIR/config/backend.env.production" 2>/dev/null || true
|
|
||||||
cp apps/backend/.env "$BACKUP_DIR/config/backend.env" 2>/dev/null || true
|
|
||||||
cp apps/frontend/.env.production "$BACKUP_DIR/config/frontend.env.production" 2>/dev/null || true
|
|
||||||
cp apps/frontend/.env "$BACKUP_DIR/config/frontend.env" 2>/dev/null || true
|
|
||||||
cp ecosystem.config.js "$BACKUP_DIR/config/" 2>/dev/null || true
|
|
||||||
|
|
||||||
# 1.4 Backup de logs actuales
|
|
||||||
echo "=== BACKUP DE LOGS ==="
|
|
||||||
cp logs/*.log "$BACKUP_DIR/logs/" 2>/dev/null || true
|
|
||||||
|
|
||||||
# 1.5 Actualizar symlink 'latest'
|
|
||||||
ln -sfn "$BACKUP_DIR" "${BACKUP_BASE:-/home/gamilit/backups}/latest"
|
|
||||||
|
|
||||||
echo "Backup completado en: $BACKUP_DIR"
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 2: DETENER SERVICIOS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "=== DETENIENDO SERVICIOS ==="
|
|
||||||
pm2 stop all
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 3: PULL DEL REPOSITORIO
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "=== ACTUALIZANDO DESDE REPOSITORIO ==="
|
|
||||||
|
|
||||||
# Mostrar estado actual
|
|
||||||
git status
|
|
||||||
git branch --show-current
|
|
||||||
|
|
||||||
# Fetch y mostrar commits pendientes
|
|
||||||
git fetch origin
|
|
||||||
git log HEAD..origin/main --oneline 2>/dev/null || echo "Ya actualizado"
|
|
||||||
|
|
||||||
# Pull forzado (preferencia a remoto)
|
|
||||||
git reset --hard origin/main
|
|
||||||
|
|
||||||
# Mostrar ultimo commit
|
|
||||||
git log --oneline -1
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 4: RESTAURAR CONFIGURACIONES
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "=== RESTAURANDO CONFIGURACIONES ==="
|
|
||||||
|
|
||||||
# Restaurar .env files desde backup
|
|
||||||
cp "$BACKUP_DIR/config/backend.env.production" apps/backend/.env.production
|
|
||||||
cp "$BACKUP_DIR/config/frontend.env.production" apps/frontend/.env.production
|
|
||||||
|
|
||||||
# Crear symlinks .env -> .env.production
|
|
||||||
cd apps/backend && ln -sf .env.production .env && cd ../..
|
|
||||||
cd apps/frontend && ln -sf .env.production .env && cd ../..
|
|
||||||
|
|
||||||
echo "Configuraciones restauradas"
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 5: RECREAR BASE DE DATOS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "=== RECREANDO BASE DE DATOS ==="
|
|
||||||
|
|
||||||
cd apps/database
|
|
||||||
export DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
|
|
||||||
|
|
||||||
# Ejecutar script de creacion limpia
|
|
||||||
chmod +x create-database.sh
|
|
||||||
./create-database.sh
|
|
||||||
|
|
||||||
cd ../..
|
|
||||||
echo "Base de datos recreada"
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 6: INSTALAR DEPENDENCIAS Y BUILD
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "=== INSTALANDO DEPENDENCIAS ==="
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
cd apps/backend
|
|
||||||
npm install --production=false
|
|
||||||
npm run build
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
cd apps/frontend
|
|
||||||
npm install --production=false
|
|
||||||
npm run build
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
echo "Build completado"
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 7: INICIAR SERVICIOS CON PM2
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "=== INICIANDO SERVICIOS ==="
|
|
||||||
|
|
||||||
# Iniciar con ecosystem.config.js
|
|
||||||
pm2 start ecosystem.config.js --env production
|
|
||||||
|
|
||||||
# Guardar configuracion PM2
|
|
||||||
pm2 save
|
|
||||||
|
|
||||||
# Mostrar estado
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 8: CONFIGURAR HTTPS CON CERTBOT (Si no esta configurado)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# SOLO SI ES PRIMERA VEZ O CERTIFICADO EXPIRADO
|
|
||||||
|
|
||||||
echo "=== CONFIGURANDO HTTPS ==="
|
|
||||||
|
|
||||||
# 1. Instalar certbot si no existe
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y certbot python3-certbot-nginx
|
|
||||||
|
|
||||||
# 2. Obtener certificado (reemplazar gamilit.com con tu dominio)
|
|
||||||
sudo certbot --nginx -d gamilit.com -d www.gamilit.com
|
|
||||||
|
|
||||||
# 3. Verificar renovacion automatica
|
|
||||||
sudo certbot renew --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 9: CONFIGURAR NGINX COMO REVERSE PROXY
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# SOLO SI ES PRIMERA VEZ
|
|
||||||
|
|
||||||
# Crear configuracion Nginx
|
|
||||||
sudo tee /etc/nginx/sites-available/gamilit << 'NGINX'
|
|
||||||
# Redirect HTTP to HTTPS
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name gamilit.com www.gamilit.com;
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS Server
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name gamilit.com www.gamilit.com;
|
|
||||||
|
|
||||||
# SSL Configuration (certbot lo configura automaticamente)
|
|
||||||
ssl_certificate /etc/letsencrypt/live/gamilit.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/gamilit.com/privkey.pem;
|
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3005;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://localhost:3006;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# WebSocket
|
|
||||||
location /socket.io {
|
|
||||||
proxy_pass http://localhost:3006;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX
|
|
||||||
|
|
||||||
# Habilitar sitio
|
|
||||||
sudo ln -sf /etc/nginx/sites-available/gamilit /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### FASE 10: VALIDACION
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "=== VALIDANDO DEPLOYMENT ==="
|
|
||||||
|
|
||||||
# Ejecutar script de diagnostico
|
|
||||||
./scripts/diagnose-production.sh
|
|
||||||
|
|
||||||
# O validacion manual:
|
|
||||||
echo "--- Health Check Backend ---"
|
|
||||||
curl -s https://gamilit.com/api/health | head -10
|
|
||||||
|
|
||||||
echo "--- Frontend Status ---"
|
|
||||||
curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" https://gamilit.com
|
|
||||||
|
|
||||||
echo "--- PM2 Status ---"
|
|
||||||
pm2 list
|
|
||||||
|
|
||||||
echo "--- Logs ---"
|
|
||||||
pm2 logs --lines 20
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CONFIGURACION CORS PARA HTTPS
|
|
||||||
|
|
||||||
Una vez configurado HTTPS, actualizar las configuraciones:
|
|
||||||
|
|
||||||
### Backend .env.production
|
|
||||||
```bash
|
|
||||||
# Actualizar CORS para HTTPS
|
|
||||||
CORS_ORIGIN=https://gamilit.com,https://www.gamilit.com
|
|
||||||
FRONTEND_URL=https://gamilit.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend .env.production
|
|
||||||
```bash
|
|
||||||
# Actualizar para HTTPS
|
|
||||||
VITE_API_PROTOCOL=https
|
|
||||||
VITE_WS_PROTOCOL=wss
|
|
||||||
VITE_API_HOST=gamilit.com
|
|
||||||
VITE_WS_HOST=gamilit.com
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ROLLBACK (Si algo falla)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Detener servicios
|
|
||||||
pm2 stop all
|
|
||||||
|
|
||||||
# 2. Restaurar base de datos desde ultimo backup
|
|
||||||
LATEST_BACKUP="${BACKUP_BASE:-/home/gamilit/backups}/latest"
|
|
||||||
gunzip -c "$LATEST_BACKUP/database/gamilit_*.sql.gz" | \
|
|
||||||
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME"
|
|
||||||
|
|
||||||
# 3. Restaurar configuraciones
|
|
||||||
cp "$LATEST_BACKUP/config/backend.env.production" apps/backend/.env.production
|
|
||||||
cp "$LATEST_BACKUP/config/frontend.env.production" apps/frontend/.env.production
|
|
||||||
|
|
||||||
# 4. Revertir codigo (si es necesario)
|
|
||||||
git reflog # Ver commits anteriores
|
|
||||||
git reset --hard HEAD~1 # Volver un commit atras
|
|
||||||
|
|
||||||
# 5. Rebuild y reiniciar
|
|
||||||
cd apps/backend && npm run build && cd ../..
|
|
||||||
cd apps/frontend && npm run build && cd ../..
|
|
||||||
pm2 start ecosystem.config.js --env production
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TROUBLESHOOTING
|
|
||||||
|
|
||||||
### Error: CORS bloqueado
|
|
||||||
```bash
|
|
||||||
# Verificar CORS_ORIGIN en backend
|
|
||||||
grep CORS apps/backend/.env.production
|
|
||||||
|
|
||||||
# Debe incluir el dominio con protocolo correcto (https://)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: Certificado SSL
|
|
||||||
```bash
|
|
||||||
# Renovar certificado
|
|
||||||
sudo certbot renew
|
|
||||||
|
|
||||||
# Verificar certificado
|
|
||||||
sudo certbot certificates
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: PM2 no inicia
|
|
||||||
```bash
|
|
||||||
# Ver logs de error
|
|
||||||
pm2 logs gamilit-backend --err --lines 50
|
|
||||||
|
|
||||||
# Verificar que el build existe
|
|
||||||
ls -la apps/backend/dist/main.js
|
|
||||||
ls -la apps/frontend/dist/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: Base de datos no conecta
|
|
||||||
```bash
|
|
||||||
# Verificar PostgreSQL
|
|
||||||
sudo systemctl status postgresql
|
|
||||||
|
|
||||||
# Verificar conexion
|
|
||||||
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MANTENIMIENTO
|
|
||||||
|
|
||||||
### Limpiar backups antiguos (mantener ultimos 10)
|
|
||||||
```bash
|
|
||||||
cd /home/gamilit/backups
|
|
||||||
ls -dt */ | tail -n +11 | xargs rm -rf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Renovar certificados SSL
|
|
||||||
```bash
|
|
||||||
# Ejecutar mensualmente o cuando expire
|
|
||||||
sudo certbot renew
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monitorear logs
|
|
||||||
```bash
|
|
||||||
pm2 logs --lines 100
|
|
||||||
pm2 monit
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Guia creada para el agente de produccion de GAMILIT*
|
|
||||||
*Ultima actualizacion: 2025-12-18*
|
|
||||||
@ -1,227 +0,0 @@
|
|||||||
# GUIA RAPIDA: Deployment en Produccion
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Servidor:** 74.208.126.102
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CHECKLIST RAPIDO (10 PASOS)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. BACKUP (SIEMPRE PRIMERO)
|
|
||||||
BACKUP_DIR="/home/gamilit/backups/$(date +%Y%m%d_%H%M%S)"
|
|
||||||
mkdir -p "$BACKUP_DIR/config"
|
|
||||||
cp apps/backend/.env.production "$BACKUP_DIR/config/"
|
|
||||||
cp apps/frontend/.env.production "$BACKUP_DIR/config/"
|
|
||||||
|
|
||||||
# 2. BACKUP BASE DE DATOS
|
|
||||||
pg_dump "$DATABASE_URL" | gzip > "$BACKUP_DIR/gamilit.sql.gz"
|
|
||||||
|
|
||||||
# 3. PULL CAMBIOS
|
|
||||||
git fetch origin && git reset --hard origin/main
|
|
||||||
|
|
||||||
# 4. RESTAURAR CONFIG
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/backend/
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/frontend/
|
|
||||||
|
|
||||||
# 5. INSTALAR DEPENDENCIAS
|
|
||||||
npm install && cd apps/backend && npm install && cd ../frontend && npm install && cd ../..
|
|
||||||
|
|
||||||
# 6. BUILD
|
|
||||||
cd apps/backend && npm run build && cd ../frontend && npm run build && cd ../..
|
|
||||||
|
|
||||||
# 7. RECREAR BASE DE DATOS (si hay cambios DDL)
|
|
||||||
cd apps/database && ./drop-and-recreate-database.sh "$DATABASE_URL" && cd ..
|
|
||||||
|
|
||||||
# 8. DEPLOY PM2
|
|
||||||
pm2 delete all 2>/dev/null; pm2 start ecosystem.config.js --env production; pm2 save
|
|
||||||
|
|
||||||
# 9. VALIDAR
|
|
||||||
./scripts/validate-deployment.sh --ssl
|
|
||||||
|
|
||||||
# 10. STARTUP (solo primera vez)
|
|
||||||
pm2 startup && pm2 save
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ESCENARIOS COMUNES
|
|
||||||
|
|
||||||
### A. Solo actualizar codigo (sin cambios BD)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git pull origin main
|
|
||||||
cd apps/backend && npm install && npm run build && cd ..
|
|
||||||
cd apps/frontend && npm install && npm run build && cd ..
|
|
||||||
pm2 restart all
|
|
||||||
```
|
|
||||||
|
|
||||||
### B. Cambios en frontend unicamente
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git pull origin main
|
|
||||||
cd apps/frontend && npm install && npm run build && cd ..
|
|
||||||
pm2 restart gamilit-frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
### C. Cambios en backend unicamente
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git pull origin main
|
|
||||||
cd apps/backend && npm install && npm run build && cd ..
|
|
||||||
pm2 restart gamilit-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### D. Cambios en variables de entorno (.env)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Editar archivo
|
|
||||||
nano apps/backend/.env.production
|
|
||||||
# O
|
|
||||||
nano apps/frontend/.env.production
|
|
||||||
|
|
||||||
# Rebuild frontend si cambian VITE_*
|
|
||||||
cd apps/frontend && npm run build && cd ..
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
pm2 restart all
|
|
||||||
```
|
|
||||||
|
|
||||||
### E. Recrear base de datos completa
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/database
|
|
||||||
./drop-and-recreate-database.sh "$DATABASE_URL"
|
|
||||||
cd ..
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMANDOS DE EMERGENCIA
|
|
||||||
|
|
||||||
### Rollback rapido
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Restaurar desde backup
|
|
||||||
pg_restore "$BACKUP_DIR/gamilit.sql.gz" | psql "$DATABASE_URL"
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/backend/
|
|
||||||
cp "$BACKUP_DIR/config/.env.production" apps/frontend/
|
|
||||||
pm2 restart all
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ver logs de errores
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 logs --err --lines 100
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restart de emergencia
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 kill && pm2 start ecosystem.config.js --env production
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status completo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 list && pm2 logs --lines 10 --nostream
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## VARIABLES DE ENTORNO CRITICAS
|
|
||||||
|
|
||||||
### Backend `.env.production`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
NODE_ENV=production
|
|
||||||
PORT=3006
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_NAME=gamilit_platform
|
|
||||||
DB_USER=gamilit_user
|
|
||||||
DB_PASSWORD=<PASSWORD>
|
|
||||||
JWT_SECRET=<GENERAR: openssl rand -base64 32>
|
|
||||||
CORS_ORIGIN=https://gamilit.com
|
|
||||||
FRONTEND_URL=https://gamilit.com
|
|
||||||
ENABLE_SWAGGER=false
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend `.env.production`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
VITE_ENV=production
|
|
||||||
VITE_API_HOST=gamilit.com
|
|
||||||
VITE_API_PROTOCOL=https
|
|
||||||
VITE_WS_HOST=gamilit.com
|
|
||||||
VITE_WS_PROTOCOL=wss
|
|
||||||
VITE_MOCK_API=false
|
|
||||||
VITE_ENABLE_DEBUG=false
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SSL CON CERTBOT (NUEVO SERVIDOR)
|
|
||||||
|
|
||||||
> **Guia completa:** Ver [GUIA-SSL-CERTBOT-DEPLOYMENT.md](./GUIA-SSL-CERTBOT-DEPLOYMENT.md) para documentacion detallada, troubleshooting y renovacion.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Automatizado con Let's Encrypt
|
|
||||||
sudo ./scripts/setup-ssl-certbot.sh gamilit.com www.gamilit.com
|
|
||||||
|
|
||||||
# Auto-firmado (sin dominio)
|
|
||||||
sudo ./scripts/setup-ssl-certbot.sh --self-signed
|
|
||||||
|
|
||||||
# Ayuda
|
|
||||||
./scripts/setup-ssl-certbot.sh --help
|
|
||||||
|
|
||||||
# Manual
|
|
||||||
sudo apt install -y nginx certbot python3-certbot-nginx
|
|
||||||
sudo certbot --nginx -d gamilit.com -d www.gamilit.com
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## VALIDACION
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basica (sin SSL)
|
|
||||||
./scripts/validate-deployment.sh
|
|
||||||
|
|
||||||
# Completa con SSL
|
|
||||||
./scripts/validate-deployment.sh --ssl --verbose
|
|
||||||
|
|
||||||
# Opciones disponibles
|
|
||||||
./scripts/validate-deployment.sh --help
|
|
||||||
# --ssl Incluir validaciones de SSL/HTTPS
|
|
||||||
# --verbose Mostrar informacion adicional
|
|
||||||
|
|
||||||
# Manual
|
|
||||||
curl http://localhost:3006/api/health
|
|
||||||
curl http://localhost:3005
|
|
||||||
curl https://gamilit.com/api/health
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Nota:** El script `validate-deployment.sh` verifica archivos .env, builds, PM2, endpoints, SSL (opcional) y base de datos.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TROUBLESHOOTING
|
|
||||||
|
|
||||||
| Problema | Solucion |
|
|
||||||
|----------|----------|
|
|
||||||
| PM2 no inicia | `pm2 kill && pm2 start ecosystem.config.js --env production` |
|
|
||||||
| CORS error | Verificar CORS_ORIGIN en backend y rebuildar frontend |
|
|
||||||
| SSL no funciona | `sudo nginx -t && sudo systemctl restart nginx` |
|
|
||||||
| BD no conecta | Verificar DB_PASSWORD y que PostgreSQL este corriendo |
|
|
||||||
| Build falla | `rm -rf node_modules && npm install` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CONTACTO
|
|
||||||
|
|
||||||
- Logs: `/home/isem/workspace/projects/gamilit/logs/`
|
|
||||||
- Config PM2: `ecosystem.config.js`
|
|
||||||
- Guia completa: `docs/95-guias-desarrollo/GUIA-DESPLIEGUE-PRODUCCION-COMPLETA.md`
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,248 +0,0 @@
|
|||||||
# GUIA: SSL Auto-firmado para Produccion (Sin Dominio)
|
|
||||||
|
|
||||||
**Servidor:** 74.208.126.102
|
|
||||||
**Uso:** Cuando NO tienes dominio configurado
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ARQUITECTURA
|
|
||||||
|
|
||||||
```
|
|
||||||
INTERNET
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Nginx :443 │ ◄── HTTPS (SSL auto-firmado)
|
|
||||||
│ (Reverse │
|
|
||||||
│ Proxy) │
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
┌─────────────┴─────────────┐
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Backend :3006 │ │ Frontend :3005 │
|
|
||||||
│ (NestJS) │ │ (Vite Preview) │
|
|
||||||
│ /api/* │ │ /* │
|
|
||||||
└─────────────────┘ └─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Puertos (NO SE CAMBIAN):**
|
|
||||||
- Frontend: 3005 (HTTP interno)
|
|
||||||
- Backend: 3006 (HTTP interno)
|
|
||||||
- Nginx: 443 (HTTPS externo)
|
|
||||||
|
|
||||||
**Acceso:**
|
|
||||||
- https://74.208.126.102 → Frontend
|
|
||||||
- https://74.208.126.102/api → Backend
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 1: Generar Certificado Auto-firmado
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo mkdir -p /etc/nginx/ssl
|
|
||||||
|
|
||||||
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
|
||||||
-keyout /etc/nginx/ssl/gamilit.key \
|
|
||||||
-out /etc/nginx/ssl/gamilit.crt \
|
|
||||||
-subj "/C=MX/ST=Estado/L=Ciudad/O=Gamilit/CN=74.208.126.102"
|
|
||||||
|
|
||||||
sudo ls -la /etc/nginx/ssl/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 2: Instalar Nginx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 3: Configurar Nginx con SSL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /etc/nginx/sites-available/gamilit << 'NGINX'
|
|
||||||
# =============================================================================
|
|
||||||
# GAMILIT Production - SSL Auto-firmado
|
|
||||||
# Acceso: https://74.208.126.102
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Redirect HTTP to HTTPS
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name 74.208.126.102;
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS Server
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name 74.208.126.102;
|
|
||||||
|
|
||||||
# SSL con certificado auto-firmado
|
|
||||||
ssl_certificate /etc/nginx/ssl/gamilit.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/ssl/gamilit.key;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
||||||
|
|
||||||
# IMPORTANTE: NO agregar headers CORS aqui
|
|
||||||
# NestJS maneja CORS internamente
|
|
||||||
|
|
||||||
# Frontend (default) - proxy a puerto 3005
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3005;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API - proxy a puerto 3006
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://localhost:3006;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# WebSocket
|
|
||||||
location /socket.io {
|
|
||||||
proxy_pass http://localhost:3006;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX
|
|
||||||
|
|
||||||
sudo ln -sf /etc/nginx/sites-available/gamilit /etc/nginx/sites-enabled/
|
|
||||||
sudo rm -f /etc/nginx/sites-enabled/default
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl restart nginx
|
|
||||||
sudo systemctl enable nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 4: Configurar Backend (.env.production)
|
|
||||||
|
|
||||||
**NO cambiar PORT.** Solo actualizar CORS:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# En apps/backend/.env.production
|
|
||||||
# Puerto se mantiene en 3006
|
|
||||||
PORT=3006
|
|
||||||
|
|
||||||
# CORS apunta al acceso HTTPS via Nginx
|
|
||||||
CORS_ORIGIN=https://74.208.126.102
|
|
||||||
|
|
||||||
# Frontend URL
|
|
||||||
FRONTEND_URL=https://74.208.126.102
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 5: Configurar Frontend (.env.production)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# En apps/frontend/.env.production
|
|
||||||
# API a través de Nginx (mismo host, path /api)
|
|
||||||
VITE_API_HOST=74.208.126.102
|
|
||||||
VITE_API_PROTOCOL=https
|
|
||||||
|
|
||||||
# WebSocket
|
|
||||||
VITE_WS_HOST=74.208.126.102
|
|
||||||
VITE_WS_PROTOCOL=wss
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 6: Rebuild Frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/frontend
|
|
||||||
npm run build
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 7: Reiniciar Servicios
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pm2 restart all
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 8: Validar
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar Nginx
|
|
||||||
sudo systemctl status nginx
|
|
||||||
|
|
||||||
# Health check via HTTPS
|
|
||||||
curl -sk https://74.208.126.102/api/v1/health
|
|
||||||
|
|
||||||
# Frontend via HTTPS
|
|
||||||
curl -sk -o /dev/null -w "HTTP Status: %{http_code}\n" https://74.208.126.102
|
|
||||||
|
|
||||||
# PM2 status
|
|
||||||
pm2 list
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## URLs de Acceso
|
|
||||||
|
|
||||||
| Servicio | URL |
|
|
||||||
|----------|-----|
|
|
||||||
| Frontend | https://74.208.126.102 |
|
|
||||||
| Backend API | https://74.208.126.102/api/v1 |
|
|
||||||
| Health Check | https://74.208.126.102/api/v1/health |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## IMPORTANTE
|
|
||||||
|
|
||||||
1. **NO cambiar puertos de las apps** - Backend 3006, Frontend 3005
|
|
||||||
2. **Solo Nginx expone HTTPS** - Puerto 443
|
|
||||||
3. **Acceso unificado** - Todo via https://74.208.126.102
|
|
||||||
4. **CORS apunta a Nginx** - https://74.208.126.102 (no a puertos internos)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Error: Puerto 443 en uso
|
|
||||||
```bash
|
|
||||||
sudo lsof -i :443
|
|
||||||
sudo systemctl stop apache2 # Si Apache está corriendo
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: CORS
|
|
||||||
Verificar que CORS_ORIGIN sea `https://74.208.126.102` (sin puerto)
|
|
||||||
|
|
||||||
### Error: Nginx no inicia
|
|
||||||
```bash
|
|
||||||
sudo nginx -t
|
|
||||||
sudo journalctl -u nginx --no-pager -n 50
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Guia actualizada: 2025-12-18*
|
|
||||||
@ -1,356 +0,0 @@
|
|||||||
# Guia SSL con Certbot - GAMILIT
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Autor:** Requirements-Analyst (SIMCO)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. INTRODUCCION Y PROPOSITO
|
|
||||||
|
|
||||||
### Que hace el script `setup-ssl-certbot.sh`
|
|
||||||
|
|
||||||
Configura SSL/HTTPS automaticamente para la plataforma GAMILIT usando:
|
|
||||||
- **Let's Encrypt (Certbot):** Certificados SSL gratuitos y automaticos para dominios reales
|
|
||||||
- **Auto-firmado:** Certificados locales para servidores sin dominio publico
|
|
||||||
|
|
||||||
### Cuando usar cada opcion
|
|
||||||
|
|
||||||
| Escenario | Opcion | Comando |
|
|
||||||
|-----------|--------|---------|
|
|
||||||
| Servidor con dominio real (produccion) | Let's Encrypt | `./scripts/setup-ssl-certbot.sh gamilit.com` |
|
|
||||||
| Servidor sin dominio (desarrollo/staging) | Auto-firmado | `./scripts/setup-ssl-certbot.sh --self-signed` |
|
|
||||||
| Multiples dominios | Let's Encrypt | `./scripts/setup-ssl-certbot.sh gamilit.com www.gamilit.com` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. ARQUITECTURA
|
|
||||||
|
|
||||||
### Diagrama de Flujo HTTP a HTTPS
|
|
||||||
|
|
||||||
```
|
|
||||||
Internet
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Puerto 80/443 │
|
|
||||||
│ (Nginx) │
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
┌──────────────┼──────────────┐
|
|
||||||
│ │ │
|
|
||||||
▼ ▼ ▼
|
|
||||||
/api/* /socket.io /*
|
|
||||||
│ │ │
|
|
||||||
▼ ▼ ▼
|
|
||||||
┌───────────┐ ┌───────────┐ ┌───────────┐
|
|
||||||
│ Backend │ │ WebSocket │ │ Frontend │
|
|
||||||
│ :3006 │ │ :3006 │ │ :3005 │
|
|
||||||
└───────────┘ └───────────┘ └───────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Puertos Utilizados
|
|
||||||
|
|
||||||
| Puerto | Servicio | Tipo |
|
|
||||||
|--------|----------|------|
|
|
||||||
| 80 | Nginx HTTP | Externo (redirect a 443) |
|
|
||||||
| 443 | Nginx HTTPS | Externo |
|
|
||||||
| 3005 | Frontend (PM2) | Interno |
|
|
||||||
| 3006 | Backend (PM2) | Interno |
|
|
||||||
| 5432 | PostgreSQL | Interno |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. REQUISITOS PREVIOS
|
|
||||||
|
|
||||||
### Sistema
|
|
||||||
- [x] Ubuntu/Debian
|
|
||||||
- [x] Acceso root (sudo)
|
|
||||||
- [x] Puertos 80 y 443 abiertos en firewall
|
|
||||||
|
|
||||||
### Servicios
|
|
||||||
- [x] PM2 instalado y ejecutando
|
|
||||||
- [x] `gamilit-backend` activo en PM2
|
|
||||||
- [x] `gamilit-frontend` activo en PM2
|
|
||||||
|
|
||||||
### DNS (solo para Let's Encrypt)
|
|
||||||
- [x] Dominio registrado
|
|
||||||
- [x] DNS A record apuntando al servidor (IP: 74.208.126.102 o tu IP)
|
|
||||||
|
|
||||||
### Verificar requisitos
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar PM2
|
|
||||||
pm2 list
|
|
||||||
|
|
||||||
# Verificar puertos
|
|
||||||
sudo ufw status
|
|
||||||
|
|
||||||
# Verificar DNS (reemplaza con tu dominio)
|
|
||||||
dig +short tu-dominio.com
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. INSTALACION RAPIDA
|
|
||||||
|
|
||||||
### Opcion A: Con Dominio Real (Let's Encrypt)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Hacer script ejecutable
|
|
||||||
chmod +x scripts/setup-ssl-certbot.sh
|
|
||||||
|
|
||||||
# 2. Ejecutar con tu dominio
|
|
||||||
sudo ./scripts/setup-ssl-certbot.sh gamilit.com
|
|
||||||
|
|
||||||
# 3. Para multiples dominios
|
|
||||||
sudo ./scripts/setup-ssl-certbot.sh gamilit.com www.gamilit.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opcion B: Sin Dominio (Auto-firmado)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Hacer script ejecutable
|
|
||||||
chmod +x scripts/setup-ssl-certbot.sh
|
|
||||||
|
|
||||||
# 2. Ejecutar con flag auto-firmado
|
|
||||||
sudo ./scripts/setup-ssl-certbot.sh --self-signed
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ayuda
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/setup-ssl-certbot.sh --help
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. CONFIGURACION DETALLADA
|
|
||||||
|
|
||||||
### Paso a Paso: Let's Encrypt
|
|
||||||
|
|
||||||
1. **Verificacion de prerequisitos**
|
|
||||||
- Instala Nginx si no existe
|
|
||||||
- Instala Certbot si no existe
|
|
||||||
- Verifica procesos PM2
|
|
||||||
|
|
||||||
2. **Verificacion DNS**
|
|
||||||
- Resuelve el dominio
|
|
||||||
- Verifica que apunte al servidor correcto
|
|
||||||
- Si hay discrepancia, pregunta confirmacion
|
|
||||||
|
|
||||||
3. **Configuracion Nginx inicial (HTTP)**
|
|
||||||
- Crea config en `/etc/nginx/sites-available/gamilit`
|
|
||||||
- Configura proxies para Frontend, API, WebSocket
|
|
||||||
- Habilita sitio y recarga Nginx
|
|
||||||
|
|
||||||
4. **Obtencion de certificado**
|
|
||||||
- Ejecuta Certbot con el dominio
|
|
||||||
- Configura renovacion automatica
|
|
||||||
- Actualiza config Nginx con SSL
|
|
||||||
|
|
||||||
5. **Actualizacion de variables de entorno**
|
|
||||||
- Modifica `.env.production` del backend
|
|
||||||
- Modifica `.env.production` del frontend
|
|
||||||
|
|
||||||
6. **Rebuild y restart**
|
|
||||||
- Rebuild del frontend con nuevas variables
|
|
||||||
- Restart de todos los servicios PM2
|
|
||||||
|
|
||||||
7. **Validacion**
|
|
||||||
- Verifica HTTPS en Frontend
|
|
||||||
- Verifica HTTPS en API
|
|
||||||
- Verifica redirect HTTP->HTTPS
|
|
||||||
|
|
||||||
### Paso a Paso: Auto-firmado
|
|
||||||
|
|
||||||
Similar al anterior pero:
|
|
||||||
- Genera certificado con OpenSSL (365 dias)
|
|
||||||
- Almacena en `/etc/nginx/ssl/`
|
|
||||||
- No requiere validacion DNS
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. VARIABLES DE ENTORNO ACTUALIZADAS
|
|
||||||
|
|
||||||
### Backend (.env.production)
|
|
||||||
|
|
||||||
```env
|
|
||||||
# ANTES
|
|
||||||
CORS_ORIGIN=http://74.208.126.102:3005
|
|
||||||
FRONTEND_URL=http://74.208.126.102:3005
|
|
||||||
|
|
||||||
# DESPUES (Let's Encrypt)
|
|
||||||
CORS_ORIGIN=https://gamilit.com
|
|
||||||
FRONTEND_URL=https://gamilit.com
|
|
||||||
|
|
||||||
# DESPUES (Auto-firmado)
|
|
||||||
CORS_ORIGIN=https://74.208.126.102
|
|
||||||
FRONTEND_URL=https://74.208.126.102
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend (.env.production)
|
|
||||||
|
|
||||||
```env
|
|
||||||
# ANTES
|
|
||||||
VITE_API_HOST=74.208.126.102:3006
|
|
||||||
VITE_API_PROTOCOL=http
|
|
||||||
VITE_WS_HOST=74.208.126.102:3006
|
|
||||||
VITE_WS_PROTOCOL=ws
|
|
||||||
|
|
||||||
# DESPUES
|
|
||||||
VITE_API_HOST=gamilit.com
|
|
||||||
VITE_API_PROTOCOL=https
|
|
||||||
VITE_WS_HOST=gamilit.com
|
|
||||||
VITE_WS_PROTOCOL=wss
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. VALIDACION POST-INSTALACION
|
|
||||||
|
|
||||||
### Validacion Automatica
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/validate-deployment.sh --ssl --verbose
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validacion Manual
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Frontend HTTPS
|
|
||||||
curl -I https://gamilit.com
|
|
||||||
|
|
||||||
# API HTTPS
|
|
||||||
curl https://gamilit.com/api/health
|
|
||||||
|
|
||||||
# Redirect HTTP->HTTPS
|
|
||||||
curl -I http://gamilit.com
|
|
||||||
|
|
||||||
# Certificado
|
|
||||||
echo | openssl s_client -connect gamilit.com:443 2>/dev/null | openssl x509 -noout -dates
|
|
||||||
```
|
|
||||||
|
|
||||||
### URLs de Acceso (post-SSL)
|
|
||||||
|
|
||||||
| Servicio | URL |
|
|
||||||
|----------|-----|
|
|
||||||
| Frontend | https://gamilit.com |
|
|
||||||
| API | https://gamilit.com/api |
|
|
||||||
| Health | https://gamilit.com/api/health |
|
|
||||||
| WebSocket | wss://gamilit.com/socket.io |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. TROUBLESHOOTING
|
|
||||||
|
|
||||||
### DNS no resuelve
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: No se pudo resolver dominio gamilit.com
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
1. Verificar DNS: `dig +short gamilit.com`
|
|
||||||
2. Esperar propagacion DNS (hasta 48h)
|
|
||||||
3. Verificar A record en registrador de dominio
|
|
||||||
|
|
||||||
### Certbot falla
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: Challenge failed for domain
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
1. Verificar puerto 80 abierto: `sudo ufw allow 80`
|
|
||||||
2. Verificar que Nginx responde: `curl http://localhost`
|
|
||||||
3. Revisar logs: `sudo tail -f /var/log/letsencrypt/letsencrypt.log`
|
|
||||||
|
|
||||||
### Nginx no inicia
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: Configuracion Nginx invalida
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
1. Verificar sintaxis: `sudo nginx -t`
|
|
||||||
2. Revisar config: `sudo cat /etc/nginx/sites-available/gamilit`
|
|
||||||
3. Revisar logs: `sudo tail -f /var/log/nginx/error.log`
|
|
||||||
|
|
||||||
### CORS errors despues de SSL
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: CORS policy blocked
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
1. Verificar CORS_ORIGIN en backend: `grep CORS apps/backend/.env.production`
|
|
||||||
2. Debe ser exactamente `https://tu-dominio.com` (sin trailing slash)
|
|
||||||
3. Restart backend: `pm2 restart gamilit-backend`
|
|
||||||
|
|
||||||
### Certificado expirado
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: SSL certificate has expired
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
1. Renovar manualmente: `sudo certbot renew`
|
|
||||||
2. Verificar cron de renovacion: `sudo systemctl status certbot.timer`
|
|
||||||
3. Si falla, regenerar: `sudo certbot certonly --nginx -d gamilit.com`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. RENOVACION Y MANTENIMIENTO
|
|
||||||
|
|
||||||
### Renovacion Automatica
|
|
||||||
|
|
||||||
Certbot configura automaticamente un cron/timer para renovacion.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar timer
|
|
||||||
sudo systemctl status certbot.timer
|
|
||||||
|
|
||||||
# Ver proxima renovacion
|
|
||||||
sudo certbot certificates
|
|
||||||
```
|
|
||||||
|
|
||||||
### Renovacion Manual
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Dry-run (sin cambios)
|
|
||||||
sudo certbot renew --dry-run
|
|
||||||
|
|
||||||
# Renovacion forzada
|
|
||||||
sudo certbot renew --force-renewal
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logs de Renovacion
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Logs de Certbot
|
|
||||||
sudo tail -f /var/log/letsencrypt/letsencrypt.log
|
|
||||||
|
|
||||||
# Historial de renovaciones
|
|
||||||
sudo ls -la /etc/letsencrypt/renewal-hooks/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. REFERENCIAS
|
|
||||||
|
|
||||||
- [Documentacion Certbot](https://certbot.eff.org/docs/)
|
|
||||||
- [Nginx SSL Configuration](https://nginx.org/en/docs/http/configuring_https_servers.html)
|
|
||||||
- [Let's Encrypt](https://letsencrypt.org/docs/)
|
|
||||||
|
|
||||||
### Documentacion Relacionada
|
|
||||||
|
|
||||||
- `GUIA-SSL-NGINX-PRODUCCION.md` - Configuracion manual de SSL
|
|
||||||
- `GUIA-SSL-AUTOFIRMADO.md` - Certificados auto-firmados
|
|
||||||
- `GUIA-DEPLOYMENT-RAPIDO.md` - Deployment general
|
|
||||||
- `GUIA-VALIDACION-PRODUCCION.md` - Validaciones post-deployment
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Script:** `/scripts/setup-ssl-certbot.sh`
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
@ -1,283 +0,0 @@
|
|||||||
# GUIA: Configuracion SSL con Nginx para Produccion
|
|
||||||
|
|
||||||
**Servidor:** 74.208.126.102
|
|
||||||
**Requisito:** Dominio apuntando al servidor (ej: gamilit.com)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ARQUITECTURA
|
|
||||||
|
|
||||||
```
|
|
||||||
INTERNET
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Nginx :443 │ ◄── SSL/HTTPS (certbot)
|
|
||||||
│ (Reverse │
|
|
||||||
│ Proxy) │
|
|
||||||
└────────┬────────┘
|
|
||||||
│
|
|
||||||
┌─────────────┴─────────────┐
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Backend :3006 │ │ Frontend :3005 │
|
|
||||||
│ (NestJS) │ │ (Vite Preview) │
|
|
||||||
│ /api/* │ │ /* │
|
|
||||||
└─────────────────┘ └─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 1: Instalar Nginx y Certbot
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y nginx certbot python3-certbot-nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 2: Configurar DNS
|
|
||||||
|
|
||||||
Asegurar que el dominio apunte al servidor:
|
|
||||||
```bash
|
|
||||||
# Verificar DNS
|
|
||||||
dig gamilit.com +short
|
|
||||||
# Debe mostrar: 74.208.126.102
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 3: Configuracion Nginx (SIN SSL primero)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /etc/nginx/sites-available/gamilit << 'NGINX'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name gamilit.com www.gamilit.com;
|
|
||||||
|
|
||||||
# Frontend (default)
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3005;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://localhost:3006;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# WebSocket
|
|
||||||
location /socket.io {
|
|
||||||
proxy_pass http://localhost:3006;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NGINX
|
|
||||||
|
|
||||||
# Habilitar sitio
|
|
||||||
sudo ln -sf /etc/nginx/sites-available/gamilit /etc/nginx/sites-enabled/
|
|
||||||
sudo rm -f /etc/nginx/sites-enabled/default
|
|
||||||
|
|
||||||
# Verificar configuracion
|
|
||||||
sudo nginx -t
|
|
||||||
|
|
||||||
# Reiniciar Nginx
|
|
||||||
sudo systemctl restart nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 4: Obtener Certificado SSL con Certbot
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Obtener certificado (reemplazar dominio)
|
|
||||||
sudo certbot --nginx -d gamilit.com -d www.gamilit.com
|
|
||||||
|
|
||||||
# Certbot modifica automaticamente la configuracion de Nginx para HTTPS
|
|
||||||
# Verificar renovacion automatica
|
|
||||||
sudo certbot renew --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 5: Configuracion Nginx FINAL (con SSL)
|
|
||||||
|
|
||||||
Despues de certbot, la configuracion se ve asi:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# Redirect HTTP to HTTPS
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name gamilit.com www.gamilit.com;
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS Server
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name gamilit.com www.gamilit.com;
|
|
||||||
|
|
||||||
# SSL (certbot configura esto automaticamente)
|
|
||||||
ssl_certificate /etc/letsencrypt/live/gamilit.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/gamilit.com/privkey.pem;
|
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
||||||
|
|
||||||
# IMPORTANTE: NO agregar headers CORS aqui
|
|
||||||
# NestJS maneja CORS internamente
|
|
||||||
# Headers duplicados causan: "multiple values" error
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3005;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Backend API
|
|
||||||
location /api {
|
|
||||||
proxy_pass http://localhost:3006;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# WebSocket
|
|
||||||
location /socket.io {
|
|
||||||
proxy_pass http://localhost:3006;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 6: Configurar Backend para HTTPS
|
|
||||||
|
|
||||||
Editar `apps/backend/.env.production`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# CORS con HTTPS
|
|
||||||
CORS_ORIGIN=https://gamilit.com,https://www.gamilit.com
|
|
||||||
|
|
||||||
# Frontend URL
|
|
||||||
FRONTEND_URL=https://gamilit.com
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 7: Configurar Frontend para HTTPS
|
|
||||||
|
|
||||||
Editar `apps/frontend/.env.production`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# API con HTTPS (a traves de Nginx)
|
|
||||||
VITE_API_HOST=gamilit.com
|
|
||||||
VITE_API_PROTOCOL=https
|
|
||||||
VITE_API_VERSION=v1
|
|
||||||
|
|
||||||
# WebSocket con SSL
|
|
||||||
VITE_WS_HOST=gamilit.com
|
|
||||||
VITE_WS_PROTOCOL=wss
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PASO 8: Rebuild y Reiniciar
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Rebuild frontend con nueva config
|
|
||||||
cd apps/frontend && npm run build && cd ../..
|
|
||||||
|
|
||||||
# Reiniciar servicios
|
|
||||||
pm2 restart all
|
|
||||||
|
|
||||||
# Verificar
|
|
||||||
curl -I https://gamilit.com
|
|
||||||
curl https://gamilit.com/api/v1/health
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TROUBLESHOOTING
|
|
||||||
|
|
||||||
### Error: CORS multiple values
|
|
||||||
```
|
|
||||||
The 'Access-Control-Allow-Origin' header contains multiple values
|
|
||||||
```
|
|
||||||
**Causa:** Nginx y NestJS ambos agregan headers CORS
|
|
||||||
**Solucion:** NO agregar headers CORS en Nginx. Solo NestJS los maneja.
|
|
||||||
|
|
||||||
### Error: SSL Certificate
|
|
||||||
```bash
|
|
||||||
# Verificar certificado
|
|
||||||
sudo certbot certificates
|
|
||||||
|
|
||||||
# Renovar manualmente
|
|
||||||
sudo certbot renew
|
|
||||||
|
|
||||||
# Ver logs
|
|
||||||
sudo tail -f /var/log/letsencrypt/letsencrypt.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: Nginx no inicia
|
|
||||||
```bash
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl status nginx
|
|
||||||
sudo journalctl -u nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PUERTOS FINALES
|
|
||||||
|
|
||||||
| Servicio | Puerto Interno | Puerto Externo | Protocolo |
|
|
||||||
|----------|---------------|----------------|-----------|
|
|
||||||
| Nginx | 80, 443 | 80, 443 | HTTP/HTTPS |
|
|
||||||
| Backend | 3006 | - (via Nginx) | HTTP interno |
|
|
||||||
| Frontend | 3005 | - (via Nginx) | HTTP interno |
|
|
||||||
| PostgreSQL | 5432 | - (local only) | TCP |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## URLS DE ACCESO
|
|
||||||
|
|
||||||
- **Frontend:** https://gamilit.com
|
|
||||||
- **Backend API:** https://gamilit.com/api/v1/health
|
|
||||||
- **Swagger:** https://gamilit.com/api/v1/docs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Guia creada: 2025-12-18*
|
|
||||||
@ -1,666 +0,0 @@
|
|||||||
# Guia de Validacion y Troubleshooting - Produccion GAMILIT
|
|
||||||
|
|
||||||
> **Version:** 1.0.0
|
|
||||||
> **Fecha:** 2025-12-18
|
|
||||||
> **Servidor:** 74.208.126.102
|
|
||||||
> **Proposito:** Validar carga correcta de BD y resolver errores comunes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Indice
|
|
||||||
|
|
||||||
1. [Validacion Rapida Post-Despliegue](#1-validacion-rapida-post-despliegue)
|
|
||||||
2. [Validacion Completa de Base de Datos](#2-validacion-completa-de-base-de-datos)
|
|
||||||
3. [Errores Comunes y Soluciones](#3-errores-comunes-y-soluciones)
|
|
||||||
4. [Scripts de Diagnostico](#4-scripts-de-diagnostico)
|
|
||||||
5. [Procedimiento de Recuperacion](#5-procedimiento-de-recuperacion)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Validacion Rapida Post-Despliegue
|
|
||||||
|
|
||||||
### 1.1 Checklist de Validacion (5 minutos)
|
|
||||||
|
|
||||||
Ejecutar estos comandos inmediatamente despues de desplegar:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Definir conexion
|
|
||||||
export DATABASE_URL="postgresql://gamilit_user:PASSWORD@localhost:5432/gamilit_platform"
|
|
||||||
|
|
||||||
# 1. Verificar conexion a BD
|
|
||||||
psql "$DATABASE_URL" -c "SELECT version();"
|
|
||||||
|
|
||||||
# 2. Verificar schemas creados (deben ser 17+)
|
|
||||||
psql "$DATABASE_URL" -c "SELECT COUNT(*) as schemas FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast');"
|
|
||||||
|
|
||||||
# 3. Verificar tenant principal (CRITICO)
|
|
||||||
psql "$DATABASE_URL" -c "SELECT id, name, slug, is_active FROM auth_management.tenants WHERE slug = 'gamilit-prod';"
|
|
||||||
|
|
||||||
# 4. Verificar usuarios cargados
|
|
||||||
psql "$DATABASE_URL" -c "SELECT COUNT(*) as usuarios FROM auth.users;"
|
|
||||||
|
|
||||||
# 5. Verificar health del backend
|
|
||||||
curl -s http://localhost:3006/api/health | head -20
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 Resultados Esperados
|
|
||||||
|
|
||||||
| Validacion | Resultado Esperado | Accion si Falla |
|
|
||||||
|------------|-------------------|-----------------|
|
|
||||||
| Conexion BD | Version PostgreSQL 16+ | Verificar credenciales |
|
|
||||||
| Schemas | 17 o mas | Ejecutar `create-database.sh` |
|
|
||||||
| Tenant Principal | 1 fila con `is_active=true` | Ver seccion 3.1 |
|
|
||||||
| Usuarios | 48 o mas | Ejecutar seeds de auth |
|
|
||||||
| Health Backend | `{"status":"ok"}` | Ver logs PM2 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Validacion Completa de Base de Datos
|
|
||||||
|
|
||||||
### 2.1 Script de Validacion Completa
|
|
||||||
|
|
||||||
Crear y ejecutar este script para validacion exhaustiva:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# validate-production-db.sh
|
|
||||||
|
|
||||||
DATABASE_URL="${DATABASE_URL:-postgresql://gamilit_user:PASSWORD@localhost:5432/gamilit_platform}"
|
|
||||||
|
|
||||||
echo "=============================================="
|
|
||||||
echo "VALIDACION COMPLETA - BASE DE DATOS GAMILIT"
|
|
||||||
echo "=============================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Funcion para ejecutar query y mostrar resultado
|
|
||||||
run_check() {
|
|
||||||
local description="$1"
|
|
||||||
local query="$2"
|
|
||||||
local expected="$3"
|
|
||||||
|
|
||||||
result=$(psql "$DATABASE_URL" -t -c "$query" 2>/dev/null | tr -d ' ')
|
|
||||||
|
|
||||||
if [ "$result" == "$expected" ] || [ -z "$expected" ]; then
|
|
||||||
echo "✅ $description: $result"
|
|
||||||
else
|
|
||||||
echo "❌ $description: $result (esperado: $expected)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== 1. SCHEMAS ==="
|
|
||||||
run_check "Total Schemas" "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast');"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== 2. TABLAS POR SCHEMA ==="
|
|
||||||
psql "$DATABASE_URL" -c "
|
|
||||||
SELECT
|
|
||||||
table_schema as schema,
|
|
||||||
COUNT(*) as tablas
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
||||||
AND table_type = 'BASE TABLE'
|
|
||||||
GROUP BY table_schema
|
|
||||||
ORDER BY table_schema;
|
|
||||||
"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== 3. TENANTS (CRITICO) ==="
|
|
||||||
psql "$DATABASE_URL" -c "SELECT id, name, slug, is_active, subscription_tier FROM auth_management.tenants ORDER BY created_at;"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== 4. USUARIOS ==="
|
|
||||||
run_check "Usuarios en auth.users" "SELECT COUNT(*) FROM auth.users;"
|
|
||||||
run_check "Perfiles en auth_management.profiles" "SELECT COUNT(*) FROM auth_management.profiles;"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== 5. CONTENIDO EDUCATIVO ==="
|
|
||||||
run_check "Modulos" "SELECT COUNT(*) FROM educational_content.modules;"
|
|
||||||
run_check "Ejercicios" "SELECT COUNT(*) FROM educational_content.exercises;"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== 6. GAMIFICACION ==="
|
|
||||||
run_check "Rangos Maya" "SELECT COUNT(*) FROM gamification_system.maya_ranks;"
|
|
||||||
run_check "Logros" "SELECT COUNT(*) FROM gamification_system.achievements;"
|
|
||||||
run_check "Categorias Tienda" "SELECT COUNT(*) FROM gamification_system.shop_categories;"
|
|
||||||
run_check "Items Tienda" "SELECT COUNT(*) FROM gamification_system.shop_items;"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== 7. SOCIAL ==="
|
|
||||||
run_check "Escuelas" "SELECT COUNT(*) FROM social_features.schools;"
|
|
||||||
run_check "Aulas" "SELECT COUNT(*) FROM social_features.classrooms;"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== 8. CONFIGURACION ==="
|
|
||||||
run_check "Feature Flags" "SELECT COUNT(*) FROM system_configuration.feature_flags;"
|
|
||||||
run_check "Parametros Gamificacion" "SELECT COUNT(*) FROM system_configuration.gamification_parameters;"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=============================================="
|
|
||||||
echo "VALIDACION COMPLETADA"
|
|
||||||
echo "=============================================="
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Valores Esperados Post-Carga
|
|
||||||
|
|
||||||
| Entidad | Tabla | Cantidad Minima |
|
|
||||||
|---------|-------|-----------------|
|
|
||||||
| **Tenants** | `auth_management.tenants` | 14 (1 principal + 13 usuarios) |
|
|
||||||
| **Usuarios** | `auth.users` | 48 |
|
|
||||||
| **Perfiles** | `auth_management.profiles` | 48 |
|
|
||||||
| **Modulos** | `educational_content.modules` | 5 |
|
|
||||||
| **Ejercicios** | `educational_content.exercises` | 23 |
|
|
||||||
| **Rangos Maya** | `gamification_system.maya_ranks` | 5 |
|
|
||||||
| **Logros** | `gamification_system.achievements` | 30 |
|
|
||||||
| **Categorias Tienda** | `gamification_system.shop_categories` | 5 |
|
|
||||||
| **Items Tienda** | `gamification_system.shop_items` | 20 |
|
|
||||||
| **Escuelas** | `social_features.schools` | 2 |
|
|
||||||
| **Aulas** | `social_features.classrooms` | 4 |
|
|
||||||
| **Feature Flags** | `system_configuration.feature_flags` | 26 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Errores Comunes y Soluciones
|
|
||||||
|
|
||||||
### 3.1 ERROR: "No hay tenants activos en el sistema"
|
|
||||||
|
|
||||||
**Sintoma:**
|
|
||||||
```
|
|
||||||
POST /api/v1/auth/register → 500 Internal Server Error
|
|
||||||
"No hay tenants activos en el sistema. Contacte al administrador."
|
|
||||||
```
|
|
||||||
|
|
||||||
**Causa:** La tabla `auth_management.tenants` esta vacia o no tiene tenants con `is_active=true`.
|
|
||||||
|
|
||||||
**Diagnostico:**
|
|
||||||
```bash
|
|
||||||
psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM auth_management.tenants WHERE is_active = true;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion Rapida (SQL directo):**
|
|
||||||
```sql
|
|
||||||
-- Conectar a la BD
|
|
||||||
psql "$DATABASE_URL"
|
|
||||||
|
|
||||||
-- Insertar tenant principal
|
|
||||||
INSERT INTO auth_management.tenants (
|
|
||||||
id, name, slug, domain, logo_url, subscription_tier,
|
|
||||||
max_users, max_storage_gb, is_active, settings, metadata,
|
|
||||||
created_at, updated_at
|
|
||||||
) VALUES (
|
|
||||||
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid,
|
|
||||||
'GAMILIT Platform',
|
|
||||||
'gamilit-prod',
|
|
||||||
'gamilit.com',
|
|
||||||
'/assets/logo-gamilit.png',
|
|
||||||
'enterprise',
|
|
||||||
10000,
|
|
||||||
100,
|
|
||||||
true,
|
|
||||||
'{"theme": "detective", "language": "es", "timezone": "America/Mexico_City", "features": {"analytics_enabled": true, "gamification_enabled": true, "social_features_enabled": true}}'::jsonb,
|
|
||||||
'{"environment": "production", "version": "2.0"}'::jsonb,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
|
||||||
is_active = true,
|
|
||||||
updated_at = NOW();
|
|
||||||
|
|
||||||
-- Verificar
|
|
||||||
SELECT id, name, slug, is_active FROM auth_management.tenants;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion Completa (Seeds):**
|
|
||||||
```bash
|
|
||||||
cd /path/to/gamilit/apps/database
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth_management/01-tenants.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth_management/02-tenants-production.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 ERROR: "relation does not exist"
|
|
||||||
|
|
||||||
**Sintoma:**
|
|
||||||
```
|
|
||||||
ERROR: relation "auth_management.tenants" does not exist
|
|
||||||
```
|
|
||||||
|
|
||||||
**Causa:** El DDL no se ejecuto correctamente.
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
```bash
|
|
||||||
cd /path/to/gamilit/apps/database
|
|
||||||
./create-database.sh "$DATABASE_URL"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 ERROR: "password authentication failed"
|
|
||||||
|
|
||||||
**Sintoma:**
|
|
||||||
```
|
|
||||||
FATAL: password authentication failed for user "gamilit_user"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
```bash
|
|
||||||
# Como usuario postgres
|
|
||||||
sudo -u postgres psql
|
|
||||||
|
|
||||||
# Resetear password
|
|
||||||
ALTER USER gamilit_user WITH PASSWORD 'nueva_password_segura';
|
|
||||||
|
|
||||||
# Verificar
|
|
||||||
\du gamilit_user
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.4 ERROR: "CORS blocked"
|
|
||||||
|
|
||||||
**Sintoma:**
|
|
||||||
```
|
|
||||||
Access to fetch at 'http://74.208.126.102:3006/api' from origin 'http://74.208.126.102:3005' has been blocked by CORS policy
|
|
||||||
```
|
|
||||||
|
|
||||||
**Diagnostico:**
|
|
||||||
```bash
|
|
||||||
grep CORS_ORIGIN apps/backend/.env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
```bash
|
|
||||||
# Editar apps/backend/.env.production
|
|
||||||
CORS_ORIGIN=http://74.208.126.102:3005,http://74.208.126.102,https://74.208.126.102
|
|
||||||
|
|
||||||
# Reiniciar backend
|
|
||||||
pm2 restart gamilit-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.5 ERROR: "Cannot find module"
|
|
||||||
|
|
||||||
**Sintoma:**
|
|
||||||
```
|
|
||||||
Error: Cannot find module '/path/to/dist/main.js'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solucion:**
|
|
||||||
```bash
|
|
||||||
cd apps/backend
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
pm2 restart gamilit-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Scripts de Diagnostico
|
|
||||||
|
|
||||||
### 4.1 Script: Diagnostico Completo del Sistema
|
|
||||||
|
|
||||||
Guardar como `diagnose-production.sh`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# diagnose-production.sh - Diagnostico completo del sistema GAMILIT
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
DATABASE_URL="${DATABASE_URL:-postgresql://gamilit_user:PASSWORD@localhost:5432/gamilit_platform}"
|
|
||||||
BACKEND_URL="http://localhost:3006"
|
|
||||||
FRONTEND_URL="http://localhost:3005"
|
|
||||||
|
|
||||||
echo "=============================================="
|
|
||||||
echo " DIAGNOSTICO SISTEMA GAMILIT PRODUCCION"
|
|
||||||
echo "=============================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 1. PM2 Status
|
|
||||||
echo -e "${YELLOW}=== 1. ESTADO PM2 ===${NC}"
|
|
||||||
pm2 list 2>/dev/null || echo -e "${RED}PM2 no disponible${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 2. Backend Health
|
|
||||||
echo -e "${YELLOW}=== 2. HEALTH BACKEND ===${NC}"
|
|
||||||
health=$(curl -s "$BACKEND_URL/api/health" 2>/dev/null)
|
|
||||||
if [ -n "$health" ]; then
|
|
||||||
echo -e "${GREEN}Backend respondiendo:${NC}"
|
|
||||||
echo "$health" | head -5
|
|
||||||
else
|
|
||||||
echo -e "${RED}Backend NO responde${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 3. Frontend
|
|
||||||
echo -e "${YELLOW}=== 3. FRONTEND ===${NC}"
|
|
||||||
frontend_status=$(curl -s -o /dev/null -w "%{http_code}" "$FRONTEND_URL" 2>/dev/null)
|
|
||||||
if [ "$frontend_status" == "200" ]; then
|
|
||||||
echo -e "${GREEN}Frontend OK (HTTP $frontend_status)${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}Frontend ERROR (HTTP $frontend_status)${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 4. Database Connection
|
|
||||||
echo -e "${YELLOW}=== 4. CONEXION BASE DE DATOS ===${NC}"
|
|
||||||
db_version=$(psql "$DATABASE_URL" -t -c "SELECT version();" 2>/dev/null | head -1)
|
|
||||||
if [ -n "$db_version" ]; then
|
|
||||||
echo -e "${GREEN}BD conectada:${NC} $db_version"
|
|
||||||
else
|
|
||||||
echo -e "${RED}No se puede conectar a la BD${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 5. Critical Tables
|
|
||||||
echo -e "${YELLOW}=== 5. TABLAS CRITICAS ===${NC}"
|
|
||||||
|
|
||||||
check_table() {
|
|
||||||
local table=$1
|
|
||||||
local count=$(psql "$DATABASE_URL" -t -c "SELECT COUNT(*) FROM $table;" 2>/dev/null | tr -d ' ')
|
|
||||||
if [ -n "$count" ] && [ "$count" -gt 0 ]; then
|
|
||||||
echo -e "${GREEN}✅ $table: $count registros${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ $table: VACIO o ERROR${NC}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_table "auth_management.tenants"
|
|
||||||
check_table "auth.users"
|
|
||||||
check_table "auth_management.profiles"
|
|
||||||
check_table "educational_content.modules"
|
|
||||||
check_table "educational_content.exercises"
|
|
||||||
check_table "gamification_system.maya_ranks"
|
|
||||||
check_table "gamification_system.achievements"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 6. Tenant Principal
|
|
||||||
echo -e "${YELLOW}=== 6. TENANT PRINCIPAL (CRITICO) ===${NC}"
|
|
||||||
tenant=$(psql "$DATABASE_URL" -t -c "SELECT slug, is_active FROM auth_management.tenants WHERE slug = 'gamilit-prod';" 2>/dev/null)
|
|
||||||
if [ -n "$tenant" ]; then
|
|
||||||
echo -e "${GREEN}Tenant encontrado:${NC} $tenant"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ TENANT PRINCIPAL NO EXISTE - REGISTRO NO FUNCIONARA${NC}"
|
|
||||||
echo -e "${YELLOW}Ejecutar: psql \$DATABASE_URL -f seeds/prod/auth_management/01-tenants.sql${NC}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 7. Disk Space
|
|
||||||
echo -e "${YELLOW}=== 7. ESPACIO EN DISCO ===${NC}"
|
|
||||||
df -h / | tail -1
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 8. Memory
|
|
||||||
echo -e "${YELLOW}=== 8. MEMORIA ===${NC}"
|
|
||||||
free -h | head -2
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "=============================================="
|
|
||||||
echo " DIAGNOSTICO COMPLETADO"
|
|
||||||
echo "=============================================="
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Script: Reparar Datos Faltantes
|
|
||||||
|
|
||||||
Guardar como `repair-missing-data.sh`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# repair-missing-data.sh - Reparar datos faltantes en produccion
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
DATABASE_URL="${DATABASE_URL:-postgresql://gamilit_user:PASSWORD@localhost:5432/gamilit_platform}"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
DB_DIR="$SCRIPT_DIR/../apps/database"
|
|
||||||
|
|
||||||
echo "=============================================="
|
|
||||||
echo " REPARACION DE DATOS FALTANTES"
|
|
||||||
echo "=============================================="
|
|
||||||
|
|
||||||
# 1. Verificar y reparar tenants
|
|
||||||
echo ""
|
|
||||||
echo "=== Verificando Tenants ==="
|
|
||||||
tenant_count=$(psql "$DATABASE_URL" -t -c "SELECT COUNT(*) FROM auth_management.tenants WHERE is_active = true;" | tr -d ' ')
|
|
||||||
|
|
||||||
if [ "$tenant_count" -eq 0 ]; then
|
|
||||||
echo "❌ No hay tenants activos. Cargando seeds..."
|
|
||||||
psql "$DATABASE_URL" -f "$DB_DIR/seeds/prod/auth_management/01-tenants.sql"
|
|
||||||
psql "$DATABASE_URL" -f "$DB_DIR/seeds/prod/auth_management/02-tenants-production.sql"
|
|
||||||
echo "✅ Tenants cargados"
|
|
||||||
else
|
|
||||||
echo "✅ Tenants OK ($tenant_count activos)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. Verificar modulos
|
|
||||||
echo ""
|
|
||||||
echo "=== Verificando Modulos ==="
|
|
||||||
module_count=$(psql "$DATABASE_URL" -t -c "SELECT COUNT(*) FROM educational_content.modules;" | tr -d ' ')
|
|
||||||
|
|
||||||
if [ "$module_count" -lt 5 ]; then
|
|
||||||
echo "❌ Modulos incompletos ($module_count). Cargando..."
|
|
||||||
psql "$DATABASE_URL" -f "$DB_DIR/seeds/prod/educational_content/01-modules.sql"
|
|
||||||
echo "✅ Modulos cargados"
|
|
||||||
else
|
|
||||||
echo "✅ Modulos OK ($module_count)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Verificar rangos maya
|
|
||||||
echo ""
|
|
||||||
echo "=== Verificando Rangos Maya ==="
|
|
||||||
rank_count=$(psql "$DATABASE_URL" -t -c "SELECT COUNT(*) FROM gamification_system.maya_ranks;" | tr -d ' ')
|
|
||||||
|
|
||||||
if [ "$rank_count" -lt 5 ]; then
|
|
||||||
echo "❌ Rangos Maya incompletos ($rank_count). Cargando..."
|
|
||||||
psql "$DATABASE_URL" -f "$DB_DIR/seeds/prod/gamification_system/03-maya_ranks.sql"
|
|
||||||
echo "✅ Rangos Maya cargados"
|
|
||||||
else
|
|
||||||
echo "✅ Rangos Maya OK ($rank_count)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. Verificar feature flags
|
|
||||||
echo ""
|
|
||||||
echo "=== Verificando Feature Flags ==="
|
|
||||||
flag_count=$(psql "$DATABASE_URL" -t -c "SELECT COUNT(*) FROM system_configuration.feature_flags;" | tr -d ' ')
|
|
||||||
|
|
||||||
if [ "$flag_count" -lt 20 ]; then
|
|
||||||
echo "❌ Feature flags incompletos ($flag_count). Cargando..."
|
|
||||||
psql "$DATABASE_URL" -f "$DB_DIR/seeds/prod/system_configuration/01-feature_flags_seeds.sql"
|
|
||||||
echo "✅ Feature flags cargados"
|
|
||||||
else
|
|
||||||
echo "✅ Feature flags OK ($flag_count)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=============================================="
|
|
||||||
echo " REPARACION COMPLETADA"
|
|
||||||
echo "=============================================="
|
|
||||||
echo ""
|
|
||||||
echo "Reiniciar backend para aplicar cambios:"
|
|
||||||
echo " pm2 restart gamilit-backend"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Procedimiento de Recuperacion
|
|
||||||
|
|
||||||
### 5.1 Recuperacion Completa (Reset Total)
|
|
||||||
|
|
||||||
Si la base de datos esta corrupta o incompleta, seguir estos pasos:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Detener aplicaciones
|
|
||||||
pm2 stop all
|
|
||||||
|
|
||||||
# 2. Ir al directorio de database
|
|
||||||
cd /path/to/gamilit/apps/database
|
|
||||||
|
|
||||||
# 3. Configurar conexion
|
|
||||||
export DATABASE_URL="postgresql://gamilit_user:PASSWORD@localhost:5432/gamilit_platform"
|
|
||||||
|
|
||||||
# 4. OPCION A: Drop y recrear (ELIMINA TODO)
|
|
||||||
./drop-and-recreate-database.sh "$DATABASE_URL"
|
|
||||||
|
|
||||||
# 4. OPCION B: Solo recrear estructura (si BD nueva)
|
|
||||||
./create-database.sh "$DATABASE_URL"
|
|
||||||
|
|
||||||
# 5. Reiniciar aplicaciones
|
|
||||||
pm2 start all
|
|
||||||
|
|
||||||
# 6. Verificar
|
|
||||||
curl http://localhost:3006/api/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Recuperacion Parcial (Solo Seeds)
|
|
||||||
|
|
||||||
Si el DDL esta correcto pero faltan datos:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /path/to/gamilit/apps/database
|
|
||||||
export DATABASE_URL="postgresql://gamilit_user:PASSWORD@localhost:5432/gamilit_platform"
|
|
||||||
|
|
||||||
# Cargar seeds en orden
|
|
||||||
# 1. System Configuration (sin dependencias)
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/system_configuration/01-system_settings.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/system_configuration/01-feature_flags_seeds.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/system_configuration/02-gamification_parameters_seeds.sql
|
|
||||||
|
|
||||||
# 2. Auth Management (tenants y auth_providers)
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth_management/01-tenants.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth_management/02-tenants-production.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth_management/02-auth_providers.sql
|
|
||||||
|
|
||||||
# 3. Auth (usuarios)
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth/01-demo-users.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth/02-production-users.sql
|
|
||||||
|
|
||||||
# 4. Educational Content (modulos ANTES de profiles)
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/educational_content/01-modules.sql
|
|
||||||
|
|
||||||
# 5. Profiles (dispara trigger initialize_user_stats)
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth_management/04-profiles-complete.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/auth_management/06-profiles-production.sql
|
|
||||||
|
|
||||||
# 6. Social Features
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/social_features/00-schools-default.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/social_features/01-schools.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/social_features/02-classrooms.sql
|
|
||||||
|
|
||||||
# 7. Educational Content (ejercicios)
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/educational_content/02-exercises-module1.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/educational_content/03-exercises-module2.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/educational_content/04-exercises-module3.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/educational_content/05-exercises-module4.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/educational_content/06-exercises-module5.sql
|
|
||||||
|
|
||||||
# 8. Gamification
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/gamification_system/01-achievement_categories.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/gamification_system/03-maya_ranks.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/gamification_system/04-achievements.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/gamification_system/12-shop_categories.sql
|
|
||||||
psql "$DATABASE_URL" -f seeds/prod/gamification_system/13-shop_items.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 Orden de Carga de Seeds (Dependencias)
|
|
||||||
|
|
||||||
```
|
|
||||||
ORDEN DE CARGA DE SEEDS
|
|
||||||
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 1. system_configuration (sin dependencias) │
|
|
||||||
│ - system_settings │
|
|
||||||
│ - feature_flags │
|
|
||||||
│ - gamification_parameters │
|
|
||||||
└─────────────────────┬───────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 2. auth_management/tenants │
|
|
||||||
│ - 01-tenants.sql (tenant principal) │
|
|
||||||
│ - 02-tenants-production.sql (usuarios) │
|
|
||||||
└─────────────────────┬───────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 3. auth/users │
|
|
||||||
│ - 01-demo-users.sql │
|
|
||||||
│ - 02-production-users.sql │
|
|
||||||
└─────────────────────┬───────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 4. educational_content/modules │
|
|
||||||
│ - 01-modules.sql (ANTES de profiles!) │
|
|
||||||
└─────────────────────┬───────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 5. auth_management/profiles │
|
|
||||||
│ - 04-profiles-complete.sql │
|
|
||||||
│ - 06-profiles-production.sql │
|
|
||||||
│ (Dispara trigger initialize_user_stats) │
|
|
||||||
└─────────────────────┬───────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 6. social_features │
|
|
||||||
│ - schools → classrooms → members │
|
|
||||||
└─────────────────────┬───────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 7. educational_content/exercises │
|
|
||||||
│ - exercises-module1 a module5 │
|
|
||||||
└─────────────────────┬───────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ 8. gamification_system │
|
|
||||||
│ - maya_ranks, achievements, shop │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Apendice: Queries de Verificacion Rapida
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Verificar tenant principal
|
|
||||||
SELECT id, name, slug, is_active
|
|
||||||
FROM auth_management.tenants
|
|
||||||
WHERE slug = 'gamilit-prod';
|
|
||||||
|
|
||||||
-- Contar entidades principales
|
|
||||||
SELECT
|
|
||||||
(SELECT COUNT(*) FROM auth_management.tenants) as tenants,
|
|
||||||
(SELECT COUNT(*) FROM auth.users) as users,
|
|
||||||
(SELECT COUNT(*) FROM auth_management.profiles) as profiles,
|
|
||||||
(SELECT COUNT(*) FROM educational_content.modules) as modules,
|
|
||||||
(SELECT COUNT(*) FROM educational_content.exercises) as exercises,
|
|
||||||
(SELECT COUNT(*) FROM gamification_system.maya_ranks) as ranks,
|
|
||||||
(SELECT COUNT(*) FROM gamification_system.achievements) as achievements;
|
|
||||||
|
|
||||||
-- Verificar usuarios por rol
|
|
||||||
SELECT role, COUNT(*)
|
|
||||||
FROM auth_management.profiles
|
|
||||||
GROUP BY role;
|
|
||||||
|
|
||||||
-- Verificar ejercicios por modulo
|
|
||||||
SELECT m.name, COUNT(e.id) as exercises
|
|
||||||
FROM educational_content.modules m
|
|
||||||
LEFT JOIN educational_content.exercises e ON e.module_id = m.id
|
|
||||||
GROUP BY m.name
|
|
||||||
ORDER BY m.order_index;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contacto
|
|
||||||
|
|
||||||
Para problemas no cubiertos en esta guia:
|
|
||||||
1. Revisar logs: `pm2 logs`
|
|
||||||
2. Consultar `GUIA-DESPLIEGUE-PRODUCCION-COMPLETA.md`
|
|
||||||
3. Revisar `docs/DEPLOYMENT.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Ultima actualizacion:** 2025-12-18
|
|
||||||
> **Version:** 1.0.0
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
# REFERENCIA: Deployment en Produccion
|
|
||||||
|
|
||||||
**Ubicacion de Documentacion Completa:**
|
|
||||||
|
|
||||||
La documentacion completa para el agente de produccion se encuentra en el **workspace de produccion** (VIEJO), ya que es donde se ejecuta el deployment.
|
|
||||||
|
|
||||||
## Archivos en Workspace de Produccion
|
|
||||||
|
|
||||||
```
|
|
||||||
~/workspace-old/wsl-ubuntu/workspace/workspace-gamilit/gamilit/projects/gamilit/
|
|
||||||
├── PROMPT-AGENTE-PRODUCCION.md # Prompts para usar con el agente
|
|
||||||
├── PRODUCTION-UPDATE.md # Instrucciones rapidas post-pull
|
|
||||||
├── docs/95-guias-desarrollo/
|
|
||||||
│ └── GUIA-DEPLOYMENT-AGENTE-PRODUCCION.md # Guia completa de deployment
|
|
||||||
└── scripts/
|
|
||||||
├── update-production.sh # Script automatizado de deployment
|
|
||||||
└── diagnose-production.sh # Script de diagnostico
|
|
||||||
```
|
|
||||||
|
|
||||||
## Resumen del Proceso
|
|
||||||
|
|
||||||
1. **Backup**: BD + configs a `/home/gamilit/backups/TIMESTAMP/`
|
|
||||||
2. **Pull**: `git reset --hard origin/main`
|
|
||||||
3. **Restaurar**: Configs desde backup
|
|
||||||
4. **Recrear BD**: `./create-database.sh`
|
|
||||||
5. **Build**: `npm install && npm run build`
|
|
||||||
6. **Deploy**: `pm2 start ecosystem.config.js`
|
|
||||||
7. **HTTPS**: Certbot + Nginx (si aplica)
|
|
||||||
8. **Validar**: `./scripts/diagnose-production.sh`
|
|
||||||
|
|
||||||
## Prompt Basico para Agente
|
|
||||||
|
|
||||||
```
|
|
||||||
Ejecuta el deployment de GAMILIT siguiendo el procedimiento en
|
|
||||||
docs/95-guias-desarrollo/GUIA-DEPLOYMENT-AGENTE-PRODUCCION.md
|
|
||||||
|
|
||||||
1. Backup BD y configs
|
|
||||||
2. pm2 stop all
|
|
||||||
3. git reset --hard origin/main
|
|
||||||
4. Restaurar configs
|
|
||||||
5. Recrear BD
|
|
||||||
6. Build backend y frontend
|
|
||||||
7. pm2 start
|
|
||||||
8. Validar
|
|
||||||
|
|
||||||
Ejecuta paso a paso mostrando outputs.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ver Documentacion Completa
|
|
||||||
|
|
||||||
Para ver la guia completa, acceder al workspace de produccion:
|
|
||||||
```bash
|
|
||||||
cat ~/workspace-old/wsl-ubuntu/workspace/workspace-gamilit/gamilit/projects/gamilit/docs/95-guias-desarrollo/GUIA-DEPLOYMENT-AGENTE-PRODUCCION.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Este archivo es solo una referencia. La documentacion real esta en el workspace de produccion.*
|
|
||||||
@ -1,292 +0,0 @@
|
|||||||
# Funcion: validate_rueda_inferencias
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Schema:** educational_content
|
|
||||||
**Ubicacion:** `apps/database/ddl/schemas/educational_content/functions/14-validate_rueda_inferencias.sql`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PROPOSITO
|
|
||||||
|
|
||||||
Validar respuestas abiertas para ejercicios de tipo "Rueda de Inferencias", soportando multiples estructuras de datos y proporcionando retroalimentacion granular.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FIRMA
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE OR REPLACE FUNCTION educational_content.validate_rueda_inferencias(
|
|
||||||
p_student_response JSONB,
|
|
||||||
p_correct_answer JSONB,
|
|
||||||
p_exercise_config JSONB DEFAULT '{}'::JSONB
|
|
||||||
) RETURNS RECORD AS $$
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PARAMETROS
|
|
||||||
|
|
||||||
| Parametro | Tipo | Descripcion |
|
|
||||||
|-----------|------|-------------|
|
|
||||||
| p_student_response | JSONB | Respuesta del estudiante |
|
|
||||||
| p_correct_answer | JSONB | Respuesta correcta esperada |
|
|
||||||
| p_exercise_config | JSONB | Configuracion adicional (opcional) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RETORNO
|
|
||||||
|
|
||||||
```sql
|
|
||||||
RECORD (
|
|
||||||
is_correct BOOLEAN, -- Si la respuesta es correcta
|
|
||||||
score INTEGER, -- Puntaje obtenido (0-100)
|
|
||||||
feedback TEXT, -- Retroalimentacion para el estudiante
|
|
||||||
details JSONB -- Detalles de evaluacion por categoria
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ESTRUCTURAS SOPORTADAS
|
|
||||||
|
|
||||||
### Estructura Nueva: categoryExpectations
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"categoryExpectations": {
|
|
||||||
"category_id_1": {
|
|
||||||
"keywords": ["palabra1", "palabra2"],
|
|
||||||
"minLength": 10,
|
|
||||||
"maxLength": 500
|
|
||||||
},
|
|
||||||
"category_id_2": {
|
|
||||||
"keywords": ["palabra3", "palabra4"],
|
|
||||||
"minLength": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fragmentStates": {
|
|
||||||
"fragment_id_1": {
|
|
||||||
"categoryId": "category_id_1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Estructura Legacy: flat
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"keywords": ["palabra1", "palabra2", "palabra3"],
|
|
||||||
"minLength": 10,
|
|
||||||
"maxLength": 500,
|
|
||||||
"minKeywords": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## LOGICA DE VALIDACION
|
|
||||||
|
|
||||||
### 1. Deteccion de Estructura
|
|
||||||
|
|
||||||
```sql
|
|
||||||
IF p_correct_answer ? 'categoryExpectations' THEN
|
|
||||||
-- Usar estructura nueva
|
|
||||||
ELSE
|
|
||||||
-- Usar estructura legacy (flat)
|
|
||||||
END IF;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Normalizacion de Texto
|
|
||||||
|
|
||||||
```sql
|
|
||||||
v_normalized_text := lower(
|
|
||||||
translate(
|
|
||||||
p_student_response->>'text',
|
|
||||||
'áéíóúÁÉÍÓÚñÑ',
|
|
||||||
'aeiouAEIOUnN'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Validacion de Longitud
|
|
||||||
|
|
||||||
```sql
|
|
||||||
v_text_length := length(p_student_response->>'text');
|
|
||||||
|
|
||||||
IF v_text_length < v_min_length THEN
|
|
||||||
v_feedback := 'Respuesta muy corta. Minimo ' || v_min_length || ' caracteres.';
|
|
||||||
v_is_correct := false;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF v_max_length IS NOT NULL AND v_text_length > v_max_length THEN
|
|
||||||
v_feedback := 'Respuesta muy larga. Maximo ' || v_max_length || ' caracteres.';
|
|
||||||
v_is_correct := false;
|
|
||||||
END IF;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Conteo de Keywords
|
|
||||||
|
|
||||||
```sql
|
|
||||||
v_keyword_count := 0;
|
|
||||||
|
|
||||||
FOR v_keyword IN SELECT jsonb_array_elements_text(v_keywords) LOOP
|
|
||||||
IF v_normalized_text LIKE '%' || lower(v_keyword) || '%' THEN
|
|
||||||
v_keyword_count := v_keyword_count + 1;
|
|
||||||
END IF;
|
|
||||||
END LOOP;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Calculo de Score
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Puntuacion parcial basada en keywords encontradas
|
|
||||||
v_keyword_ratio := v_keyword_count::FLOAT / v_total_keywords::FLOAT;
|
|
||||||
v_score := LEAST(100, ROUND(v_keyword_ratio * 100));
|
|
||||||
|
|
||||||
-- Bonus por longitud adecuada
|
|
||||||
IF v_text_length >= v_ideal_length THEN
|
|
||||||
v_score := v_score + 10;
|
|
||||||
END IF;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EJEMPLOS DE USO
|
|
||||||
|
|
||||||
### Estructura categoryExpectations
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT educational_content.validate_rueda_inferencias(
|
|
||||||
'{"text": "El texto habla sobre la importancia de la lectura critica"}'::JSONB,
|
|
||||||
'{
|
|
||||||
"categoryExpectations": {
|
|
||||||
"cat-inferencias": {
|
|
||||||
"keywords": ["lectura", "critica", "importancia", "texto"],
|
|
||||||
"minLength": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}'::JSONB
|
|
||||||
);
|
|
||||||
-- Retorna: (true, 75, 'Respuesta aceptable', {"keywords_found": 3, "keywords_total": 4})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Estructura Legacy
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT educational_content.validate_rueda_inferencias(
|
|
||||||
'{"text": "Pienso que el autor quiere transmitir un mensaje sobre la sociedad"}'::JSONB,
|
|
||||||
'{
|
|
||||||
"keywords": ["autor", "mensaje", "sociedad", "transmitir"],
|
|
||||||
"minLength": 30,
|
|
||||||
"minKeywords": 2
|
|
||||||
}'::JSONB
|
|
||||||
);
|
|
||||||
-- Retorna: (true, 100, 'Excelente respuesta', {"keywords_found": 4, "keywords_total": 4})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MANEJO DE ERRORES
|
|
||||||
|
|
||||||
### Categoria No Encontrada
|
|
||||||
|
|
||||||
Si un fragmentId no tiene categoryId mapeado, se usa fallback:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
v_category_id := COALESCE(
|
|
||||||
p_correct_answer->'fragmentStates'->v_fragment_id->>'categoryId',
|
|
||||||
'cat-literal' -- Fallback
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Respuesta Vacia
|
|
||||||
|
|
||||||
```sql
|
|
||||||
IF p_student_response IS NULL OR p_student_response->>'text' = '' THEN
|
|
||||||
RETURN (false, 0, 'No se proporciono respuesta', '{}'::JSONB);
|
|
||||||
END IF;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RETROALIMENTACION
|
|
||||||
|
|
||||||
### Mensajes Predefinidos
|
|
||||||
|
|
||||||
| Condicion | Mensaje |
|
|
||||||
|-----------|---------|
|
|
||||||
| Score >= 90 | "Excelente respuesta" |
|
|
||||||
| Score >= 70 | "Buena respuesta" |
|
|
||||||
| Score >= 50 | "Respuesta aceptable, considera agregar mas detalles" |
|
|
||||||
| Score < 50 | "Respuesta insuficiente, revisa los conceptos clave" |
|
|
||||||
| Muy corta | "Respuesta muy corta. Minimo X caracteres" |
|
|
||||||
| Muy larga | "Respuesta muy larga. Maximo X caracteres" |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DETALLES DE RETORNO
|
|
||||||
|
|
||||||
### Estructura de details
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"keywords_found": 3,
|
|
||||||
"keywords_total": 5,
|
|
||||||
"text_length": 85,
|
|
||||||
"min_length": 20,
|
|
||||||
"max_length": 500,
|
|
||||||
"categories_evaluated": [
|
|
||||||
{
|
|
||||||
"category_id": "cat-inferencias",
|
|
||||||
"keywords_found": 2,
|
|
||||||
"keywords_total": 3,
|
|
||||||
"passed": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## INTEGRACION
|
|
||||||
|
|
||||||
### Trigger de Validacion
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TRIGGER trg_validate_rueda_response
|
|
||||||
BEFORE INSERT ON educational_content.exercise_attempts
|
|
||||||
FOR EACH ROW
|
|
||||||
WHEN (NEW.exercise_type = 'rueda_inferencias')
|
|
||||||
EXECUTE FUNCTION educational_content.validate_rueda_response_trigger();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Uso desde Backend
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const result = await db.query(`
|
|
||||||
SELECT * FROM educational_content.validate_rueda_inferencias($1, $2)
|
|
||||||
`, [studentResponse, correctAnswer]);
|
|
||||||
|
|
||||||
const { is_correct, score, feedback, details } = result.rows[0];
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## HISTORIAL DE CAMBIOS
|
|
||||||
|
|
||||||
| Fecha | Version | Cambio |
|
|
||||||
|-------|---------|--------|
|
|
||||||
| 2025-12-15 | 1.0.0 | Version inicial con soporte dual |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## REFERENCIAS
|
|
||||||
|
|
||||||
- [04-FUNCTIONS-INVENTORY.md](../../90-transversal/inventarios-database/inventarios/04-FUNCTIONS-INVENTORY.md)
|
|
||||||
- Ejercicios de Rueda de Inferencias en Modulo 2
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
@ -1,438 +0,0 @@
|
|||||||
# Arquitectura de Componentes de Alertas - Admin Portal
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Modulo:** Admin Portal - Sistema de Alertas
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ESTRUCTURA GENERAL
|
|
||||||
|
|
||||||
```
|
|
||||||
apps/frontend/src/apps/admin/
|
|
||||||
├── hooks/
|
|
||||||
│ └── useAlerts.ts # Hook principal
|
|
||||||
├── components/alerts/
|
|
||||||
│ ├── alertUtils.ts # Utilidades compartidas
|
|
||||||
│ ├── AlertsStats.tsx # Cards de estadisticas
|
|
||||||
│ ├── AlertFilters.tsx # Panel de filtros
|
|
||||||
│ ├── AlertsList.tsx # Lista paginada
|
|
||||||
│ ├── AlertCard.tsx # Card individual
|
|
||||||
│ ├── AlertDetailsModal.tsx # Modal de detalles
|
|
||||||
│ ├── AcknowledgeAlertModal.tsx # Modal reconocer
|
|
||||||
│ └── ResolveAlertModal.tsx # Modal resolver
|
|
||||||
└── pages/
|
|
||||||
└── AdminAlertsPage.tsx # Pagina principal
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## HOOK PRINCIPAL: useAlerts
|
|
||||||
|
|
||||||
**Ubicacion:** `hooks/useAlerts.ts`
|
|
||||||
|
|
||||||
### Proposito
|
|
||||||
|
|
||||||
Gestion completa del ciclo de vida de alertas:
|
|
||||||
- Fetch con filtros y paginacion
|
|
||||||
- Estadisticas en tiempo real
|
|
||||||
- Acciones de gestion (acknowledge, resolve, suppress)
|
|
||||||
|
|
||||||
### API Retornada
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const {
|
|
||||||
// Data
|
|
||||||
alerts,
|
|
||||||
stats,
|
|
||||||
selectedAlert,
|
|
||||||
|
|
||||||
// Estado
|
|
||||||
isLoading,
|
|
||||||
isLoadingStats,
|
|
||||||
error,
|
|
||||||
|
|
||||||
// Filtros & Paginacion
|
|
||||||
filters,
|
|
||||||
setFilters,
|
|
||||||
pagination,
|
|
||||||
|
|
||||||
// Acciones
|
|
||||||
fetchAlerts,
|
|
||||||
fetchStats,
|
|
||||||
refreshAlerts,
|
|
||||||
acknowledgeAlert,
|
|
||||||
resolveAlert,
|
|
||||||
suppressAlert,
|
|
||||||
nextPage,
|
|
||||||
prevPage,
|
|
||||||
goToPage
|
|
||||||
} = useAlerts();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pagination Object
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface Pagination {
|
|
||||||
page: number;
|
|
||||||
totalPages: number;
|
|
||||||
totalItems: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validaciones de Acciones
|
|
||||||
|
|
||||||
| Accion | Validacion |
|
|
||||||
|--------|------------|
|
|
||||||
| acknowledgeAlert | Nota opcional |
|
|
||||||
| resolveAlert | Nota requerida (min 10 chars) |
|
|
||||||
| suppressAlert | Sin validacion adicional |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## UTILITY MODULE: alertUtils.ts
|
|
||||||
|
|
||||||
**Ubicacion:** `components/alerts/alertUtils.ts`
|
|
||||||
|
|
||||||
### Funciones Exportadas
|
|
||||||
|
|
||||||
#### getSeverityColor(severity) -> string
|
|
||||||
|
|
||||||
Retorna clases Tailwind con fondo solido para badges prominentes.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
getSeverityColor('critical'); // 'bg-red-500 text-white'
|
|
||||||
getSeverityColor('high'); // 'bg-orange-500 text-white'
|
|
||||||
getSeverityColor('medium'); // 'bg-yellow-500 text-black'
|
|
||||||
getSeverityColor('low'); // 'bg-blue-500 text-white'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### getSeverityColorWithBorder(severity) -> string
|
|
||||||
|
|
||||||
Retorna clases con fondo transparente + borde para badges sutiles.
|
|
||||||
|
|
||||||
#### getStatusColor(status) -> string
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
getStatusColor('open'); // Rojo con borde
|
|
||||||
getStatusColor('acknowledged'); // Naranja con borde
|
|
||||||
getStatusColor('resolved'); // Verde con borde
|
|
||||||
getStatusColor('suppressed'); // Gris con borde
|
|
||||||
```
|
|
||||||
|
|
||||||
#### getStatusTextColor(status) -> string
|
|
||||||
|
|
||||||
Solo colores de texto para etiquetas inline.
|
|
||||||
|
|
||||||
#### getSeverityLabel(severity) -> string
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
getSeverityLabel('critical'); // 'Critica'
|
|
||||||
getSeverityLabel('high'); // 'Alta'
|
|
||||||
getSeverityLabel('medium'); // 'Media'
|
|
||||||
getSeverityLabel('low'); // 'Baja'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### getStatusLabel(status) -> string
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
getStatusLabel('open'); // 'Abierta'
|
|
||||||
getStatusLabel('acknowledged'); // 'Reconocida'
|
|
||||||
getStatusLabel('resolved'); // 'Resuelta'
|
|
||||||
getStatusLabel('suppressed'); // 'Suprimida'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### formatAlertTimestamp(timestamp) -> string
|
|
||||||
|
|
||||||
Formato compacto para espacios limitados.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
formatAlertTimestamp(fecha); // 'Hace 5m', 'Hace 2h', 'Hace 3d', 'dic 15'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### formatAlertTimestampDetailed(timestamp) -> string
|
|
||||||
|
|
||||||
Formato detallado para mas informacion.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
formatAlertTimestampDetailed(fecha); // 'Hace 5 min', 'Hace 2 horas'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## COMPONENTES
|
|
||||||
|
|
||||||
### 1. AlertsStats
|
|
||||||
|
|
||||||
**Ubicacion:** `components/alerts/AlertsStats.tsx`
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
```typescript
|
|
||||||
interface AlertsStatsProps {
|
|
||||||
stats: AlertsStatsType | null;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Render:**
|
|
||||||
- Grid de 4 cards con metricas:
|
|
||||||
1. Alertas Abiertas (icono AlertTriangle)
|
|
||||||
2. Reconocidas (icono AlertCircle)
|
|
||||||
3. Resueltas (icono CheckCircle)
|
|
||||||
4. Tiempo Promedio Resolucion (icono Clock)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. AlertFilters
|
|
||||||
|
|
||||||
**Ubicacion:** `components/alerts/AlertFilters.tsx`
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
```typescript
|
|
||||||
interface AlertFiltersProps {
|
|
||||||
filters: AlertFiltersType;
|
|
||||||
onFiltersChange: (filters: AlertFiltersType) => void;
|
|
||||||
onRefresh: () => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Campos de Filtro:**
|
|
||||||
| Campo | Tipo | Opciones |
|
|
||||||
|-------|------|----------|
|
|
||||||
| Severidad | select | low, medium, high, critical |
|
|
||||||
| Estado | select | open, acknowledged, resolved, suppressed |
|
|
||||||
| Tipo Alerta | select | performance_degradation, high_error_rate, etc. |
|
|
||||||
| Desde | date | Date picker |
|
|
||||||
| Hasta | date | Date picker |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. AlertsList
|
|
||||||
|
|
||||||
**Ubicacion:** `components/alerts/AlertsList.tsx`
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
```typescript
|
|
||||||
interface AlertsListProps {
|
|
||||||
alerts: SystemAlert[];
|
|
||||||
isLoading: boolean;
|
|
||||||
pagination: Pagination;
|
|
||||||
onAlertClick: (alert: SystemAlert) => void;
|
|
||||||
onAcknowledge: (alert: SystemAlert) => void;
|
|
||||||
onResolve: (alert: SystemAlert) => void;
|
|
||||||
onSuppress: (alert: SystemAlert) => void;
|
|
||||||
onNextPage: () => void;
|
|
||||||
onPrevPage: () => void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Estados:**
|
|
||||||
- Loading: skeleton rows animados
|
|
||||||
- Empty: mensaje con icono
|
|
||||||
- Data: grid de AlertCard con paginacion
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. AlertCard
|
|
||||||
|
|
||||||
**Ubicacion:** `components/alerts/AlertCard.tsx`
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
```typescript
|
|
||||||
interface AlertCardProps {
|
|
||||||
alert: SystemAlert;
|
|
||||||
onViewDetails: (alert: SystemAlert) => void;
|
|
||||||
onAcknowledge: (alert: SystemAlert) => void;
|
|
||||||
onResolve: (alert: SystemAlert) => void;
|
|
||||||
onSuppress: (alert: SystemAlert) => void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Secciones:**
|
|
||||||
1. **Badges** (max 3): Severidad, Estado, Tipo
|
|
||||||
2. **Contenido:** Titulo, Descripcion (clamp 2 lineas)
|
|
||||||
3. **Metadata:** Usuarios afectados, Timestamp
|
|
||||||
4. **Acciones:** Botones dinamicos segun estado
|
|
||||||
|
|
||||||
**Logica de Botones:**
|
|
||||||
| Estado | Detalles | Reconocer | Resolver | Suprimir |
|
|
||||||
|--------|----------|-----------|----------|----------|
|
|
||||||
| open | Si | Si | Si | Si |
|
|
||||||
| acknowledged | Si | No | Si | Si |
|
|
||||||
| resolved | Si | No | No | No |
|
|
||||||
| suppressed | Si | No | No | No |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. AlertDetailsModal
|
|
||||||
|
|
||||||
**Ubicacion:** `components/alerts/AlertDetailsModal.tsx`
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
```typescript
|
|
||||||
interface AlertDetailsModalProps {
|
|
||||||
alert: SystemAlert | null;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Secciones:**
|
|
||||||
1. Header con titulo y boton cerrar
|
|
||||||
2. Badges (severidad, estado)
|
|
||||||
3. Titulo y descripcion completa
|
|
||||||
4. Grid de informacion clave
|
|
||||||
5. Seccion Sistema (si aplica)
|
|
||||||
6. Seccion Gestion (si aplica)
|
|
||||||
7. JSON expandible (contexto, metricas)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. AcknowledgeAlertModal
|
|
||||||
|
|
||||||
**Ubicacion:** `components/alerts/AcknowledgeAlertModal.tsx`
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
```typescript
|
|
||||||
interface AcknowledgeAlertModalProps {
|
|
||||||
alert: SystemAlert | null;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: (note?: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Campos:**
|
|
||||||
- Titulo de alerta (readonly)
|
|
||||||
- Textarea para nota (opcional)
|
|
||||||
- Botones: Cancelar, Reconocer
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. ResolveAlertModal
|
|
||||||
|
|
||||||
**Ubicacion:** `components/alerts/ResolveAlertModal.tsx`
|
|
||||||
|
|
||||||
**Props:**
|
|
||||||
```typescript
|
|
||||||
interface ResolveAlertModalProps {
|
|
||||||
alert: SystemAlert | null;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: (note: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Validacion:**
|
|
||||||
- Nota REQUERIDA
|
|
||||||
- Minimo 10 caracteres
|
|
||||||
- Contador en vivo: "5/10"
|
|
||||||
- Boton deshabilitado si < 10 chars
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TIPOS PRINCIPALES
|
|
||||||
|
|
||||||
### SystemAlert
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface SystemAlert {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
||||||
status: 'open' | 'acknowledged' | 'resolved' | 'suppressed';
|
|
||||||
alert_type: SystemAlertType;
|
|
||||||
affected_users: number;
|
|
||||||
triggered_at: string;
|
|
||||||
source_system?: string;
|
|
||||||
source_module?: string;
|
|
||||||
error_code?: string;
|
|
||||||
escalation_level?: number;
|
|
||||||
acknowledged_by_name?: string;
|
|
||||||
acknowledged_at?: string;
|
|
||||||
acknowledgment_note?: string;
|
|
||||||
resolved_by_name?: string;
|
|
||||||
resolved_at?: string;
|
|
||||||
resolution_note?: string;
|
|
||||||
context_data?: Record<string, unknown>;
|
|
||||||
metrics?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### AlertFilters
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface AlertFilters {
|
|
||||||
severity?: SystemAlertSeverity;
|
|
||||||
status?: SystemAlertStatus;
|
|
||||||
alert_type?: SystemAlertType;
|
|
||||||
date_from?: string;
|
|
||||||
date_to?: string;
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### AlertsStats
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface AlertsStats {
|
|
||||||
open_alerts: number;
|
|
||||||
acknowledged_alerts: number;
|
|
||||||
resolved_alerts: number;
|
|
||||||
avg_resolution_time_hours: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DIAGRAMA DE FLUJO
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ AdminAlertsPage │
|
|
||||||
├─────────────────────────────────────────────────────┤
|
|
||||||
│ ┌─────────────────────────────────────────────┐ │
|
|
||||||
│ │ AlertsStats │ │
|
|
||||||
│ │ [Abiertas] [Reconocidas] [Resueltas] [Avg] │ │
|
|
||||||
│ └─────────────────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
│ ┌─────────────────────────────────────────────┐ │
|
|
||||||
│ │ AlertFilters │ │
|
|
||||||
│ │ [Severidad] [Estado] [Tipo] [Fechas] │ │
|
|
||||||
│ └─────────────────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
│ ┌─────────────────────────────────────────────┐ │
|
|
||||||
│ │ AlertsList │ │
|
|
||||||
│ │ ┌─────────────┐ ┌─────────────┐ │ │
|
|
||||||
│ │ │ AlertCard │ │ AlertCard │ │ │
|
|
||||||
│ │ │ - Badges │ │ - Badges │ │ │
|
|
||||||
│ │ │ - Content │ │ - Content │ │ │
|
|
||||||
│ │ │ - Actions │ │ - Actions │ │ │
|
|
||||||
│ │ └─────────────┘ └─────────────┘ │ │
|
|
||||||
│ │ [< Prev] 1 2 3 4 5 [Next >] │ │
|
|
||||||
│ └─────────────────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
│ ┌──────────────────┐ ┌────────────────────┐ │
|
|
||||||
│ │ AlertDetailsModal│ │ AcknowledgeModal │ │
|
|
||||||
│ └──────────────────┘ └────────────────────┘ │
|
|
||||||
│ ┌────────────────────┐ │
|
|
||||||
│ │ ResolveAlertModal │ │
|
|
||||||
│ └────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## REFERENCIAS
|
|
||||||
|
|
||||||
- `AdminAlertsPage.tsx` - Implementacion de pagina
|
|
||||||
- `useAlerts.ts` - Hook de gestion
|
|
||||||
- `adminTypes.ts` - Tipos compartidos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
@ -1,165 +0,0 @@
|
|||||||
# Hook: useClassroomsList
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Ubicacion:** `apps/frontend/src/apps/admin/hooks/useClassroomsList.ts`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PROPOSITO
|
|
||||||
|
|
||||||
Hook simple y especializado para obtener lista de aulas (classrooms) para selectores en el modulo de progreso administrativo. Reemplaza datos mock con datos reales del API.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### Parametros de Entrada
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface UseClassroomsListParams {
|
|
||||||
schoolId?: string; // Filtrar por institucion especifica
|
|
||||||
enabled?: boolean; // Control de ejecucion (default: true)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Retorno
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface UseClassroomsListReturn {
|
|
||||||
classrooms: ClassroomBasic[]; // Lista de aulas
|
|
||||||
isLoading: boolean; // Estado de carga
|
|
||||||
error: Error | null; // Error si aplica
|
|
||||||
refetch: () => void; // Funcion para recargar
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CONFIGURACION DE REACT QUERY
|
|
||||||
|
|
||||||
| Configuracion | Valor |
|
|
||||||
|---------------|-------|
|
|
||||||
| Query Key | `['admin', 'classrooms', schoolId]` |
|
|
||||||
| staleTime | 5 minutos |
|
|
||||||
| gcTime | 10 minutos |
|
|
||||||
| refetchOnFocus | false |
|
|
||||||
| retry | 2 intentos |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EJEMPLO DE USO
|
|
||||||
|
|
||||||
### Basico
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useClassroomsList } from '../hooks/useClassroomsList';
|
|
||||||
|
|
||||||
function ClassroomSelector() {
|
|
||||||
const { classrooms, isLoading, error } = useClassroomsList();
|
|
||||||
|
|
||||||
if (isLoading) return <Spinner />;
|
|
||||||
if (error) return <ErrorMessage error={error} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<select>
|
|
||||||
{classrooms.map(classroom => (
|
|
||||||
<option key={classroom.id} value={classroom.id}>
|
|
||||||
{classroom.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Con Filtro por Institucion
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function ClassroomSelectorBySchool({ schoolId }: { schoolId: string }) {
|
|
||||||
const { classrooms, isLoading, error, refetch } = useClassroomsList({
|
|
||||||
schoolId,
|
|
||||||
enabled: !!schoolId
|
|
||||||
});
|
|
||||||
|
|
||||||
// Solo ejecuta query cuando schoolId esta disponible
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={() => refetch()}>Actualizar</button>
|
|
||||||
<ClassroomList classrooms={classrooms} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Control Manual de Ejecucion
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function ConditionalClassrooms() {
|
|
||||||
const [shouldFetch, setShouldFetch] = useState(false);
|
|
||||||
|
|
||||||
const { classrooms, isLoading } = useClassroomsList({
|
|
||||||
enabled: shouldFetch
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={() => setShouldFetch(true)}>
|
|
||||||
Cargar Aulas
|
|
||||||
</button>
|
|
||||||
{isLoading && <Spinner />}
|
|
||||||
{classrooms.length > 0 && <ClassroomList classrooms={classrooms} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DEPENDENCIAS
|
|
||||||
|
|
||||||
| Dependencia | Uso |
|
|
||||||
|-------------|-----|
|
|
||||||
| @tanstack/react-query | Gestion de queries |
|
|
||||||
| @/services/api/adminAPI | Cliente API |
|
|
||||||
| @/services/api/adminTypes | Tipos (ClassroomBasic) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TIPOS RELACIONADOS
|
|
||||||
|
|
||||||
### ClassroomBasic
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ClassroomBasic {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
grade?: string;
|
|
||||||
section?: string;
|
|
||||||
schoolId?: string;
|
|
||||||
teacherId?: string;
|
|
||||||
studentCount?: number;
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NOTAS DE IMPLEMENTACION
|
|
||||||
|
|
||||||
1. **Cache inteligente:** El query se invalida automaticamente cuando cambia `schoolId`
|
|
||||||
2. **Fallback a array vacio:** Si el API falla, retorna array vacio en lugar de undefined
|
|
||||||
3. **Retry limitado:** Maximo 2 reintentos para evitar sobrecarga
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## REFERENCIAS
|
|
||||||
|
|
||||||
- `AdminProgressPage.tsx` - Pagina que usa este hook
|
|
||||||
- `adminAPI.ts` - Cliente API
|
|
||||||
- `adminTypes.ts` - Definiciones de tipos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
@ -1,345 +0,0 @@
|
|||||||
# Hook: useGamificationConfig
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Fecha:** 2025-12-18
|
|
||||||
**Ubicacion:** `apps/frontend/src/apps/admin/hooks/useGamificationConfig.ts`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PROPOSITO
|
|
||||||
|
|
||||||
Hook de React personalizado que gestiona toda la configuracion de gamificacion del sistema, proporcionando:
|
|
||||||
- Parametros de gamificacion (XP, coins, multiplicadores)
|
|
||||||
- Rangos Maya (niveles, multiplicadores, perks)
|
|
||||||
- Estadisticas del sistema
|
|
||||||
- Operaciones CRUD completas con cache inteligente mediante React Query
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API EXPUESTA
|
|
||||||
|
|
||||||
### Objeto Retornado
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const {
|
|
||||||
// QUERIES
|
|
||||||
useParameters,
|
|
||||||
useParameter,
|
|
||||||
useMayaRanks,
|
|
||||||
useMayaRank,
|
|
||||||
useStats,
|
|
||||||
|
|
||||||
// MUTATIONS
|
|
||||||
updateParameter,
|
|
||||||
resetParameter,
|
|
||||||
bulkUpdateParameters,
|
|
||||||
updateMayaRank,
|
|
||||||
previewImpact,
|
|
||||||
restoreDefaults
|
|
||||||
} = useGamificationConfig();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QUERIES
|
|
||||||
|
|
||||||
### useParameters(query?: ListParametersQuery)
|
|
||||||
|
|
||||||
Obtiene lista paginada de parametros de gamificacion.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ListParametersQuery {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
category?: 'points' | 'coins' | 'levels' | 'ranks' | 'penalties' | 'bonuses';
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, isLoading, error } = useParameters({ category: 'points' });
|
|
||||||
|
|
||||||
// Retorno
|
|
||||||
{
|
|
||||||
data: Parameter[],
|
|
||||||
total: number,
|
|
||||||
page: number,
|
|
||||||
limit: number
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cache:** 5 minutos
|
|
||||||
|
|
||||||
### useParameter(key: string, enabled?: boolean)
|
|
||||||
|
|
||||||
Obtiene un parametro especifico por clave.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const { data, isLoading, error } = useParameter('xp_per_exercise', true);
|
|
||||||
```
|
|
||||||
|
|
||||||
### useMayaRanks()
|
|
||||||
|
|
||||||
Obtiene todos los rangos Maya configurados.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const { data, isLoading, error } = useMayaRanks();
|
|
||||||
|
|
||||||
// Retorno: MayaRankConfig[]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cache:** 10 minutos
|
|
||||||
|
|
||||||
### useMayaRank(id: string, enabled?: boolean)
|
|
||||||
|
|
||||||
Obtiene un rango Maya especifico.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const { data, isLoading, error } = useMayaRank('rank-uuid', true);
|
|
||||||
```
|
|
||||||
|
|
||||||
### useStats()
|
|
||||||
|
|
||||||
Obtiene estadisticas del sistema de gamificacion.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const { data, isLoading, error } = useStats();
|
|
||||||
|
|
||||||
// Retorno
|
|
||||||
{
|
|
||||||
totalParameters: number,
|
|
||||||
activeParameters: number,
|
|
||||||
totalRanks: number,
|
|
||||||
activeRanks: number,
|
|
||||||
lastModified: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cache:** 2 minutos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MUTATIONS
|
|
||||||
|
|
||||||
### updateParameter
|
|
||||||
|
|
||||||
Actualiza un parametro individual.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const { mutate, isPending, error } = updateParameter;
|
|
||||||
|
|
||||||
mutate({
|
|
||||||
key: 'xp_per_exercise',
|
|
||||||
data: {
|
|
||||||
value: 50,
|
|
||||||
reason: 'Ajuste de balanceo'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Efectos:**
|
|
||||||
- Invalida queries de parametros
|
|
||||||
- Invalida query de stats
|
|
||||||
- Toast de exito/error automatico
|
|
||||||
|
|
||||||
### resetParameter
|
|
||||||
|
|
||||||
Restaura un parametro a su valor por defecto.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
resetParameter.mutate('xp_per_exercise');
|
|
||||||
```
|
|
||||||
|
|
||||||
### bulkUpdateParameters
|
|
||||||
|
|
||||||
Actualiza multiples parametros en una sola operacion.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
bulkUpdateParameters.mutate({
|
|
||||||
updates: [
|
|
||||||
{ key: 'xp_per_exercise', value: 50 },
|
|
||||||
{ key: 'coins_per_streak', value: 10 }
|
|
||||||
],
|
|
||||||
reason: 'Rebalanceo de economia'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### updateMayaRank
|
|
||||||
|
|
||||||
Modifica umbrales de un rango Maya.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
updateMayaRank.mutate({
|
|
||||||
id: 'rank-uuid',
|
|
||||||
data: {
|
|
||||||
minXp: 500,
|
|
||||||
maxXp: 999
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### previewImpact
|
|
||||||
|
|
||||||
Genera preview del impacto de cambios sin aplicarlos.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
previewImpact.mutate({
|
|
||||||
parameterKey: 'xp_multiplier',
|
|
||||||
newValue: 1.5
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retorno: Estadisticas de usuarios afectados
|
|
||||||
```
|
|
||||||
|
|
||||||
### restoreDefaults
|
|
||||||
|
|
||||||
Restaura TODOS los parametros a valores por defecto.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
restoreDefaults.mutate();
|
|
||||||
|
|
||||||
// Retorno: { restored_count: number }
|
|
||||||
```
|
|
||||||
|
|
||||||
**Efectos:**
|
|
||||||
- Invalida todas las queries bajo 'gamification'
|
|
||||||
- Requiere confirmacion del usuario (implementada en UI)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CONFIGURACION DE REACT QUERY
|
|
||||||
|
|
||||||
| Query | staleTime | gcTime |
|
|
||||||
|-------|-----------|--------|
|
|
||||||
| parameters | 5 min | 10 min |
|
|
||||||
| parameter (single) | 5 min | 10 min |
|
|
||||||
| mayaRanks | 10 min | 15 min |
|
|
||||||
| stats | 2 min | 5 min |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## VALIDACION DEFENSIVA
|
|
||||||
|
|
||||||
El hook implementa validaciones robustas para manejar respuestas inesperadas del backend:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Ejemplo de validacion en useParameters
|
|
||||||
const data = response?.data;
|
|
||||||
if (!data || !Array.isArray(data.parameters)) {
|
|
||||||
console.warn('Unexpected response structure');
|
|
||||||
return { data: [], total: 0, page: 1, limit: 10 };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Caracteristicas:**
|
|
||||||
- Valida estructura de objetos antes de usar
|
|
||||||
- Proporciona fallbacks sensatos (arrays vacios, valores por defecto)
|
|
||||||
- Log de advertencias en consola para debugging
|
|
||||||
- Maneja campos snake_case y camelCase del backend
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EJEMPLO DE USO
|
|
||||||
|
|
||||||
### En AdminGamificationPage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useGamificationConfig } from '../hooks/useGamificationConfig';
|
|
||||||
|
|
||||||
function AdminGamificationPage() {
|
|
||||||
const {
|
|
||||||
useParameters,
|
|
||||||
useMayaRanks,
|
|
||||||
useStats,
|
|
||||||
updateParameter,
|
|
||||||
resetParameter
|
|
||||||
} = useGamificationConfig();
|
|
||||||
|
|
||||||
const { data: stats, isLoading: loadingStats } = useStats();
|
|
||||||
const { data: parametersData, isLoading: loadingParams } = useParameters();
|
|
||||||
const { data: mayaRanks, isLoading: loadingRanks } = useMayaRanks();
|
|
||||||
|
|
||||||
const handleUpdateParameter = (key: string, value: any) => {
|
|
||||||
updateParameter.mutate({
|
|
||||||
key,
|
|
||||||
data: { value, reason: 'Manual update' }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loadingStats || loadingParams || loadingRanks) {
|
|
||||||
return <LoadingSpinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<StatsCards stats={stats} />
|
|
||||||
<ParametersList
|
|
||||||
parameters={parametersData?.data || []}
|
|
||||||
onUpdate={handleUpdateParameter}
|
|
||||||
/>
|
|
||||||
<MayaRanksList ranks={mayaRanks || []} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DEPENDENCIAS
|
|
||||||
|
|
||||||
| Dependencia | Uso |
|
|
||||||
|-------------|-----|
|
|
||||||
| @tanstack/react-query | Gestion de estado y cache |
|
|
||||||
| @/services/api/admin/gamificationConfigApi | Cliente API |
|
|
||||||
| react-hot-toast | Notificaciones |
|
|
||||||
| @/types/admin/gamification.types | Tipos TypeScript |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TIPOS RELACIONADOS
|
|
||||||
|
|
||||||
### Parameter
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface Parameter {
|
|
||||||
id: string;
|
|
||||||
key: string;
|
|
||||||
value: any;
|
|
||||||
category: 'points' | 'coins' | 'levels' | 'ranks' | 'penalties' | 'bonuses';
|
|
||||||
dataType: string;
|
|
||||||
description?: string;
|
|
||||||
defaultValue?: any;
|
|
||||||
minValue?: number;
|
|
||||||
maxValue?: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### MayaRankConfig
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface MayaRankConfig {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
level: number;
|
|
||||||
minXp: number;
|
|
||||||
maxXp?: number | null;
|
|
||||||
multiplierXp: number;
|
|
||||||
multiplierMlCoins: number;
|
|
||||||
bonusMlCoins: number;
|
|
||||||
color: string;
|
|
||||||
icon?: string | null;
|
|
||||||
description?: string;
|
|
||||||
perks?: string[];
|
|
||||||
isActive: boolean;
|
|
||||||
order?: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## REFERENCIAS
|
|
||||||
|
|
||||||
- `AdminGamificationPage.tsx` - Pagina que usa este hook
|
|
||||||
- `gamificationConfigApi.ts` - Cliente API
|
|
||||||
- `gamification.types.ts` - Definiciones de tipos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ultima actualizacion:** 2025-12-18
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user