887 lines
52 KiB
HTML
887 lines
52 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Configuración Global del Sistema - Admin Edu-Space{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.settings-nav-tabs .nav-link {
|
|
color: var(--bs-secondary-color);
|
|
font-weight: 500;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
padding: 0.75rem 1.25rem;
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
.settings-nav-tabs .nav-link.active {
|
|
color: #B43E8E;
|
|
border-bottom: 2px solid #B43E8E;
|
|
background: transparent;
|
|
font-weight: 600;
|
|
}
|
|
[data-bs-theme="dark"] .settings-nav-tabs .nav-link.active {
|
|
color: #FF8AB6;
|
|
border-bottom-color: #FF8AB6;
|
|
}
|
|
.config-card {
|
|
border-radius: 12px;
|
|
transition: box-shadow 0.2s ease;
|
|
}
|
|
.stat-badge-pill {
|
|
border-radius: 30px;
|
|
font-size: 0.82rem;
|
|
padding: 0.35rem 0.75rem;
|
|
}
|
|
.dlq-alert {
|
|
border-left: 4px solid #ef4444;
|
|
}
|
|
.pass-toggle-btn {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid pt-4 pb-5 px-3 px-md-4">
|
|
<!-- Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
|
<div>
|
|
<h1 class="h4 mb-1 d-flex align-items-center">
|
|
<i class="bi bi-gear-wide-connected text-primary me-2"></i>
|
|
<span>Panel de Configuración Global</span>
|
|
</h1>
|
|
<p class="text-muted small mb-0">Gestión centralizada de Servidor de Correo SMTP, Métodos de Autenticación SSO y Sincronización Moodle 4.1</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="refreshQueueStats()" id="btnRefreshStats">
|
|
<i class="bi bi-arrow-clockwise me-1"></i>Actualizar Estado
|
|
</button>
|
|
<a href="/dashboard" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-arrow-left me-1"></i>Volver
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Feedback Alerts -->
|
|
<div id="settingsAlertContainer"></div>
|
|
|
|
<!-- Navegación por Pestañas -->
|
|
<div class="card border-0 shadow-sm mb-4 config-card">
|
|
<div class="card-header bg-transparent border-bottom px-3 pt-2 pb-0">
|
|
<ul class="nav settings-nav-tabs" id="settingsTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="tab-smtp-btn" data-bs-toggle="tab" data-bs-target="#tab-smtp" type="button" role="tab">
|
|
<i class="bi bi-envelope-at me-2 text-warning"></i>Servidor de Correo (SMTP)
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="tab-sso-btn" data-bs-toggle="tab" data-bs-target="#tab-sso" type="button" role="tab">
|
|
<i class="bi bi-shield-lock me-2 text-info"></i>Métodos de Login & Google OAuth
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="tab-moodle-btn" data-bs-toggle="tab" data-bs-target="#tab-moodle" type="button" role="tab">
|
|
<i class="bi bi-mortarboard me-2 text-danger"></i>Moodle 4.1 & Cola de Sync
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card-body p-3 p-md-4">
|
|
<div class="tab-content" id="settingsTabsContent">
|
|
|
|
<!-- ========================================== -->
|
|
<!-- PESTAÑA 1: SERVIDOR DE CORREO (SMTP) -->
|
|
<!-- ========================================== -->
|
|
<div class="tab-pane fade show active" id="tab-smtp" role="tabpanel">
|
|
<div class="row g-4">
|
|
<div class="col-lg-8">
|
|
<form id="smtpForm" onsubmit="saveSmtpConfig(event)">
|
|
<div class="row g-3">
|
|
<div class="col-md-8">
|
|
<label class="form-label small fw-semibold">Servidor Host SMTP</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text"><i class="bi bi-hdd-network"></i></span>
|
|
<input type="text" class="form-control" id="smtp_host" name="host" placeholder="smtp.gmail.com" value="{{ settings.smtp.host if settings and settings.smtp else 'smtp.gmail.com' }}" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Puerto</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text"><i class="bi bi-hash"></i></span>
|
|
<input type="number" class="form-control" id="smtp_port" name="port" placeholder="587" value="{{ settings.smtp.port if settings and settings.smtp else '587' }}" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-semibold">Usuario / Cuenta de Envío</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
|
<input type="text" class="form-control" id="smtp_user" name="user" placeholder="notificaciones@unicaba.edu.ar" value="{{ settings.smtp.user if settings and settings.smtp else '' }}">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-semibold">Contraseña / App Password (Cifrada)</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
|
<input type="password" class="form-control" id="smtp_password" name="password" placeholder="••••••••••••" value="{{ settings.smtp.password_masked if settings and settings.smtp else '' }}">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="togglePassVisibility('smtp_password')">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>
|
|
<small class="text-muted" style="font-size: 11px;">Almacenada con cifrado simétrico Fernet. Ingrese nuevo valor solo si desea cambiarla.</small>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Protocolo de Seguridad</label>
|
|
<select class="form-select form-select-sm" id="smtp_security" name="security">
|
|
<option value="tls" {% if not settings or not settings.smtp or settings.smtp.security == 'tls' %}selected{% endif %}>TLS / STARTTLS (Puerto 587)</option>
|
|
<option value="ssl" {% if settings and settings.smtp and settings.smtp.security == 'ssl' %}selected{% endif %}>SSL Directo (Puerto 465)</option>
|
|
<option value="none" {% if settings and settings.smtp and settings.smtp.security == 'none' %}selected{% endif %}>Sin Cifrado (Puerto 25)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Email Remitente (From)</label>
|
|
<input type="email" class="form-control form-control-sm" id="smtp_from_email" name="from_email" placeholder="notificaciones@unicaba.edu.ar" value="{{ settings.smtp.from_email if settings and settings.smtp else '' }}">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Nombre Remitente</label>
|
|
<input type="text" class="form-control form-control-sm" id="smtp_from_name" name="from_name" placeholder="Admin Edu-Space UniCABA" value="{{ settings.smtp.from_name if settings and settings.smtp else 'Admin Edu-Space' }}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3 border-top d-flex gap-2 flex-wrap">
|
|
<button type="submit" class="btn btn-sm btn-primary px-3" id="btnSaveSmtp">
|
|
<i class="bi bi-check2-circle me-1"></i>Guardar Parámetros SMTP
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-success px-3" data-bs-toggle="modal" data-bs-target="#testSmtpModal">
|
|
<i class="bi bi-send-check me-1"></i>Probar Conexión SMTP
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<div class="card bg-body-tertiary border-0 p-3 rounded-3 h-100">
|
|
<h6 class="fw-bold mb-2 d-flex align-items-center">
|
|
<i class="bi bi-info-circle text-primary me-2"></i>Instrucciones de Correo
|
|
</h6>
|
|
<p class="small text-muted mb-2">Este módulo administra el transporte dinámico para el envío de correos transaccionales:</p>
|
|
<ul class="small text-muted ps-3 mb-3">
|
|
<li>Notificación de asignación de comisiones a profesores.</li>
|
|
<li>Convocatorias a mesas de exámenes finales.</li>
|
|
<li>Alertas de auditoría e incidentes de seguridad.</li>
|
|
</ul>
|
|
<div class="alert alert-info py-2 px-3 small mb-0">
|
|
<i class="bi bi-shield-lock-fill me-1"></i>
|
|
<strong>Google Workspace:</strong> Use contraseñas de aplicación (App Passwords) generadas desde la consola de Google si tiene 2FA activo.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ========================================== -->
|
|
<!-- PESTAÑA 2: MÉTODOS DE LOGIN & GOOGLE OAUTH -->
|
|
<!-- ========================================== -->
|
|
<div class="tab-pane fade" id="tab-sso" role="tabpanel">
|
|
<div class="row g-4">
|
|
<div class="col-lg-6">
|
|
<div class="card border-0 bg-body-tertiary p-3 rounded-3 mb-3">
|
|
<h6 class="fw-bold mb-3 d-flex align-items-center">
|
|
<i class="bi bi-toggle2-on text-primary me-2"></i>Métodos de Autenticación Habilitados
|
|
</h6>
|
|
<form id="providersForm" onsubmit="saveAuthProviders(event)">
|
|
<div class="d-flex align-items-center justify-content-between p-2 rounded mb-2 border bg-body">
|
|
<div>
|
|
<div class="fw-semibold small">Login Nativo con Contraseña</div>
|
|
<small class="text-muted" style="font-size: 11px;">Permite inicio de sesión local. Si se desactiva, queda reservado exclusivamente para rol ADMIN como contingencia.</small>
|
|
</div>
|
|
<div class="form-check form-switch fs-5 ms-3">
|
|
<input class="form-check-input" type="checkbox" id="auth_local_enabled" {% if not settings or not settings.auth_providers or settings.auth_providers.local %}checked{% endif %}>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between p-2 rounded mb-2 border bg-body">
|
|
<div>
|
|
<div class="fw-semibold small">Google OAuth 2.0 (Google Workspace)</div>
|
|
<small class="text-muted" style="font-size: 11px;">Inicio de sesión con cuentas institucionales @unicaba.edu.ar mediante SSO.</small>
|
|
</div>
|
|
<div class="form-check form-switch fs-5 ms-3">
|
|
<input class="form-check-input" type="checkbox" id="auth_google_enabled" {% if settings and settings.auth_providers and settings.auth_providers.google %}checked{% endif %}>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between p-2 rounded mb-3 border bg-body">
|
|
<div>
|
|
<div class="fw-semibold small">Moodle SSO Delegado</div>
|
|
<small class="text-muted" style="font-size: 11px;">Valida credenciales contra el servidor de Moodle 4.1 y aprovisiona perfil.</small>
|
|
</div>
|
|
<div class="form-check form-switch fs-5 ms-3">
|
|
<input class="form-check-input" type="checkbox" id="auth_moodle_enabled" {% if settings and settings.auth_providers and settings.auth_providers.moodle %}checked{% endif %}>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-sm btn-primary" id="btnSaveProviders">
|
|
<i class="bi bi-save me-1"></i>Actualizar Métodos de Login
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Control de Modo Demo y Cuentas de Prueba Iniciales -->
|
|
<div class="card border border-warning-subtle shadow-sm p-3 rounded-3 mb-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h6 class="fw-bold mb-0 d-flex align-items-center text-body">
|
|
<i class="bi bi-shield-lock-fill text-warning me-2"></i>Acceso Rápido Demo & Cuentas de Prueba
|
|
</h6>
|
|
<span id="demoModeBadge" class="badge {% if demo_access_enabled %}bg-warning-subtle text-warning border border-warning-subtle{% else %}bg-secondary-subtle text-secondary border border-secondary-subtle{% endif %}">
|
|
{% if demo_access_enabled %}Activo en Login{% else %}Deshabilitado{% endif %}
|
|
</span>
|
|
</div>
|
|
<p class="small text-muted mb-2" style="font-size: 0.82rem;">
|
|
Permite ocultar el bloque de <em>"Acceso Rápido Demo (1 Clic)"</em> de la pantalla de inicio de sesión y deshabilitar las 3 cuentas de usuario de prueba iniciales para pasar a producción.
|
|
</p>
|
|
<div class="p-2 rounded bg-body-tertiary border mb-3 small">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<span><i class="bi bi-building-gear me-1 text-warning"></i><strong>Bedelía:</strong> <code>bedelia@edu-space.com</code></span>
|
|
<span id="badgeBedelia" class="badge {% if demo_accounts_active %}bg-success-subtle text-success{% else %}bg-danger-subtle text-danger{% endif %}">
|
|
{% if demo_accounts_active %}Habilitado{% else %}Desactivado{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<span><i class="bi bi-person-video3 me-1 text-info"></i><strong>Docente:</strong> <code>docente@edu-space.com</code></span>
|
|
<span id="badgeDocente" class="badge {% if demo_accounts_active %}bg-success-subtle text-success{% else %}bg-danger-subtle text-danger{% endif %}">
|
|
{% if demo_accounts_active %}Habilitado{% else %}Desactivado{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span><i class="bi bi-mortarboard me-1 text-success"></i><strong>Alumno:</strong> <code>alumno@edu-space.com</code></span>
|
|
<span id="badgeAlumno" class="badge {% if demo_accounts_active %}bg-success-subtle text-success{% else %}bg-danger-subtle text-danger{% endif %}">
|
|
{% if demo_accounts_active %}Habilitado{% else %}Desactivado{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<button type="button" class="btn btn-sm {% if demo_access_enabled or demo_accounts_active %}btn-outline-danger{% else %}btn-outline-success{% endif %}" id="btnToggleDemoMode" onclick="toggleDemoMode()">
|
|
<i class="bi {% if demo_access_enabled or demo_accounts_active %}bi-shield-slash{% else %}bi-shield-check{% endif %} me-1"></i>
|
|
<span id="btnToggleDemoText">
|
|
{% if demo_access_enabled or demo_accounts_active %}
|
|
Deshabilitar Acceso Demo y Cuentas Default
|
|
{% else %}
|
|
Reactivar Acceso Demo y Cuentas Default
|
|
{% endif %}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-6">
|
|
<div class="card border p-3 rounded-3">
|
|
<h6 class="fw-bold mb-3 d-flex align-items-center">
|
|
<i class="bi bi-google text-danger me-2"></i>Parámetros Google OAuth 2.0
|
|
</h6>
|
|
<form id="googleOauthForm" onsubmit="saveGoogleOAuth(event)">
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Client ID</label>
|
|
<input type="text" class="form-control form-control-sm" id="google_client_id" placeholder="apps.googleusercontent.com" value="{{ settings.google_oauth.client_id if settings and settings.google_oauth else '' }}">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Client Secret (Cifrado)</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="password" class="form-control" id="google_client_secret" placeholder="••••••••••••" value="{{ settings.google_oauth.client_secret_masked if settings and settings.google_oauth else '' }}">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="togglePassVisibility('google_client_secret')">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Dominios Permitidos (separados por coma)</label>
|
|
<input type="text" class="form-control form-control-sm" id="google_allowed_domains" placeholder="unicaba.edu.ar, lasalle.edu.ar" value="{{ settings.google_oauth.allowed_domains if settings and settings.google_oauth else 'unicaba.edu.ar' }}">
|
|
<small class="text-muted" style="font-size: 11px;">Restricción arquitectónica estricta similar al módulo de AlumnosLS.</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Ruta de Redirección Autorizada (Callback URL)</label>
|
|
<input type="text" class="form-control form-control-sm" id="google_callback_url" value="{{ settings.google_oauth.callback_url if settings and settings.google_oauth else '/auth/google/callback' }}">
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-sm btn-primary" id="btnSaveGoogle">
|
|
<i class="bi bi-check-circle me-1"></i>Guardar Credenciales Google
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ========================================== -->
|
|
<!-- PESTAÑA 3: MOODLE 4.1 & COLA ASÍNCRONA -->
|
|
<!-- ========================================== -->
|
|
<div class="tab-pane fade" id="tab-moodle" role="tabpanel">
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-lg-6">
|
|
<div class="card border p-3 rounded-3 h-100">
|
|
<h6 class="fw-bold mb-3 d-flex align-items-center">
|
|
<i class="bi bi-cpu text-primary me-2"></i>Conexión Web Services Moodle 4.1
|
|
</h6>
|
|
<form id="moodleConfigForm" onsubmit="saveMoodleConfig(event)">
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">URL Base del Servidor Moodle</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text"><i class="bi bi-link-45deg"></i></span>
|
|
<input type="url" class="form-control" id="moodle_server_url" placeholder="http://10.0.0.207/moodle" value="{{ settings.moodle.server_url if settings and settings.moodle else 'http://10.0.0.207/moodle' }}" required>
|
|
</div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small fw-semibold">Token de Web Services (Cifrado)</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
|
<input type="password" class="form-control" id="moodle_ws_token" placeholder="••••••••••••" value="{{ settings.moodle.ws_token_masked if settings and settings.moodle else '' }}" required>
|
|
<button class="btn btn-outline-secondary" type="button" onclick="togglePassVisibility('moodle_ws_token')">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-6">
|
|
<label class="form-label small fw-semibold">Timeout (segundos)</label>
|
|
<input type="number" class="form-control form-control-sm" id="moodle_timeout" value="{{ settings.moodle.timeout if settings and settings.moodle else '10' }}">
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small fw-semibold">Intervalo Cron (min)</label>
|
|
<input type="number" class="form-control form-control-sm" id="moodle_sync_interval" value="{{ settings.moodle.sync_interval_minutes if settings and settings.moodle else '15' }}">
|
|
</div>
|
|
</div>
|
|
<div class="form-check form-switch mb-3">
|
|
<input class="form-check-input" type="checkbox" id="moodle_auto_sync" {% if not settings or not settings.moodle or settings.moodle.auto_sync_enabled %}checked{% endif %}>
|
|
<label class="form-check-label small fw-semibold" for="moodle_auto_sync">Habilitar Sincronización Automática en Background</label>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-sm btn-primary" id="btnSaveMoodle">
|
|
<i class="bi bi-save me-1"></i>Guardar Parámetros
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="testMoodleConn()" id="btnTestMoodle">
|
|
<i class="bi bi-broadcast me-1"></i>Probar Conexión
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-6">
|
|
<div class="card border p-3 rounded-3 h-100 bg-body-tertiary">
|
|
<h6 class="fw-bold mb-3 d-flex align-items-center">
|
|
<i class="bi bi-bar-chart-steps text-info me-2"></i>Monitor de Cola & Tolerancia a Fallos
|
|
</h6>
|
|
<p class="small text-muted mb-3">
|
|
Las creaciones de usuarios, matriculaciones y asignaciones no bloquean a Edu-Space. Se encolan y reintentan con <strong>Exponential Backoff</strong>. Si superan los intentos máximos, van a la <strong>Dead Letter Queue (DLQ)</strong>.
|
|
</p>
|
|
|
|
<div class="row g-2 mb-3 text-center">
|
|
<div class="col-4">
|
|
<div class="p-2 border rounded bg-body">
|
|
<div class="fs-4 fw-bold text-primary" id="statPending">0</div>
|
|
<small class="text-muted" style="font-size:11px;">Pendientes</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="p-2 border rounded bg-body">
|
|
<div class="fs-4 fw-bold text-warning" id="statRetrying">0</div>
|
|
<small class="text-muted" style="font-size:11px;">Reintentando</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="p-2 border rounded bg-body">
|
|
<div class="fs-4 fw-bold text-danger" id="statDlq">0</div>
|
|
<small class="text-muted" style="font-size:11px;">Fallidas (DLQ)</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<button class="btn btn-sm btn-primary flex-grow-1" onclick="processQueueNow()" id="btnProcessNow">
|
|
<i class="bi bi-lightning-charge me-1"></i>Forzar Sincronización Ahora
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="retryAllDlq()" id="btnRetryDlq">
|
|
<i class="bi bi-arrow-repeat me-1"></i>Reintentar Fallidas DLQ
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabla de Tareas Recientes / DLQ -->
|
|
<div class="card border-0 shadow-sm rounded-3">
|
|
<div class="card-header bg-transparent border-bottom d-flex justify-content-between align-items-center px-3 py-2">
|
|
<span class="fw-semibold small"><i class="bi bi-list-task me-1"></i>Tareas Recientes de Sincronización</span>
|
|
<div class="d-flex gap-2">
|
|
<select class="form-select form-select-sm" id="filterStatus" onchange="loadQueueTasks(1)" style="width: 140px;">
|
|
<option value="">Todas</option>
|
|
<option value="FAILED">Fallidas (DLQ)</option>
|
|
<option value="PENDING">Pendientes</option>
|
|
<option value="RETRYING">Reintentando</option>
|
|
<option value="COMPLETED">Completadas</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm align-middle mb-0" style="font-size: 12.5px;">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Acción</th>
|
|
<th>Entidad</th>
|
|
<th>Estado</th>
|
|
<th>Intentos</th>
|
|
<th>Detalle / Error</th>
|
|
<th>Fecha</th>
|
|
<th class="text-end">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="queueTasksTableBody">
|
|
<tr>
|
|
<td colspan="8" class="text-center py-3 text-muted">Cargando tareas de sincronización...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Prueba Conexión SMTP -->
|
|
<div class="modal fade" id="testSmtpModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<h6 class="modal-title fw-bold"><i class="bi bi-send-check text-primary me-2"></i>Prueba de Conexión SMTP</h6>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="small text-muted mb-3">Se verificará el handshake con el servidor SMTP y opcionalmente se enviará un correo real de prueba con diseño institucional.</p>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold">Correo de Destino para Prueba</label>
|
|
<input type="email" class="form-control form-control-sm" id="test_smtp_recipient" placeholder="su-email@unicaba.edu.ar">
|
|
</div>
|
|
<div id="testSmtpFeedback"></div>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
|
<button type="button" class="btn btn-sm btn-primary" onclick="runSmtpTest()" id="btnRunSmtpTest">
|
|
<i class="bi bi-play-fill me-1"></i>Ejecutar Prueba
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function showAlert(message, type = 'success') {
|
|
const container = document.getElementById('settingsAlertContainer');
|
|
container.innerHTML = `
|
|
<div class="alert alert-${type} alert-dismissible fade show py-2 px-3 small" role="alert">
|
|
<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-triangle'} me-2"></i>
|
|
${message}
|
|
<button type="button" class="btn-close py-2" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
`;
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
function togglePassVisibility(inputId) {
|
|
const input = document.getElementById(inputId);
|
|
input.type = input.type === 'password' ? 'text' : 'password';
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Guardar Configuración SMTP
|
|
// -------------------------------------------------------------
|
|
async function saveSmtpConfig(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('btnSaveSmtp');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
|
|
|
const payload = {
|
|
host: document.getElementById('smtp_host').value.trim(),
|
|
port: document.getElementById('smtp_port').value.trim(),
|
|
user: document.getElementById('smtp_user').value.trim(),
|
|
password: document.getElementById('smtp_password').value.trim(),
|
|
security: document.getElementById('smtp_security').value,
|
|
from_email: document.getElementById('smtp_from_email').value.trim(),
|
|
from_name: document.getElementById('smtp_from_name').value.trim()
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/admin/settings/smtp', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
showAlert(data.message || 'Configuración SMTP guardada exitosamente.', 'success');
|
|
} else {
|
|
showAlert(data.message || 'Error al guardar configuración SMTP.', 'danger');
|
|
}
|
|
} catch (err) {
|
|
showAlert('Error de comunicación con el servidor: ' + err.message, 'danger');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>Guardar Parámetros SMTP';
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Probar Conexión SMTP en Vivo
|
|
// -------------------------------------------------------------
|
|
async function runSmtpTest() {
|
|
const btn = document.getElementById('btnRunSmtpTest');
|
|
const fb = document.getElementById('testSmtpFeedback');
|
|
const recipient = document.getElementById('test_smtp_recipient').value.trim();
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Probando...';
|
|
fb.innerHTML = '<div class="text-muted small py-2"><span class="spinner-border spinner-border-sm me-1"></span>Conectando al servidor SMTP...</div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/admin/settings/smtp/test', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ test_recipient: recipient })
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
fb.innerHTML = `<div class="alert alert-success py-2 px-3 small mt-2 mb-0"><i class="bi bi-check-circle me-1"></i>${data.message}</div>`;
|
|
} else {
|
|
fb.innerHTML = `<div class="alert alert-danger py-2 px-3 small mt-2 mb-0"><i class="bi bi-x-circle me-1"></i>${data.message}</div>`;
|
|
}
|
|
} catch (err) {
|
|
fb.innerHTML = `<div class="alert alert-danger py-2 px-3 small mt-2 mb-0">Error: ${err.message}</div>`;
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-play-fill me-1"></i>Ejecutar Prueba';
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Guardar Métodos de Autenticación
|
|
// -------------------------------------------------------------
|
|
async function saveAuthProviders(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('btnSaveProviders');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
|
|
|
const payload = {
|
|
local_enabled: document.getElementById('auth_local_enabled').checked,
|
|
google_enabled: document.getElementById('auth_google_enabled').checked,
|
|
moodle_enabled: document.getElementById('auth_moodle_enabled').checked
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/admin/settings/auth-providers', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
showAlert(data.message || 'Métodos de acceso actualizados.', 'success');
|
|
} else {
|
|
showAlert(data.message || 'Error al actualizar métodos de acceso.', 'danger');
|
|
}
|
|
} catch (err) {
|
|
showAlert('Error de comunicación: ' + err.message, 'danger');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-save me-1"></i>Actualizar Métodos de Login';
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Alternar Modo Demo y Cuentas de Prueba Default
|
|
// -------------------------------------------------------------
|
|
async function toggleDemoMode() {
|
|
const btn = document.getElementById('btnToggleDemoMode');
|
|
const btnText = document.getElementById('btnToggleDemoText');
|
|
const badge = document.getElementById('demoModeBadge');
|
|
btn.disabled = true;
|
|
const isCurrentlyActive = btn.classList.contains('btn-outline-danger');
|
|
const targetEnable = !isCurrentlyActive;
|
|
|
|
try {
|
|
const res = await fetch('/admin/settings/demo-mode', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: JSON.stringify({ enabled: targetEnable })
|
|
});
|
|
const contentType = res.headers.get('content-type') || '';
|
|
let data = {};
|
|
if (contentType.includes('application/json')) {
|
|
data = await res.json();
|
|
} else {
|
|
const rawText = await res.text();
|
|
throw new Error(res.ok ? 'Respuesta no válida' : `Error del servidor (${res.status})`);
|
|
}
|
|
if (res.ok) {
|
|
showAlert(data.message, targetEnable ? 'info' : 'success');
|
|
if (targetEnable) {
|
|
btn.className = 'btn btn-sm btn-outline-danger';
|
|
btn.querySelector('i').className = 'bi bi-shield-slash me-1';
|
|
btnText.textContent = 'Deshabilitar Acceso Demo y Cuentas Default';
|
|
badge.className = 'badge bg-warning-subtle text-warning border border-warning-subtle';
|
|
badge.textContent = 'Activo en Login';
|
|
['badgeBedelia', 'badgeDocente', 'badgeAlumno'].forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (el) { el.className = 'badge bg-success-subtle text-success'; el.textContent = 'Habilitado'; }
|
|
});
|
|
} else {
|
|
btn.className = 'btn btn-sm btn-outline-success';
|
|
btn.querySelector('i').className = 'bi bi-shield-check me-1';
|
|
btnText.textContent = 'Reactivar Acceso Demo y Cuentas Default';
|
|
badge.className = 'badge bg-secondary-subtle text-secondary border border-secondary-subtle';
|
|
badge.textContent = 'Deshabilitado';
|
|
['badgeBedelia', 'badgeDocente', 'badgeAlumno'].forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (el) { el.className = 'badge bg-danger-subtle text-danger'; el.textContent = 'Desactivado'; }
|
|
});
|
|
}
|
|
} else {
|
|
showAlert(data.message || 'Error al cambiar modo demo', 'danger');
|
|
}
|
|
} catch (err) {
|
|
showAlert('Error de conexión: ' + err.message, 'danger');
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Guardar Google OAuth 2.0
|
|
// -------------------------------------------------------------
|
|
async function saveGoogleOAuth(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('btnSaveGoogle');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
|
|
|
const payload = {
|
|
client_id: document.getElementById('google_client_id').value.trim(),
|
|
client_secret: document.getElementById('google_client_secret').value.trim(),
|
|
allowed_domains: document.getElementById('google_allowed_domains').value.trim(),
|
|
callback_url: document.getElementById('google_callback_url').value.trim()
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/admin/settings/google-oauth', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
showAlert(data.message || 'Credenciales de Google OAuth guardadas.', 'success');
|
|
} else {
|
|
showAlert(data.message || 'Error al guardar Google OAuth.', 'danger');
|
|
}
|
|
} catch (err) {
|
|
showAlert('Error: ' + err.message, 'danger');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-check-circle me-1"></i>Guardar Credenciales Google';
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Guardar Configuración Moodle
|
|
// -------------------------------------------------------------
|
|
async function saveMoodleConfig(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('btnSaveMoodle');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
|
|
|
const payload = {
|
|
server_url: document.getElementById('moodle_server_url').value.trim(),
|
|
ws_token: document.getElementById('moodle_ws_token').value.trim(),
|
|
timeout: document.getElementById('moodle_timeout').value.trim(),
|
|
auto_sync_enabled: document.getElementById('moodle_auto_sync').checked,
|
|
sync_interval_minutes: document.getElementById('moodle_sync_interval').value.trim()
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/admin/settings/moodle', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
showAlert(data.message || 'Configuración Moodle guardada.', 'success');
|
|
} else {
|
|
showAlert(data.message || 'Error al guardar Moodle.', 'danger');
|
|
}
|
|
} catch (err) {
|
|
showAlert('Error: ' + err.message, 'danger');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-save me-1"></i>Guardar Parámetros';
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Probar Conexión Moodle
|
|
// -------------------------------------------------------------
|
|
async function testMoodleConn() {
|
|
const btn = document.getElementById('btnTestMoodle');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Probando...';
|
|
|
|
const payload = {
|
|
server_url: document.getElementById('moodle_server_url').value.trim(),
|
|
ws_token: document.getElementById('moodle_ws_token').value.trim()
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/admin/settings/moodle/test', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) {
|
|
showAlert(`Conexión exitosa con Moodle: Sitio '${data.sitename}' (Versión Moodle ${data.version || '4.1'})`, 'success');
|
|
} else {
|
|
showAlert(`Fallo de conexión Moodle: ${data.message || 'Verifique URL y Token'}`, 'danger');
|
|
}
|
|
} catch (err) {
|
|
showAlert('Error de conexión con Moodle: ' + err.message, 'danger');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-broadcast me-1"></i>Probar Conexión';
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// Monitor de Cola & DLQ
|
|
// -------------------------------------------------------------
|
|
async function refreshQueueStats() {
|
|
try {
|
|
const res = await fetch('/api/v1/admin/moodle/queue/stats');
|
|
const data = await res.json();
|
|
if (res.ok && data.data) {
|
|
const s = data.data.summary || {};
|
|
document.getElementById('statPending').innerText = (s.PENDING || 0) + (s.PROCESSING || 0);
|
|
document.getElementById('statRetrying').innerText = s.RETRYING || 0;
|
|
document.getElementById('statDlq').innerText = s.FAILED || 0;
|
|
}
|
|
await loadQueueTasks(1);
|
|
} catch (err) {
|
|
console.warn('Queue stats refresh notice:', err);
|
|
}
|
|
}
|
|
|
|
async function loadQueueTasks(page = 1) {
|
|
const tbody = document.getElementById('queueTasksTableBody');
|
|
const status = document.getElementById('filterStatus').value;
|
|
try {
|
|
const res = await fetch(`/api/v1/admin/moodle/queue/tasks?page=${page}&status=${status}`);
|
|
const data = await res.json();
|
|
if (!res.ok || !data.tasks || data.tasks.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-3 text-muted">No hay tareas de sincronización registradas.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.tasks.map(t => {
|
|
let statusBadge = '<span class="badge bg-secondary">PENDIENTE</span>';
|
|
if (t.status === 'COMPLETED') statusBadge = '<span class="badge bg-success">COMPLETADA</span>';
|
|
else if (t.status === 'PROCESSING') statusBadge = '<span class="badge bg-info text-dark">PROCESANDO</span>';
|
|
else if (t.status === 'RETRYING') statusBadge = '<span class="badge bg-warning text-dark">REINTENTANDO</span>';
|
|
else if (t.status === 'FAILED') statusBadge = '<span class="badge bg-danger">FALLIDA (DLQ)</span>';
|
|
|
|
const errText = t.error_message ? `<span class="text-danger" title="${escapeHtml(t.error_message)}">${escapeHtml(t.error_message.slice(0, 45))}...</span>` : '<span class="text-muted">-</span>';
|
|
const actionBtn = (t.status === 'FAILED' || t.status === 'RETRYING')
|
|
? `<button class="btn btn-xs btn-outline-primary" onclick="retrySingleTask(${t.id})" title="Reintentar"><i class="bi bi-arrow-repeat"></i></button>`
|
|
: '';
|
|
|
|
return `
|
|
<tr>
|
|
<td><strong>#${t.id}</strong></td>
|
|
<td><code>${escapeHtml(t.action)}</code></td>
|
|
<td>${escapeHtml(t.entity_type)}:${escapeHtml(t.entity_id || '')}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>${t.attempts}/${t.max_attempts}</td>
|
|
<td>${errText}</td>
|
|
<td><small class="text-muted">${t.created_at ? t.created_at.slice(0, 16) : '-'}</small></td>
|
|
<td class="text-end">${actionBtn}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
} catch (err) {
|
|
tbody.innerHTML = `<tr><td colspan="8" class="text-center py-3 text-danger">Error al cargar tareas: ${err.message}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
async function processQueueNow() {
|
|
const btn = document.getElementById('btnProcessNow');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Procesando...';
|
|
try {
|
|
const res = await fetch('/api/v1/admin/moodle/queue/process-now', { method: 'POST' });
|
|
const data = await res.json();
|
|
showAlert(data.message || 'Sincronización procesada.', res.ok ? 'success' : 'danger');
|
|
await refreshQueueStats();
|
|
} catch (err) {
|
|
showAlert('Error al disparar sincronización: ' + err.message, 'danger');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-lightning-charge me-1"></i>Forzar Sincronización Ahora';
|
|
}
|
|
}
|
|
|
|
async function retryAllDlq() {
|
|
if (!confirm('¿Confirma que desea reintentar todas las tareas fallidas de la Dead Letter Queue?')) return;
|
|
try {
|
|
const res = await fetch('/api/v1/admin/moodle/queue/retry-all', { method: 'POST' });
|
|
const data = await res.json();
|
|
showAlert(data.message || 'Tareas reiniciadas.', 'info');
|
|
await refreshQueueStats();
|
|
} catch (err) {
|
|
showAlert('Error: ' + err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
async function retrySingleTask(id) {
|
|
try {
|
|
const res = await fetch(`/api/v1/admin/moodle/queue/tasks/${id}/retry`, { method: 'POST' });
|
|
const data = await res.json();
|
|
showAlert(data.message || `Tarea #${id} reiniciada.`, 'info');
|
|
await refreshQueueStats();
|
|
} catch (err) {
|
|
showAlert('Error: ' + err.message, 'danger');
|
|
}
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/[&<>"']/g, function(m) {
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m];
|
|
});
|
|
}
|
|
|
|
// Cargar estadísticas y tareas al iniciar
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
refreshQueueStats();
|
|
});
|
|
</script>
|
|
{% endblock %}
|