From f6b6f8442720fd98657a2887c1dfaba93c19836d Mon Sep 17 00:00:00 2001 From: Nurfog Date: Mon, 30 Mar 2026 13:32:51 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20A=C3=B1adir=20configuraci=C3=B3n=20de?= =?UTF-8?q?=20Nginx=20para=20API=20de=20OpenCCB=20y=20corregir=20URLs=20de?= =?UTF-8?q?=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHAT_RESUMEN_2026-03-26.md | 177 ++++++++++++++++++++++++------------- deploy.sh | 3 + docker-compose.yml | 2 +- nginx/custom.conf | 26 ++++++ nginx/studio.conf | 143 ++++++++++++++++++++++++++++++ web/studio/src/lib/api.ts | 70 ++++++++------- 6 files changed, 328 insertions(+), 93 deletions(-) create mode 100644 nginx/custom.conf create mode 100644 nginx/studio.conf diff --git a/CHAT_RESUMEN_2026-03-26.md b/CHAT_RESUMEN_2026-03-26.md index 5fb8033..8100e0f 100644 --- a/CHAT_RESUMEN_2026-03-26.md +++ b/CHAT_RESUMEN_2026-03-26.md @@ -66,7 +66,11 @@ El script preguntará: 3. Contraseña (oculta) 4. Nombre de la organización 5. ¿Usar SSL? [y/N] + - **y**: Usará HTTPS (con o sin staging) + - **N**: Usará HTTP (recomendado para staging) 6. ¿Usar STAGING? [y/N] (solo si elegiste SSL) + - **y**: Certificados de prueba (sin rate limits) + - **N**: Certificados reales (con rate limits) ### Conexión al servidor: @@ -77,41 +81,56 @@ cd /var/www/openccb --- -## ⚠️ Problemas Conocidos (Para Resolver) +## ⚠️ Problemas Conocidos y Soluciones -### 1. Variables NEXT_PUBLIC en Studio +### 1. Login Pegado / Error de Conexión API -**Problema**: El contenedor `openccb-studio` no está recibiendo ambas variables `NEXT_PUBLIC`: -- ✅ `NEXT_PUBLIC_LMS_API_URL=http://learning.norteamericano.com` -- ❌ `NEXT_PUBLIC_CMS_API_URL` - **FALTA** +**Problema**: El botón de login se queda procesando infinitamente. + +**Causa**: Las URLs de la API incluyen el puerto `:3001` incorrectamente. **Solución Aplicada**: -1. Actualizado `web/studio/Dockerfile` para aceptar ambos ARG -2. Actualizado `docker-compose.yml` para pasar ambos argumentos -3. Actualizado `deploy.sh` para verificar y reconstruir con `--no-cache` +- Actualizado `web/studio/src/lib/api.ts` para hardcodear las URLs de producción +- El código ahora detecta el hostname y usa la URL sin puerto -**Comandos para Verificar**: +**Comandos para Solucionar**: ```bash -# En el servidor -sudo docker exec openccb-studio env | grep NEXT_PUBLIC - -# Debería mostrar: -# NEXT_PUBLIC_CMS_API_URL=http://studio.norteamericano.com -# NEXT_PUBLIC_LMS_API_URL=http://learning.norteamericano.com -``` - -**Si persiste el problema**: -```bash -# Reconstruir manualmente +# Conectarse al servidor +ssh -i "ubuntu.pem" ubuntu@ec2-18-224-137-67.us-east-2.compute.amazonaws.com cd /var/www/openccb + +# Detener todo sudo docker compose down + +# Eliminar imágenes cacheadas sudo docker rmi openccb-studio 2>/dev/null || true -sudo docker builder prune -f +sudo docker images | grep openccb | awk '{print $3}' | xargs sudo docker rmi -f 2>/dev/null || true + +# Limpiar caché de Docker +sudo docker builder prune -af +sudo docker system prune -af + +# Reconstruir DESDE CERO (CRÍTICO usar --no-cache) sudo docker compose build --no-cache studio -sudo docker compose up -d studio -sudo docker exec openccb-studio env | grep NEXT_PUBLIC + +# Iniciar todo +sudo docker compose up -d + +# Esperar 1 minuto +sleep 60 + +# Verificar +sudo docker compose ps +docker logs openccb-studio --tail 20 ``` +**Verificación en el Navegador**: +1. Abrir ventana de incógnito (Ctrl+Shift+N) +2. Ir a `http://studio.norteamericano.com` +3. Abrir consola (F12) → Pestaña Network +4. Intentar loguearse +5. Verificar que la URL sea: `http://studio.norteamericano.com/auth/login` (SIN puerto) + ### 2. Rate Limit de Let's Encrypt **Problema**: Se alcanzó el límite de 5 certificados por semana. @@ -129,13 +148,16 @@ sudo docker exec openccb-studio env | grep NEXT_PUBLIC ### 1. `deploy.sh` - ✅ Pregunta datos del administrador - ✅ Pregunta sobre SSL y Staging -- ✅ Actualiza docker-compose.yml según elección +- ✅ Actualiza docker-compose.yml según elección (HTTP/HTTPS) - ✅ Reconstruye contenedores con `--no-cache` -- ✅ Verifica variables de entorno +- ✅ Verifica variables de entorno en los contenedores +- ✅ Muestra URLs correctas según protocolo elegido ### 2. `docker-compose.yml` - ✅ URLs en HTTP por defecto -- ✅ Ambos argumentos de build para studio +- ✅ Ambos argumentos de build para studio: + - `NEXT_PUBLIC_CMS_API_URL: http://studio.norteamericano.com` + - `NEXT_PUBLIC_LMS_API_URL: http://learning.norteamericano.com` - ✅ Variables de entorno correctas ### 3. `web/studio/Dockerfile` @@ -143,7 +165,9 @@ sudo docker exec openccb-studio env | grep NEXT_PUBLIC - ✅ Agrega `ENV NEXT_PUBLIC_LMS_API_URL` ### 4. `web/studio/src/lib/api.ts` -- ✅ Corrige función `getApiBaseUrl` para priorizar variable de entorno +- ✅ Corrige función `getApiBaseUrl` para producción +- ✅ Hardcodea URLs para `studio.norteamericano.com` y `learning.norteamericano.com` +- ✅ Elimina el puerto de las URLs en producción --- @@ -186,6 +210,10 @@ sudo docker exec openccb-studio env | grep NEXT_PUBLIC # Experience sudo docker exec openccb-experience env | grep NEXT_PUBLIC + +# Debería mostrar: +# NEXT_PUBLIC_CMS_API_URL=http://studio.norteamericano.com +# NEXT_PUBLIC_LMS_API_URL=http://learning.norteamericano.com ``` ### Reconstruir contenedores @@ -199,6 +227,15 @@ sudo docker compose build --no-cache studio sudo docker compose up -d studio ``` +### Limpiar caché de Docker +```bash +# Limpiar builder +sudo docker builder prune -af + +# Limpiar sistema +sudo docker system prune -af +``` + ### Verificar certificados SSL ```bash docker logs acme-companion --tail 50 @@ -206,30 +243,37 @@ docker logs acme-companion --tail 50 --- -## 🎯 Próximos Pasos (Para Continuar Mañana) +## 🎯 Próximos Pasos -### 1. Verificar Variables NEXT_PUBLIC +### 1. Reconstruir Studio con --no-cache ```bash ssh -i "ubuntu.pem" ubuntu@ec2-18-224-137-67.us-east-2.compute.amazonaws.com cd /var/www/openccb -# Verificar -sudo docker exec openccb-studio env | grep NEXT_PUBLIC - -# Si falta CMS_API_URL, reconstruir: sudo docker compose down sudo docker rmi openccb-studio 2>/dev/null || true -sudo docker builder prune -f +sudo docker builder prune -af sudo docker compose build --no-cache studio -sudo docker compose up -d studio +sudo docker compose up -d +sleep 60 +sudo docker compose ps ``` ### 2. Probar Login -- Acceder a `http://studio.norteamericano.com` -- Intentar loguearse con las credenciales del admin -- Verificar que no haya errores de conexión +- Abrir ventana de incógnito +- Ir a `http://studio.norteamericano.com` +- Ver consola (F12) → Network +- Verificar URL: `http://studio.norteamericano.com/auth/login` +- Intentar loguearse -### 3. Cambiar a HTTPS (Después del Rate Limit) +### 3. Verificar Funcionalidades +- [ ] Login de administrador +- [ ] Creación de cursos +- [ ] Subida de archivos +- [ ] Integración con LMS +- [ ] Certificados SSL generados + +### 4. Cambiar a HTTPS (Después del Rate Limit) ```bash # Después del 2026-03-27 ./deploy.sh @@ -237,29 +281,21 @@ sudo docker compose up -d studio # Responder "n" a "¿Usar STAGING?" ``` -### 4. Verificar Funcionalidades -- [ ] Login de administrador -- [ ] Creación de cursos -- [ ] Subida de archivos -- [ ] Integración con LMS -- [ ] Certificados SSL generados - ---- - -## 📝 Notas Importantes - -1. **HTTP vs HTTPS**: Actualmente se usa HTTP porque los certificados de staging no son válidos para las llamadas API entre dominios. - -2. **Rate Limit**: Let's Encrypt permite 5 certificados por semana por dominio. El límite se reinicia el 2026-03-27. - -3. **Variables de Entorno**: Es crítico que ambos `NEXT_PUBLIC_*` estén presentes en el contenedor de Studio para que las llamadas API funcionen correctamente. - -4. **Reconstrucción**: Siempre usar `--no-cache` al reconstruir para asegurar que los cambios en las variables de entorno se apliquen. - --- ## 🔧 Solución de Problemas Comunes +### Login se queda procesando +```bash +# Verificar URL en consola del navegador +# Debe ser: http://studio.norteamericano.com/auth/login +# NO debe tener :3001 + +# Si tiene puerto, reconstruir con --no-cache +sudo docker compose build --no-cache studio +sudo docker compose up -d +``` + ### Error 502 Bad Gateway ```bash # Verificar que los servicios están corriendo @@ -278,8 +314,8 @@ sudo docker compose restart # Ver docker-compose.yml cat docker-compose.yml | grep -A 10 "studio:" -# Verificar argumentos -sudo docker compose config | grep NEXT_PUBLIC +# Verificar en contenedor +sudo docker exec openccb-studio env | grep NEXT_PUBLIC # Reconstruir sudo docker compose build --no-cache studio @@ -300,6 +336,22 @@ sudo netstat -tlnp | grep :80 --- +## 📝 Notas Importantes + +1. **HTTP vs HTTPS**: Actualmente se usa HTTP porque: + - Los certificados de staging no son válidos para producción + - Las llamadas API entre dominios requieren HTTP o certificados válidos + +2. **Rate Limit**: Let's Encrypt permite 5 certificados por semana por dominio. El límite se reinicia el 2026-03-27. + +3. **--no-cache es CRÍTICO**: Siempre usar `--no-cache` al reconstruir Studio para que los cambios en el código se apliquen. Docker usa caché por defecto. + +4. **Ventana de Incógnito**: Después de reconstruir, siempre probar en ventana de incógnito para evitar caché del navegador. + +5. **URLs Hardcodeadas**: El código ahora tiene las URLs de producción hardcodeadas para `studio.norteamericano.com` y `learning.norteamericano.com`. Esto evita problemas con variables de entorno. + +--- + ## 📞 Contacto y Soporte **Documentación**: @@ -311,9 +363,10 @@ sudo netstat -tlnp | grep :80 - `/var/www/openccb/.env` - Variables de entorno - `/var/www/openccb/docker-compose.yml` - Servicios Docker - `web/studio/Dockerfile` - Build de Studio +- `web/studio/src/lib/api.ts` - Configuración de APIs - `deploy.sh` - Script de despliegue --- -**Última Actualización**: 26 de Marzo de 2026 -**Estado**: Pendiente verificar variables NEXT_PUBLIC en Studio +**Última Actualización**: 26 de Marzo de 2026 +**Estado**: ✅ Solución aplicada - Pendiente reconstruir con --no-cache y probar login diff --git a/deploy.sh b/deploy.sh index 9f9230e..bb8c2f1 100755 --- a/deploy.sh +++ b/deploy.sh @@ -111,6 +111,9 @@ mkdir -p "$PROD_DIR/nginx" if [ -f "nginx/proxy.conf" ]; then cp nginx/proxy.conf "$PROD_DIR/nginx/" fi +if [ -f "nginx/studio.conf" ]; then + cp nginx/studio.conf "$PROD_DIR/nginx/" +fi echo " ✅ Archivos esenciales copiados" diff --git a/docker-compose.yml b/docker-compose.yml index 7d89776..994d2c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: - vhost:/etc/nginx/vhost.d - html:/usr/share/nginx/html - ./nginx/proxy.conf:/etc/nginx/conf.d/proxy.conf:ro - - ./nginx/studio.norteamericano.com:/etc/nginx/vhost.d/studio.norteamericano.com:ro + - ./nginx/studio.conf:/etc/nginx/vhost.d/studio.norteamericano.com:ro restart: always networks: - openccb-network diff --git a/nginx/custom.conf b/nginx/custom.conf new file mode 100644 index 0000000..762389f --- /dev/null +++ b/nginx/custom.conf @@ -0,0 +1,26 @@ +# Custom nginx proxy configuration for OpenCCB +# This file configures routing for Studio (CMS) API endpoints + +# Map to detect API requests that should go to port 3001 +map $uri $api_backend { + default 0; + ~/auth/ 1; + ~/courses/ 1; + ~/modules/ 1; + ~/lessons/ 1; + ~/assets/ 1; + ~/organization/ 1; + ~/users/ 1; + ~/grading/ 1; + ~/question-bank/ 1; + ~/test-templates/ 1; + ~/admin/ 1; + ~/api/ 1; + ~/branding 1; +} + +# Override upstream for API requests +map $api_backend $upstream_addr_override { + 0 ""; + 1 "openccb-studio:3001"; +} diff --git a/nginx/studio.conf b/nginx/studio.conf new file mode 100644 index 0000000..b0a8d50 --- /dev/null +++ b/nginx/studio.conf @@ -0,0 +1,143 @@ +# Custom nginx configuration for OpenCCB Studio +# This overrides the default location block to route API requests correctly + +# API routes that need to go to port 3001 +location = /auth/login { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /auth/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location = /organization { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /organization/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location = /branding { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /courses/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /modules/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /lessons/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /assets/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /users/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /grading/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /question-bank/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /admin/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} + +location /api/ { + proxy_pass http://openccb-studio:3001; + 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 $http_x_forwarded_proto; + proxy_set_header Connection ""; + proxy_http_version 1.1; +} diff --git a/web/studio/src/lib/api.ts b/web/studio/src/lib/api.ts index 2eeae66..c837803 100644 --- a/web/studio/src/lib/api.ts +++ b/web/studio/src/lib/api.ts @@ -1,17 +1,27 @@ const getApiBaseUrl = (defaultPort: string, envVar?: string) => { - // Si hay una variable de entorno definida, usarla siempre - if (envVar && envVar.trim() !== '') { - return envVar; - } - - // Fallback para desarrollo local + // Producción - dominios específicos if (typeof window !== 'undefined') { const hostname = window.location.hostname; const protocol = window.location.protocol; + + // Forzar URLs correctas para producción (sin puerto) + if (hostname === 'studio.norteamericano.com') { + return `${protocol}//studio.norteamericano.com`; + } + if (hostname === 'learning.norteamericano.com') { + return `${protocol}//learning.norteamericano.com`; + } + + // Usar variable de entorno si está definida + if (envVar && envVar.trim() !== '') { + return envVar; + } + + // Desarrollo local return `${protocol}//${hostname}:${defaultPort}`; } - return `http://localhost:${defaultPort}`; + return envVar || `http://localhost:${defaultPort}`; }; export const API_BASE_URL = getApiBaseUrl("3001", process.env.NEXT_PUBLIC_CMS_API_URL); @@ -800,7 +810,7 @@ export const cmsApi = { getLesson: (id: string): Promise => apiFetch(`/lessons/${id}`), updateLesson: (id: string, payload: Partial): Promise => apiFetch(`/lessons/${id}`, { method: 'PUT', body: JSON.stringify(payload) }), summarizeLesson: (id: string): Promise => apiFetch(`/lessons/${id}/summarize`, { method: 'POST' }), - async generateQuiz(lessonId: string, payload: { prompt_hint?: string, quiz_type?: string }): Promise { + async generateQuiz(lessonId: string, payload: { prompt_hint?: string, quiz_type?: string }): Promise<{ questions: QuizQuestion[] }> { return apiFetch(`/lessons/${lessonId}/generate-quiz`, { method: 'POST', body: JSON.stringify(payload) @@ -1019,7 +1029,7 @@ export const cmsApi = { // Test Templates listTestTemplates: (filters?: TestTemplateFilters): Promise => - apiFetch('/test-templates', { method: 'GET', query: filters as any }, false), + apiFetch('/test-templates', { method: 'GET', query: filters as Record }, false), getTestTemplate: (templateId: string): Promise => apiFetch(`/test-templates/${templateId}`, {}, false), createTestTemplate: (payload: CreateTestTemplatePayload): Promise => @@ -1055,13 +1065,13 @@ export interface QuestionBank { organization_id: string; question_text: string; question_type: QuestionBankType; - options?: any; - correct_answer?: any; + options?: unknown; + correct_answer?: unknown; explanation?: string; audio_url?: string; audio_text?: string; audio_status?: 'pending' | 'generating' | 'ready' | 'failed'; - audio_metadata?: any; + audio_metadata?: unknown; media_url?: string; media_type?: string; points: number; @@ -1069,7 +1079,7 @@ export interface QuestionBank { tags?: string[]; skill_assessed?: 'reading' | 'listening' | 'speaking' | 'writing'; source?: 'manual' | 'ai-generated' | 'imported-mysql' | 'imported-csv'; - source_metadata?: any; + source_metadata?: unknown; usage_count?: number; last_used_at?: string; is_active: boolean; @@ -1091,8 +1101,8 @@ export interface QuestionBankFilters { export interface CreateQuestionBankPayload { question_text: string; question_type: QuestionBankType; - options?: any; - correct_answer?: any; + options?: unknown; + correct_answer?: unknown; explanation?: string; points?: number; difficulty?: string; @@ -1107,8 +1117,8 @@ export interface CreateQuestionBankPayload { export interface UpdateQuestionBankPayload { question_text?: string; question_type?: QuestionBankType; - options?: any; - correct_answer?: any; + options?: unknown; + correct_answer?: unknown; explanation?: string; points?: number; difficulty?: string; @@ -1122,7 +1132,7 @@ export interface UpdateQuestionBankPayload { export const questionBankApi = { list: (filters?: QuestionBankFilters): Promise => - apiFetch('/question-bank', { method: 'GET', query: filters as any }, false), + apiFetch('/question-bank', { method: 'GET', query: filters as Record }, false), get: (id: string): Promise => apiFetch(`/question-bank/${id}`, {}, false), create: (payload: CreateQuestionBankPayload): Promise => @@ -1265,7 +1275,7 @@ export interface Badge { name: string; description: string; icon_url: string; - criteria: any; + criteria: unknown; created_at: string; } @@ -1320,7 +1330,7 @@ export interface TestTemplate { passing_score: number; total_points: number; instructions?: string; - template_data: any; + template_data: unknown; tags?: string[]; is_active: boolean; usage_count: number; @@ -1337,7 +1347,7 @@ export interface TestTemplateSection { section_order: number; points: number; instructions?: string; - section_data?: any; + section_data?: unknown; created_at: string; } @@ -1348,11 +1358,11 @@ export interface TestTemplateQuestion { question_order: number; question_type: QuestionType; question_text: string; - options?: any; - correct_answer?: any; + options?: unknown; + correct_answer?: unknown; explanation?: string; points: number; - metadata?: any; + metadata?: unknown; created_at: string; } @@ -1373,7 +1383,7 @@ export interface CreateTestTemplatePayload { passing_score: number; total_points: number; instructions?: string; - template_data: any; + template_data: unknown; tags?: string[]; } @@ -1388,7 +1398,7 @@ export interface UpdateTestTemplatePayload { passing_score?: number; total_points?: number; instructions?: string; - template_data?: any; + template_data?: unknown; tags?: string[]; is_active?: boolean; } @@ -1398,11 +1408,11 @@ export interface CreateQuestionPayload { question_order: number; question_type: string; question_text: string; - options?: any; - correct_answer?: any; + options?: unknown; + correct_answer?: unknown; explanation?: string; points: number; - metadata?: any; + metadata?: unknown; } export interface CreateSectionPayload { @@ -1411,7 +1421,7 @@ export interface CreateSectionPayload { section_order: number; points: number; instructions?: string; - section_data?: any; + section_data?: unknown; } export interface TestTemplateFilters {