From e5657d6b0fde41a04d70dfa563619390c152eec0 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 4 Feb 2026 14:21:53 -0600 Subject: [PATCH 1/5] =?UTF-8?q?chore(db):=20actualizar=20esquema/backups?= =?UTF-8?q?=20(auditor=C3=ADa=20de=20planes,=20hashes=20y=20conversation?= =?UTF-8?q?=5Fid)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agrega fn_log_cambios_planes_estudio y trigger para registrar cambios en cambios_plan, usando response_id (reemplaza interaccion_ia_id). - Extiende planes_estudio con conversation_id (unique) y plan_hash; agrega asignatura_hash en asignaturas; corrige unaccent_immutable y añade extensión http. - Actualiza seeds/backups (data_backup.sql / scheme_backup.sql) y ajusta script de diff (sql_para_local_como_remoto.sql), elimina el diff viejo y actualiza archivos auxiliares (deno.lock, CLI temp). --- deno.lock | 51 +++- diff.txt | Bin 0 -> 278366 bytes scripts/sql_para_local_como_remoto.ts | 2 +- supabase/.temp/cli-latest | 2 +- supabase/data_backup.sql | 179 +++++++++++- supabase/diffs/sql_para_local_como_remoto.sql | 205 +++++++++++++ supabase/diffs/sql_para_remoto_como_local.sql | 56 ---- .../20260130215440_cambios_asignatura.sql | 21 ++ ...260203204213_sincronizacion_con_remoto.sql | 205 +++++++++++++ supabase/scheme_backup.sql | 274 +++++++++++++++++- supabase/snippets/Untitled query 238.sql | 205 +++++++++++++ .../inserts a facultades y a carreras.sql | 128 ++++++++ 12 files changed, 1231 insertions(+), 97 deletions(-) create mode 100644 diff.txt create mode 100644 supabase/diffs/sql_para_local_como_remoto.sql delete mode 100644 supabase/diffs/sql_para_remoto_como_local.sql create mode 100644 supabase/migrations/20260130215440_cambios_asignatura.sql create mode 100644 supabase/migrations/20260203204213_sincronizacion_con_remoto.sql create mode 100644 supabase/snippets/Untitled query 238.sql create mode 100644 supabase/snippets/inserts a facultades y a carreras.sql diff --git a/deno.lock b/deno.lock index 55a3dca..1ea3a90 100644 --- a/deno.lock +++ b/deno.lock @@ -89,33 +89,57 @@ "npm": { "@deno/darwin-arm64@2.6.4": { "integrity": "sha512-5pocuV2IVWYUzomXze+bDSO86uX1WPxUn82BS7BrLI3ix8Eq7l8DhvYgz064w7rwCM4CiLLSc6MAQpAI+0gUag==", - "os": ["darwin"], - "cpu": ["arm64"] + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ] }, "@deno/darwin-x64@2.6.4": { "integrity": "sha512-tX7Fz47HcoXTYR4wN2WzXjqJtEPTftzjEknCYkZa8D4BzvpN7Zo1ra3Jd6bWPN9+T6mgTL6YMPMMSigXddIUcA==", - "os": ["darwin"], - "cpu": ["x64"] + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ] }, "@deno/linux-arm64-glibc@2.6.4": { "integrity": "sha512-wY4hjjVLq5idClWj6e1de9IUZ0QZ1rxo16o0dwCJpMi+w1lolxtYxGF4y7pkcxAdZOpMGsXZuYMPq1Z2n51esA==", - "os": ["linux"], - "cpu": ["arm64"] + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ] }, "@deno/linux-x64-glibc@2.6.4": { "integrity": "sha512-H9b0WJw+eWyC6GcMGCXxC/3lz2kVveyWyUrzGyQ8hJmtDJpWiDnCU2iPqEpEcwXHgAUFFyjnRaFwedwwv5ApdA==", - "os": ["linux"], - "cpu": ["x64"] + "os": [ + "linux" + ], + "cpu": [ + "x64" + ] }, "@deno/win32-arm64@2.6.4": { "integrity": "sha512-fMH1dx54ORbCTpMorGjDshwonIu6KfgfPC+Pq+14jeq/8604EIFXiA484p3Ov+ViZycndNHEPe8ZKPNd8xLjJA==", - "os": ["win32"], - "cpu": ["arm64"] + "os": [ + "win32" + ], + "cpu": [ + "arm64" + ] }, "@deno/win32-x64@2.6.4": { "integrity": "sha512-hAxqNrBLHCINtqDgKcRiu8QtMbtdnHOVW3OFQjzA3gZEcpnGUK3JiF5ql++83SjuQH8ViZKGcdqkioLR7Bf14A==", - "os": ["win32"], - "cpu": ["x64"] + "os": [ + "win32" + ], + "cpu": [ + "x64" + ] }, "@isaacs/fs-minipass@4.0.1": { "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", @@ -352,7 +376,8 @@ "dependencies": [ "jsr:@david/dax@~0.44.2", "jsr:@openai/openai@^6.16.0", - "jsr:@zod/zod@^4.3.5" + "jsr:@zod/zod@^4.3.5", + "jsr:@david/dax@~0.44.2" ], "packageJson": { "dependencies": [ diff --git a/diff.txt b/diff.txt new file mode 100644 index 0000000000000000000000000000000000000000..864c3967c9e919e37830ebc4d1de6b34861fbacc GIT binary patch literal 278366 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 Integrador y Estancia/Residencia. Tutoría y asesoría: obligatoria el primer año y bajo demanda en cursos superiores. Servicio social: 480 horas conforme a normativa aplicable. Acreditación y titulación: por créditos y opción de titulación por examen general, proyecto aplicado, portafolio de evidencias o tesis.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "La evaluación del plan se realizará con una periodicidad bienal y cortes anuales, mediante: Método: ciclo PDCA con evidencias OBE: mapeo resultados de programa-asignatura, rúbricas, portafolios y KPIs institucionales. Instrumentos: encuestas a estudiantes y egresados, panel de empleadores, análisis de EGEL/EXANI (cuando aplique), analítica del LMS, informes de prácticas/residencias y auditorías académicas internas. Indicadores: eficiencia terminal, tiempo de titulación, empleabilidad, resultados de aprendizaje, certificaciones profesionales obtenidas, producción de proyectos y patentes/prototipos. Vinculación sectorial: Consejo Consultivo con industria (TI/OT, energía, manufactura, finanzas) para actualización de contenidos al menos cada 2 años. Uso de resultados: actualización de programas, seriación y recursos didácticos; capacitación docente; adecuaciones a laboratorios y plataforma."}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'IA', '{}', NULL, NULL, '2026-01-09 21:48:48.404283+00', '2026-01-09 21:48:48.404283+00'), - ('0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '8208da08-d549-4359-8865-9d806bc54f19', '69fb2b77-5a95-47e0-bf1f-389d384200e4', 'Ingeniería en Cibernética y Ciberseguridad (2025)', 'Licenciatura', 'Semestre', 8, '{"nivel": "Licenciatura", "nombre": "Ingeniería Cibernética", "area_de_estudio": "Tecnologías de la Información y la Comunicación (CMPE 2016)", "perfil_de_egreso": "Conocimientos: redes, sistemas, criptografía. Habilidades: pentesting, hardening, respuesta a incidentes. Actitudes: ética, responsabilidad.", "diseno_curricular": "Rígido", "perfil_de_ingreso": "Pensamiento lógico, bases de álgebra, nociones de programación y comunicación escrita.", "curso_propedeutico": null, "modalidad_educativa": "Escolar", "antecedente_academico": "Bachillerato concluido; afinidad con matemáticas y computación.", "carga_horaria_a_la_semana": 40, "programa_de_investigacion": null, "clave_del_plan_de_estudios": "2026-CIB-ING", "duracion_del_ciclo_escolar": "16", "fines_de_aprendizaje_o_formacion": "Formar profesionales capaces de diseñar, implementar y asegurar sistemas ciberfísicos y plataformas digitales.", "total_de_ciclos_del_plan_de_estudios": "8", "sustento_teorico_del_modelo_curricular": "Enfoque por competencias, aprendizaje basado en problemas y laboratorios.", "justificacion_de_la_propuesta_curricular": null, "administracion_y_operatividad_del_plan_de_estudios": "Plan rígido con cargas sugeridas por semestre; asignaturas seriadas por áreas.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "Revisión anual por comité académico; actualización por tendencias y retroalimentación de egresados."}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'MANUAL', '{"seed": true, "version": "2026-01-08"}', '11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2026-01-09 13:45:37.784586+00', '2026-01-21 19:38:38.888504+00'); +INSERT INTO "public"."planes_estudio" ("id", "carrera_id", "estructura_id", "nombre", "nivel", "tipo_ciclo", "numero_ciclos", "datos", "estado_actual_id", "activo", "tipo_origen", "meta_origen", "creado_por", "actualizado_por", "creado_en", "actualizado_en", "conversation_id") VALUES + ('4660b774-a6e2-44f4-a340-747b41235c7c', '089cdeda-d557-4a57-b49f-eb44921dfa3a', '69fb2b77-5a95-47e0-bf1f-389d384200e4', 'Ingeniería en Sistemas Domóticos (2027)', 'Otro', 'Semestre', 9, '{"nivel": "Bachillerato", "nombre": "Especialización en Gravedad Cuántica de Bucles", "area_de_estudio": "Ciencias Físico-Matemáticas", "perfil_de_egreso": "La Specialización en Gravedad Cuántica de Bucles formará a un especialista capaz de integrar creativamente conocimientos avanzados en física, matemáticas y computación para enfrentar problemas teóricos y fenomenológicos en la intersección de la relatividad general y la mecánica cuántica. Al finalizar el programa, el egresado será capaz de: 1) Comprender y aplicar los fundamentos de la gravedad cuántica de bucles, incluyendo su formulación canónica, las variables de Ashtekar y el formalismo de redes de espín; 2) Desarrollar modelos y simulaciones rigurosas de fenómenos cuántico-gravitacionales, empleando métodos numéricos y herramientas de cómputo científico para analizar redes y espumas de espín, así como su relación con la cosmología cuántica; 3) Utilizar y conectar conceptos avanzados de geometría diferencial, teoría de grupos de Lie, representaciones y espinores para la construcción de estados cuánticos geométricos; 4) Resolver y comunicar de manera crítica problemas de frontera en física teórica contemporánea, integrando procesos de modelado matemático, análisis computacional y validación fenomenológica; 5) Evaluar y proponer soluciones innovadoras en entornos de trabajo colaborativo, mostrando habilidad para desarrollar proyectos científicos reproducibles bajo principios éticos y de buenas prácticas en la investigación; 6) Integrarse a comunidades científicas y continuar con estudios de posgrado o desempeñarse en entornos interdisciplinares que involucren física matemática, simulación, cosmología, información cuántica o modelado de sistemas complejos.", "diseno_curricular": "Rígido", "perfil_de_ingreso": "El aspirante debe haber concluido el bachillerato con perfil Área 1 (Ciencias Físico-Matemáticas), lo que implica sólidas bases en matemáticas, física, química, razonamiento lógico y competencia en análisis de problemas. Se espera capacidad para el aprendizaje autónomo y familiaridad con herramientas y conceptos científicos y tecnológicos, así como interés en la resolución de problemas complejos y trabajo experimental.", "curso_propedeutico": "No tiene", "modalidad_educativa": "Escolar", "antecedente_academico": "Bachillerato en Ciencias Físico-Matemáticas, con orientación en matemáticas y física; requisito previo para ingreso a la especialización. El plan asume que el aspirante ha concluido un bachillerato con áreas de Ciencias Físico-Matemáticas.", "carga_horaria_a_la_semana": 27, "programa_de_investigacion": "No aplica", "clave_del_plan_de_estudios": "2020", "duracion_del_ciclo_escolar": "16 semanas", "fines_de_aprendizaje_o_formacion": "Analizar fundamentos de la gravedad cuántica de bucles y su relación con la relatividad general; Diseñar modelos computacionales para explorar redes de espín y espumas cuánticas, con enfoque en cosmología cuántica; Evaluar resultados numéricos mediante criterios de reproducibilidad y validación fenomenológica; Comunicar resultados científicos de forma clara, estructurada y técnica; Integrar conceptos de domótica, electrónica, control y sistemas computacionales mediante herramientas de simulación para proyectos interdisciplinarios y el desarrollo de asistentes virtuales; Aplicar herramientas de cómputo para la creación de proyectos con código reproducible y documentación adecuada; Desarrollar capacidades de trabajo en equipo, ética científica y buenas prácticas en investigación.", "total_de_ciclos_del_plan_de_estudios": "9", "sustento_teorico_del_modelo_curricular": "No aplica", "justificacion_de_la_propuesta_curricular": "No aplica", "administracion_y_operatividad_del_plan_de_estudios": "La administración y operatividad del plan de estudios se ubica bajo la Dirección de Asuntos Académicos - Anexo 1; modalidad escolar; estructura rígida con 9 ciclos; carga horaria semanal de 27 horas; responsable: Lic. María González Pérez, Directora Académica; procesos de revisión y actualización conforme a la Propuesta de Evaluación Periódica y a los lineamientos del Acuerdo 17/11/17.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "Nivel de evaluación curricular interna: diagnóstico de estructura y operación. Análisis técnico-pedagógico del planteamiento curricular vigente; consulta a directivos del área académica para valorar problemáticas de formación y operación; consulta a docentes sobre problemáticas de formación, operación y necesidades disciplinarias; estudio de opinión de estudiantes sobre la formación y operación. Nivel de evaluación curricular externa: diagnóstico de impacto y prácticas profesionales. Análisis del estado del conocimiento en campos disciplinarios vinculados al programa, comparación de planes y tendencias en criterios de organismos evaluadores; estudio con egresados y empleadores para valorar inserción laboral y requerimientos actuales; recopilación de criterios y reformas necesarias para mantener el programa actualizado y acorde con la disciplina."}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'IA', '{"iaConfig": {"usarMCP": false, "notasAdicionales": "Agrega a los asistentes virtuales", "descripcionEnfoque": "Carrera relacionada con la domótica, electrónica, control y sistemas computacionales"}, "referencias": {"repositoriosIds": [], "archivosReferenciaIds": []}, "generado_por": "ai_generate_plan", "ai_structured": {"model": "gpt-5-nano-2025-08-07", "usage": {"input_tokens": 9812, "total_tokens": 19204, "output_tokens": 9392, "input_tokens_details": {"cached_tokens": 0}, "output_tokens_details": {"reasoning_tokens": 8256}}, "responseId": "resp_0b1339b15ea08d51006973b4892a20819c954b7fe00a35e1ed", "conversationId": null}}', NULL, NULL, '2026-01-23 17:50:00.85346+00', '2026-01-23 22:00:33.119999+00', NULL), + ('c90aebbb-454e-4a8e-b2a3-99dd1c200e86', '8208da08-d549-4359-8865-9d806bc54f19', '69fb2b77-5a95-47e0-bf1f-389d384200e4', 'Ingeniería en Cibernética y Ciberseguridad (2026)', 'Licenciatura', 'Semestre', 9, '{"nivel": "Licenciatura", "nombre": "Ingeniería Cibernética y Ciberseguridad", "area_de_estudio": "5. Ingeniería, manufactura y construcción (CMPE 2016)", "perfil_de_egreso": "Al finalizar, el egresado demostrará los siguientes atributos: Conocimientos Fundamentos de matemáticas, física y probabilidad aplicados a señales, control y comunicaciones. Arquitecturas de redes, sistemas operativos, virtualización, servicios en la nube e integración TI/OT. Modelado y control de sistemas ciberfísicos y embebidos, protocolos industriales e IoT. Criptografía aplicada, gestión de identidades, gobierno y gestión de seguridad de la información. Normativa y marcos de referencia: ISO/IEC 27001/27002, NIST CSF/800-53, MITRE ATT&CK, OWASP, ENS (referencial), legislación mexicana aplicable en protección de datos y delitos informáticos. Auditoría de seguridad, pruebas de penetración éticas, respuesta a incidentes y continuidad del negocio. Gestión de proyectos, economía de la ciberseguridad y ética profesional. HabilidadesDiseñar y desplegar arquitecturas seguras para redes, nube y sistemas ciberfísicos. Programar en C/C++, Python y scripts para automatización, pruebas y análisis. Configurar y operar firewalls, IDS/IPS, SIEM, WAF, VPN, segmentación y Zero Trust básico. Conducir análisis de riesgo, planes de tratamiento y auditorías técnicas y de cumplimiento. Orquestar procesos de detección, respuesta y recuperación ante incidentes. Desarrollar prototipos de IoT seguro y realizar hardening de dispositivos embebidos. Comunicar resultados técnicos mediante informes ejecutivos y presentaciones efectivas. Actitudes y valoresÉtica, responsabilidad social y legalidad en el manejo de información y en pruebas de seguridad. Aprendizaje autónomo y actualización permanente. Trabajo colaborativo, liderazgo y orientación a resultados. Respeto a la diversidad, perspectiva de género e inclusión.", "diseno_curricular": "Flexible", "perfil_de_ingreso": " ConocimientosMatemáticas de bachillerato: álgebra, funciones y trigonometría. Ofimática e informática básica; navegación segura en Internet. Inglés técnico básico (lectura de manuales). HabilidadesPensamiento lógico-matemático y resolución de problemas. Comunicación escrita y oral. Gestión del tiempo y aprendizaje autónomo. ActitudesInterés por la tecnología, la seguridad digital y la innovación. Ética, responsabilidad y respeto a la legalidad. Trabajo colaborativo y apertura al aprendizaje continuo.", "curso_propedeutico": "Aplicará cuando el diagnóstico de ingreso identifique brechas en matemáticas y programación. Población: aspirantes provenientes de bachilleratos no afines. Duración: 40 horas (4 semanas). Contenidos: Aritmética y álgebra para ingeniería; pensamiento lógico; introducción a programación (Python); fundamentos de redes; ciudadanía digital y nociones de ciberseguridad. Evaluación: examen criterio-referido (70% mínimo) y evidencias prácticas. Vinculación: articulado con Programación I, Matemáticas I y Fundamentos de Redes.", "modalidad_educativa": "Mixta", "antecedente_academico": "Bachillerato general o tecnológico concluido, o Técnico Superior Universitario equivalente, preferentemente en áreas físico-matemáticas, informática, electrónica o afines.", "carga_horaria_a_la_semana": 40, "programa_de_investigacion": null, "clave_del_plan_de_estudios": "2025", "duracion_del_ciclo_escolar": "18 semanas efectivas por ciclo semestral", "fines_de_aprendizaje_o_formacion": "El plan de estudios se orienta al desarrollo de competencias profesionales para diseñar, implementar y gestionar sistemas ciberfísicos y de ciberseguridad en organizaciones públicas y privadas, con enfoque de resultados y apego normativo. Al concluir, el egresado será capaz de: Analizar arquitecturas de sistemas ciberfísicos, redes y servicios para identificar riesgos, amenazas y vulnerabilidades. Diseñar estrategias de ciberseguridad basadas en estándares (ISO/IEC 27001, NIST, MITRE ATT&CK) y en principios de ingeniería segura. Implementar controles técnicos y administrativos, incluyendo criptografía aplicada, segmentación de redes, endurecimiento de sistemas y gestión de identidades. Programar y modelar sistemas embebidos y de IoT con prácticas de seguridad por diseño. Administrar infraestructuras de TI/OT y plataformas de monitoreo (SIEM, IDS/IPS) para la detección y respuesta a incidentes. Evaluar la conformidad normativa y el riesgo tecnológico mediante auditorías técnicas y de cumplimiento. Integrar metodologías de ciencia de datos e inteligencia artificial para la detección de anomalías y ciberamenazas. Simular, probar y validar el desempeño y la resiliencia de sistemas cibernéticos en entornos controlados (cyber range/laboratorios). Gestionar proyectos tecnológicos aplicando enfoques ágiles y de calidad, con perspectiva ética y de sostenibilidad. Comunicar hallazgos técnicos y recomendaciones a audiencias técnicas y directivas, sustentando decisiones con evidencia.", "total_de_ciclos_del_plan_de_estudios": "8 a 10 ciclos (semestres)", "sustento_teorico_del_modelo_curricular": "Se adopta un modelo por competencias y resultados de aprendizaje, con base en: Constructivismo y aprendizaje situado para problemas reales. Taxonomía de Bloom para progresión cognitiva. OBE/ABET para trazabilidad de resultados de programa-asignatura. CDIO (Concebir-Diseñar-Implementar-Operar) en proyectos integradores. Evaluación auténtica mediante rúbricas y desempeño en laboratorios físicos y virtuales.", "justificacion_de_la_propuesta_curricular": "La modalidad mixta combina sesiones presenciales/laboratorio y trabajo en línea para garantizar pertinencia, flexibilidad espacio-temporal y autogestión del aprendizaje. Plataformas tecnológicas: LMS institucional (p. ej., Moodle 4.x) con repositorio de contenidos, foros, cuestionarios, rúbricas y analítica; videoconferencia para sesiones sincrónicas; repositorios de código (Git) y laboratorios virtuales (simuladores de redes y ciberseguridad, sandboxes y máquinas virtuales) articulados con las asignaturas. Distribución horaria: 40-60% de horas bajo conducción (presencial o síncrona) y 60-40% de horas independientes (asíncronas) según la naturaleza de cada asignatura. Funcionalidad con instalaciones: laboratorios de redes, ciberseguridad e IoT; cyber range institucional; aulas de cómputo; todos con políticas de seguridad, bitácoras y control de acceso. Estrategias de aprendizaje: aula invertida, aprendizaje basado en problemas/proyectos, retos de blue/red team, estudios de caso y simulaciones. Evaluación: tareas prácticas en plataforma, laboratorios verificados, exámenes en línea con supervisión, presentaciones y defensa técnica. Perfil docente: ingenierías afines con experiencia en TI/OT, certificaciones deseables (CCNA/Network Security, ISO 27001 LA/LI, CEH o equivalentes) y dominio de herramientas LMS.", "administracion_y_operatividad_del_plan_de_estudios": "El plan es de diseño flexible con seriación académica por competencias y créditos. Se cursa en 8 a 10 semestres con un total referencial de 320 créditos (≥300 conforme al Art. 10). La operación académica considera: Carga por ciclo: de 5 a 7 asignaturas por semestre. Promedio semanal: 18-22 horas bajo conducción y 18-22 horas independientes (carga máxima 40 h/sem). Modalidad mixta: al menos el 40% de las horas del programa son bajo conducción de un académico (≥960 h totales); el resto en actividades independientes mediadas por plataforma. Seriación típica: Programación I → Programación II → Estructuras de Datos → Sistemas Operativos; Matemáticas I → Matemáticas II → Probabilidad y Estadística; Redes I → Redes II → Seguridad en Redes; Control I → Sistemas Embebidos → IoT Seguro. Componentes de formación: básica (matemáticas, ciencias y programación), profesional (redes, sistemas, control, seguridad), terminal optativa (ciberdefensa, forense, nube, OT/SCADA), Proyecto Integrador y Estancia/Residencia. Tutoría y asesoría: obligatoria el primer año y bajo demanda en cursos superiores. Servicio social: 480 horas conforme a normativa aplicable. Acreditación y titulación: por créditos y opción de titulación por examen general, proyecto aplicado, portafolio de evidencias o tesis.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "La evaluación del plan se realizará con una periodicidad bienal y cortes anuales, mediante: Método: ciclo PDCA con evidencias OBE: mapeo resultados de programa-asignatura, rúbricas, portafolios y KPIs institucionales. Instrumentos: encuestas a estudiantes y egresados, panel de empleadores, análisis de EGEL/EXANI (cuando aplique), analítica del LMS, informes de prácticas/residencias y auditorías académicas internas. Indicadores: eficiencia terminal, tiempo de titulación, empleabilidad, resultados de aprendizaje, certificaciones profesionales obtenidas, producción de proyectos y patentes/prototipos. Vinculación sectorial: Consejo Consultivo con industria (TI/OT, energía, manufactura, finanzas) para actualización de contenidos al menos cada 2 años. Uso de resultados: actualización de programas, seriación y recursos didácticos; capacitación docente; adecuaciones a laboratorios y plataforma."}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'IA', '{"response_id": "lsadjf"}', NULL, NULL, '2026-01-09 21:48:48.404283+00', '2026-01-23 22:18:37.320091+00', NULL), + ('ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', '089cdeda-d557-4a57-b49f-eb44921dfa3a', '69fb2b77-5a95-47e0-bf1f-389d384200e4', 'Ingeniería Mecatrónica (2026)', 'Licenciatura', 'Semestre', 9, '{"nombre_autorizado_de_la_institucion": "Agregando nuevos datos con persistencia"}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'IA', '{"iaConfig": {"usarMCP": false, "notasAdicionales": "", "descripcionEnfoque": "Dame un plan de estudios realista y enfocado en física médica"}, "referencias": {"repositoriosIds": [], "archivosReferenciaIds": []}, "generado_por": "ai_generate_plan", "ai_structured": {"model": "gpt-5-nano-2025-08-07", "usage": {"input_tokens": 202327, "total_tokens": 209102, "output_tokens": 6775, "input_tokens_details": {"cached_tokens": 0}, "output_tokens_details": {"reasoning_tokens": 5696}}, "responseId": "resp_08633e9c3f0021c000697299eb9c2881a2a3bdc31d43c4bf0d", "conversationId": null}}', NULL, NULL, '2026-01-22 21:44:28.093643+00', '2026-01-29 21:58:12.447523+00', NULL), + ('f6280a08-226d-480b-ab67-561e4657bfc4', 'b56eb209-245d-4258-a6a7-9dcb1d231cb4', '69fb2b77-5a95-47e0-bf1f-389d384200e4', 'Ingeniería Económica y Financiera', 'Licenciatura', 'Semestre', 9, '{}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'MANUAL', '{}', NULL, NULL, '2026-01-30 14:33:50.818+00', '2026-01-30 14:33:50.818+00', NULL), + ('79fba458-aea4-4ca8-ab76-943ede479f15', 'b56eb209-245d-4258-a6a7-9dcb1d231cb4', '69fb2b77-5a95-47e0-bf1f-389d384200e4', 'prueba', 'Licenciatura', 'Semestre', 9, '{}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'MANUAL', '{}', NULL, NULL, '2026-01-30 14:44:48.312+00', '2026-01-30 14:44:48.312+00', NULL), + ('0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '8208da08-d549-4359-8865-9d806bc54f19', '69fb2b77-5a95-47e0-bf1f-389d384200e4', 'Ingeniería en Cibernética y Ciberseguridad (2025)', 'Maestría', 'Semestre', 8, '{"nivel": "Licenciatura", "nombre": "Ingeniería Cibernética", "area_de_estudio": "Tecnologías de la Información y la Comunicación (CMPE 2016)", "perfil_de_egreso": "Conocimientos: redes, sistemas, criptografía. Habilidades: pentesting, hardening, respuesta a incidentes. Actitudes: ética, responsabilidad.", "diseno_curricular": "Rígido", "perfil_de_ingreso": "Pensamiento lógico, bases de álgebra, nociones de programación y comunicación escrita.", "curso_propedeutico": null, "modalidad_educativa": "Probando guardado", "antecedente_academico": "Bachillerato concluido; afinidad con matemáticas y computación.", "carga_horaria_a_la_semana": 40, "programa_de_investigacion": null, "clave_del_plan_de_estudios": "2026-CIB-ING", "duracion_del_ciclo_escolar": "16", "fines_de_aprendizaje_o_formacion": "Formar profesionales capaces de diseñar, implementar y asegurar sistemas ciberfísicos y plataformas digitales.", "nombre_autorizado_de_la_institucion": "Agregando informacion persistente", "total_de_ciclos_del_plan_de_estudios": "8", "sustento_teorico_del_modelo_curricular": "Enfoque por competencias, aprendizaje basado en problemas y laboratorios.", "justificacion_de_la_propuesta_curricular": null, "administracion_y_operatividad_del_plan_de_estudios": "Plan rígido con cargas sugeridas por semestre; asignaturas seriadas por áreas.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "Revisión anual por comité académico; actualización por tendencias y retroalimentación de egresados."}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'MANUAL', '{"seed": true, "version": "2026-01-08"}', '11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2026-01-09 13:45:37.784586+00', '2026-01-30 20:18:43.823019+00', '123'), + ('c5d80b6d-5d3d-4730-a4d5-8eaec3287910', '20593041-d605-4174-8404-4b152041eece', '69fb2b77-5a95-47e0-bf1f-389d384200e4', 'Derecho', 'Licenciatura', 'Semestre', 10, '{}', '18f49b67-8077-4371-be6e-2019a3be3562', true, 'MANUAL', '{}', NULL, NULL, '2026-01-30 21:03:52.036+00', '2026-01-30 21:03:52.035+00', NULL); -- @@ -1983,8 +2088,8 @@ INSERT INTO "public"."lineas_plan" ("id", "plan_estudio_id", "nombre", "orden", -- Data for Name: asignaturas; Type: TABLE DATA; Schema: public; Owner: postgres -- -INSERT INTO "public"."asignaturas" ("id", "plan_estudio_id", "estructura_id", "facultad_propietaria_id", "codigo", "nombre", "tipo", "creditos", "horas_semana", "numero_ciclo", "linea_plan_id", "orden_celda", "datos", "contenido_tematico", "tipo_origen", "meta_origen", "creado_por", "actualizado_por", "creado_en", "actualizado_en") VALUES - ('9d4dda6a-488f-428a-8a07-38081592a641', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '7856e9cc-9ac1-4a31-b93a-0624d4c5e1de', '155b5fe7-9e09-420f-8d9d-ddd8219f193d', 'CIB-CS-401', 'OffSec (Seguridad Ofensiva)', 'OBLIGATORIA', 8, 6, 4, 'c261fbce-893a-4900-b03c-2f3ad3ce4987', 1, '{"nombre": "Inteligencia Artificial Aplicada y Ética Profesional", "descripcion": "Esta asignatura ofrece una visión integral sobre el funcionamiento de los sistemas de IA, su implementación en entornos productivos y el análisis crítico de las implicaciones éticas y sociales de su uso.", "perfil_de_egreso": "Al concluir la materia, el egresado habrá desarrollado los siguientes atributos: \n\nCONOCIMIENTOS: Comprende los fundamentos de Machine Learning, procesamiento de lenguaje natural y los marcos legales actuales sobre privacidad de datos. \nHABILIDADES: Capacidad para integrar herramientas de IA generativa en flujos de trabajo profesionales y evaluar sesgos en modelos algorítmicos. \nACTITUDES: Actúa con responsabilidad social, promoviendo la transparencia y la equidad en el desarrollo tecnológico.", "fines_de_aprendizaje_o_formacion": "Al finalizar el curso, el estudiante será capaz de diseñar e implementar soluciones básicas basadas en inteligencia artificial que optimicen procesos organizacionales, asegurando en todo momento el cumplimiento de marcos normativos y principios éticos internacionales para el manejo responsable de datos y algoritmos."}', '[{"temas": ["Ética y legalidad", "PTES/OSSTMM (visión general)", "Alcance y reglas"], "titulo": "Fundamentos y metodología", "unidad": 1}, {"temas": ["OSINT", "Descubrimiento de servicios", "Enumeración"], "titulo": "Reconocimiento y enumeración", "unidad": 2}, {"temas": ["Vulnerabilidades comunes", "Explotación controlada", "Post-explotación básica"], "titulo": "Explotación en laboratorio", "unidad": 3}, {"temas": ["Evidencias", "Severidad", "Recomendaciones"], "titulo": "Reporte y remediación", "unidad": 4}]', 'MANUAL', '{"seed": true}', '11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2026-01-09 13:45:37.784586+00', '2026-01-13 20:41:33.238058+00'); +INSERT INTO "public"."asignaturas" ("id", "plan_estudio_id", "estructura_id", "codigo", "nombre", "tipo", "creditos", "horas_semana", "numero_ciclo", "linea_plan_id", "orden_celda", "datos", "contenido_tematico", "tipo_origen", "meta_origen", "creado_por", "actualizado_por", "creado_en", "actualizado_en") VALUES + ('9d4dda6a-488f-428a-8a07-38081592a641', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '7856e9cc-9ac1-4a31-b93a-0624d4c5e1de', 'CIB-CS-401', 'OffSec (Seguridad Ofensiva)', 'OBLIGATORIA', 10, 6, 5, 'c261fbce-893a-4900-b03c-2f3ad3ce4987', 1, '{"ciclo": "Hola 123", "contenido_tematico": "Enunciar, en idioma español, los temas y subtemas que se abordarán en la asignatura o unidad de aprendizaje; estructurados de manera lógica y secuencial y numerados en orden jerárquico. Éstos deberán ser acordes con la carga horaria asignada, sustentar la denominación de la asignatura y contribuir al logro de los fines de aprendizaje.", "clave_de_la_asignatura": "Anotar la clave que identifica a la asignatura o unidad de aprendizaje; la cual debe coincidir con la señalada en el mapa curricular.", "criterios_de_evaluacion": "Enlistar las actividades o productos que considerarán para valorar los aprendizajes logrados por los estudiantes, señalar el porcentaje para cada uno, de tal manera que sumen el 100%. Éstos deberán ser acordes con las actividades de aprendizaje.", "fines_de_aprendizaje_o_formacion": "Describir los aprendizajes y fines que alcanzará el estudiante al concluir la asignatura. Deberá sustentar la denominación de la asignatura, vincularse con los contenidos temáticos y coadyuvar al logro del fin de aprendizaje y perfil de egreso del plan de estudios.", "modalidades_tecnologicas_e_informaticas": "Describir los recursos tecnológicos y de comunicación que se emplearán para el desarrollo del proceso de enseñanza-aprendizaje de la asignatura. Para las modalidades no escolarizada y mixta, este elemento es obligatorio.", "actividades_de_aprendizaje_independientes": "Señalar las actividades de aprendizaje que desarrollará el estudiante de manera independiente. Éstas deberán ser acordes con la naturaleza de la asignatura y la modalidad educativa.", "denominacion_de_la_asignatura_o_unidad_de_aprendizaje": "Señalar el nombre de la asignatura o unidad de aprendizaje de igual manera que en el mapa curricular.", "actividades_de_aprendizaje_bajo_conduccion_de_un_academico": "Señalar las actividades de aprendizaje que desarrollará el estudiante en las horas-clase. Éstas deberán ser acordes con la naturaleza de la asignatura, modalidad educativa e instalaciones empleadas, además de contribuir al logro de los fines de aprendizaje."}', '[{"temas": ["Ética y legalidad", "PTES/OSSTMM (visión general)", "Alcance y reglas"], "titulo": "Fundamentos y metodología", "unidad": 1}, {"temas": ["OSINT", "Descubrimiento de servicios", "Enumeración"], "titulo": "Reconocimiento y enumeración", "unidad": 2}, {"temas": ["Vulnerabilidades comunes", "Explotación controlada", "Post-explotación básica"], "titulo": "Explotación en laboratorio", "unidad": 3}, {"temas": ["Evidencias", "Severidad", "Recomendaciones"], "titulo": "Reporte y remediación", "unidad": 4}]', 'MANUAL', '{"seed": true}', '11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2026-01-09 13:45:37.784586+00', '2026-01-30 21:38:02.89992+00'); -- @@ -2008,8 +2113,54 @@ INSERT INTO "public"."cambios_asignatura" ("id", "asignatura_id", "cambiado_por" -- Data for Name: cambios_plan; Type: TABLE DATA; Schema: public; Owner: postgres -- -INSERT INTO "public"."cambios_plan" ("id", "plan_estudio_id", "cambiado_por", "cambiado_en", "tipo", "campo", "valor_anterior", "valor_nuevo", "interaccion_ia_id") VALUES - ('9750ab31-f898-4e2e-971d-07e21bea6d50', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-09 13:45:37.784586+00', 'CREACION', 'datos', NULL, '{"nivel": "Licenciatura", "nombre": "Ingeniería Cibernética", "area_de_estudio": "Tecnologías de la Información y la Comunicación (CMPE 2016)", "perfil_de_egreso": "Conocimientos: redes, sistemas, criptografía. Habilidades: pentesting, hardening, respuesta a incidentes. Actitudes: ética, responsabilidad.", "diseno_curricular": "Rígido", "perfil_de_ingreso": "Pensamiento lógico, bases de álgebra, nociones de programación y comunicación escrita.", "curso_propedeutico": null, "modalidad_educativa": "Escolar", "antecedente_academico": "Bachillerato concluido; afinidad con matemáticas y computación.", "carga_horaria_a_la_semana": 40, "programa_de_investigacion": null, "clave_del_plan_de_estudios": "2026-CIB-ING", "duracion_del_ciclo_escolar": "16", "fines_de_aprendizaje_o_formacion": "Formar profesionales capaces de diseñar, implementar y asegurar sistemas ciberfísicos y plataformas digitales.", "total_de_ciclos_del_plan_de_estudios": "8", "sustento_teorico_del_modelo_curricular": "Enfoque por competencias, aprendizaje basado en problemas y laboratorios.", "justificacion_de_la_propuesta_curricular": null, "administracion_y_operatividad_del_plan_de_estudios": "Plan rígido con cargas sugeridas por semestre; asignaturas seriadas por áreas.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "Revisión anual por comité académico; actualización por tendencias y retroalimentación de egresados."}', NULL); +INSERT INTO "public"."cambios_plan" ("id", "plan_estudio_id", "cambiado_por", "cambiado_en", "tipo", "campo", "valor_anterior", "valor_nuevo", "response_id") VALUES + ('9750ab31-f898-4e2e-971d-07e21bea6d50', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-09 13:45:37.784586+00', 'CREACION', 'datos', NULL, '{"nivel": "Licenciatura", "nombre": "Ingeniería Cibernética", "area_de_estudio": "Tecnologías de la Información y la Comunicación (CMPE 2016)", "perfil_de_egreso": "Conocimientos: redes, sistemas, criptografía. Habilidades: pentesting, hardening, respuesta a incidentes. Actitudes: ética, responsabilidad.", "diseno_curricular": "Rígido", "perfil_de_ingreso": "Pensamiento lógico, bases de álgebra, nociones de programación y comunicación escrita.", "curso_propedeutico": null, "modalidad_educativa": "Escolar", "antecedente_academico": "Bachillerato concluido; afinidad con matemáticas y computación.", "carga_horaria_a_la_semana": 40, "programa_de_investigacion": null, "clave_del_plan_de_estudios": "2026-CIB-ING", "duracion_del_ciclo_escolar": "16", "fines_de_aprendizaje_o_formacion": "Formar profesionales capaces de diseñar, implementar y asegurar sistemas ciberfísicos y plataformas digitales.", "total_de_ciclos_del_plan_de_estudios": "8", "sustento_teorico_del_modelo_curricular": "Enfoque por competencias, aprendizaje basado en problemas y laboratorios.", "justificacion_de_la_propuesta_curricular": null, "administracion_y_operatividad_del_plan_de_estudios": "Plan rígido con cargas sugeridas por semestre; asignaturas seriadas por áreas.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "Revisión anual por comité académico; actualización por tendencias y retroalimentación de egresados."}', NULL), + ('565ef79e-4a20-42cd-9aa9-41fa22fd476d', '4660b774-a6e2-44f4-a340-747b41235c7c', NULL, '2026-01-23 22:00:33.119999+00', 'ACTUALIZACION', 'nombre', '"Ingeniería en Sistemas Domóticos (2026)"', '"Ingeniería en Sistemas Domóticos (2027)"', NULL), + ('e37fb4cc-1280-4d71-8d97-23e8d5c3162c', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-23 22:37:05.875968+00', 'ACTUALIZACION', 'nivel', '"Licenciatura"', '"Maestría"', NULL), + ('4e1edec0-8723-403f-a856-6aa22505fc1f', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-23 22:39:07.266896+00', 'ACTUALIZACION', 'conversation_id', NULL, '"123"', NULL), + ('c9a74e54-653d-4e21-a961-98acd3efcd0c', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'perfil_de_ingreso', '"Ingreso esperado para la Licenciatura en Física Médica: antecedentes académicos en bachillerato con fuerte formación en matemáticas y física (álgebra lineal, cálculo diferencial e integral, física general); habilidades de razonamiento lógico y resolución de problemas; aptitudes para trabajo colaborativo y cumplimiento de normas de seguridad y ética; interés en aplicaciones biomédicas y manejo básico de herramientas de laboratorio."', NULL, NULL), + ('82be7c12-5b7b-465e-9018-cea7f11f4213', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'programa_de_investigacion', 'null', NULL, NULL), + ('36a747b7-1105-4f8c-b9f5-0e59c73b1173', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'justificacion_de_la_propuesta_curricular', '"La necesidad de profesionales especializados en física médica que aporten diagnósticos precisos y tratamientos seguros justifica una formación que combine fundamentos teóricos sólidos con prácticas clínicas y laboratoriales. La modalidad mixta facilita la integración de laboratorios, simulaciones y aprendizaje autogestionado, manteniendo flexibilidad espacial y temporal para estudiantes trabajadores. Se incorporan plataformas tecnológicas, simuladores y experiencias clínicas supervisadas para asegurar la calidad educativa y la empleabilidad, alineándose con las metas y lineamientos del Acuerdo 17/11/17 y el marco de la SEP."', NULL, NULL), + ('ea313ce3-898f-40d5-9e6c-288d7f711009', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'perfil_de_egreso', '"Al finalizar el plan, el egresado estará equipado para: 1) Conocer y comprender fundamentos teóricos y prácticos de la física médica aplicados a diagnóstico y tratamiento; 2) Desarrollar habilidades de evaluación y diseño de protocolos clínicos en radiología, medicina nuclear y radioterapia; 3) Aplicar técnicas de dosimetría, calibración de equipos y control de calidad en contextos médicos; 4) Interpretar resultados de imágenes y de pruebas de diagnóstico por imagen con rigor científico; 5) Actuar con ética profesional, seguridad de pacientes y manejo responsable de información clínica; 6) Trabajar de forma colaborativa en equipos interdisciplinarios para la mejora continua de procesos en medios de salud."', NULL, NULL), + ('76a0e4dc-d93d-409b-8ce5-97400efcc51d', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'nivel', '"Licenciatura"', NULL, NULL), + ('6542239b-1d70-447c-a15f-569a9d7178b2', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'propuesta_de_evaluacion_periodica_del_plan_de_estudios', '"La evaluación periódica del Plan de Estudio se realizará de manera anual y cada tres años se revisará en conjunto con comités de docentes, estudiantes y empleadores. Se emplearán rúbricas de competencias, evaluaciones prácticas, portafolios, indicadores de empleabilidad y calidad de servicios, con mecanismos de retroalimentación y acciones correctivas para actualizar contenidos, métodos pedagógicos y recursos tecnológicos. Los resultados guiarán ajustes curriculares y la mejora institucional."', NULL, NULL), + ('e4f4992f-149b-44a3-860d-eccbaf7cbb65', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'carga_horaria_a_la_semana', '16', NULL, NULL), + ('56ae5448-0842-4b4f-97fe-5c29ca8fbd3f', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'area_de_estudio', '"Ciencias Físicas – Física Médica (CMPE 2016: Física)"', NULL, NULL), + ('90154137-ae23-4017-813f-d30c7b90443b', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'curso_propedeutico', '"Curso propedéutico de 4 semanas dirigido a reforzar fundamentos de cálculo, física general y conceptos básicos de biomedicina. Este curso se ofrece a la población con deficientes antecedentes en áreas de ciencias; su implementación se vincula como preparación para los cursos introductorios de la población clínica y técnica."', NULL, NULL), + ('9deff71b-4eaf-463e-be1b-9a8c6a127046', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'administracion_y_operatividad_del_plan_de_estudios', '"La administración del plan estará a cargo de la Dirección Académica y un Comité de Planes y Programas. Existirá un Responsable de Carrera, coordinadores de laboratorio y tutores. Se establecerá un mapa curricular con asignaturas y unidades de aprendizaje, criterios de evaluación y un ciclo de revisión anual para ajustes conforme a necesidades regionales y nacionales. Se dispondrá de un Comité de Acreditación interna para garantizar la congruencia entre plan, laboratorio y clínica."', NULL, NULL), + ('0a6bd6f5-e376-42c2-a72f-e86d7f24701e', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'sustento_teorico_del_modelo_curricular', '"El sustento teórico del plan se apoya en modelos de diseño curricular por competencias, aprendizaje basado en problemas y blended learning, con énfasis en la seguridad, la ética y la calidad en servicios de salud. Se integran marcos de referencia de educación superior y estándares SEP para garantizar pertinencia y rigor científico, así como la interoperabilidad con normativas de dispositivos médicos y protección radiológica."', NULL, NULL), + ('da561ec9-1156-46db-88cf-5d5a9e2f3fe7', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'duracion_del_ciclo_escolar', '"16 semanas por ciclo"', NULL, NULL), + ('f1d9cfc3-12bb-4a9d-8b78-affc1ff793b6', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'total_de_ciclos_del_plan_de_estudios', '"8"', NULL, NULL), + ('13779695-78e1-4f10-8ca2-16ee34b572c1', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'fines_de_aprendizaje_o_formacion', '":[\"Analizar fundamentos de física médica, radiación, dosimetría y protección radiológica para su implementación clínica.\",\"Diseñar protocolos de adquisición de imágenes y dosimetría en entornos clínicos con criterios de calidad.\",\"Aplicar técnicas de imagen médica y radioterapia cumpliendo normativas de protección y seguridad.\",\"Evaluar equipos y procesos de calibración, mantenimiento y control de calidad en laboratorios y unidades clínicas.\",\"Comunicar resultados, hallazgos y recomendaciones técnicas a equipos médicos y pacientes con claridad y ética.\",\"Integrar enfoques de investigación y mejora continua para optimizar resultados en radiología y medicina física.\"]"', NULL, NULL), + ('8b8fb619-54b7-4fa1-bf85-b604bee00472', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'nombre', '"Física Médica"', NULL, NULL), + ('58d5d433-3fdc-455b-ac8b-41dcb94ff52e', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'clave_del_plan_de_estudios', '"2026-FM-01"', NULL, NULL), + ('aebfca6d-f418-4bca-86cd-c15398fb6f6c', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'diseno_curricular', '"Flexible"', NULL, NULL), + ('8409cf58-4551-4282-8f50-e68466bc329e', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'antecedente_academico', '"Bachillerato en Ciencias con énfasis en física y matemáticas; cursos de física general, química, cálculo diferencial e integral; promedio mínimo recomendado de ingreso 7.0; acreditar fundamentos de laboratorio y prácticas básicas cuando aplique."', NULL, NULL), + ('bcd7e8b2-87e8-4f73-a834-9a15cfea62b1', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-27 15:34:16.6225+00', 'ACTUALIZACION_CAMPO', 'modalidad_educativa', '"Mixta"', NULL, NULL), + ('5fd1577f-c430-4744-b541-dfa35b39bc6e', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-29 21:17:13.395925+00', 'ACTUALIZACION_CAMPO', 'ciclo', NULL, '{"description": "agregando campo nuevo con hook"}', NULL), + ('76e826fd-ac58-44f6-a28a-3cd5e2223ce0', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-29 21:19:10.511118+00', 'ACTUALIZACION_CAMPO', 'ciclo', '{"description": "agregando campo nuevo con hook"}', '{"description": "agregando nuevo campo a datos"}', NULL), + ('574ec330-0c3d-4b68-a078-369b6353e583', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-29 21:26:48.623104+00', 'ACTUALIZACION_CAMPO', 'ciclo', '{"description": "agregando nuevo campo a datos"}', '{"title": "Ciclo", "description": "agregando nuevo campo a datos"}', NULL), + ('e1fc6e45-a37f-45c1-9e37-12fc82d4960a', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-29 21:31:03.219219+00', 'ACTUALIZACION_CAMPO', 'ciclo', '{"title": "Ciclo", "description": "agregando nuevo campo a datos"}', '{"title": "Ciclo", "description": "hola"}', NULL), + ('bd739972-6d4e-44b6-a15a-9409adb87a14', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-29 21:40:29.585195+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', NULL, '{"description": "guardar desde hook"}', NULL), + ('507284a0-9789-4058-98f3-d9f0176ca793', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-29 21:55:58.683479+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', '{"description": "guardar desde hook"}', '{"description": "hola a todos"}', NULL), + ('c4262294-c67d-4c0c-816a-4d06d0b118be', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-29 21:57:05.346704+00', 'ACTUALIZACION_CAMPO', 'modalidad_educativa', '"Escolar"', '"Escolar2"', NULL), + ('4f6e8013-1626-414d-85b0-066587f09366', 'ddf2203b-2f55-4a32-9ee6-a99a6ff5947e', NULL, '2026-01-29 21:58:12.447523+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', NULL, '"Agregando nuevos datos con persistencia"', NULL), + ('54c2e912-6845-4aa2-a052-9839c78f44a7', 'f6280a08-226d-480b-ab67-561e4657bfc4', NULL, '2026-01-30 14:33:51.01724+00', 'CREACION', NULL, NULL, '{"id": "f6280a08-226d-480b-ab67-561e4657bfc4", "datos": {}, "nivel": "Licenciatura", "activo": true, "nombre": "Ingeniería Económica y Financiera", "creado_en": "2026-01-30T14:33:50.818+00:00", "plan_hash": "ad41e1fcbf8ed0ba4c898086", "carrera_id": "b56eb209-245d-4258-a6a7-9dcb1d231cb4", "creado_por": null, "tipo_ciclo": "Semestre", "meta_origen": {}, "tipo_origen": "MANUAL", "estructura_id": "69fb2b77-5a95-47e0-bf1f-389d384200e4", "nombre_search": "ingenieria economica y financiera", "numero_ciclos": 9, "actualizado_en": "2026-01-30T14:33:50.818+00:00", "actualizado_por": null, "conversation_id": null, "estado_actual_id": "18f49b67-8077-4371-be6e-2019a3be3562"}', NULL), + ('a037db36-acbf-4243-a297-e819700a61c1', '79fba458-aea4-4ca8-ab76-943ede479f15', NULL, '2026-01-30 14:44:47.489255+00', 'CREACION', NULL, NULL, '{"id": "79fba458-aea4-4ca8-ab76-943ede479f15", "datos": {}, "nivel": "Licenciatura", "activo": true, "nombre": "prueba", "creado_en": "2026-01-30T14:44:48.312+00:00", "plan_hash": "a288feae19ba5ab74aca5fc0", "carrera_id": "b56eb209-245d-4258-a6a7-9dcb1d231cb4", "creado_por": null, "tipo_ciclo": "Semestre", "meta_origen": {}, "tipo_origen": "MANUAL", "estructura_id": "69fb2b77-5a95-47e0-bf1f-389d384200e4", "nombre_search": "prueba", "numero_ciclos": 9, "actualizado_en": "2026-01-30T14:44:48.312+00:00", "actualizado_por": null, "conversation_id": null, "estado_actual_id": "18f49b67-8077-4371-be6e-2019a3be3562"}', NULL), + ('15a2ee61-96fc-42e0-96f3-ab343dcb6538', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 16:59:49.3536+00', 'ACTUALIZACION_CAMPO', 'modalidad_educativa', '"Escolar2"', '"Escolar2222"', NULL), + ('7d4962c5-1ffa-4faf-928d-e96648ecba81', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:03:04.061333+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', '{"description": "hola a todos"}', '{"description": "hola"}', NULL), + ('685bd46c-7302-411f-97dc-601e0d609641', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:09:00.477359+00', 'ACTUALIZACION_CAMPO', 'modalidad_educativa', '"Escolar2222"', '"Probando guardado"', NULL), + ('9030308b-fb15-409b-a969-1fc4d9b4b592', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:09:46.207083+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', '{"description": "hola"}', '{"description": "lkdslkñdsksdñl"}', NULL), + ('18a1a5de-3395-4e9f-aaf3-d2fdb290f207', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:25:23.336995+00', 'ACTUALIZACION_CAMPO', 'ciclo', '{"title": "Ciclo", "description": "hola"}', NULL, NULL), + ('22fc0807-b98c-4664-a49c-f8224b3a6cc3', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:25:23.336995+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', '{"description": "lkdslkñdsksdñl"}', NULL, NULL), + ('b98eb094-7ff5-41a5-98eb-ac85633a0045', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:25:23.336995+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_inst', NULL, '"holis"', NULL), + ('5c423e8d-f3e0-42e9-94a1-71dea398e151', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:26:22.769183+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', NULL, '"holis"', NULL), + ('3096ae88-1522-47d8-bc0e-61a255f37af0', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:26:22.769183+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_inst', '"holis"', NULL, NULL), + ('297687c4-64a3-42fe-8eac-70ae555061e2', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 17:47:00.432194+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', '"holis"', '"Probando persistencia\n"', NULL), + ('761bc69b-0a28-4840-8ac8-ed6b77621454', '6756a45c-e6b7-477b-918d-821ad7efbc58', NULL, '2026-01-30 18:31:10.882192+00', 'CREACION', NULL, NULL, '{"id": "6756a45c-e6b7-477b-918d-821ad7efbc58", "datos": {"vigencia": "2026-2031", "area_de_estudio": "Ingeniería y Tecnología", "perfil_de_egreso": "Conocimientos: fundamentos de mecánica, electrónica, control, robótica, sensores y actuadores; Habilidades: diseño y simulación de sistemas mecatrónicos, integración de hardware y software, gestión de proyectos y comunicación técnica; Actitudes: ética profesional, seguridad, trabajo en equipo, aprendizaje continuo y enfoque a la mejora continua.", "diseno_curricular": "Flexible", "perfil_de_ingreso": "Bachillerato en ciencias, tecnología o equivalente, con base sólida en matemáticas (álgebra, cálculo), física general y química; habilidades básicas de lectura técnica, razonamiento lógico y manejo de herramientas informáticas (al menos conceptos de programación y modelado).", "curso_propedeutico": null, "modalidad_educativa": "Mixta", "antecedente_academico": "Bachillerato en Ciencias y/o Tecnologías con promedio mínimo de 7.0; acceso preferente a carreras de ingeniería y tecnología; cumplimiento de requisitos de ingreso institucional.", "carga_horaria_a_la_semana": 28, "programa_de_investigacion": null, "clave_del_plan_de_estudios": "IM-2026-LIC-MEC", "duracion_del_ciclo_escolar": "16 semanas", "fines_de_aprendizaje_o_formacion": "Analizar problemas de ingeniería desde una visión integral de mecánica, electrónica y control; Diseñar sistemas mecatrónicos que integren hardware y software; Implementar modelos y simulaciones para validación de desempeño; Evaluar alternativas de diseño considerando seguridad, costo y sostenibilidad; Aplicar metodologías de gestión de proyectos para desarrollo de prototipos; Integrar tecnologías de manufactura avanzada y robótica en entornos industriales; Desarrollar habilidades de comunicación técnica y trabajo en equipo en contextos multidisciplinarios.", "nivel_y_nombre_del_plan_de_estudios": "Licenciatura en Ingeniería Mecatrónica", "nombre_autorizado_de_la_institucion": "Universidad La Salle México", "total_de_ciclos_del_plan_de_estudios": "9", "sustento_teorico_del_modelo_curricular": "El modelo curricular se fundamenta en un enfoque de ingeniería multidisciplinaria que integra principios de mecánica, electrónica, control, robótica y manufactura. Se sustenta en un continuum de aprendizaje basado en proyectos, ciencias de la ingeniería y prácticas de laboratorio, con énfasis en competencias profesionales y capacidad de adaptación tecnológica. Se alinea con el registro de planes de estudio vigente y con el marco de la CMPE 2016 para planes de estudio por campos de formación académica.", "justificacion_de_la_propuesta_curricular": "La modalidad Mixta exige flexibilidad para autogestión y aprendizaje autónomo, soportada por plataformas tecnológicas y recursos digitales. La propuesta aprovecha Moodle u otras LMS, laboratorios virtuales y contenidos asincrónicos para complementar la enseñanza presencial, permitiendo continuidad educativa ante variaciones geográficas o laborales de los estudiantes.", "administracion_y_operatividad_del_plan_de_estudios": "La administración del plan está a cargo de la Dirección de Ingeniería y la Coordinación de Planes de Estudio. Se asigna un Coordinador Académico por ciclo y se mantiene un registro central en el sistema de gestión institucional. Se establecen reglas para la aprobación de cambios curriculares, distribución de asignaturas por ciclo y seguimiento de requisitos de egreso. Se definen criterios de admisión, permanencia y titulación, con responsable de cumplimiento institucional.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "Evaluación anual del plan mediante análisis de resultados de aprendizaje, desempeño de proyectos y prácticas; Instrumentos: rúbricas de cursos, guías de laboratorio, evaluaciones de conocimiento y competencias, y cuestionarios de satisfacción. Periodicidad: revisión anual con reporte a la instancia institucional y a SEP cuando corresponda; indicadores de calidad: tasa de retención, satisfacción estudiantil, empleabilidad y ajuste curricular por áreas de conocimiento.", "nombre_y_cargo_de_la_persona_facultada_para_autorizar_el_plan_de_estudios": "Lic. María González Pérez, Directora Académica"}, "nivel": "Licenciatura", "activo": true, "nombre": "Ingeniería Mecatrónica (2026)", "creado_en": "2026-01-30T18:31:10.882192+00:00", "plan_hash": "49aaec4fef4d574887fc6a60", "carrera_id": "089cdeda-d557-4a57-b49f-eb44921dfa3a", "creado_por": null, "tipo_ciclo": "Semestre", "meta_origen": {"iaConfig": {"usarMCP": false, "instruccionesAdicionalesIA": "", "descripcionEnfoqueAcademico": "Genera un plan de estudios sin rodeos."}, "referencias": {"repositoriosIds": [], "archivosReferenciaIds": []}, "generado_por": "ai_generate_plan", "ai_structured": {"model": "gpt-5-nano-2025-08-07", "usage": {"input_tokens": 3565, "total_tokens": 10864, "output_tokens": 7299, "input_tokens_details": {"cached_tokens": 0}, "output_tokens_details": {"reasoning_tokens": 6400}}, "responseId": "resp_07eb32291fa2232900697cf8a8e7f4819dbaab7110202db4bf", "conversationId": null}}, "tipo_origen": "IA", "estructura_id": "69fb2b77-5a95-47e0-bf1f-389d384200e4", "nombre_search": "ingenieria mecatronica (2026)", "numero_ciclos": 9, "actualizado_en": "2026-01-30T18:31:10.882192+00:00", "actualizado_por": null, "conversation_id": null, "estado_actual_id": "18f49b67-8077-4371-be6e-2019a3be3562"}', NULL), + ('41b1fb1c-7a8e-441a-b53c-d6f712bc97a5', '6756a45c-e6b7-477b-918d-821ad7efbc58', NULL, '2026-01-30 18:38:32.633716+00', 'OTRO', 'DELETE', '{"id": "6756a45c-e6b7-477b-918d-821ad7efbc58", "datos": {"vigencia": "2026-2031", "area_de_estudio": "Ingeniería y Tecnología", "perfil_de_egreso": "Conocimientos: fundamentos de mecánica, electrónica, control, robótica, sensores y actuadores; Habilidades: diseño y simulación de sistemas mecatrónicos, integración de hardware y software, gestión de proyectos y comunicación técnica; Actitudes: ética profesional, seguridad, trabajo en equipo, aprendizaje continuo y enfoque a la mejora continua.", "diseno_curricular": "Flexible", "perfil_de_ingreso": "Bachillerato en ciencias, tecnología o equivalente, con base sólida en matemáticas (álgebra, cálculo), física general y química; habilidades básicas de lectura técnica, razonamiento lógico y manejo de herramientas informáticas (al menos conceptos de programación y modelado).", "curso_propedeutico": null, "modalidad_educativa": "Mixta", "antecedente_academico": "Bachillerato en Ciencias y/o Tecnologías con promedio mínimo de 7.0; acceso preferente a carreras de ingeniería y tecnología; cumplimiento de requisitos de ingreso institucional.", "carga_horaria_a_la_semana": 28, "programa_de_investigacion": null, "clave_del_plan_de_estudios": "IM-2026-LIC-MEC", "duracion_del_ciclo_escolar": "16 semanas", "fines_de_aprendizaje_o_formacion": "Analizar problemas de ingeniería desde una visión integral de mecánica, electrónica y control; Diseñar sistemas mecatrónicos que integren hardware y software; Implementar modelos y simulaciones para validación de desempeño; Evaluar alternativas de diseño considerando seguridad, costo y sostenibilidad; Aplicar metodologías de gestión de proyectos para desarrollo de prototipos; Integrar tecnologías de manufactura avanzada y robótica en entornos industriales; Desarrollar habilidades de comunicación técnica y trabajo en equipo en contextos multidisciplinarios.", "nivel_y_nombre_del_plan_de_estudios": "Licenciatura en Ingeniería Mecatrónica", "nombre_autorizado_de_la_institucion": "Universidad La Salle México", "total_de_ciclos_del_plan_de_estudios": "9", "sustento_teorico_del_modelo_curricular": "El modelo curricular se fundamenta en un enfoque de ingeniería multidisciplinaria que integra principios de mecánica, electrónica, control, robótica y manufactura. Se sustenta en un continuum de aprendizaje basado en proyectos, ciencias de la ingeniería y prácticas de laboratorio, con énfasis en competencias profesionales y capacidad de adaptación tecnológica. Se alinea con el registro de planes de estudio vigente y con el marco de la CMPE 2016 para planes de estudio por campos de formación académica.", "justificacion_de_la_propuesta_curricular": "La modalidad Mixta exige flexibilidad para autogestión y aprendizaje autónomo, soportada por plataformas tecnológicas y recursos digitales. La propuesta aprovecha Moodle u otras LMS, laboratorios virtuales y contenidos asincrónicos para complementar la enseñanza presencial, permitiendo continuidad educativa ante variaciones geográficas o laborales de los estudiantes.", "administracion_y_operatividad_del_plan_de_estudios": "La administración del plan está a cargo de la Dirección de Ingeniería y la Coordinación de Planes de Estudio. Se asigna un Coordinador Académico por ciclo y se mantiene un registro central en el sistema de gestión institucional. Se establecen reglas para la aprobación de cambios curriculares, distribución de asignaturas por ciclo y seguimiento de requisitos de egreso. Se definen criterios de admisión, permanencia y titulación, con responsable de cumplimiento institucional.", "propuesta_de_evaluacion_periodica_del_plan_de_estudios": "Evaluación anual del plan mediante análisis de resultados de aprendizaje, desempeño de proyectos y prácticas; Instrumentos: rúbricas de cursos, guías de laboratorio, evaluaciones de conocimiento y competencias, y cuestionarios de satisfacción. Periodicidad: revisión anual con reporte a la instancia institucional y a SEP cuando corresponda; indicadores de calidad: tasa de retención, satisfacción estudiantil, empleabilidad y ajuste curricular por áreas de conocimiento.", "nombre_y_cargo_de_la_persona_facultada_para_autorizar_el_plan_de_estudios": "Lic. María González Pérez, Directora Académica"}, "nivel": "Licenciatura", "activo": true, "nombre": "Ingeniería Mecatrónica (2026)", "creado_en": "2026-01-30T18:31:10.882192+00:00", "plan_hash": "49aaec4fef4d574887fc6a60", "carrera_id": "089cdeda-d557-4a57-b49f-eb44921dfa3a", "creado_por": null, "tipo_ciclo": "Semestre", "meta_origen": {"iaConfig": {"usarMCP": false, "instruccionesAdicionalesIA": "", "descripcionEnfoqueAcademico": "Genera un plan de estudios sin rodeos."}, "referencias": {"repositoriosIds": [], "archivosReferenciaIds": []}, "generado_por": "ai_generate_plan", "ai_structured": {"model": "gpt-5-nano-2025-08-07", "usage": {"input_tokens": 3565, "total_tokens": 10864, "output_tokens": 7299, "input_tokens_details": {"cached_tokens": 0}, "output_tokens_details": {"reasoning_tokens": 6400}}, "responseId": "resp_07eb32291fa2232900697cf8a8e7f4819dbaab7110202db4bf", "conversationId": null}}, "tipo_origen": "IA", "estructura_id": "69fb2b77-5a95-47e0-bf1f-389d384200e4", "nombre_search": "ingenieria mecatronica (2026)", "numero_ciclos": 9, "actualizado_en": "2026-01-30T18:31:10.882192+00:00", "actualizado_por": null, "conversation_id": null, "estado_actual_id": "18f49b67-8077-4371-be6e-2019a3be3562"}', NULL, NULL), + ('39f3fc54-e2bf-4c8e-a886-82f186eda0bd', '0e0aea4d-b8b4-4e75-8279-6224c3ac769f', '11111111-1111-1111-1111-111111111111', '2026-01-30 20:18:43.823019+00', 'ACTUALIZACION_CAMPO', 'nombre_autorizado_de_la_institucion', '"Probando persistencia\n"', '"Agregando informacion persistente"', NULL), + ('84ff1732-b6ca-4fef-be82-ff7f0e0a4ce4', 'c5d80b6d-5d3d-4730-a4d5-8eaec3287910', NULL, '2026-01-30 21:03:51.728513+00', 'CREACION', NULL, NULL, '{"id": "c5d80b6d-5d3d-4730-a4d5-8eaec3287910", "datos": {}, "nivel": "Licenciatura", "activo": true, "nombre": "Derecho", "creado_en": "2026-01-30T21:03:52.036+00:00", "plan_hash": "697b32c2a0a186d1c66c9f6d", "carrera_id": "20593041-d605-4174-8404-4b152041eece", "creado_por": null, "tipo_ciclo": "Semestre", "meta_origen": {}, "tipo_origen": "MANUAL", "estructura_id": "69fb2b77-5a95-47e0-bf1f-389d384200e4", "nombre_search": "derecho", "numero_ciclos": 10, "actualizado_en": "2026-01-30T21:03:52.035+00:00", "actualizado_por": null, "conversation_id": null, "estado_actual_id": "18f49b67-8077-4371-be6e-2019a3be3562"}', NULL); -- @@ -2107,7 +2258,11 @@ INSERT INTO "storage"."buckets" ("id", "name", "owner", "created_at", "updated_a INSERT INTO "storage"."objects" ("id", "bucket_id", "name", "owner", "created_at", "updated_at", "last_accessed_at", "metadata", "version", "owner_id", "user_metadata", "level") VALUES ('c0813220-b62e-4503-be27-2f07cbc09914', 'plantillas', '.emptyFolderPlaceholder', NULL, '2026-01-08 20:03:05.299192+00', '2026-01-08 20:03:05.299192+00', '2026-01-08 20:03:05.299192+00', '{"eTag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "size": 0, "mimetype": "application/octet-stream", "cacheControl": "max-age=3600", "lastModified": "2026-01-08T20:03:05.301Z", "contentLength": 0, "httpStatusCode": 200}', '29cb57d7-e0e2-4ee7-9637-a175084b694a', NULL, '{}', 1), ('923eeeab-dbdb-4de2-82af-2ee31fe5a985', 'ai-storage', 'tmp/95d1f381-7023-4f33-bc54-08ccd08abf41/701a1fdc-9dd6-4654-8c67-0aadcb8eadc2-carta.pdf', NULL, '2026-01-14 21:36:54.932547+00', '2026-01-14 21:36:54.932547+00', '2026-01-14 21:36:54.932547+00', '{"eTag": "\"f99de4473b8a614c53508a981284b605\"", "size": 38600, "mimetype": "application/pdf", "cacheControl": "max-age=3600", "lastModified": "2026-01-14T21:36:55.000Z", "contentLength": 38600, "httpStatusCode": 200}', '5bd1f251-6615-4520-a772-db3f993376a4', NULL, '{}', 3), - ('145dd597-a7d1-4c6c-b446-0aa6b1c2e9d3', 'ai-storage', 'tmp/95d1f381-7023-4f33-bc54-08ccd08abf41/61d07fea-43fc-40d8-a43b-7852ea57d612-carta.pdf', NULL, '2026-01-15 20:15:47.957006+00', '2026-01-15 20:15:47.957006+00', '2026-01-15 20:15:47.957006+00', '{"eTag": "\"f99de4473b8a614c53508a981284b605\"", "size": 38600, "mimetype": "application/pdf", "cacheControl": "max-age=3600", "lastModified": "2026-01-15T20:15:48.000Z", "contentLength": 38600, "httpStatusCode": 200}', '4809478e-7c7f-4dd3-9f1c-a007ca7b5f90', NULL, '{}', 3); + ('145dd597-a7d1-4c6c-b446-0aa6b1c2e9d3', 'ai-storage', 'tmp/95d1f381-7023-4f33-bc54-08ccd08abf41/61d07fea-43fc-40d8-a43b-7852ea57d612-carta.pdf', NULL, '2026-01-15 20:15:47.957006+00', '2026-01-15 20:15:47.957006+00', '2026-01-15 20:15:47.957006+00', '{"eTag": "\"f99de4473b8a614c53508a981284b605\"", "size": 38600, "mimetype": "application/pdf", "cacheControl": "max-age=3600", "lastModified": "2026-01-15T20:15:48.000Z", "contentLength": 38600, "httpStatusCode": 200}', '4809478e-7c7f-4dd3-9f1c-a007ca7b5f90', NULL, '{}', 3), + ('192a4460-8334-4523-b232-f0e6c533d552', 'ai-storage', 'd932ce8a-b58f-4367-899c-22ca0f6974e0-Guia_para_el_sustentante_EGEL_Plus_IMECATRO_octubre_de_2025.pdf', NULL, '2026-01-22 21:43:02.023451+00', '2026-01-22 21:43:02.023451+00', '2026-01-22 21:43:02.023451+00', '{"eTag": "\"4d18550ebc9cd37c35c5bb92d8f29208\"", "size": 2426430, "mimetype": "application/pdf", "cacheControl": "max-age=3600", "lastModified": "2026-01-22T21:43:02.000Z", "contentLength": 2426430, "httpStatusCode": 200}', '31bfa884-2b06-437b-87e5-80c0cd90b804', NULL, '{}', 1), + ('4949d3bb-5063-473e-85d6-aa5fbee74026', 'ai-storage', 'cf64d8d5-ba79-4300-b346-5b566c894c79-Acuerdo_171117.pdf', NULL, '2026-01-22 21:43:02.525822+00', '2026-01-22 21:43:02.525822+00', '2026-01-22 21:43:02.525822+00', '{"eTag": "\"7efd81f7f78678b134ff2bcc4f44bce0\"", "size": 3852210, "mimetype": "application/pdf", "cacheControl": "max-age=3600", "lastModified": "2026-01-22T21:43:03.000Z", "contentLength": 3852210, "httpStatusCode": 200}', 'a714a24a-8cd5-4e04-9920-2598629b6d9f', NULL, '{}', 1), + ('22f2e7ce-cdf8-4325-8c51-91f494cdf01f', 'ai-storage', 'd36b3616-d686-4125-8719-d2160d2b37d0-file-sample_150kB.pdf', NULL, '2026-01-23 17:48:53.729633+00', '2026-01-23 17:48:53.729633+00', '2026-01-23 17:48:53.729633+00', '{"eTag": "\"5afaf79789a776d81ec91ccbdc9fdaba\"", "size": 142786, "mimetype": "application/pdf", "cacheControl": "max-age=3600", "lastModified": "2026-01-23T17:48:54.000Z", "contentLength": 142786, "httpStatusCode": 200}', '4badfeaa-422a-4905-9547-b46b700e8aa8', NULL, '{}', 1), + ('7cd652b2-8e19-4925-959e-785fb0ce29a3', 'ai-storage', 'a9cbe04d-b5ab-4917-846d-4f1e8745c95c-Plan_Genera_un_plan_de_estudios_claro_y_realista__De_fisica_cuantica_relacionada_con_blucles_Institucional.pdf', NULL, '2026-01-23 17:48:53.865584+00', '2026-01-23 17:48:53.865584+00', '2026-01-23 17:48:53.865584+00', '{"eTag": "\"2f33192d3446c4924424de3276b46c1f\"", "size": 40523, "mimetype": "application/pdf", "cacheControl": "max-age=3600", "lastModified": "2026-01-23T17:48:54.000Z", "contentLength": 40523, "httpStatusCode": 200}', '37e5691d-16c4-4392-b316-8aeecfda77e2', NULL, '{}', 1); -- diff --git a/supabase/diffs/sql_para_local_como_remoto.sql b/supabase/diffs/sql_para_local_como_remoto.sql new file mode 100644 index 0000000..4ddfd04 --- /dev/null +++ b/supabase/diffs/sql_para_local_como_remoto.sql @@ -0,0 +1,205 @@ +create extension if not exists "http" with schema "extensions"; + +alter table "public"."asignaturas" drop constraint "asignaturas_facultad_propietaria_id_fkey"; + +alter table "public"."cambios_plan" drop constraint "cambios_plan_plan_estudio_id_fkey"; + +alter type "public"."tipo_cambio" rename to "tipo_cambio__old_version_to_be_dropped"; + +create type "public"."tipo_cambio" as enum ('ACTUALIZACION_CAMPO', 'ACTUALIZACION_MAPA', 'TRANSICION_ESTADO', 'OTRO', 'CREACION', 'ACTUALIZACION'); + +alter table "public"."cambios_asignatura" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio"; + +alter table "public"."cambios_plan" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio"; + +drop type "public"."tipo_cambio__old_version_to_be_dropped"; + +alter table "public"."asignaturas" drop column "facultad_propietaria_id"; + +alter table "public"."asignaturas" add column "asignatura_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored; + +alter table "public"."cambios_plan" drop column "interaccion_ia_id"; + +alter table "public"."cambios_plan" add column "response_id" text; + +alter table "public"."planes_estudio" add column "conversation_id" text; + +alter table "public"."planes_estudio" add column "plan_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored; + +CREATE UNIQUE INDEX planes_estudio_conversation_id_key ON public.planes_estudio USING btree (conversation_id); + +alter table "public"."planes_estudio" add constraint "planes_estudio_conversation_id_key" UNIQUE using index "planes_estudio_conversation_id_key"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.fn_log_cambios_planes_estudio() + RETURNS trigger + LANGUAGE plpgsql +AS $function$declare + k text; + old_val jsonb; + new_val jsonb; + + v_response_id text; +begin + v_response_id := nullif(new.meta_origen->>'response_id',''); + + -- INSERT -> CREACION + if tg_op = 'INSERT' then + insert into public.cambios_plan ( + plan_estudio_id, + cambiado_por, + tipo, + campo, + valor_anterior, + valor_nuevo, + response_id + ) + values ( + new.id, + new.creado_por, + 'CREACION'::public.tipo_cambio, + null, + null, + to_jsonb(new), + null + ); + + return new; + end if; + + -- DELETE (opcional): si no lo quieres, bórralo + if tg_op = 'DELETE' then + insert into public.cambios_plan ( + plan_estudio_id, + cambiado_por, + tipo, + campo, + valor_anterior, + valor_nuevo, + response_id + ) + values ( + old.id, + old.actualizado_por, + 'OTRO'::public.tipo_cambio, + 'DELETE', + to_jsonb(old), + null, + null + ); + + return old; + end if; + + -- UPDATE ---------------------------------------------------------- + -- 1) Transición de estado + if (new.estado_actual_id is distinct from old.estado_actual_id) then + insert into public.cambios_plan ( + plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id + ) + values ( + new.id, + new.actualizado_por, + 'TRANSICION_ESTADO'::public.tipo_cambio, + 'estado_actual_id', + to_jsonb(old.estado_actual_id), + to_jsonb(new.estado_actual_id), + null + ); + end if; + + -- 2) Cambios en JSONB "datos" (diff top-level por llave) + if (new.datos is distinct from old.datos) then + for k in + select distinct key + from ( + select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key + union all + select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key + ) t + loop + old_val := coalesce(old.datos, '{}'::jsonb) -> k; + new_val := coalesce(new.datos, '{}'::jsonb) -> k; + + if (old_val is distinct from new_val) then + insert into public.cambios_plan ( + plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id + ) + values ( + new.id, + new.actualizado_por, + 'ACTUALIZACION_CAMPO'::public.tipo_cambio, + k, + old_val, + new_val, + v_response_id + ); + end if; + end loop; + + end if; + + -- 3) Cambios en columnas "normales" (uno por columna) + if (new.nombre is distinct from old.nombre) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null); + end if; + + if (new.nivel is distinct from old.nivel) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null); + end if; + + if (new.tipo_ciclo is distinct from old.tipo_ciclo) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null); + end if; + + if (new.numero_ciclos is distinct from old.numero_ciclos) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null); + end if; + + if (new.activo is distinct from old.activo) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null); + end if; + + if (new.carrera_id is distinct from old.carrera_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null); + end if; + + if (new.estructura_id is distinct from old.estructura_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null); + end if; + + if (new.tipo_origen is distinct from old.tipo_origen) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null); + end if; + + + if (new.conversation_id is distinct from old.conversation_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'conversation_id', to_jsonb(old.conversation_id), to_jsonb(new.conversation_id), null); + end if; + + -- 🔥 consumirlo para que NO se guarde en planes_estudio + if v_response_id is not null then + new.meta_origen := new.meta_origen - 'response_id'; + end if; + + return new; +end;$function$ +; + +CREATE TRIGGER "agregar-conversation_id-asignaturas" AFTER INSERT ON public.asignaturas FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000'); + +CREATE TRIGGER "agregar-conversation_id-planes_estudio" AFTER INSERT ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000'); + +CREATE TRIGGER trg_planes_estudio_log_cambios AFTER INSERT OR DELETE OR UPDATE ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION public.fn_log_cambios_planes_estudio(); + + diff --git a/supabase/diffs/sql_para_remoto_como_local.sql b/supabase/diffs/sql_para_remoto_como_local.sql deleted file mode 100644 index f17c71e..0000000 --- a/supabase/diffs/sql_para_remoto_como_local.sql +++ /dev/null @@ -1,56 +0,0 @@ -set check_function_bodies = off; - -CREATE OR REPLACE FUNCTION public.set_actualizado_en() - RETURNS trigger - LANGUAGE plpgsql -AS $function$ -begin - new.actualizado_en = now(); - return new; -end; -$function$ -; - -CREATE OR REPLACE FUNCTION public.unaccent_immutable(text) - RETURNS text - LANGUAGE sql - IMMUTABLE PARALLEL SAFE STRICT -AS $function$ - SELECT public.unaccent('public.unaccent', $1); -$function$ -; - -CREATE OR REPLACE FUNCTION public.validar_numero_ciclo_asignatura() - RETURNS trigger - LANGUAGE plpgsql -AS $function$ -declare - v_numero_ciclos int; -begin - if new.numero_ciclo is null then - return new; - end if; - - select pe.numero_ciclos into v_numero_ciclos - from planes_estudio pe - where pe.id = new.plan_estudio_id; - - if v_numero_ciclos is null then - raise exception 'plan_estudio_id inválido %, plan no encontrado', new.plan_estudio_id; - end if; - - if new.numero_ciclo < 1 then - raise exception 'numero_ciclo debe ser >= 1 (recibido %)', new.numero_ciclo; - end if; - - if new.numero_ciclo > v_numero_ciclos then - raise exception 'numero_ciclo % excede planes_estudio.numero_ciclos % para plan_estudio_id %', - new.numero_ciclo, v_numero_ciclos, new.plan_estudio_id; - end if; - - return new; -end; -$function$ -; - - diff --git a/supabase/migrations/20260130215440_cambios_asignatura.sql b/supabase/migrations/20260130215440_cambios_asignatura.sql new file mode 100644 index 0000000..cbf5fee --- /dev/null +++ b/supabase/migrations/20260130215440_cambios_asignatura.sql @@ -0,0 +1,21 @@ +-- alter table "public"."asignaturas" drop constraint "asignaturas_facultad_propietaria_id_fkey"; + +-- alter table "public"."asignaturas" drop constraint "asignaturas_horas_semana_check"; + +-- alter table "public"."asignaturas" drop column "facultad_propietaria_id"; + +alter table "public"."asignaturas" drop column "horas_semana"; + +alter table "public"."asignaturas" add column "horas_academicas" integer; + +alter table "public"."asignaturas" add column "horas_independientes" integer; + +alter table "public"."asignaturas" add constraint "asignaturas_horas_academicas_check" CHECK (((horas_academicas IS NULL) OR (horas_academicas >= 0))) not valid; + +alter table "public"."asignaturas" validate constraint "asignaturas_horas_academicas_check"; + +alter table "public"."asignaturas" add constraint "asignaturas_horas_independientes_check" CHECK (((horas_independientes IS NULL) OR (horas_independientes >= 0))) not valid; + +alter table "public"."asignaturas" validate constraint "asignaturas_horas_independientes_check"; + + diff --git a/supabase/migrations/20260203204213_sincronizacion_con_remoto.sql b/supabase/migrations/20260203204213_sincronizacion_con_remoto.sql new file mode 100644 index 0000000..133a53f --- /dev/null +++ b/supabase/migrations/20260203204213_sincronizacion_con_remoto.sql @@ -0,0 +1,205 @@ +create extension if not exists "http" with schema "extensions"; + +alter table "public"."asignaturas" drop constraint if exists "asignaturas_facultad_propietaria_id_fkey"; + +alter table "public"."cambios_plan" drop constraint "cambios_plan_plan_estudio_id_fkey"; + +alter type "public"."tipo_cambio" rename to "tipo_cambio__old_version_to_be_dropped"; + +create type "public"."tipo_cambio" as enum ('ACTUALIZACION_CAMPO', 'ACTUALIZACION_MAPA', 'TRANSICION_ESTADO', 'OTRO', 'CREACION', 'ACTUALIZACION'); + +alter table "public"."cambios_asignatura" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio"; + +alter table "public"."cambios_plan" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio"; + +drop type "public"."tipo_cambio__old_version_to_be_dropped"; + +alter table "public"."asignaturas" drop column if exists "facultad_propietaria_id"; + +alter table "public"."asignaturas" add column "asignatura_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored; + +alter table "public"."cambios_plan" drop column "interaccion_ia_id"; + +alter table "public"."cambios_plan" add column "response_id" text; + +alter table "public"."planes_estudio" add column "conversation_id" text; + +alter table "public"."planes_estudio" add column "plan_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored; + +CREATE UNIQUE INDEX planes_estudio_conversation_id_key ON public.planes_estudio USING btree (conversation_id); + +alter table "public"."planes_estudio" add constraint "planes_estudio_conversation_id_key" UNIQUE using index "planes_estudio_conversation_id_key"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.fn_log_cambios_planes_estudio() + RETURNS trigger + LANGUAGE plpgsql +AS $function$declare + k text; + old_val jsonb; + new_val jsonb; + + v_response_id text; +begin + v_response_id := nullif(new.meta_origen->>'response_id',''); + + -- INSERT -> CREACION + if tg_op = 'INSERT' then + insert into public.cambios_plan ( + plan_estudio_id, + cambiado_por, + tipo, + campo, + valor_anterior, + valor_nuevo, + response_id + ) + values ( + new.id, + new.creado_por, + 'CREACION'::public.tipo_cambio, + null, + null, + to_jsonb(new), + null + ); + + return new; + end if; + + -- DELETE (opcional): si no lo quieres, bórralo + if tg_op = 'DELETE' then + insert into public.cambios_plan ( + plan_estudio_id, + cambiado_por, + tipo, + campo, + valor_anterior, + valor_nuevo, + response_id + ) + values ( + old.id, + old.actualizado_por, + 'OTRO'::public.tipo_cambio, + 'DELETE', + to_jsonb(old), + null, + null + ); + + return old; + end if; + + -- UPDATE ---------------------------------------------------------- + -- 1) Transición de estado + if (new.estado_actual_id is distinct from old.estado_actual_id) then + insert into public.cambios_plan ( + plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id + ) + values ( + new.id, + new.actualizado_por, + 'TRANSICION_ESTADO'::public.tipo_cambio, + 'estado_actual_id', + to_jsonb(old.estado_actual_id), + to_jsonb(new.estado_actual_id), + null + ); + end if; + + -- 2) Cambios en JSONB "datos" (diff top-level por llave) + if (new.datos is distinct from old.datos) then + for k in + select distinct key + from ( + select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key + union all + select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key + ) t + loop + old_val := coalesce(old.datos, '{}'::jsonb) -> k; + new_val := coalesce(new.datos, '{}'::jsonb) -> k; + + if (old_val is distinct from new_val) then + insert into public.cambios_plan ( + plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id + ) + values ( + new.id, + new.actualizado_por, + 'ACTUALIZACION_CAMPO'::public.tipo_cambio, + k, + old_val, + new_val, + v_response_id + ); + end if; + end loop; + + end if; + + -- 3) Cambios en columnas "normales" (uno por columna) + if (new.nombre is distinct from old.nombre) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null); + end if; + + if (new.nivel is distinct from old.nivel) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null); + end if; + + if (new.tipo_ciclo is distinct from old.tipo_ciclo) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null); + end if; + + if (new.numero_ciclos is distinct from old.numero_ciclos) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null); + end if; + + if (new.activo is distinct from old.activo) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null); + end if; + + if (new.carrera_id is distinct from old.carrera_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null); + end if; + + if (new.estructura_id is distinct from old.estructura_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null); + end if; + + if (new.tipo_origen is distinct from old.tipo_origen) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null); + end if; + + + if (new.conversation_id is distinct from old.conversation_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'conversation_id', to_jsonb(old.conversation_id), to_jsonb(new.conversation_id), null); + end if; + + -- 🔥 consumirlo para que NO se guarde en planes_estudio + if v_response_id is not null then + new.meta_origen := new.meta_origen - 'response_id'; + end if; + + return new; +end;$function$ +; + +CREATE TRIGGER "agregar-conversation_id-asignaturas" AFTER INSERT ON public.asignaturas FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000'); + +CREATE TRIGGER "agregar-conversation_id-planes_estudio" AFTER INSERT ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000'); + +CREATE TRIGGER trg_planes_estudio_log_cambios AFTER INSERT OR DELETE OR UPDATE ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION public.fn_log_cambios_planes_estudio(); + + diff --git a/supabase/scheme_backup.sql b/supabase/scheme_backup.sql index a056869..4006508 100644 --- a/supabase/scheme_backup.sql +++ b/supabase/scheme_backup.sql @@ -30,6 +30,13 @@ COMMENT ON SCHEMA "public" IS 'standard public schema'; +CREATE EXTENSION IF NOT EXISTS "http" WITH SCHEMA "extensions"; + + + + + + CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; @@ -159,7 +166,8 @@ CREATE TYPE "public"."tipo_cambio" AS ENUM ( 'ACTUALIZACION_MAPA', 'TRANSICION_ESTADO', 'OTRO', - 'CREACION' + 'CREACION', + 'ACTUALIZACION' ); @@ -230,6 +238,172 @@ CREATE TYPE "public"."tipo_origen" AS ENUM ( ALTER TYPE "public"."tipo_origen" OWNER TO "postgres"; +CREATE OR REPLACE FUNCTION "public"."fn_log_cambios_planes_estudio"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$declare + k text; + old_val jsonb; + new_val jsonb; + + v_response_id text; +begin + v_response_id := nullif(new.meta_origen->>'response_id',''); + + -- INSERT -> CREACION + if tg_op = 'INSERT' then + insert into public.cambios_plan ( + plan_estudio_id, + cambiado_por, + tipo, + campo, + valor_anterior, + valor_nuevo, + response_id + ) + values ( + new.id, + new.creado_por, + 'CREACION'::public.tipo_cambio, + null, + null, + to_jsonb(new), + null + ); + + return new; + end if; + + -- DELETE (opcional): si no lo quieres, bórralo + if tg_op = 'DELETE' then + insert into public.cambios_plan ( + plan_estudio_id, + cambiado_por, + tipo, + campo, + valor_anterior, + valor_nuevo, + response_id + ) + values ( + old.id, + old.actualizado_por, + 'OTRO'::public.tipo_cambio, + 'DELETE', + to_jsonb(old), + null, + null + ); + + return old; + end if; + + -- UPDATE ---------------------------------------------------------- + -- 1) Transición de estado + if (new.estado_actual_id is distinct from old.estado_actual_id) then + insert into public.cambios_plan ( + plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id + ) + values ( + new.id, + new.actualizado_por, + 'TRANSICION_ESTADO'::public.tipo_cambio, + 'estado_actual_id', + to_jsonb(old.estado_actual_id), + to_jsonb(new.estado_actual_id), + null + ); + end if; + + -- 2) Cambios en JSONB "datos" (diff top-level por llave) + if (new.datos is distinct from old.datos) then + for k in + select distinct key + from ( + select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key + union all + select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key + ) t + loop + old_val := coalesce(old.datos, '{}'::jsonb) -> k; + new_val := coalesce(new.datos, '{}'::jsonb) -> k; + + if (old_val is distinct from new_val) then + insert into public.cambios_plan ( + plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id + ) + values ( + new.id, + new.actualizado_por, + 'ACTUALIZACION_CAMPO'::public.tipo_cambio, + k, + old_val, + new_val, + v_response_id + ); + end if; + end loop; + + end if; + + -- 3) Cambios en columnas "normales" (uno por columna) + if (new.nombre is distinct from old.nombre) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null); + end if; + + if (new.nivel is distinct from old.nivel) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null); + end if; + + if (new.tipo_ciclo is distinct from old.tipo_ciclo) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null); + end if; + + if (new.numero_ciclos is distinct from old.numero_ciclos) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null); + end if; + + if (new.activo is distinct from old.activo) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null); + end if; + + if (new.carrera_id is distinct from old.carrera_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null); + end if; + + if (new.estructura_id is distinct from old.estructura_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null); + end if; + + if (new.tipo_origen is distinct from old.tipo_origen) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null); + end if; + + + if (new.conversation_id is distinct from old.conversation_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'conversation_id', to_jsonb(old.conversation_id), to_jsonb(new.conversation_id), null); + end if; + + -- 🔥 consumirlo para que NO se guarde en planes_estudio + if v_response_id is not null then + new.meta_origen := new.meta_origen - 'response_id'; + end if; + + return new; +end;$$; + + +ALTER FUNCTION "public"."fn_log_cambios_planes_estudio"() OWNER TO "postgres"; + + CREATE OR REPLACE FUNCTION "public"."set_actualizado_en"() RETURNS "trigger" LANGUAGE "plpgsql" AS $$ @@ -246,7 +420,7 @@ ALTER FUNCTION "public"."set_actualizado_en"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."unaccent_immutable"("text") RETURNS "text" LANGUAGE "sql" IMMUTABLE STRICT PARALLEL SAFE AS $_$ - SELECT unaccent('unaccent', $1); + SELECT public.unaccent('public.unaccent', $1); $_$; @@ -313,7 +487,6 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" ( "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, "plan_estudio_id" "uuid" NOT NULL, "estructura_id" "uuid", - "facultad_propietaria_id" "uuid", "codigo" "text", "nombre" "text" NOT NULL, "tipo" "public"."tipo_asignatura" DEFAULT 'OBLIGATORIA'::"public"."tipo_asignatura" NOT NULL, @@ -330,6 +503,7 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" ( "actualizado_por" "uuid", "creado_en" timestamp with time zone DEFAULT "now"() NOT NULL, "actualizado_en" timestamp with time zone DEFAULT "now"() NOT NULL, + "asignatura_hash" "text" GENERATED ALWAYS AS ("encode"(SUBSTRING("extensions"."digest"(("id")::"text", 'sha512'::"text") FROM 1 FOR 12), 'hex'::"text")) STORED, CONSTRAINT "asignaturas_ciclo_chk" CHECK ((("numero_ciclo" IS NULL) OR ("numero_ciclo" > 0))), CONSTRAINT "asignaturas_creditos_check" CHECK (("creditos" >= (0)::numeric)), CONSTRAINT "asignaturas_horas_semana_check" CHECK ((("horas_semana" IS NULL) OR ("horas_semana" >= 0))), @@ -382,13 +556,17 @@ CREATE TABLE IF NOT EXISTS "public"."cambios_plan" ( "campo" "text", "valor_anterior" "jsonb", "valor_nuevo" "jsonb", - "interaccion_ia_id" "uuid" + "response_id" "text" ); ALTER TABLE "public"."cambios_plan" OWNER TO "postgres"; +COMMENT ON COLUMN "public"."cambios_plan"."response_id" IS 'El ID de respuesta de OpenAI'; + + + CREATE TABLE IF NOT EXISTS "public"."carreras" ( "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, "facultad_id" "uuid" NOT NULL, @@ -525,6 +703,8 @@ CREATE TABLE IF NOT EXISTS "public"."planes_estudio" ( "creado_en" timestamp with time zone DEFAULT "now"() NOT NULL, "actualizado_en" timestamp with time zone DEFAULT "now"() NOT NULL, "nombre_search" "text" GENERATED ALWAYS AS ("lower"("public"."unaccent_immutable"("nombre"))) STORED, + "plan_hash" "text" GENERATED ALWAYS AS ("encode"(SUBSTRING("extensions"."digest"(("id")::"text", 'sha512'::"text") FROM 1 FOR 12), 'hex'::"text")) STORED, + "conversation_id" "text", CONSTRAINT "planes_estudio_numero_ciclos_check" CHECK (("numero_ciclos" > 0)) ); @@ -532,6 +712,10 @@ CREATE TABLE IF NOT EXISTS "public"."planes_estudio" ( ALTER TABLE "public"."planes_estudio" OWNER TO "postgres"; +COMMENT ON COLUMN "public"."planes_estudio"."conversation_id" IS 'ID de conversación de OpenAI'; + + + CREATE OR REPLACE VIEW "public"."plantilla_plan" AS SELECT "plan"."id" AS "plan_estudio_id", "struct"."id" AS "estructura_id", @@ -714,6 +898,11 @@ ALTER TABLE ONLY "public"."notificaciones" +ALTER TABLE ONLY "public"."planes_estudio" + ADD CONSTRAINT "planes_estudio_conversation_id_key" UNIQUE ("conversation_id"); + + + ALTER TABLE ONLY "public"."planes_estudio" ADD CONSTRAINT "planes_estudio_pkey" PRIMARY KEY ("id"); @@ -826,6 +1015,10 @@ CREATE OR REPLACE TRIGGER "trg_planes_estudio_actualizado_en" BEFORE UPDATE ON " +CREATE OR REPLACE TRIGGER "trg_planes_estudio_log_cambios" AFTER INSERT OR DELETE OR UPDATE ON "public"."planes_estudio" FOR EACH ROW EXECUTE FUNCTION "public"."fn_log_cambios_planes_estudio"(); + + + CREATE OR REPLACE TRIGGER "trg_usuarios_app_actualizado_en" BEFORE UPDATE ON "public"."usuarios_app" FOR EACH ROW EXECUTE FUNCTION "public"."set_actualizado_en"(); @@ -854,11 +1047,6 @@ ALTER TABLE ONLY "public"."asignaturas" -ALTER TABLE ONLY "public"."asignaturas" - ADD CONSTRAINT "asignaturas_facultad_propietaria_id_fkey" FOREIGN KEY ("facultad_propietaria_id") REFERENCES "public"."facultades"("id") ON DELETE SET NULL; - - - ALTER TABLE ONLY "public"."asignaturas" ADD CONSTRAINT "asignaturas_linea_plan_fk_compuesta" FOREIGN KEY ("linea_plan_id", "plan_estudio_id") REFERENCES "public"."lineas_plan"("id", "plan_estudio_id") ON DELETE SET NULL; @@ -894,11 +1082,6 @@ ALTER TABLE ONLY "public"."cambios_plan" -ALTER TABLE ONLY "public"."cambios_plan" - ADD CONSTRAINT "cambios_plan_plan_estudio_id_fkey" FOREIGN KEY ("plan_estudio_id") REFERENCES "public"."planes_estudio"("id") ON DELETE CASCADE; - - - ALTER TABLE ONLY "public"."carreras" ADD CONSTRAINT "carreras_facultad_id_fkey" FOREIGN KEY ("facultad_id") REFERENCES "public"."facultades"("id") ON DELETE RESTRICT; @@ -1220,6 +1403,69 @@ GRANT USAGE ON SCHEMA "public" TO "service_role"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "anon"; +GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "authenticated"; +GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "service_role"; + + + GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "anon"; GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "authenticated"; GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "service_role"; diff --git a/supabase/snippets/Untitled query 238.sql b/supabase/snippets/Untitled query 238.sql new file mode 100644 index 0000000..133a53f --- /dev/null +++ b/supabase/snippets/Untitled query 238.sql @@ -0,0 +1,205 @@ +create extension if not exists "http" with schema "extensions"; + +alter table "public"."asignaturas" drop constraint if exists "asignaturas_facultad_propietaria_id_fkey"; + +alter table "public"."cambios_plan" drop constraint "cambios_plan_plan_estudio_id_fkey"; + +alter type "public"."tipo_cambio" rename to "tipo_cambio__old_version_to_be_dropped"; + +create type "public"."tipo_cambio" as enum ('ACTUALIZACION_CAMPO', 'ACTUALIZACION_MAPA', 'TRANSICION_ESTADO', 'OTRO', 'CREACION', 'ACTUALIZACION'); + +alter table "public"."cambios_asignatura" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio"; + +alter table "public"."cambios_plan" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio"; + +drop type "public"."tipo_cambio__old_version_to_be_dropped"; + +alter table "public"."asignaturas" drop column if exists "facultad_propietaria_id"; + +alter table "public"."asignaturas" add column "asignatura_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored; + +alter table "public"."cambios_plan" drop column "interaccion_ia_id"; + +alter table "public"."cambios_plan" add column "response_id" text; + +alter table "public"."planes_estudio" add column "conversation_id" text; + +alter table "public"."planes_estudio" add column "plan_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored; + +CREATE UNIQUE INDEX planes_estudio_conversation_id_key ON public.planes_estudio USING btree (conversation_id); + +alter table "public"."planes_estudio" add constraint "planes_estudio_conversation_id_key" UNIQUE using index "planes_estudio_conversation_id_key"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.fn_log_cambios_planes_estudio() + RETURNS trigger + LANGUAGE plpgsql +AS $function$declare + k text; + old_val jsonb; + new_val jsonb; + + v_response_id text; +begin + v_response_id := nullif(new.meta_origen->>'response_id',''); + + -- INSERT -> CREACION + if tg_op = 'INSERT' then + insert into public.cambios_plan ( + plan_estudio_id, + cambiado_por, + tipo, + campo, + valor_anterior, + valor_nuevo, + response_id + ) + values ( + new.id, + new.creado_por, + 'CREACION'::public.tipo_cambio, + null, + null, + to_jsonb(new), + null + ); + + return new; + end if; + + -- DELETE (opcional): si no lo quieres, bórralo + if tg_op = 'DELETE' then + insert into public.cambios_plan ( + plan_estudio_id, + cambiado_por, + tipo, + campo, + valor_anterior, + valor_nuevo, + response_id + ) + values ( + old.id, + old.actualizado_por, + 'OTRO'::public.tipo_cambio, + 'DELETE', + to_jsonb(old), + null, + null + ); + + return old; + end if; + + -- UPDATE ---------------------------------------------------------- + -- 1) Transición de estado + if (new.estado_actual_id is distinct from old.estado_actual_id) then + insert into public.cambios_plan ( + plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id + ) + values ( + new.id, + new.actualizado_por, + 'TRANSICION_ESTADO'::public.tipo_cambio, + 'estado_actual_id', + to_jsonb(old.estado_actual_id), + to_jsonb(new.estado_actual_id), + null + ); + end if; + + -- 2) Cambios en JSONB "datos" (diff top-level por llave) + if (new.datos is distinct from old.datos) then + for k in + select distinct key + from ( + select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key + union all + select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key + ) t + loop + old_val := coalesce(old.datos, '{}'::jsonb) -> k; + new_val := coalesce(new.datos, '{}'::jsonb) -> k; + + if (old_val is distinct from new_val) then + insert into public.cambios_plan ( + plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id + ) + values ( + new.id, + new.actualizado_por, + 'ACTUALIZACION_CAMPO'::public.tipo_cambio, + k, + old_val, + new_val, + v_response_id + ); + end if; + end loop; + + end if; + + -- 3) Cambios en columnas "normales" (uno por columna) + if (new.nombre is distinct from old.nombre) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null); + end if; + + if (new.nivel is distinct from old.nivel) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null); + end if; + + if (new.tipo_ciclo is distinct from old.tipo_ciclo) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null); + end if; + + if (new.numero_ciclos is distinct from old.numero_ciclos) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null); + end if; + + if (new.activo is distinct from old.activo) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null); + end if; + + if (new.carrera_id is distinct from old.carrera_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null); + end if; + + if (new.estructura_id is distinct from old.estructura_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null); + end if; + + if (new.tipo_origen is distinct from old.tipo_origen) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null); + end if; + + + if (new.conversation_id is distinct from old.conversation_id) then + insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(), + 'ACTUALIZACION'::public.tipo_cambio, 'conversation_id', to_jsonb(old.conversation_id), to_jsonb(new.conversation_id), null); + end if; + + -- 🔥 consumirlo para que NO se guarde en planes_estudio + if v_response_id is not null then + new.meta_origen := new.meta_origen - 'response_id'; + end if; + + return new; +end;$function$ +; + +CREATE TRIGGER "agregar-conversation_id-asignaturas" AFTER INSERT ON public.asignaturas FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000'); + +CREATE TRIGGER "agregar-conversation_id-planes_estudio" AFTER INSERT ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000'); + +CREATE TRIGGER trg_planes_estudio_log_cambios AFTER INSERT OR DELETE OR UPDATE ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION public.fn_log_cambios_planes_estudio(); + + diff --git a/supabase/snippets/inserts a facultades y a carreras.sql b/supabase/snippets/inserts a facultades y a carreras.sql new file mode 100644 index 0000000..71c6556 --- /dev/null +++ b/supabase/snippets/inserts a facultades y a carreras.sql @@ -0,0 +1,128 @@ +INSERT INTO public.facultades (nombre, nombre_corto, color, icono) VALUES +--('Otros', 'OTRO', '#64748B', 'MoreHorizontal'), +('Coordinación de Desarrollo Humano Profesional', 'CDHP', '#F472B6', 'HeartHandshake'), +('Centro de Idiomas', 'CI', '#2DD4BF', 'Languages'), +--('Campus Virtual La Salle', 'CVLS', '#8B5CF6', 'Monitor'), +('Escuela de Altos Estudios en Salud', 'EAES', '#F43F5E', 'Stethoscope'), +--('Escuela Preparatoria Unidad Santa Teresa', 'EPST', '#EAB308', 'Backpack'), +--('Escuela Preparatoria Unidad Condesa', 'EPUC', '#F59E0B', 'BookOpen'), +('Facultad Mexicana de Arquitectura, Diseño y Comunicación', 'FAMADYC', '#EC4899', 'DraftingCompass'), +--('Facultad de Negocios', 'NEG', '#3B82F6', 'Briefcase'), +('Facultad de Derecho', 'DER', '#64748B', 'Scale'), +('Facultad de Humanidades y Ciencias Sociales', 'HUM', '#6366F1', 'Users'), +--('Facultad de Ingeniería', 'ING', '#F97316', 'Cpu'), +('Facultad Mexicana de Medicina', 'MED', '#10B981', 'HeartPulse'), +('Facultad de Ciencias Químicas', 'FCQ', '#84CC16', 'FlaskConical'), +('Escuela de Ciencias Religiosas', 'ECR', '#0EA5E9', 'BookHeart'), +('Escuela de Ciencias de la Educación', 'EDU', '#FB7185', 'GraduationCap'); +--('Dirección Campus Virtual', 'DCV', '#A855F7', 'Globe'); + +INSERT INTO public.carreras (facultad_id, nombre, nombre_corto) VALUES +-- Coordinación de Desarrollo Humano Profesional +((SELECT id FROM public.facultades WHERE nombre = 'Coordinación de Desarrollo Humano Profesional' LIMIT 1), 'Área Curricular Común 2013', 'ACC'), +((SELECT id FROM public.facultades WHERE nombre = 'Coordinación de Desarrollo Humano Profesional' LIMIT 1), 'Área Curricular Común 2021', 'ACC'), + +-- Centro de Idiomas +((SELECT id FROM public.facultades WHERE nombre = 'Centro de Idiomas' LIMIT 1), 'Área Curricular Común (Centro de Idiomas) 2013', 'ACCI'), +((SELECT id FROM public.facultades WHERE nombre = 'Centro de Idiomas' LIMIT 1), 'Área Curricular Común (Centro de Idiomas) 2021', 'ACCI'), + +-- Escuela de Altos Estudios en Salud +((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Enfermería', 'LENF'), +((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Fisioterapia y Promoción para la Salud', 'LFPS'), +((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Ciencias en el Deporte', 'LCD'), +((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Medicina Dental', 'LMD'), +((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Maestría en Nutrición Clínica', 'MNC'), +((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Fisioterapia', 'LFIS'), + +-- Facultad Mexicana de Arquitectura, Diseño y Comunicación +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Arquitectura', 'ARQ'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Ciencias de la Comunicación', 'LCC'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Diseño Gráfico y Digital', 'LDGD'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Diseño de Ambientes Interiores y Exteriores', 'LDAI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Especialidad en Gestión y Administración de Proyectos', 'EGAP'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Maestría en Estrategia e Innovación en Marcas', 'MEIM'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Maestría en Dirección Estratégica de Comunicación', 'MDEC'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Comunicación', 'LCOM'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Diseño de Productos', 'LDP'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Especialidad en Dirección de Proyectos', 'EDP'), + +-- Facultad de Derecho +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Licenciatura en Derecho', 'LDER'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Licenciatura en Relaciones Internacionales', 'LRI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Derecho Financiero', 'MDF'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Especialidad en Derecho de Empresa', 'EDE'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Gobernanza y Estrategia Internacional', 'MGEI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Doctorado en Ciencias Jurídicas', 'DCJ'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Derecho Civil', 'MDC'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Derecho de Empresa', 'MDE'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Justicia Penal', 'MJP'), + +-- Facultad de Humanidades y Ciencias Sociales +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Educación Preescolar', 'LEP'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Ciencias Religiosas', 'LCR'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Filosofía', 'LFIL'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Maestría en Filosofía Social', 'MFS'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Área de Gestión Educativa', 'AGE'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Área de Intervención Docente', 'AID'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Especialidad en Gestión de los Aprendizajes', 'EGA'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Doctorado en Educación', 'DOCE'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Psicología', 'LPS'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Educación Primaria', 'LEPR'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Ciencias de la Educación', 'LCE'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Pedagogía', 'LPED'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Teología', 'LTEO'), + +-- Facultad Mexicana de Medicina +((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Medicina' LIMIT 1), 'Licenciatura en Médico Cirujano', 'LMC'), + +-- Facultad de Ciencias Químicas +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Licenciatura en Ingeniería Química', 'LIQ'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Licenciatura en Químico Farmacéutico Biólogo', 'QFB'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Licenciatura en Química de Alimentos', 'LQA'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Licenciatura en Ingeniería Ambiental', 'LIA'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Maestría en Ciencia de los Alimentos y Nutrición Humana', 'MCAN'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Maestría en Farmacología Clínica', 'MFC'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Maestría en Calidad y Estadística Aplicada', 'MCEA'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Maestría en Ingeniería de Proyectos', 'MIP'), + +-- Facultad de Negocios +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Contaduría y Finanzas', 'LCF'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Mercadotecnia', 'LMER'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Comercio y Negocios Internacionales', 'LCNI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Administración', 'LADM'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Tecnologías de Información', 'LTI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Dirección de Organizaciones de la Salud', 'MDOS'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Actuaría', 'LACT'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Especialidad en Mercadotecnia y Publicidad', 'EMP'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Especialidad en Finanzas Corporativas y Bursátiles', 'EFCB'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Especialidad en Logística y Cadena de Suministro', 'ELCS'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Especialidad en Administración de Organizaciones de la Salud', 'EAOS'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Ciencias Actuariales', 'MCA'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Doctorado en Administración', 'DADM'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Administración de Organizaciones de la Salud', 'MAOS'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Administración de Negocios Internacionales', 'MANI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Tecnologías de Información en la Dirección de Negocios', 'MTID'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Administración', 'MADM'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Ingeniería Económica y Financiera', 'MIEF'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Administración de Negocios Internacionales (MIEX)', 'MIEX'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Gestión Estratégica del Capital Humano', 'MGEC'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Administración e Innovación de Negocios', 'LAIN'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Ingeniería Económica y Financiera', 'LIEF'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Transformación Digital para los Negocios', 'LTDN'), + +-- Facultad de Ingeniería +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Electrónica', 'LIE'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Civil', 'LIC'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Mecánica y en Sistemas Energéticos', 'LIME'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Electrónica para Sistemas Inteligentes', 'LIES'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Mecatrónica', 'LIM'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Industrial', 'LII'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Cibernética y Sistemas Computacionales', 'LICS'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Biomédica', 'LIB'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Maestría en Gerencia de Proyectos Inmobiliarios', 'MGPI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Maestría en Dirección Industrial', 'MDI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Especialidad en Dirección Industrial', 'EDI'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Maestría en Gestión de Proyectos y de Empresas Constructoras', 'MGPC'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Especialidad en Energías Renovables', 'EER'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Maestría en Ciberseguridad', 'MCIB'), +((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Especialidad en Inteligencia de Datos', 'EID'); \ No newline at end of file -- 2.52.0 From 7660d2c3a0876fa99f7bf1cff8a9b03e55faf0af Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 4 Feb 2026 16:35:28 -0600 Subject: [PATCH 2/5] =?UTF-8?q?inicio=20de=20funcion=20de=20generaci=C3=B3?= =?UTF-8?q?n=20de=20asignatura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/config.toml | 11 ++++++ supabase/functions/_shared/openai-service.ts | 36 +++++++------------ supabase/functions/ai-generate-subject/.npmrc | 3 ++ .../functions/ai-generate-subject/deno.json | 3 ++ .../functions/ai-generate-subject/index.ts | 25 +++++++++++++ 5 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 supabase/functions/ai-generate-subject/.npmrc create mode 100644 supabase/functions/ai-generate-subject/deno.json create mode 100644 supabase/functions/ai-generate-subject/index.ts diff --git a/supabase/config.toml b/supabase/config.toml index d81e000..5f119d7 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -26,3 +26,14 @@ entrypoint = "./functions/create-chat-conversation/index.ts" # Specifies static files to be bundled with the function. Supports glob patterns. # For example, if you want to serve static HTML pages in your function: # static_files = [ "./functions/create-chat-conversation/*.html" ] + +[functions.ai-generate-subject] +enabled = true +verify_jwt = true +import_map = "./functions/ai-generate-subject/deno.json" +# Uncomment to specify a custom file path to the entrypoint. +# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx +entrypoint = "./functions/ai-generate-subject/index.ts" +# Specifies static files to be bundled with the function. Supports glob patterns. +# For example, if you want to serve static HTML pages in your function: +# static_files = [ "./functions/ai-generate-subject/*.html" ] diff --git a/supabase/functions/_shared/openai-service.ts b/supabase/functions/_shared/openai-service.ts index afa0cd4..a0a0258 100644 --- a/supabase/functions/_shared/openai-service.ts +++ b/supabase/functions/_shared/openai-service.ts @@ -3,11 +3,9 @@ import OpenAI from "npm:openai@6.16.0"; import type * as OpenAITypes from "npm:openai@6.16.0"; import { createClient, type SupabaseClient } from "npm:@supabase/supabase-js@2"; - // Use non-streaming params to ensure `responses.create` returns a typed Response export type StructuredResponseOptions = OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming; - export type StructuredResponseSuccess = { ok: true; output?: TOutput; // parsed JSON when available @@ -22,7 +20,6 @@ export type StructuredResponseSuccess = { }; openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers }; - export type StructuredResponseFailure = { ok: false; code: @@ -33,23 +30,19 @@ export type StructuredResponseFailure = { message: string; cause?: unknown; }; - export type StructuredResponseResult = | StructuredResponseSuccess | StructuredResponseFailure; - export interface OpenAIServiceConfig { supabaseUrl: string; serviceRoleKey: string; openAIApiKey: string; bucket?: string; // default: ai-storage } - export class OpenAIService { private readonly supabase: SupabaseClient; private readonly openai: OpenAI; private readonly bucket: string; - private constructor( client: SupabaseClient, openai: OpenAI, @@ -59,13 +52,11 @@ export class OpenAIService { this.openai = openai; this.bucket = bucket; } - static fromEnv(): StructuredResponseFailure | OpenAIService { const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? ""; const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; const openAIApiKey = Deno.env.get("OPENAI_API_KEY") ?? ""; const bucket = Deno.env.get("SUPABASE_BUCKET") ?? "ai-storage"; - if (!supabaseUrl || !serviceRoleKey || !openAIApiKey) { return { ok: false, @@ -74,12 +65,16 @@ export class OpenAIService { "Required env vars missing: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY", }; } - const client = createClient(supabaseUrl, serviceRoleKey); const openai = new OpenAI({ apiKey: openAIApiKey }); return new OpenAIService(client, openai, bucket); } - + async createConversation(metadata?: Record) { + const conversation = await this.openai.conversations.create({ + metadata, + }); + return conversation; + } async createStructuredResponse( options: StructuredResponseOptions, files?: File[], @@ -91,7 +86,6 @@ export class OpenAIService { const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []); const newOptions = { ...options }; - // Attach file references to the request as an extra user message if (openaiFileIds.length > 0) { const fileParts: @@ -100,7 +94,6 @@ export class OpenAIService { type: "input_file", file_id: id, })); - const arr = Array.isArray(options.input) ? options.input : []; arr.push({ role: "user", @@ -119,7 +112,6 @@ export class OpenAIService { const openaiRaw = (await this.openai.responses.create( newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming, )) as OpenAITypes.OpenAI.Responses.Response; - const { model, id: responseId } = openaiRaw; const usage = openaiRaw?.usage ?? null; const conversationId = ( @@ -127,11 +119,9 @@ export class OpenAIService { conversation_id?: string | null; } ).conversation_id ?? null; - // Try to read structured JSON output let output: TOutput | undefined = undefined; let outputText: string | undefined = undefined; - // Prefer `output_text` if present (SDK convenience) const maybeOutputText = openaiRaw.output_text; if ( @@ -141,7 +131,9 @@ export class OpenAIService { outputText = maybeOutputText; try { output = JSON.parse(maybeOutputText) as TOutput; - } catch { /* non-JSON text, keep as text only */ } + } catch { + /* non-JSON text, keep as text only */ + } } else { // Fallback: attempt to serialize `openaiRaw.output` into text const maybeOutput = openaiRaw.output as unknown; @@ -149,10 +141,11 @@ export class OpenAIService { try { outputText = JSON.stringify(maybeOutput); output = maybeOutput as TOutput; - } catch { /* ignore */ } + } catch { + /* ignore */ + } } } - return { ok: true, output, @@ -172,11 +165,9 @@ export class OpenAIService { : message.includes("OpenAI file upload failed") ? "OpenAIFileUploadFailed" : "OpenAIRequestFailed"; - return { ok: false, code, message, cause: err }; } } - private async uploadFilesToStorage(files: File[]): Promise { const paths: string[] = []; for (const file of files) { @@ -188,7 +179,6 @@ export class OpenAIService { contentType: file.type || "application/octet-stream", upsert: false, }); - if (error) { throw new Error(`Supabase upload failed: ${error.message}`); } @@ -196,7 +186,6 @@ export class OpenAIService { } return paths; } - private async uploadFilesToOpenAI(files: File[]): Promise { const ids: string[] = []; for (const file of files) { @@ -216,7 +205,6 @@ export class OpenAIService { } return ids; } - private sanitizeFilename(name: string): string { return name .normalize("NFD") diff --git a/supabase/functions/ai-generate-subject/.npmrc b/supabase/functions/ai-generate-subject/.npmrc new file mode 100644 index 0000000..48c6388 --- /dev/null +++ b/supabase/functions/ai-generate-subject/.npmrc @@ -0,0 +1,3 @@ +# Configuration for private npm package dependencies +# For more information on using private registries with Edge Functions, see: +# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries diff --git a/supabase/functions/ai-generate-subject/deno.json b/supabase/functions/ai-generate-subject/deno.json new file mode 100644 index 0000000..f6ca845 --- /dev/null +++ b/supabase/functions/ai-generate-subject/deno.json @@ -0,0 +1,3 @@ +{ + "imports": {} +} diff --git a/supabase/functions/ai-generate-subject/index.ts b/supabase/functions/ai-generate-subject/index.ts new file mode 100644 index 0000000..8cb545f --- /dev/null +++ b/supabase/functions/ai-generate-subject/index.ts @@ -0,0 +1,25 @@ +import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +import { corsHeaders } from "../_shared/cors.ts"; + +console.log("Hello from Functions!"); + +Deno.serve(async (req) => { + const url = new URL(req.url); + const functionName = url.pathname.split("/").pop(); + console.log( + `[${new Date().toISOString()}][${functionName}]: Request received`, + ); + + if (req.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders }); + } + const { name } = await req.json(); + const data = { + message: `Hello ${name}!`, + }; + + return new Response( + JSON.stringify(data), + { headers: { "Content-Type": "application/json" } }, + ); +}); -- 2.52.0 From 42a437afa31a6f676a2df0c74daa867c9778e8c7 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 5 Feb 2026 12:00:31 -0600 Subject: [PATCH 3/5] fix(functions): clasificar y estandarizar errores en ai-generate-plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Corregido el problema donde la función devolvía siempre 400; ahora se lanzan HttpError con status adecuados (405, 401, 415, 422, 404, 409, 502, 500) según el caso. - Refactor: usar sendError/sendSuccess y HttpError desde _shared/utils.ts para respuestas consistentes y logging seguro. - Validación: agrupar errores de parse/validación en un único mensaje numerado (cada error en nueva línea). - OpenAI: manejo explícito de fallos (MissingEnv, JSON inválido, salida estructurada ausente) y mapeo a status apropiados. - Supabase: distinguir errores 404/409/500 en consultas e inserts; incluir detalles en internalDetails para diagnóstico server-side. - Eliminada dependencia de invocación HTTP a ai-structured; usar servicio compartido directo. - Ajustes menores en mensajes y configuración (deno imports/deno.json) y tests marcados como ignorados donde corresponde. --- diff.txt | Bin 278366 -> 26264 bytes supabase/functions/_shared/utils.ts | 49 ++++ supabase/functions/ai-generate-plan/deno.json | 5 +- supabase/functions/ai-generate-plan/index.ts | 225 +++++++++++++----- .../functions/tests/ai-structured-test.ts | 3 - 5 files changed, 217 insertions(+), 65 deletions(-) create mode 100644 supabase/functions/_shared/utils.ts diff --git a/diff.txt b/diff.txt index 864c3967c9e919e37830ebc4d1de6b34861fbacc..b2704fc12fbf93bc76dd1ff384e32a1099a0a7ff 100644 GIT binary patch literal 26264 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 literal 278366 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 h*&gGrUAhtFe>7O1-!s^c@o#Gu$8h`Y%(data: T, status = 200): Response { + return new Response( + JSON.stringify(data), + { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); +} + +// --- HELPER DE ERROR --- +export function sendError( + status: number, + message: string, + code: string, +): Response { + return new Response( + JSON.stringify({ + error: { message, code }, + }), + { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); +} diff --git a/supabase/functions/ai-generate-plan/deno.json b/supabase/functions/ai-generate-plan/deno.json index f5db280..8b204ca 100644 --- a/supabase/functions/ai-generate-plan/deno.json +++ b/supabase/functions/ai-generate-plan/deno.json @@ -1,5 +1,8 @@ { "imports": { - "zod": "https://deno.land/x/zod@v3.22.4/mod.ts" + "zod": "https://deno.land/x/zod@v3.22.4/mod.ts", + "openai": "npm:openai@6.16.0", + "@supabase/supabase-js": "npm:@supabase/supabase-js@2", + "@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts" } } diff --git a/supabase/functions/ai-generate-plan/index.ts b/supabase/functions/ai-generate-plan/index.ts index e3ff851..374a5f3 100644 --- a/supabase/functions/ai-generate-plan/index.ts +++ b/supabase/functions/ai-generate-plan/index.ts @@ -5,6 +5,7 @@ // Setup type definitions for built-in Supabase Runtime APIs import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { corsHeaders } from "../_shared/cors.ts"; +import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts"; import { createClient } from "npm:@supabase/supabase-js@2"; import type { Database, Json } from "../_shared/database.types.ts"; import type { AIGeneratePlanInput } from "./types.ts"; @@ -18,7 +19,7 @@ type NivelType = type TipoCicloType = Database["public"]["Tables"]["planes_estudio"]["Insert"]["tipo_ciclo"]; -Deno.serve(async (req) => { +Deno.serve(async (req: Request): Promise => { const url = new URL(req.url); const functionName = url.pathname.split("/").pop(); console.log( @@ -37,12 +38,11 @@ Deno.serve(async (req) => { new Date().toISOString() }][${functionName}]: Invalid method: ${method}`, ); - return new Response( - JSON.stringify({ error: "Method not allowed" }), - { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - status: 405, - }, + throw new HttpError( + 405, + "Método no permitido.", + "METHOD_NOT_ALLOWED", + { method }, ); } @@ -54,12 +54,11 @@ Deno.serve(async (req) => { new Date().toISOString() }][${functionName}]: Missing Authorization header`, ); - return new Response( - JSON.stringify({ error: "Authorization header missing" }), - { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - status: 401, - }, + throw new HttpError( + 401, + "No autorizado.", + "UNAUTHORIZED", + { reason: "missing_authorization_header" }, ); } @@ -70,19 +69,28 @@ Deno.serve(async (req) => { new Date().toISOString() }][${functionName}]: Unsupported content type: ${contentType}`, ); - return new Response( - JSON.stringify({ error: "Unsupported content type" }), - { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - status: 400, - }, + throw new HttpError( + 415, + "Content-Type no soportado.", + "UNSUPPORTED_MEDIA_TYPE", + { contentType }, ); } const SUPABASE_URL = Deno.env.get("SUPABASE_URL"); const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY"); if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { - throw new Error("Supabase environment variables are not set"); + throw new HttpError( + 500, + "Configuración del servidor incompleta.", + "MISSING_ENV", + { + missing: [ + !SUPABASE_URL ? "SUPABASE_URL" : null, + !SUPABASE_ANON_KEY ? "SUPABASE_ANON_KEY" : null, + ].filter(Boolean), + }, + ); } // If needed for RLS-protected reads, create an anon client with user's JWT @@ -97,7 +105,12 @@ Deno.serve(async (req) => { const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (!SERVICE_ROLE_KEY) { - throw new Error("SUPABASE_SERVICE_ROLE_KEY is not set"); + throw new HttpError( + 500, + "Configuración del servidor incompleta.", + "MISSING_ENV", + { missing: ["SUPABASE_SERVICE_ROLE_KEY"] }, + ); } const supabaseService = createClient( @@ -112,12 +125,15 @@ Deno.serve(async (req) => { `[${new Date().toISOString()}][${functionName}]: Validation errors:`, validation.errors, ); - return new Response( - JSON.stringify({ errors: validation.errors }), - { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - status: 400, - }, + const message = validation.errors + .map((e, i) => `(${i + 1}) ${e}`) + .join("\n"); + + throw new HttpError( + 422, + message, + "VALIDATION_ERROR", + { errors: validation.errors }, ); } @@ -130,8 +146,23 @@ Deno.serve(async (req) => { .eq("id", payload.datosBasicos.estructuraPlanId) .single(); if (estructuraPlanError) { - throw new Error( - "Error fetching estructuraPlan: " + estructuraPlanError.message, + const maybeCode = (estructuraPlanError as { code?: string }).code; + if (maybeCode === "PGRST116") { + throw new HttpError( + 404, + "No se encontró la estructura del plan.", + "NOT_FOUND", + { + table: "estructuras_plan", + id: payload.datosBasicos.estructuraPlanId, + }, + ); + } + throw new HttpError( + 500, + "No se pudo obtener la estructura del plan.", + "SUPABASE_QUERY_FAILED", + estructuraPlanError, ); } @@ -165,25 +196,64 @@ Deno.serve(async (req) => { // Use shared OpenAI service directly (no HTTP invoke) const svc = OpenAIService.fromEnv(); if (!(svc instanceof OpenAIService)) { - throw new Error(`OpenAI service misconfiguration: ${svc.message}`); + throw new HttpError( + 500, + "Configuración del servidor incompleta.", + "OPENAI_MISCONFIGURED", + svc, + ); } + // INICIO DE CÓDIGO PARA DEBBUGGING + // console.log( + // `[${ + // new Date().toISOString() + // }][${functionName}]: Request processed successfully`, + // ); + // const { data: plan_debug } = await supabaseService + // .from("planes_estudio") + // .select("*") + // .eq("id", "7ce657b1-1abf-4972-858d-5fffe1d51499") + // .maybeSingle(); + // return sendSuccess(plan_debug); + // FIN DE CÓDIGO PARA DEBBUGGING + const aiResult = await svc.createStructuredResponse( aiStructuredPayload, payload.archivosAdjuntos, ); if (!aiResult.ok) { - throw new Error( - `OpenAI call failed [${aiResult.code}]: ${aiResult.message}`, + const status = aiResult.code === "MissingEnv" ? 500 : 502; + throw new HttpError( + status, + "No se pudo generar el plan con IA.", + "OPENAI_REQUEST_FAILED", + aiResult, ); } // Prefer parsed output; fallback to parse outputText - const aiOutput = aiResult.output ?? - (aiResult.outputText ? JSON.parse(aiResult.outputText) : null); + let aiOutput = aiResult.output ?? null; + if (aiOutput == null && aiResult.outputText) { + try { + aiOutput = JSON.parse(aiResult.outputText); + } catch { + throw new HttpError( + 502, + "La respuesta de la IA no es JSON válido.", + "OPENAI_INVALID_JSON", + { outputText: aiResult.outputText }, + ); + } + } const aiOutputJson: Json = aiOutput as unknown as Json; if (!aiOutput) { - throw new Error("OpenAI response did not contain structured output"); + throw new HttpError( + 502, + "La respuesta de la IA no contiene salida estructurada.", + "OPENAI_MISSING_STRUCTURED_OUTPUT", + { outputText: aiResult.outputText ?? null }, + ); } // TODO: Insertar interacciones con IA y quizas forzar a que la informacion de datosBasicos sea la misma que la recibida @@ -202,7 +272,20 @@ Deno.serve(async (req) => { .eq("id", payload.datosBasicos.carreraId) .maybeSingle(); if (carreraError) { - throw new Error("Error fetching carrera: " + carreraError.message); + throw new HttpError( + 500, + "No se pudo obtener la carrera.", + "SUPABASE_QUERY_FAILED", + carreraError, + ); + } + if (!carrera) { + throw new HttpError( + 404, + "No se encontró la carrera.", + "NOT_FOUND", + { table: "carreras", id: payload.datosBasicos.carreraId }, + ); } const planInsert: Database["public"]["Tables"]["planes_estudio"]["Insert"] = @@ -248,42 +331,62 @@ Deno.serve(async (req) => { // TODO: interaccion con IA y cambio de estado if (planError) { - throw new Error("Error inserting plan: " + planError.message); + const maybeCode = (planError as { code?: string }).code; + // Common cases: + // - foreign key / constraint violations -> 409 + // - others -> 500 + const status = maybeCode ? 409 : 500; + throw new HttpError( + status, + "No se pudo guardar el plan de estudios.", + "SUPABASE_INSERT_FAILED", + { ...planError, code: maybeCode }, + ); } // TODO: update a interaccion_ia y e insert a cambios_plan con id de plan generado - const jsonResponse = { - ok: true, - plan, - }; console.log( `[${ new Date().toISOString() }][${functionName}]: Request processed successfully`, ); - return new Response( - JSON.stringify(jsonResponse), - { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - status: 200, - }, - ); + return sendSuccess(plan); } catch (error) { - // Log full error server-side for diagnostics - if (error instanceof Error) { + if (error instanceof HttpError) { console.error( - `[${ - new Date().toISOString() - }][${functionName}]: Request handler error:`, - error.message, + `[${new Date().toISOString()}][${functionName}] ⚠️ Handled Error:`, + { + message: error.message, + code: error.code, + internalDetails: error.internalDetails || "N/A", + }, ); + + // RESPONSE: Solo enviamos el mensaje limpio y el código + return sendError(error.status, error.message, error.code); } - const message = error instanceof Error ? error.message : String(error); - return new Response(JSON.stringify({ error: message }), { - headers: { ...corsHeaders, "Content-Type": "application/json" }, - status: 400, - }); + + // CASO B: Error Inesperado (Crash, Bug, Syntax Error, etc.) + // El usuario NO debe ver esto. + const unexpectedError = error instanceof Error + ? error + : new Error(String(error)); + + // LOG: Full stack trace y mensaje real + console.error( + `[${ + new Date().toISOString() + }][${functionName}] 💥 CRITICAL UNHANDLED ERROR:`, + unexpectedError.stack || unexpectedError.message, // Esto es lo que necesitas para debuguear + ); + + // RESPONSE: Mensaje genérico y seguro + return sendError( + 500, + "Ocurrió un error inesperado en el servidor.", + "INTERNAL_SERVER_ERROR", + ); } }); @@ -315,8 +418,8 @@ const DatosBasicosSchema: z.ZodType = z }); const IAConfigSchema: z.ZodType = z.object({ - descripcionEnfoque: z.string().min(10, "La descripción es muy corta"), - notasAdicionales: z.string().optional(), + descripcionEnfoqueAcademico: z.string(), + instruccionesAdicionalesIA: z.string().optional(), archivosReferencia: z.array(z.string().uuid()).optional(), repositoriosIds: z.array(z.string().uuid()).optional(), usarMCP: z.boolean().optional(), diff --git a/supabase/functions/tests/ai-structured-test.ts b/supabase/functions/tests/ai-structured-test.ts index 5a52e1e..0145f7d 100644 --- a/supabase/functions/tests/ai-structured-test.ts +++ b/supabase/functions/tests/ai-structured-test.ts @@ -25,9 +25,6 @@ function mustEnv() { if (!SUPABASE_ANON_KEY) throw new Error("SUPABASE_ANON_KEY is required"); } -async function getAuthedClient(): Promise< - { client: SupabaseClient; accessToken: string } -> { async function getAuthedClient(): Promise< { client: SupabaseClient; accessToken: string } > { -- 2.52.0 From 1ca861c51df453e7962f4f43281c42dd267b0d25 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 5 Feb 2026 12:38:41 -0600 Subject: [PATCH 4/5] =?UTF-8?q?Primera=20versi=C3=B3n=20de=20la=20funci?= =?UTF-8?q?=C3=B3n=20de=20ai-generate-subject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../functions/ai-generate-subject/index.ts | 98 +++++++++++++++++-- 1 file changed, 90 insertions(+), 8 deletions(-) diff --git a/supabase/functions/ai-generate-subject/index.ts b/supabase/functions/ai-generate-subject/index.ts index 8cb545f..e974ea3 100644 --- a/supabase/functions/ai-generate-subject/index.ts +++ b/supabase/functions/ai-generate-subject/index.ts @@ -1,5 +1,8 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { corsHeaders } from "../_shared/cors.ts"; +import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts"; +import { createClient } from "npm:@supabase/supabase-js@2"; +import type { Database, Json } from "../_shared/database.types.ts"; console.log("Hello from Functions!"); @@ -13,13 +16,92 @@ Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: corsHeaders }); } - const { name } = await req.json(); - const data = { - message: `Hello ${name}!`, - }; - return new Response( - JSON.stringify(data), - { headers: { "Content-Type": "application/json" } }, - ); + try { + const SUPABASE_URL = Deno.env.get("SUPABASE_URL"); + const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY"); + if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { + throw new HttpError( + 500, + "Configuración del servidor incompleta.", + "MISSING_ENV", + { + missing: [ + !SUPABASE_URL ? "SUPABASE_URL" : null, + !SUPABASE_ANON_KEY ? "SUPABASE_ANON_KEY" : null, + ].filter(Boolean), + }, + ); + } + + // If needed for RLS-protected reads, create an anon client with user's JWT + // Currently not used; kept here for future expansion. + // const supabaseAnon = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { + // global: { + // headers: { + // Authorization: authHeaderRaw, + // }, + // }, + // }); + + const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); + if (!SERVICE_ROLE_KEY) { + throw new HttpError( + 500, + "Configuración del servidor incompleta.", + "MISSING_ENV", + { missing: ["SUPABASE_SERVICE_ROLE_KEY"] }, + ); + } + + const supabaseService = createClient( + SUPABASE_URL, + SERVICE_ROLE_KEY, + ); + + // INICIO DE CODIGO PARA DEBBUGGING + console.log( + `[${ + new Date().toISOString() + }][${functionName}]: Request processed successfully`, + ); + const { data: asignatura_debug, error: asignatura_error } = + await supabaseService + .from("asignaturas") + .select("*") + .eq("id", "9d4dda6a-488f-428a-8a07-38081592a641") + .single(); + return sendSuccess(asignatura_debug); + // FIN DE CODIGO PARA DEBBUGGING + } catch (error) { + if (error instanceof HttpError) { + console.error( + `[${new Date().toISOString()}][${functionName}] ⚠️ Handled Error:`, + { + message: error.message, + code: error.code, + internalDetails: error.internalDetails || "N/A", + }, + ); + + return sendError(error.status, error.message, error.code); + } + + const unexpectedError = error instanceof Error + ? error + : new Error(String(error)); + + console.error( + `[${ + new Date().toISOString() + }][${functionName}] 💥 CRITICAL UNHANDLED ERROR:`, + unexpectedError.stack || unexpectedError.message, + ); + + return sendError( + 500, + "Ocurrió un error inesperado en el servidor.", + "INTERNAL_SERVER_ERROR", + ); + } }); -- 2.52.0 From 00f0bddf1cf90113e33d730d91985f33bed81286 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 5 Feb 2026 16:00:07 -0600 Subject: [PATCH 5/5] Arreglo de reverts --- deno.json | 8 +++-- supabase/functions/_shared/cors.ts | 2 +- supabase/functions/ai-generate-plan/index.ts | 17 +++++++--- supabase/functions/ai-generate-plan/types.ts | 4 +-- .../create-chat-conversation/index.ts | 32 +++---------------- 5 files changed, 26 insertions(+), 37 deletions(-) diff --git a/deno.json b/deno.json index c46e55a..60f816d 100644 --- a/deno.json +++ b/deno.json @@ -1,8 +1,12 @@ { - "allowScripts": ["npm:deno@2.6.4", "npm:supabase-js@1.0.4", "npm:supabase@2.72.6"], + "allowScripts": [ + "npm:deno@2.6.4", + "npm:supabase-js@1.0.4", + "npm:supabase@2.72.6" + ], "imports": { "@openai/openai": "jsr:@openai/openai@^6.16.0", - "@zod/zod": "jsr:@zod/zod@^4.3.5" + "@zod/zod": "jsr:@zod/zod@^4.3.5", "@david/dax": "jsr:@david/dax@^0.44.2" } } diff --git a/supabase/functions/_shared/cors.ts b/supabase/functions/_shared/cors.ts index 455c898..5da4b63 100644 --- a/supabase/functions/_shared/cors.ts +++ b/supabase/functions/_shared/cors.ts @@ -1,5 +1,5 @@ export const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": - "authorization, x-client-info, apikey, content-type", + "authorization, x-client-info, apikey, content-type, x-supabase-client-platform", }; diff --git a/supabase/functions/ai-generate-plan/index.ts b/supabase/functions/ai-generate-plan/index.ts index 374a5f3..0f3185b 100644 --- a/supabase/functions/ai-generate-plan/index.ts +++ b/supabase/functions/ai-generate-plan/index.ts @@ -168,8 +168,15 @@ Deno.serve(async (req: Request): Promise => { const userPrompt = `Genera un borrador completo del PLAN DE ESTUDIOS con base en lo siguiente: - - Descripción del enfoque: ${payload.iaConfig.descripcionEnfoque} - - Notas adicionales: ${payload.iaConfig.notasAdicionales ?? "Ninguna"}`; + - Nombre de la institución: Universidad La Salle México + - Nombre del plan: ${payload.datosBasicos.nombrePlan} + - Nivel: ${payload.datosBasicos.nivel} + - Tipo de ciclo: ${payload.datosBasicos.tipoCiclo} + - Número de ciclos: ${payload.datosBasicos.numCiclos} + - Descripción del enfoque académico (sobre el contenido de la respuesta generada): ${payload.iaConfig.descripcionEnfoqueAcademico} + - Notas adicionales (sobre el formato de la respuesta generada): ${ + payload.iaConfig.instruccionesAdicionalesIA ?? "Ninguna" + }`; // Ensure the JSON schema is an object as required by OpenAI types const schemaDef: Record = typeof estructuraPlan?.definicion === "object" && @@ -313,8 +320,10 @@ Deno.serve(async (req: Request): Promise => { repositoriosIds: payload.iaConfig?.repositoriosIds ?? null, }, iaConfig: { - descripcionEnfoque: payload.iaConfig?.descripcionEnfoque ?? null, - notasAdicionales: payload.iaConfig?.notasAdicionales ?? null, + descripcionEnfoqueAcademico: + payload.iaConfig?.descripcionEnfoqueAcademico ?? null, + instruccionesAdicionalesIA: + payload.iaConfig?.instruccionesAdicionalesIA ?? null, usarMCP: Boolean(payload.iaConfig?.usarMCP), }, } as unknown as Json, diff --git a/supabase/functions/ai-generate-plan/types.ts b/supabase/functions/ai-generate-plan/types.ts index 08b016a..9e8e9d1 100644 --- a/supabase/functions/ai-generate-plan/types.ts +++ b/supabase/functions/ai-generate-plan/types.ts @@ -9,8 +9,8 @@ export type AIGeneratePlanInput = { estructuraPlanId: string; }; iaConfig: { - descripcionEnfoque: string; - notasAdicionales?: string; + descripcionEnfoqueAcademico: string; + instruccionesAdicionalesIA?: string; archivosReferencia?: Array; repositoriosIds?: Array; usarMCP?: boolean; diff --git a/supabase/functions/create-chat-conversation/index.ts b/supabase/functions/create-chat-conversation/index.ts index cadbed2..ee7b7dd 100644 --- a/supabase/functions/create-chat-conversation/index.ts +++ b/supabase/functions/create-chat-conversation/index.ts @@ -1,7 +1,6 @@ // Setup type definitions for built-in Supabase Runtime APIs import "@supabase/functions-js/edge-runtime.d.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; - type WebhookPayload = { type: "INSERT" | "UPDATE" | "DELETE"; table: string; @@ -9,28 +8,23 @@ type WebhookPayload = { record: Record | null; old_record: Record | null; }; - const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? ""; -const SUPABASE_SERVICE_ROLE_KEY = - Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; +const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? + ""; const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY") ?? ""; -const OPENAI_BASE_URL = - Deno.env.get("OPENAI_BASE_URL") ?? "https://api.openai.com/v1"; - +const OPENAI_BASE_URL = Deno.env.get("OPENAI_BASE_URL") ?? + "https://api.openai.com/v1"; const ALLOWED_SCHEMA = "public"; const ALLOWED_TABLES = new Set(["planes_estudio", "asignaturas"]); - const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { persistSession: false }, }); - function jsonResponse(status: number, body: Record) { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" }, }); } - async function createConversationId(metadata: Record) { const response = await fetch(`${OPENAI_BASE_URL}/conversations`, { method: "POST", @@ -40,61 +34,47 @@ async function createConversationId(metadata: Record) { }, body: JSON.stringify({ metadata }), }); - if (!response.ok) { const errorText = await response.text(); throw new Error(`OpenAI error: ${response.status} ${errorText}`); } - const data = await response.json(); const conversationId = data?.id as string | undefined; - if (!conversationId) { throw new Error("OpenAI response missing conversation id"); } - return conversationId; } - Deno.serve(async (req) => { if (req.method !== "POST") { return jsonResponse(405, { error: "Method not allowed" }); } - if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) { return jsonResponse(500, { error: "Supabase env vars missing" }); } - if (!OPENAI_API_KEY) { return jsonResponse(500, { error: "OPENAI_API_KEY missing" }); } - let payload: WebhookPayload; try { payload = await req.json(); } catch { return jsonResponse(400, { error: "Invalid JSON body" }); } - const { type, table, schema, record } = payload; - if (schema !== ALLOWED_SCHEMA || !ALLOWED_TABLES.has(table)) { return jsonResponse(400, { error: "Table not allowed" }); } - if (type !== "INSERT") { return jsonResponse(200, { status: "ignored", reason: "event_type" }); } - if (!record || typeof record !== "object") { return jsonResponse(400, { error: "Missing record" }); } - const recordId = record["id"] as string | number | undefined; if (!recordId) { return jsonResponse(400, { error: "Record id missing" }); } - const existingConversationId = record["conversation_id"] as | string | null @@ -105,7 +85,6 @@ Deno.serve(async (req) => { conversation_id: existingConversationId, }); } - let conversationId: string; try { conversationId = await createConversationId({ @@ -115,16 +94,13 @@ Deno.serve(async (req) => { } catch (error) { return jsonResponse(502, { error: (error as Error).message }); } - const { error } = await supabase .from(table) .update({ conversation_id: conversationId }) .eq("id", recordId); - if (error) { return jsonResponse(500, { error: error.message }); } - return jsonResponse(200, { status: "updated", table, -- 2.52.0