diff --git a/CHANGELOG_MVP.md b/CHANGELOG_MVP.md index 5f5bac9..faafeed 100644 --- a/CHANGELOG_MVP.md +++ b/CHANGELOG_MVP.md @@ -33,6 +33,8 @@ Registro de hitos y versiones correspondientes a las Fases del [ROADMAP_MVP.md]( ### 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. +* **Optimizador Genético de Reservas:** Reparación de enrutamiento 404, migración a JWT (`@jwt_required`), corrección de UI de métricas dinámicas de algoritmo, y refactorización de layout para superposición de herramientas. +* **UI/UX General:** Ajustes de CSS en modo claro (soporte dinámico de contraste de isologotipo blanco a rosa institucional mediante filtros) y traducciones de menús de usuario en `base.html`. --- ## [Fase 1: Paridad Legacy y Estabilización] — v2.5.0 / v2.4.0 (2026-09-19) diff --git a/backend/app/__init__.py b/backend/app/__init__.py index f0c390f..a07daed 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -30,6 +30,7 @@ def create_app(config_class=Config): from app.routes.api.dashboard import api_dashboard_bp from app.routes.api.admin import api_admin_bp from app.routes.api.openapi import api_openapi_bp + from app.routes.genetic_algorithm import genetic_bp from app.security import SecurityFilterChain # Register API v1 blueprints @@ -39,6 +40,7 @@ def create_app(config_class=Config): app.register_blueprint(api_optimizer_bp) app.register_blueprint(api_dashboard_bp) app.register_blueprint(api_openapi_bp) + app.register_blueprint(genetic_bp) app.register_blueprint(api_admin_bp, url_prefix='/api/v1') app.register_blueprint(api_admin_bp, url_prefix='/api/v1/admin', name='api_admin_prefixed') diff --git a/backend/app/routes/genetic_algorithm.py b/backend/app/routes/genetic_algorithm.py index 913214e..d3abe74 100644 --- a/backend/app/routes/genetic_algorithm.py +++ b/backend/app/routes/genetic_algorithm.py @@ -1,15 +1,15 @@ -from flask import Blueprint, request, jsonify, current_app, render_template +from flask import Blueprint, request, jsonify, current_app, render_template, g from datetime import datetime, timedelta from app.models.genetic_algorithm import ReservationOptimizer from app.models.reservation import Reservation, ReservationStatus from app import db -from flask_login import login_required, current_user +from app.utils.jwt_decorators import jwt_required genetic_bp = Blueprint('genetic_algorithm', __name__, url_prefix='/api/genetic') @genetic_bp.route('/optimize', methods=['POST']) -@login_required +@jwt_required def optimize_reservations(): """Optimize reservations using genetic algorithm""" try: @@ -40,14 +40,14 @@ def optimize_reservations(): return jsonify({'error': 'Invalid end_date format. Use ISO format.'}), 400 # Check if user has permission (admin, teacher or RBAC optimizer write) - if not (current_user.is_admin() or current_user.has_permission('optimizer', 'read_write') or current_user.role in ['ADMIN', 'teacher']): + if not (g.jwt_user.is_admin() or g.jwt_user.has_permission('optimizer', 'read_write') or g.jwt_user.role in ['ADMIN', 'teacher']): return jsonify({'error': 'Insufficient permissions to optimize reservations'}), 403 # Initialize optimizer and run optimization optimizer = ReservationOptimizer() result = optimizer.optimize_schedule( commission_ids=commission_ids, - admin_user_id=current_user.id, + admin_user_id=g.jwt_user.id, start_date=start_date, end_date=end_date ) @@ -69,7 +69,7 @@ def optimize_reservations(): @genetic_bp.route('/apply-optimization', methods=['POST']) -@login_required +@jwt_required def apply_optimization(): """Apply optimized reservations to the database""" try: @@ -84,7 +84,7 @@ def apply_optimization(): return jsonify({'error': 'reservations must be a list'}), 400 # Check if user has permission - if not (current_user.is_admin() or current_user.has_permission('optimizer', 'read_write') or current_user.role in ['ADMIN', 'teacher']): + if not (g.jwt_user.is_admin() or g.jwt_user.has_permission('optimizer', 'read_write') or g.jwt_user.role in ['ADMIN', 'teacher']): return jsonify({'error': 'Insufficient permissions to apply reservations'}), 403 # Initialize optimizer and apply reservations @@ -106,7 +106,7 @@ def apply_optimization(): @genetic_bp.route('/preview-optimization', methods=['POST']) -@login_required +@jwt_required def preview_optimization(): """Preview optimization without applying to database""" try: @@ -124,14 +124,14 @@ def preview_optimization(): return jsonify({'error': 'commission_ids must be a non-empty list'}), 400 # Check if user has permission - if current_user.role not in ['ADMIN', 'teacher']: + if g.jwt_user.role not in ['ADMIN', 'teacher']: return jsonify({'error': 'Insufficient permissions to preview optimization'}), 403 # Initialize optimizer and run optimization optimizer = ReservationOptimizer() result = optimizer.optimize_schedule( commission_ids=commission_ids, - admin_user_id=current_user.id + admin_user_id=g.jwt_user.id ) # Add additional preview information @@ -166,12 +166,12 @@ def preview_optimization(): @genetic_bp.route('/commissions', methods=['GET']) -@login_required +@jwt_required def get_commissions(): """Get available commissions for optimization""" try: # Check if user has permission - if current_user.role not in ['ADMIN', 'teacher']: + if g.jwt_user.role not in ['ADMIN', 'teacher']: return jsonify({'error': 'Insufficient permissions'}), 403 from app.models.subject import Commission, Subject @@ -196,12 +196,12 @@ def get_commissions(): @genetic_bp.route('/algorithm-status', methods=['GET']) -@login_required +@jwt_required def get_algorithm_status(): """Get genetic algorithm configuration status""" try: # Check if user has permission - if current_user.role not in ['ADMIN']: + if g.jwt_user.role not in ['ADMIN']: return jsonify({'error': 'Insufficient permissions'}), 403 # Get system status @@ -246,10 +246,10 @@ genetic_web_bp = Blueprint('genetic_algorithm_web', __name__) @genetic_web_bp.route('/genetic-optimizer') -@login_required +@jwt_required def genetic_optimizer(): """Render the genetic algorithm optimizer interface""" - if not (current_user.has_permission('optimizer', 'read') or (current_user.role and current_user.role.upper() == 'ADMIN')): + if not (g.jwt_user.has_permission('optimizer', 'read') or (g.jwt_user.role and g.jwt_user.role.upper() == 'ADMIN')): return render_template('auth/403.html'), 403 return render_template('genetic_optimizer.html') \ No newline at end of file diff --git a/frontend/views/base.html b/frontend/views/base.html index 1e01f61..04adc15 100644 --- a/frontend/views/base.html +++ b/frontend/views/base.html @@ -42,6 +42,10 @@ background: #F8FAFC !important; color: #1E293B !important; } + [data-bs-theme="light"] .navbar-logo { + /* Filtro para convertir blanco a rosa (#B43E8E) */ + filter: brightness(0) saturate(100%) invert(35%) sepia(55%) saturate(2250%) hue-rotate(289deg) brightness(90%) contrast(90%); + } [data-bs-theme="light"] .glass-navbar { background: rgba(255, 255, 255, 0.88) !important; backdrop-filter: blur(12px); @@ -410,7 +414,7 @@ diff --git a/frontend/views/genetic_optimizer.html b/frontend/views/genetic_optimizer.html index 3d8893e..878ef1f 100644 --- a/frontend/views/genetic_optimizer.html +++ b/frontend/views/genetic_optimizer.html @@ -83,7 +83,8 @@
-
+
+
@@ -123,14 +124,60 @@
-
-
+ +
+
+
+ + Optimization Controls +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + + + +
+
+
+
+ +
@@ -161,54 +208,7 @@
- -
-
-
-
-
- - Optimization Controls -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
- -
- - - - -
-
-
-
-
+
@@ -292,11 +292,25 @@ function resetOptimization() { document.getElementById('end-date').value = ''; } +window.refreshStatus = async function() { + if (window.geneticManager) { + try { + const statusResult = await geneticManager.getAlgorithmStatus(); + if (statusResult && statusResult.success && statusResult.data) { + const sysStats = statusResult.data.system_status; + document.getElementById('available-classrooms').textContent = sysStats.available_classrooms; + document.getElementById('active-commissions').textContent = sysStats.active_commissions; + document.getElementById('existing-reservations').textContent = sysStats.confirmed_reservations; + } + } catch (e) { + console.error("Error refreshing status:", e); + } + } +} + document.addEventListener('DOMContentLoaded', async function() { await loadCommissions(); - if (window.geneticManager) { - await geneticManager.getAlgorithmStatus(); - } + await window.refreshStatus(); }); async function loadCommissions() {