# ✉️ Guía de Implementación: Sistema Modular de Envío Masivo de Correos (SSE + Modal Institucional)
### Convocatorias FOCINE / IMCINE — Gobierno de México

Esta guía documenta detalladamente la arquitectura técnica, el código fuente canónico y el procedimiento de replicación paso a paso para implementar de forma uniforme el **Sistema de Envío Masivo de Correos Electrónicos con Server-Sent Events (SSE) y Modal Institucional (SND / Sisdai / Gob.mx)** en las convocatorias de FOCINE/IMCINE:

1. **`acervos`**
2. **`animacion`**
3. **`cortometraje`**
4. **`documental`**
5. **`estados`**
6. **`exhibicion_equipamientos`**
7. **`exhibicion_programas`**
8. **`experimental`**
9. **`ficcion`**
10. **`infancias`**
11. **`operasprimas`**
12. **`postproduccion`**

---

## 📑 Índice
1. [Resumen y Arquitectura Técnica](#1-resumen-y-arquitectura-técnica)
2. [Capa 1: Activos Aislados Centralizados (`api_imcine_v2` / `api_imcine_v4`)](#2-capa-1-activos-aislados-centralizados-api_imcine_v2--api_imcine_v4)
3. [Capa 2: Motor Backend SSE (`envio_masivo_stream.php`)](#3-capa-2-motor-backend-sse-envio_masivo_streamphp)
4. [Capa 3: Integración Front-End en `lista_proyectos.php`](#4-capa-3-integración-front-end-en-lista_proyectosphp)
5. [Consideraciones Críticas y Reglas de Negocio](#5-consideraciones-críticas-y-reglas-de-negocio)
6. [Script de Replicación Masiva Automatizada](#6-script-de-replicación-masiva-automatizada)
7. [Prompt Maestro para Replicación con Agentes de Desarrollo](#7-prompt-maestro-para-replicación-con-agentes-de-desarrollo)

---

## 1. Resumen y Arquitectura Técnica

El sistema sustituye el envío tradicional sincrónico (que provocaba bloqueos en PHP, timeouts de servidor, desconexiones y falta de retroalimentación al administrador) por una transmisión unidireccional en tiempo real vía **Server-Sent Events (SSE)**.

```
┌────────────────────────────────────────────────────────────────────────────────────────┐
│                               NAVEGADOR (ADMINISTRADOR)                                │
│                                                                                        │
│   [ Botón 'Enviar correo(s)' ]                                                         │
│               │                                                                        │
│               ▼                                                                        │
│   confiramcionEnvio() en envio_correos.js                                              │
│               │                                                                        │
│               ├── 1. Guarda en localStorage (persistencia ante recarga de página)      │
│               ├── 2. Activa listener beforeunload (blindaje de pestaña)                │
│               ├── 3. Abre #modalEnvioMasivo con backdrop estático y teclado bloqueado │
│               └── 4. Inicia conexión EventSource('envio_masivo_stream.php?...')        │
└───────────────────────────────────────────┬────────────────────────────────────────────┘
                                            │ HTTP GET (SSE Stream)
                                            ▼
┌────────────────────────────────────────────────────────────────────────────────────────┐
│                         SERVIDOR PHP (envio_masivo_stream.php)                         │
│                                                                                        │
│   1. session_write_close()  -> Libera el bloqueo de sesión inmediatamente              │
│   2. ignore_user_abort(true) / set_time_limit(0)                                      │
│   3. Headers SSE + Anti-buffering (Padding 2KB, no-gzip, ob_implicit_flush)            │
│   4. Obtiene lista de proyectos (con soporte opcional de categoria_filtro)             │
│   5. Procesa en lotes de 50 correos:                                                   │
│      ├── Emite evento SSE 'progreso' correo por correo                                 │
│      └── Si hay más proyectos: Emite evento 'espera' y pausa 121 seg (Anti-Spam)       │
│   6. Al terminar todos los lotes: Emite evento SSE 'fin' y cierra stream               │
└────────────────────────────────────────────────────────────────────────────────────────┘
```

---

## 2. Capa 1: Activos Aislados Centralizados (`api_imcine_v2` / `api_imcine_v4`)

Para no alterar los archivos base `estilo.css` ni `user_admin.js` de las APIs centrales, el sistema se diseñó en tres componentes completamente autónomos:

### 2.1 Hoja de Estilos Institucional: `envio_correos.css`
Ubicación:
- `/home/convocatoriasdev/public_html/api_imcine_v2/css_base/envio_correos.css`
- `/home/convocatoriasdev/public_html/api_imcine_v4/css_base/envio_correos.css`

**Características clave:**
- Embebe la tipografía oficial `Montserrat` desde Google Fonts.
- Define las variables `:root` de GobMX (`--gobmx-guinda: #691C32`, `--gobmx-dorado: #BC955C`, `--gobmx-verde: #103923`, etc.).
- Reglas específicas para `#modalEnvioMasivo`, barra de progreso con gradiente institucional, cuadrícula de 4 tarjetas estadísticas (`Enviados`, `Errores`, `Pendientes`, `Total`), aviso de cuenta regresiva con animación y botón institucional `#modalEnvio_btnCerrar`.

### 2.2 Lógica en JavaScript: `envio_correos.js`
Ubicación:
- `/home/convocatoriasdev/public_html/api_imcine_v2/js/envio_correos.js`
- `/home/convocatoriasdev/public_html/api_imcine_v4/js/envio_correos.js`

**Funciones principales:**
- `_bloquearSalidaEnvio(e)`: Manejador de `beforeunload` para advertir si el usuario intenta cerrar o recargar la pestaña mientras hay un envío en curso.
- `_modalEnvio_reset()`: Restaura contadores, barra de progreso y log al estado inicial (0%).
- `_modalEnvio_addLog(tipo, correo, nombre, exito)`: Inserta eventos en el visor de actividad con badges SVG / colores y autoscroll hacia el final.
- `_modalEnvio_setProgress(pct)` y `_modalEnvio_setContadores(enviados, errores, total)`.
- `_modalEnvio_setEspera(mensaje, segundos)`: Activa el countdown dinámico segundo a segundo entre lotes.
- `_conectarStreamEnvio(estatus, admin, categoria)`: Instancia `EventSource` hacia `envio_masivo_stream.php`.
- `_abrirModalEnvioMasivo()` / `_cerrarModalEnvioMasivo()`: Compatibles con Bootstrap 3/4 (`$.fn.modal`), Bootstrap 5 nativo (`bootstrap.Modal`) y fallback CSS puro.
- `_verificarEnvioMasivoPersistente()`: Ejecutado en `$(document).ready()`. Si el usuario recargó la página y el envío comenzó hace menos de 2 horas, reconecta la visualización en tiempo real automáticamente sin reiniciar el proceso en servidor.

### 2.3 Plantilla Modular del Modal: `modal_envio_masivo.html`
Ubicación:
- `/home/convocatoriasdev/public_html/api_imcine_v2/modal_envio_masivo.html`
- `/home/convocatoriasdev/public_html/api_imcine_v4/modal_envio_masivo.html`

Estructura institucional con encabezado FOCINE/IMCINE, ícono oficial, cuerpo modular y botón de cierre.

---

## 3. Capa 2: Motor Backend SSE (`envio_masivo_stream.php`)

Este archivo reside en la raíz de cada convocatoria: `/home/convocatoriasdev/public_html/focine/{convocatoria}/envio_masivo_stream.php`.

### Código Canónico:
```php
<?php
/**
 * envio_masivo_stream.php
 * Endpoint Server-Sent Events (SSE) para envío masivo de correos.
 * Transmite el progreso en tiempo real al navegador sin bloquear la UI.
 *
 * Parámetros GET esperados:
 *   - estatus_proyecto : ID del estatus a procesar
 *   - administrador    : nombre del admin en sesión (validado contra $_SESSION)
 *   - categoria_filtro : ID de categoría (opcional)
 */

// ── 1. Sesión y seguridad ────────────────────────────────────────────────────
session_start();

if (empty($_SESSION['ses'])) {
    http_response_code(403);
    exit('Acceso no autorizado');
}

// Liberar bloqueo de sesión inmediatamente para permitir comunicación concurrente
session_write_close();

// ── 2. Cabeceras SSE y persistencia de ejecución ────────────────────────────
ignore_user_abort(true);
set_time_limit(0);

if (function_exists('apache_setenv')) {
    @apache_setenv('no-gzip', '1');
}
@ini_set('zlib.output_compression', '0');
@ini_set('implicit_flush', '1');

while (ob_get_level() > 0) {
    @ob_end_flush();
}
ob_implicit_flush(true);

header('Content-Type: text/event-stream; charset=UTF-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
header('X-Accel-Buffering: no');
header('Content-Encoding: none');

// Enviar padding inicial (2KB) para forzar al proxy a abrir el canal SSE inmediatamente
echo ":" . str_repeat(" ", 2048) . "\n\n";
@flush();

// ── 3. Dependencias del proyecto ─────────────────────────────────────────────
$rutaPHP = getcwd() . DIRECTORY_SEPARATOR . "backEnd" . DIRECTORY_SEPARATOR;
require_once $rutaPHP . "utilerias"  . DIRECTORY_SEPARATOR . "herramientas.php";
require_once $rutaPHP . "CONTROL"    . DIRECTORY_SEPARATOR . "control_sistema.php";
require_once $rutaPHP . "CONTROL"    . DIRECTORY_SEPARATOR . "control_convocatoria.php";

$tools              = new Herramientas();
$ctrlSys            = new ClaseControlSistema();
$templateCorreo     = new TemplatesCorreos();
$correo             = new CorreoElectronico();
$dao                = new ClaseDaoSistema();
$daoConvocatoria    = new ClaseDaoConvocatoria();
$validacionSistema  = new ValidacionSistema();

// ── 4. Helper: emitir evento SSE ─────────────────────────────────────────────
function sseEmit(string $type, array $payload): void
{
    $json = json_encode($payload, JSON_UNESCAPED_UNICODE);
    echo "event: {$type}\n";
    echo "data: {$json}\n\n";
    while (ob_get_level() > 0) {
        @ob_end_flush();
    }
    @flush();
}

// ── 5. Validar parámetros ────────────────────────────────────────────────────
$estatus_proyecto = isset($_GET['estatus_proyecto']) ? intval($_GET['estatus_proyecto']) : 0;
$administrador    = isset($_GET['administrador'])    ? trim($_GET['administrador'])       : '';
$categoria_filtro = isset($_GET['categoria_filtro'])  ? trim($_GET['categoria_filtro'])    : '';

if ($estatus_proyecto === 0) {
    sseEmit('error', ['mensaje' => 'Estatus de proyecto no válido.']);
    exit;
}

// ── 6. Parámetros del correo según estatus ───────────────────────────────────
$arrayVmail = $validacionSistema->asignacionParametrosCorreo($estatus_proyecto);
$etiquetas  = $ctrlSys->etiquetasDocumentosProyecto();

// ── 7. Obtener proyectos (Compatibilidad defensiva con filtros de categoría) ──
if (!empty($categoria_filtro) && method_exists($daoConvocatoria, 'datos_proyectos_registrados_estatus_categoria')) {
    $proyectos = $daoConvocatoria->datos_proyectos_registrados_estatus_categoria($estatus_proyecto, $categoria_filtro);
} else {
    $proyectos = $daoConvocatoria->datos_proyectos_registrados_estatus($estatus_proyecto);
}
$total = is_array($proyectos) ? count($proyectos) : 0;

if ($total === 0) {
    sseEmit('fin', [
        'mensaje'  => 'No se encontraron proyectos con el estatus seleccionado.',
        'enviados' => 0,
        'errores'  => 0,
        'total'    => 0,
    ]);
    exit;
}

sseEmit('progreso', [
    'mensaje'  => "Se encontraron {$total} proyecto(s). Iniciando envío...",
    'enviados' => 0,
    'errores'  => 0,
    'total'    => $total,
]);

// ── 8. Configuración de lotes ────────────────────────────────────────────────
$lote     = 50;   // Correos por lote (anti-spam)
$espera   = 121;  // Segundos de pausa entre lotes
$enviados = 0;
$errores  = 0;

// ── 9. Loop principal de envío ───────────────────────────────────────────────
for ($i = 0; $i < $total; $i += $lote) {

    $proyectos_lote  = array_slice($proyectos, $i, $lote);
    $num_lote_actual = intdiv($i, $lote) + 1;
    $total_lotes     = ceil($total / $lote);

    foreach ($proyectos_lote as $proyecto) {

        $nombre_responsable =
            ($proyecto["razon_social"] ?? '')             . ' ' .
            ($proyecto["representante_nombre"] ?? '')      . ' ' .
            ($proyecto["representante_ap_paterno"] ?? '')  . ' ' .
            ($proyecto["representante_ap_materno"] ?? '');
        $nombre_responsable = trim($nombre_responsable);

        $id_proyecto = $proyecto["id_proyecto"] ?? '';
        $correo_dest = $proyecto["correo_responsable"] ?? '';

        $arreglo = [];
        $arreglo["asunto"]            = $arrayVmail["asunto_mail"] ?? '';
        $arreglo["titulo"]            = $arrayVmail["titulo_mail"] ?? '';
        $arreglo["nombre_responsable"]= $nombre_responsable;
        $arreglo["destinatario"]      = $nombre_responsable;
        $arreglo["titulo_proyecto"]   = $proyecto["nombre_proyecto"] ?? '';
        $arreglo["correo"]            = $correo_dest;
        $arreglo["tipo_persona"]      = $proyecto["persona_fiscal"] ?? '';
        $arreglo["administrador"]     = $administrador;

        $comentarios           = $ctrlSys->comentarios_documentos($id_proyecto);
        $arreglo["contenido"]  = $ctrlSys->contenidoSeccion($arrayVmail["contenido"] ?? '');
        $arreglo["pie_correo"] = $ctrlSys->contenidoSeccion(defined('PIE_CORREO') ? PIE_CORREO : '');

        if (
            (defined('EN_CORRECCION') && $estatus_proyecto == EN_CORRECCION) ||
            (defined('NO_COMPLETO') && $estatus_proyecto == NO_COMPLETO)   ||
            (defined('DESCALIFICADO') && $estatus_proyecto == DESCALIFICADO) ||
            (defined('NO_COMPLETO_2DA') && $estatus_proyecto == NO_COMPLETO_2DA)
        ) {
            $template = $templateCorreo->template_observaciones($etiquetas, $comentarios, $arreglo);
        } else {
            $template = $templateCorreo->template_correo_usuario($arreglo);
        }

        $respuestaEnvio = $correo->enviar($template, $arreglo);

        if ($respuestaEnvio == true) {
            $enviados++;
            $mensaje_log = "Correo enviado";
            $dao->registroEnvioComentarios($id_proyecto);
        } else {
            $errores++;
            $mensaje_log = "Error en el envío";
        }

        $dao->logTransaccion([
            "usuario"      => $administrador,
            "descripcion"  => ($arrayVmail["comentario_log"] ?? 'Envío de correo') .
                              " " . $nombre_responsable .
                              " (" . $mensaje_log . " en grupo)."
        ]);

        sseEmit('progreso', [
            'id_proyecto'   => $id_proyecto,
            'correo'        => $correo_dest,
            'nombre'        => $nombre_responsable,
            'exito'         => ($respuestaEnvio == true),
            'enviados'      => $enviados,
            'errores'       => $errores,
            'total'         => $total,
            'lote_actual'   => $num_lote_actual,
            'total_lotes'   => $total_lotes,
        ]);

    } // fin foreach lote

    $quedan_por_enviar = $total - ($i + $lote);

    if ($quedan_por_enviar > 0) {
        sseEmit('espera', [
            'mensaje'       => "Lote {$num_lote_actual} de {$total_lotes} completado. " .
                               "Por favor espere, en un minuto se reanudará el envío de correos " .
                               "hasta finalizar ({$quedan_por_enviar} restante(s)).",
            'segundos'      => $espera,
            'enviados'      => $enviados,
            'errores'       => $errores,
            'total'         => $total,
            'lote_actual'   => $num_lote_actual,
            'total_lotes'   => $total_lotes,
        ]);
        sleep($espera);
    }

} // fin for lotes

// ── 10. Evento final ─────────────────────────────────────────────────────────
sseEmit('fin', [
    'mensaje'  => "Envío completado. {$enviados} correo(s) enviado(s), {$errores} error(es).",
    'enviados' => $enviados,
    'errores'  => $errores,
    'total'    => $total,
]);
```

---

## 4. Capa 3: Integración Front-End en `lista_proyectos.php`

En cada una de las 12 convocatorias, se debe asegurar lo siguiente en `lista_proyectos.php`:

### 4.1 Inclusión Modular en el Pie de la Vista (Antes de `footer.php`)
```php
<!-- ==========================================================================
     MODAL INSTITUCIONAL DE ENVÍO MASIVO DE CORREOS (SSE) — IMCINE / FOCINE
     ========================================================================== -->
<link href="<?php echo DIRECCION_CSS . 'envio_correos.css?v=' . time(); ?>" rel="stylesheet" type="text/css">

<?php 
$modal_envio_html = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . (defined('CARPETA_API') ? CARPETA_API : 'api_imcine_v2') . DIRECTORY_SEPARATOR . 'modal_envio_masivo.html';
if (file_exists($modal_envio_html)) {
    include($modal_envio_html);
}
?>

<script src="<?php echo DIRECCION_JS . 'envio_correos.js?v=' . time(); ?>"></script>

<?php include ("footer.php");  ?>
```

### 4.2 Eliminación de Overrides Incompatibles en JavaScript
En convocatorias con código heredado (ej. `animacion`, `infancias`, `postproduccion`), existían funciones inline que forzaban el submit síncrono del formulario:
```javascript
// ❌ ELIMINAR ESTE BLOQUE SI EXISTE EN lista_proyectos.php:
window.confiramcionEnvio = function() {
    ...
    if (confirm(mensaje)) {
        $('#form_lista_proyectos').submit(); // Bloqueaba la apertura del modal SSE
    }
};
```
Al retirar dicho bloque, la llamada `onClick="confiramcionEnvio();"` del botón `#btn_send_mail` ejecuta de manera limpia la función provista por `envio_correos.js`.

---

## 5. Consideraciones Críticas y Reglas de Negocio

1. **Liberación Inmediata de Sesión PHP (`session_write_close()`):**  
   Si no se cierra la sesión en `envio_masivo_stream.php`, el archivo de sesión de PHP se mantiene bloqueado durante los minutos que dure el envío, impidiendo que el administrador abra cualquier otra pestaña del sistema.
2. **Padding Inicial Anti-Proxy (2048 bytes):**  
   Algunos proxies inversos y navegadores no comienzan a despachar eventos SSE hasta recibir una ráfaga inicial de datos en el buffer. El comentario SSE `": " . str_repeat(" ", 2048) . "\n\n"` resuelve este problema.
3. **Pausa Anti-Spam (121 segundos):**  
   Los servidores de correo corporativo e institucional imponen límites de tasa (rate limiting). Procesar en bloques de 50 con pausa de 121 segundos previene que la IP de IMCINE caiga en listas negras (RBL).
4. **Verificación Defensiva de Métodos DAO:**  
   No todas las convocatorias manejan categorías (por ejemplo, `acervos`, `documental` o `estados` no dividen en Cortometraje/Largometraje). Usar `method_exists($daoConvocatoria, 'datos_proyectos_registrados_estatus_categoria')` evita que PHP lance errores fatales en convocatorias de categoría única.

---

## 6. Script de Replicación Masiva Automatizada

Para replicar o validar esta implementación en las 12 convocatorias de FOCINE en cualquier servidor o entorno de staging:

```bash
#!/bin/bash
# ==============================================================================
# Script de Despliegue Masivo: Envio Masivo SSE en Convocatorias FOCINE
# ==============================================================================

CONVOCATORIAS=(
  "acervos"
  "animacion"
  "cortometraje"
  "documental"
  "estados"
  "exhibicion_equipamientos"
  "exhibicion_programas"
  "experimental"
  "ficcion"
  "infancias"
  "operasprimas"
  "postproduccion"
)

BASE_DIR="/home/convocatoriasdev/public_html/focine"
API_V2="/home/convocatoriasdev/public_html/api_imcine_v2"

echo "=== 1. Verificando Activos Centralizados en api_imcine_v2 ==="
for file in "$API_V2/css_base/envio_correos.css" "$API_V2/js/envio_correos.js" "$API_V2/modal_envio_masivo.html"; do
  if [ -f "$file" ]; then
    echo "  ✔ Existe: $file"
  else
    echo "  ❌ Falta archivo centralizado: $file"
    exit 1
  fi
done

echo ""
echo "=== 2. Replicando envio_masivo_stream.php en las 12 Convocatorias ==="
for conv in "${CONVOCATORIAS[@]}"; do
  TARGET="$BASE_DIR/$conv/envio_masivo_stream.php"
  cp "$BASE_DIR/infancias_2027/envio_masivo_stream.php" "$TARGET"
  chmod 644 "$TARGET"
  php -l "$TARGET" >/dev/null && echo "  ✔ $conv: envio_masivo_stream.php verificado (Sintaxis OK)"
done

echo ""
echo "=== 3. Validación de Sintaxis en lista_proyectos.php ==="
for conv in "${CONVOCATORIAS[@]}"; do
  VIEW="$BASE_DIR/$conv/lista_proyectos.php"
  php -l "$VIEW" >/dev/null && echo "  ✔ $conv: lista_proyectos.php (Sintaxis OK)"
done

echo ""
echo "✔ Proceso de replicación y verificación completado con éxito."
```

---

## 7. Prompt Maestro para Replicación con Agentes de Desarrollo

Puedes copiar y pegar este prompt en cualquier sesión de trabajo con un agente de IA para reproducir la implementación de forma autónoma:

```markdown
# Prompt Maestro: Implementación del Sistema de Envío Masivo de Correos (SSE + Modal) en FOCINE

Actúa como Ingeniero de Software Senior en PHP y desarrollo web institucional para IMCINE / Gobierno de México.

## Objetivo
Debes implementar de forma homogénea y modular el Sistema de Envío Masivo de Correos con Server-Sent Events (SSE) y Modal Institucional en las 12 convocatorias de FOCINE:
1. `acervos`
2. `animacion`
3. `cortometraje`
4. `documental`
5. `estados`
6. `exhibicion_equipamientos`
7. `exhibicion_programas`
8. `experimental`
9. `ficcion`
10. `infancias`
11. `operasprimas`
12. `postproduccion`
13. `infancias_2027`

## Referencia Técnica
Guíate por:
- Guía oficial: `/home/convocatoriasdev/public_html/focine/GUIA_IMPLEMENTACION_ENVIO_CORREOS_MASIVOS.md`
- Proyecto de referencia: `/home/convocatoriasdev/public_html/focine/infancias_2027/`
- Activos aislados: `/home/convocatoriasdev/public_html/api_imcine_v2/css_base/envio_correos.css`, `/home/convocatoriasdev/public_html/api_imcine_v2/js/envio_correos.js` y `/home/convocatoriasdev/public_html/api_imcine_v2/modal_envio_masivo.html`.

## Pasos Requeridos por Convocatoria:
1. Asegurar `envio_masivo_stream.php` en la raíz de cada convocatoria con `session_write_close()`, buffers desactivados, padding inicial de 2KB, procesamiento en lotes de 50 y pausa de 121s.
2. En `lista_proyectos.php`:
   - Enlazar `<link href="<?php echo DIRECCION_CSS . 'envio_correos.css?v=' . time(); ?>" rel="stylesheet" type="text/css">`.
   - Incluir dinámicamente `modal_envio_masivo.html`.
   - Enlazar `<script src="<?php echo DIRECCION_JS . 'envio_correos.js?v=' . time(); ?>"></script>`.
   - Remover cualquier función local `window.confiramcionEnvio` que fuerce submit tradicional por POST.
3. Validar con `php -l` todos los archivos modificados.
```
