diff --git a/.gitea/workflows/security-audit.yaml b/.gitea/workflows/security-audit.yaml index ccab568..4f2f898 100644 --- a/.gitea/workflows/security-audit.yaml +++ b/.gitea/workflows/security-audit.yaml @@ -26,7 +26,7 @@ jobs: - name: Install Python Dependencies & Security Tools run: | pip3 install -r backend/requirements.txt - pip3 install bandit pip-audit schemathesis njsscan + pip3 install -r backend/requirements-dev.txt # 4. Instalar librerías de Node.js (Express BFF) - name: Install Node.js Dependencies @@ -67,7 +67,7 @@ jobs: sleep 5 # Verificar con un curl rápido si el JSON de OpenAPI está disponible - curl -s http://127.0.0 > /dev/null + curl -s http://127.0.0.1:5000/api/v1/openapi.json > /dev/null # Ejecutar Schemathesis contra la instancia local efímera - schemathesis run http://127.0.0 --checks not_a_server_error --max-examples=10 + schemathesis run http://127.0.0.1:5000/api/v1/openapi.json --checks not_a_server_error --max-examples=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index b09144b..278a929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,11 @@ El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1 - Adaptación completa de UI en `/admin/commission_detail` con selector de estudiantes, toggle interactivo de excepción de Bedelía, campo de motivo justificado, y tabla de estudiantes matriculados con badges de excepción. - **Suite de Pruebas Unitarias e Integrales de Reglas de Negocio:** - Archivo `backend/tests/test_phase2_rules.py` con 6 tests completos verificando: bloqueo físico, conflicto de aforo, detección docente con bypass de co-docencia, restricción diaria de cursada, excepción por curso corto y excepción por autorización manual de Bedelía. 100% de tests aprobados. - + - **Mejoras UI/UX en Dashboard de Bedelía:** + - Paneles interactivos para visualizar **Comisiones Sin Reserva de Aula** y **Aulas Disponibles** con soporte de filtrado temporal (Semana/Mes) directamente consumiendo estadísticas en tiempo real del motor backend. + - **Fase 1: Auditoría y Seguridad Automatizada:** + - **Control de Entorno Dev:** Aislamiento de herramientas de seguridad en `requirements-dev.txt`. + - **Automatización Local e Integración Continua:** Correcciones de evaluación en `security_audit.bat` para Windows y actualización de `.gitea/workflows/security-audit.yaml` para asegurar la correcta ejecución del stack de auditoría (Bandit, pip-audit, njsscan y Schemathesis). ## [2.5.0] - 2026-09-19 ### Añadido - **Auditoría y Seguridad Automatizada Completa (Python & Node.js):** diff --git a/CHANGELOG_MVP.md b/CHANGELOG_MVP.md index 012543c..5f5bac9 100644 --- a/CHANGELOG_MVP.md +++ b/CHANGELOG_MVP.md @@ -30,6 +30,9 @@ Registro de hitos y versiones correspondientes a las Fases del [ROADMAP_MVP.md]( ### Cobertura de Pruebas * Suite completa en `backend/tests/test_phase2_rules.py` validando los 6 criterios de aceptación (conflicto físico, aforo, co-docencia, restricción diaria, bypass de curso corto y bypass de Bedelía). 6/6 tests aprobados con 100% de éxito. +### Complementos y Seguridad Transversal +* **Dashboard de Bedelía:** Integración de paneles en tiempo real para análisis de **Comisiones Sin Reserva** y grilla de **Aulas Disponibles** (Filtros Semana/Mes) conectando frontend UI con el servicio analítico backend. +* **Auditoría Automatizada (Fase 1):** Implantación de un pipeline de seguridad local/Gitea que aísla dependencias de desarrollo (`requirements-dev.txt`) y ejecuta controles SAST, DAST y SCA (Bandit, pip-audit, njsscan y Schemathesis) sin vulnerabilidades detectadas. --- ## [Fase 1: Paridad Legacy y Estabilización] — v2.5.0 / v2.4.0 (2026-09-19) diff --git a/backend/app/routes/api/dashboard.py b/backend/app/routes/api/dashboard.py index 8cc6d15..ef391ea 100644 --- a/backend/app/routes/api/dashboard.py +++ b/backend/app/routes/api/dashboard.py @@ -149,6 +149,92 @@ def get_stats(): upcoming_reservations = [serialize_reservation(r) for r in upcoming_reservations_q] milestones = [serialize_milestone(m) for m in milestones_q] + # ---- Bedelía: Comisiones sin aula asignada (semana y mes) ---- + import calendar + # Calcular rango de semana actual (lunes a domingo) + today_dt = datetime.utcnow() + week_start = today_dt - timedelta(days=today_dt.weekday()) # lunes + week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0) + week_end = week_start + timedelta(days=6, hours=23, minutes=59, seconds=59) + + # Calcular rango del mes actual + month_start = today_dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + last_day = calendar.monthrange(today_dt.year, today_dt.month)[1] + month_end = today_dt.replace(day=last_day, hour=23, minute=59, second=59, microsecond=0) + + # Comisiones activas totales + all_active_commissions = Commission.query.filter_by(active=True).all() + total_active_commissions = len(all_active_commissions) + + # IDs de comisiones que SÍ tienen reserva en la semana + comm_ids_with_res_week = set( + r[0] for r in db.session.query(Reservation.commission_id).filter( + Reservation.commission_id.isnot(None), + Reservation.start_time >= week_start, + Reservation.start_time <= week_end + ).distinct().all() + ) + # IDs de comisiones que SÍ tienen reserva en el mes + comm_ids_with_res_month = set( + r[0] for r in db.session.query(Reservation.commission_id).filter( + Reservation.commission_id.isnot(None), + Reservation.start_time >= month_start, + Reservation.start_time <= month_end + ).distinct().all() + ) + + unassigned_commissions_week = [] + unassigned_commissions_month = [] + for comm in all_active_commissions: + comm_data = { + 'id': comm.id, + 'code': comm.code, + 'full_code': comm.get_full_code(), + 'subject_name': comm.subject.name if comm.subject else 'Sin materia', + 'subject_code': comm.subject.code if comm.subject else '', + 'teacher_name': comm.teacher_name, + 'schedule': comm.schedule or 'Sin horario', + 'shift': comm.shift or 'Sin turno', + 'max_students': comm.max_students, + 'current_students': comm.current_students + } + if comm.id not in comm_ids_with_res_week: + unassigned_commissions_week.append(comm_data) + if comm.id not in comm_ids_with_res_month: + unassigned_commissions_month.append(comm_data) + + # ---- Bedelía: Aulas disponibles (sin reservas) en la semana y mes ---- + classroom_ids_used_week = set( + r[0] for r in db.session.query(Reservation.classroom_id).filter( + Reservation.classroom_id.isnot(None), + Reservation.start_time >= week_start, + Reservation.start_time <= week_end + ).distinct().all() + ) + classroom_ids_used_month = set( + r[0] for r in db.session.query(Reservation.classroom_id).filter( + Reservation.classroom_id.isnot(None), + Reservation.start_time >= month_start, + Reservation.start_time <= month_end + ).distinct().all() + ) + + available_classrooms_week = [] + available_classrooms_month = [] + for c in all_active_classrooms: + c_data = { + 'id': c.id, + 'code': c.code, + 'building': c.building or 'Sin edificio', + 'floor': c.floor or '', + 'capacity': c.capacity, + 'is_virtual': getattr(c, 'is_virtual', False) or 'virtual' in (c.building or '').lower() or 'virtual' in (c.code or '').lower() + } + if c.id not in classroom_ids_used_week: + available_classrooms_week.append(c_data) + if c.id not in classroom_ids_used_month: + available_classrooms_month.append(c_data) + except Exception as e: print(f"Error fetching stats: {e}") total_classrooms = 0 @@ -171,6 +257,11 @@ def get_stats(): pending_reservations = [] today_schedule_list = [] upcoming_reservations = [] + unassigned_commissions_week = [] + unassigned_commissions_month = [] + available_classrooms_week = [] + available_classrooms_month = [] + total_active_commissions = 0 milestones = [] # Mocked monthly data for chart since complex SQL group by month might differ based on dialect (SQLite vs Postgres) @@ -207,14 +298,21 @@ def get_stats(): 'assigned_today_classrooms': assigned_today_count, 'assigned_today_pct': assigned_today_pct, 'unassigned_today_classrooms': unassigned_today_count, - 'unassigned_today_pct': unassigned_today_pct + 'unassigned_today_pct': unassigned_today_pct, + 'total_active_commissions': total_active_commissions }, 'audit_logs': audit_data, 'monthly_data': monthly_data, 'pending_reservations': pending_reservations, 'today_schedule_list': today_schedule_list, 'upcoming_reservations': upcoming_reservations, - 'milestones': milestones + 'milestones': milestones, + 'bedelia': { + 'unassigned_commissions_week': unassigned_commissions_week, + 'unassigned_commissions_month': unassigned_commissions_month, + 'available_classrooms_week': available_classrooms_week, + 'available_classrooms_month': available_classrooms_month + } }), 200 @api_dashboard_bp.route('/sync-sheets', methods=['POST']) diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 0000000..ca9c507 --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,4 @@ +bandit>=1.7.5 +pip-audit>=2.7.2 +schemathesis>=3.28.6 +njsscan>=0.3.5 diff --git a/frontend/src/routes/dashboard.js b/frontend/src/routes/dashboard.js index 43e5d50..1800c4d 100644 --- a/frontend/src/routes/dashboard.js +++ b/frontend/src/routes/dashboard.js @@ -68,6 +68,9 @@ router.get('/', async (req, res) => { ? apiData.milestones : null; + // Bedelía-specific data + const bedeliaData = apiData.bedelia || {}; + // Consolidated and role-specific stats const stats = { total_classrooms, @@ -135,7 +138,11 @@ router.get('/', async (req, res) => { { start_time: '08:00', end_time: '12:00', classroom: { code: 'Aula 102' }, subject_name: 'Algoritmos y Estructuras de Datos', user: { name: 'Prof. Ana Vega' } }, { start_time: '08:30', end_time: '12:30', classroom: { code: 'Aula 204' }, subject_name: 'Matemática Discreta', user: { name: 'Prof. Carlos López' } }, { start_time: '14:00', end_time: '18:00', classroom: { code: 'Lab Redes' }, subject_name: 'Redes y Telecomunicaciones', user: { name: 'Ing. Marcos Ruiz' } } - ] + ], + unassigned_commissions_week: bedeliaData.unassigned_commissions_week || [], + unassigned_commissions_month: bedeliaData.unassigned_commissions_month || [], + available_classrooms_week: bedeliaData.available_classrooms_week || [], + available_classrooms_month: bedeliaData.available_classrooms_month || [] }; } else if (role === 'docente') { const myClasses = apiTodaySchedule ? apiTodaySchedule.slice(0, 3) : null; diff --git a/frontend/views/dashboard.html b/frontend/views/dashboard.html index d7dd3ce..83c533d 100644 --- a/frontend/views/dashboard.html +++ b/frontend/views/dashboard.html @@ -633,6 +633,254 @@ + +
| Código | +Materia | +Docente | +Horario Programado | +Turno | +Cupo | +Acción | +
|---|---|---|---|---|---|---|
| + {{ comm.full_code }} + | +
+ {{ comm.subject_name }}
+ {{ comm.subject_code }}
+ |
+ {{ comm.teacher_name }} | ++ {{ comm.schedule }} + | +{{ comm.shift }} | +{{ comm.current_students }}/{{ comm.max_students }} | ++ + Asignar + + + Detalle + + | +
No hay comisiones activas sin aula reservada en la semana actual.
+No hay aulas sin reservas activas en la semana actual.
+