feat: implement Bedelía dashboard panels and Phase 1 Automated Security Audit

This commit is contained in:
2026-09-19 15:17:17 -03:00
parent ada39248d7
commit 71fb20a271
8 changed files with 392 additions and 11 deletions
+3 -3
View File
@@ -26,7 +26,7 @@ jobs:
- name: Install Python Dependencies & Security Tools - name: Install Python Dependencies & Security Tools
run: | run: |
pip3 install -r backend/requirements.txt 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) # 4. Instalar librerías de Node.js (Express BFF)
- name: Install Node.js Dependencies - name: Install Node.js Dependencies
@@ -67,7 +67,7 @@ jobs:
sleep 5 sleep 5
# Verificar con un curl rápido si el JSON de OpenAPI está disponible # 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 # 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
+5 -1
View File
@@ -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. - 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:** - **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. - 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 ## [2.5.0] - 2026-09-19
### Añadido ### Añadido
- **Auditoría y Seguridad Automatizada Completa (Python & Node.js):** - **Auditoría y Seguridad Automatizada Completa (Python & Node.js):**
+3
View File
@@ -30,6 +30,9 @@ Registro de hitos y versiones correspondientes a las Fases del [ROADMAP_MVP.md](
### Cobertura de Pruebas ### 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. * 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) ## [Fase 1: Paridad Legacy y Estabilización] — v2.5.0 / v2.4.0 (2026-09-19)
+100 -2
View File
@@ -149,6 +149,92 @@ def get_stats():
upcoming_reservations = [serialize_reservation(r) for r in upcoming_reservations_q] upcoming_reservations = [serialize_reservation(r) for r in upcoming_reservations_q]
milestones = [serialize_milestone(m) for m in milestones_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: except Exception as e:
print(f"Error fetching stats: {e}") print(f"Error fetching stats: {e}")
total_classrooms = 0 total_classrooms = 0
@@ -171,6 +257,11 @@ def get_stats():
pending_reservations = [] pending_reservations = []
today_schedule_list = [] today_schedule_list = []
upcoming_reservations = [] upcoming_reservations = []
unassigned_commissions_week = []
unassigned_commissions_month = []
available_classrooms_week = []
available_classrooms_month = []
total_active_commissions = 0
milestones = [] milestones = []
# Mocked monthly data for chart since complex SQL group by month might differ based on dialect (SQLite vs Postgres) # 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_classrooms': assigned_today_count,
'assigned_today_pct': assigned_today_pct, 'assigned_today_pct': assigned_today_pct,
'unassigned_today_classrooms': unassigned_today_count, '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, 'audit_logs': audit_data,
'monthly_data': monthly_data, 'monthly_data': monthly_data,
'pending_reservations': pending_reservations, 'pending_reservations': pending_reservations,
'today_schedule_list': today_schedule_list, 'today_schedule_list': today_schedule_list,
'upcoming_reservations': upcoming_reservations, '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 }), 200
@api_dashboard_bp.route('/sync-sheets', methods=['POST']) @api_dashboard_bp.route('/sync-sheets', methods=['POST'])
+4
View File
@@ -0,0 +1,4 @@
bandit>=1.7.5
pip-audit>=2.7.2
schemathesis>=3.28.6
njsscan>=0.3.5
+8 -1
View File
@@ -68,6 +68,9 @@ router.get('/', async (req, res) => {
? apiData.milestones ? apiData.milestones
: null; : null;
// Bedelía-specific data
const bedeliaData = apiData.bedelia || {};
// Consolidated and role-specific stats // Consolidated and role-specific stats
const stats = { const stats = {
total_classrooms, 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: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: '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' } } { 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') { } else if (role === 'docente') {
const myClasses = apiTodaySchedule ? apiTodaySchedule.slice(0, 3) : null; const myClasses = apiTodaySchedule ? apiTodaySchedule.slice(0, 3) : null;
+265
View File
@@ -633,6 +633,254 @@
</div> </div>
</div> </div>
<!-- ========================= Panel: Comisiones Sin Reserva (Semana/Mes) ========================= -->
<div class="card border-0 shadow-sm mb-4" id="bedelia-unassigned-commissions">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-calendar-x-fill text-danger fs-5"></i>
<div>
<h5 class="mb-0 h6 fw-bold">Comisiones Sin Reserva de Aula</h5>
<small class="text-muted">Comisiones activas sin clase reservada en el período seleccionado</small>
</div>
</div>
<div class="d-flex gap-2 align-items-center flex-wrap">
<div class="btn-group btn-group-sm" role="group" id="commPeriodSwitcher">
<button type="button" class="btn btn-outline-danger active" data-period="week" onclick="switchCommPeriod('week')">
<i class="bi bi-calendar-week me-1"></i>Esta Semana
<span class="badge bg-danger ms-1" id="commWeekBadge">{{ role_data.unassigned_commissions_week | length }}</span>
</button>
<button type="button" class="btn btn-outline-danger" data-period="month" onclick="switchCommPeriod('month')">
<i class="bi bi-calendar-month me-1"></i>Este Mes
<span class="badge bg-danger ms-1" id="commMonthBadge">{{ role_data.unassigned_commissions_month | length }}</span>
</button>
</div>
<a href="/admin/commissions_list" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-list-ul me-1"></i>Ver Todas
</a>
</div>
</div>
<div class="card-body p-0">
<!-- Tabla Semana -->
<div id="commTableWeek">
{% if role_data.unassigned_commissions_week and role_data.unassigned_commissions_week | length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 small">
<thead class="table-light">
<tr>
<th class="ps-3">Código</th>
<th>Materia</th>
<th>Docente</th>
<th>Horario Programado</th>
<th>Turno</th>
<th>Cupo</th>
<th class="text-end pe-3">Acción</th>
</tr>
</thead>
<tbody>
{% for comm in role_data.unassigned_commissions_week %}
<tr>
<td class="ps-3">
<span class="badge bg-danger-subtle text-danger border font-monospace">{{ comm.full_code }}</span>
</td>
<td>
<div class="fw-semibold text-body">{{ comm.subject_name }}</div>
<small class="text-muted font-monospace">{{ comm.subject_code }}</small>
</td>
<td class="small">{{ comm.teacher_name }}</td>
<td>
<span class="badge bg-body-tertiary text-body border"><i class="bi bi-clock me-1"></i>{{ comm.schedule }}</span>
</td>
<td><span class="badge bg-info-subtle text-info-emphasis border">{{ comm.shift }}</span></td>
<td class="font-monospace">{{ comm.current_students }}/{{ comm.max_students }}</td>
<td class="text-end pe-3">
<a href="/schedule/add_reservation?commission_id={{ comm.id }}" class="btn btn-sm btn-outline-success py-0 px-2" title="Asignar aula a esta comisión">
<i class="bi bi-plus-circle me-1"></i>Asignar
</a>
<a href="/admin/commission_detail?id={{ comm.id }}" class="btn btn-sm btn-outline-primary py-0 px-2 ms-1" title="Ver detalle">
<i class="bi bi-eye me-1"></i>Detalle
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-check-circle-fill text-success fs-1 d-block mb-2"></i>
<h6 class="text-success fw-bold">¡Todas las comisiones tienen reserva esta semana!</h6>
<p class="text-muted small mb-0">No hay comisiones activas sin aula reservada en la semana actual.</p>
</div>
{% endif %}
</div>
<!-- Tabla Mes -->
<div id="commTableMonth" style="display: none;">
{% if role_data.unassigned_commissions_month and role_data.unassigned_commissions_month | length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 small">
<thead class="table-light">
<tr>
<th class="ps-3">Código</th>
<th>Materia</th>
<th>Docente</th>
<th>Horario Programado</th>
<th>Turno</th>
<th>Cupo</th>
<th class="text-end pe-3">Acción</th>
</tr>
</thead>
<tbody>
{% for comm in role_data.unassigned_commissions_month %}
<tr>
<td class="ps-3">
<span class="badge bg-danger-subtle text-danger border font-monospace">{{ comm.full_code }}</span>
</td>
<td>
<div class="fw-semibold text-body">{{ comm.subject_name }}</div>
<small class="text-muted font-monospace">{{ comm.subject_code }}</small>
</td>
<td class="small">{{ comm.teacher_name }}</td>
<td>
<span class="badge bg-body-tertiary text-body border"><i class="bi bi-clock me-1"></i>{{ comm.schedule }}</span>
</td>
<td><span class="badge bg-info-subtle text-info-emphasis border">{{ comm.shift }}</span></td>
<td class="font-monospace">{{ comm.current_students }}/{{ comm.max_students }}</td>
<td class="text-end pe-3">
<a href="/schedule/add_reservation?commission_id={{ comm.id }}" class="btn btn-sm btn-outline-success py-0 px-2" title="Asignar aula a esta comisión">
<i class="bi bi-plus-circle me-1"></i>Asignar
</a>
<a href="/admin/commission_detail?id={{ comm.id }}" class="btn btn-sm btn-outline-primary py-0 px-2 ms-1" title="Ver detalle">
<i class="bi bi-eye me-1"></i>Detalle
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-check-circle-fill text-success fs-1 d-block mb-2"></i>
<h6 class="text-success fw-bold">¡Todas las comisiones tienen reserva este mes!</h6>
<p class="text-muted small mb-0">No hay comisiones activas sin aula reservada en el mes actual.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- ========================= Panel: Aulas Disponibles (Semana/Mes) ========================= -->
<div class="card border-0 shadow-sm mb-4" id="bedelia-available-classrooms">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-door-open-fill text-success fs-5"></i>
<div>
<h5 class="mb-0 h6 fw-bold">Aulas Disponibles (Sin Reservas)</h5>
<small class="text-muted">Espacios sin ninguna reserva cargada en el período seleccionado</small>
</div>
</div>
<div class="d-flex gap-2 align-items-center flex-wrap">
<div class="btn-group btn-group-sm" role="group" id="classroomPeriodSwitcher">
<button type="button" class="btn btn-outline-success active" data-period="week" onclick="switchClassroomPeriod('week')">
<i class="bi bi-calendar-week me-1"></i>Esta Semana
<span class="badge bg-success ms-1" id="classWeekBadge">{{ role_data.available_classrooms_week | length }}</span>
</button>
<button type="button" class="btn btn-outline-success" data-period="month" onclick="switchClassroomPeriod('month')">
<i class="bi bi-calendar-month me-1"></i>Este Mes
<span class="badge bg-success ms-1" id="classMonthBadge">{{ role_data.available_classrooms_month | length }}</span>
</button>
</div>
<a href="/classrooms/list_classrooms" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-list-ul me-1"></i>Catálogo Completo
</a>
</div>
</div>
<div class="card-body p-3">
<!-- Vista Semana -->
<div id="classroomGridWeek">
{% if role_data.available_classrooms_week and role_data.available_classrooms_week | length > 0 %}
<div class="row g-2">
{% for aula in role_data.available_classrooms_week %}
<div class="col-sm-6 col-md-4 col-lg-3">
<a href="/schedule/add_reservation?classroom_id={{ aula.id }}" class="text-decoration-none text-reset d-block h-100">
<div class="p-3 rounded border h-100 transition-all {% if aula.is_virtual %}bg-info bg-opacity-10 border-info-subtle{% else %}bg-success bg-opacity-10 border-success-subtle{% endif %}">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge {% if aula.is_virtual %}bg-info-subtle text-info{% else %}bg-success-subtle text-success{% endif %} border font-monospace fw-bold">
{{ aula.code }}
</span>
{% if aula.is_virtual %}
<i class="bi bi-camera-video text-info"></i>
{% else %}
<i class="bi bi-door-open text-success"></i>
{% endif %}
</div>
<div class="small text-muted mb-1">
<i class="bi bi-building me-1"></i>{{ aula.building }}{% if aula.floor %} · {{ aula.floor }}{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="small fw-semibold text-body">
<i class="bi bi-people-fill me-1"></i>Cap: {{ aula.capacity }}
</span>
<span class="badge bg-success text-white small"><i class="bi bi-plus-circle me-1"></i>Asignar</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-calendar-check-fill text-primary fs-1 d-block mb-2"></i>
<h6 class="text-primary fw-bold">¡Todas las aulas están siendo utilizadas esta semana!</h6>
<p class="text-muted small mb-0">No hay aulas sin reservas activas en la semana actual.</p>
</div>
{% endif %}
</div>
<!-- Vista Mes -->
<div id="classroomGridMonth" style="display: none;">
{% if role_data.available_classrooms_month and role_data.available_classrooms_month | length > 0 %}
<div class="row g-2">
{% for aula in role_data.available_classrooms_month %}
<div class="col-sm-6 col-md-4 col-lg-3">
<a href="/schedule/add_reservation?classroom_id={{ aula.id }}" class="text-decoration-none text-reset d-block h-100">
<div class="p-3 rounded border h-100 transition-all {% if aula.is_virtual %}bg-info bg-opacity-10 border-info-subtle{% else %}bg-success bg-opacity-10 border-success-subtle{% endif %}">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge {% if aula.is_virtual %}bg-info-subtle text-info{% else %}bg-success-subtle text-success{% endif %} border font-monospace fw-bold">
{{ aula.code }}
</span>
{% if aula.is_virtual %}
<i class="bi bi-camera-video text-info"></i>
{% else %}
<i class="bi bi-door-open text-success"></i>
{% endif %}
</div>
<div class="small text-muted mb-1">
<i class="bi bi-building me-1"></i>{{ aula.building }}{% if aula.floor %} · {{ aula.floor }}{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="small fw-semibold text-body">
<i class="bi bi-people-fill me-1"></i>Cap: {{ aula.capacity }}
</span>
<span class="badge bg-success text-white small"><i class="bi bi-plus-circle me-1"></i>Asignar</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-calendar-check-fill text-primary fs-1 d-block mb-2"></i>
<h6 class="text-primary fw-bold">¡Todas las aulas están siendo utilizadas este mes!</h6>
<p class="text-muted small mb-0">No hay aulas sin reservas activas en el mes actual.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Bedelía Operational Launchpad --> <!-- Bedelía Operational Launchpad -->
<div class="card border-0 shadow-sm mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body-tertiary border-0 py-3"> <div class="card-header bg-body-tertiary border-0 py-3">
@@ -1314,5 +1562,22 @@ document.querySelectorAll('[data-color]').forEach(function(el) {
const color = el.getAttribute('data-color'); const color = el.getAttribute('data-color');
if (color) el.style.setProperty('background-color', color, 'important'); if (color) el.style.setProperty('background-color', color, 'important');
}); });
// Bedelía: Switchers de período para paneles de Comisiones y Aulas
function switchCommPeriod(period) {
document.getElementById('commTableWeek').style.display = period === 'week' ? 'block' : 'none';
document.getElementById('commTableMonth').style.display = period === 'month' ? 'block' : 'none';
document.querySelectorAll('#commPeriodSwitcher button').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-period') === period);
});
}
function switchClassroomPeriod(period) {
document.getElementById('classroomGridWeek').style.display = period === 'week' ? 'block' : 'none';
document.getElementById('classroomGridMonth').style.display = period === 'month' ? 'block' : 'none';
document.querySelectorAll('#classroomPeriodSwitcher button').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-period') === period);
});
}
</script> </script>
{% endblock %} {% endblock %}
+4 -4
View File
@@ -19,7 +19,7 @@ call "%VENV_BANDIT%" -r "%ROOT_DIR%backend\app" -ll -ii
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo [!] Warning: Bandit reported potential security concerns. echo [!] Warning: Bandit reported potential security concerns.
) else ( ) else (
echo [OK] Bandit scan completed cleanly (0 Medium/High issues). echo [OK] Bandit scan completed cleanly - 0 Medium/High issues.
) )
echo. echo.
@@ -29,7 +29,7 @@ call "%VENV_PIPAUDIT%" -s osv --progress-spinner off -r "%ROOT_DIR%backend\requi
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo [!] Warning: pip-audit reported package vulnerabilities. echo [!] Warning: pip-audit reported package vulnerabilities.
) else ( ) else (
echo [OK] All Python dependencies are secure (0 known CVEs). echo [OK] All Python dependencies are secure - 0 known CVEs.
) )
echo. echo.
@@ -50,7 +50,7 @@ call cmd.exe /c npm audit
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo [!] Warning: npm audit reported package vulnerabilities. echo [!] Warning: npm audit reported package vulnerabilities.
) else ( ) else (
echo [OK] All Node.js dependencies are secure (0 vulnerabilities). echo [OK] All Node.js dependencies are secure - 0 vulnerabilities.
) )
cd /d "%ROOT_DIR%" cd /d "%ROOT_DIR%"
echo. echo.
@@ -64,7 +64,7 @@ call "%VENV_ST%" run http://127.0.0.1:5000/api/v1/openapi.json --checks not_a_se
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo [!] Note: Schemathesis found potential unhandled edge cases or backend server is not running on port 5000. echo [!] Note: Schemathesis found potential unhandled edge cases or backend server is not running on port 5000.
) else ( ) else (
echo [OK] API DAST fuzzing passed (0 unhandled 500 server errors). echo [OK] API DAST fuzzing passed - 0 unhandled 500 server errors.
) )
echo. echo.