feat: implement Bedelía dashboard panels and Phase 1 Automated Security Audit
This commit is contained in:
@@ -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
@@ -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):**
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
bandit>=1.7.5
|
||||||
|
pip-audit>=2.7.2
|
||||||
|
schemathesis>=3.28.6
|
||||||
|
njsscan>=0.3.5
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user