# 05 - Operación y Diagnóstico

## Doctor de entorno

```bash
php bin/doctor           # Diagnóstico estándar
php bin/doctor --strict  # Con validaciones adicionales
```

### Qué valida

| Verificación | Detalle |
|---|---|
| Archivos de config | `Config.php`, `ConfigEnv.php`, `.env` existen y son legibles |
| Variables de BD | `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS` definidas |
| Variables SMTP | `MAIL_HOST`, `MAIL_USER`, `MAIL_PASS` definidas |
| Versión PHP | >= 8.0 |
| Extensiones PHP | `gd`, `mbstring`, `pdo`, `pdo_mysql`, `openssl` cargadas |
| Permisos de escritura | `storage/`, `logs/`, `files/` son escribibles |
| Classmap | Integridad del autoload fallback verificada |
| Conexión BD | Conexión PDO exitosa a la base de datos |

### Interpretación de salida

```
[ok]  Config.php exists
[ok]  ConfigEnv.php exists
[ok]  .env loaded
[ok]  DB variables set
[ok]  SMTP variables set
[ok]  PHP 8.2.12
[ok]  Extension: gd
...
[ok]  Directory writable: storage/
[ok]  Classmap integrity
[ok]  Database connection

errors=0 warnings=0
```

- **0 errors**: listo para desarrollo/producción.
- **warnings**: no bloqueantes pero deben atenderse.
- **errors**: el sistema no funcionará correctamente.

## Cache y classmap

```bash
php bin/cache status    # Muestra estado actual del cache
php bin/cache verify    # Verifica integridad del classmap
php bin/cache clear     # Limpia cache (regenera classmap si aplica)
```

**Flujo recomendado:**

1. `php bin/cache verify` — verificar si hay inconsistencias.
2. Solo si hay problemas: `php bin/cache clear` — limpiar y regenerar.
3. `php bin/cache status` — confirmar estado limpio.

## Smoke tests

```bash
php bin/smoke           # Ejecutar tests HTTP
php bin/smoke --json    # Salida en formato JSON
```

### Variables de entorno para smoke tests

| Variable | Descripción | Default |
|---|---|---|
| `SMOKE_BASE_URL` | URL base de la aplicación | `http://localhost` |
| `SMOKE_USER` | Usuario para login | *(vacío)* |
| `SMOKE_PASS` | Contraseña para login | *(vacío)* |
| `SMOKE_TIMEOUT` | Timeout en segundos | `30` |
| `SMOKE_JSON` | Forzar salida JSON | `false` |
| `SMOKE_JSON_FILE` | Guardar resultado en archivo | *(vacío)* |

### Qué verifica

- Conectividad HTTP al entry point.
- Login funcional (si se proporcionan credenciales).
- Endpoints principales accesibles.
- Códigos de respuesta esperados.

## Logs

Los logs se guardan en `logs/`. El archivo `logs/viewer.html` permite visualizarlos en el navegador.

### Tipos de log

| Componente | Descripción |
|---|---|
| `LoggerManager` | Interfaz unificada de logging |
| `LoggerConfig` | Configuración de niveles y destinos |
| `LogsConsole` | Logs de depuración en consola del navegador |
| `Logger` | Logger base del framework |

### Activación por .env

```dotenv
LOG_ACTIONS=true   # Registrar acciones de usuario
LOG_MODULES=true   # Registrar CRUD en módulos (HasAuditLog)
LOG_ACCESS=true    # Registrar login/logout
```

## Runbook de incidencias

| Paso | Comando | Propósito |
|---|---|---|
| 1 | `php bin/doctor` | Diagnosticar estado general |
| 2 | `php bin/cache verify` | Verificar integridad de classmap |
| 3 | Revisar `logs/` | Buscar errores recientes |
| 4 | Verificar `.env` | Credenciales BD y SMTP correctas |
| 5 | `php bin/migrate status` | Verificar migraciones pendientes |
| 6 | `php bin/smoke` | Verificar conectividad HTTP (si aplica) |

## Comandos CLI — Referencia rápida

```bash
# Scaffolding
php bin/make module NombreModulo [--basic] [--no-migration] [--category=X]
php bin/make seeder NombreSeeder

# Base de datos
php bin/migrate up        # Ejecutar migraciones pendientes
php bin/migrate down      # Revertir última migración
php bin/migrate status    # Ver estado de migraciones

# Seeders
php bin/seed              # Ejecutar todos en orden
php bin/seed NombreSeeder # Ejecutar uno específico
php bin/seed --list       # Listar disponibles

# Cache
php bin/cache status
php bin/cache verify
php bin/cache clear

# Diagnóstico
php bin/doctor [--strict]
php bin/smoke [--json]
php bin/guac-smoke [--json]   # Salud de la integración de acceso remoto

# Workers de negocio (requieren cron — ver más abajo)
php bin/expire-citas [--dry-run] [--json]
php bin/email-queue [--dry-run] [--json] [--limit=N]
php bin/waitlist expire
php bin/analytics-refresh [--dry-run] [--json]
php bin/clientes-bloqueo status|block|unblock|list
php bin/recurring create|list|instances|show|cancel
```

---

## Trabajos programados (cron)

> **Sin estas entradas el sistema queda funcionalmente congelado.** No es una
> exageración operativa: el estado `4 = No asistió` **sólo lo genera
> `expire-citas`**, así que sin cron la tasa de no-show mide un fenómeno que
> nunca se materializa; y sin `email-queue` nadie recibe confirmación,
> aprobación, negación ni cancelación de su reserva.

### Entradas mínimas

```cron
# Ciclo de vida de reservas: expira pendientes, marca no-shows,
# aplica sanciones automáticas y notifica la lista de espera.
*/5 * * * * php /ruta/timeklee2/bin/expire-citas --json >> /var/log/timeklee/expire-citas.log 2>&1

# Despacho de la cola de correo. Tope por corrida: CORREOS_DIARIOS (.env).
*/5 * * * * php /ruta/timeklee2/bin/email-queue --json >> /var/log/timeklee/email-queue.log 2>&1

# Avance de la lista de espera. El intervalo debe ser <= WAITLIST_RESERVE_GRACE_MIN.
*/10 * * * * php /ruta/timeklee2/bin/waitlist expire >> /var/log/timeklee/waitlist.log 2>&1

# Capa analítica: dim_tiempo, capacidad ofertada y agregado de ocupación.
30 2 * * * php /ruta/timeklee2/bin/analytics-refresh --json >> /var/log/timeklee/analytics.log 2>&1

# Healthcheck del acceso remoto (opcional pero recomendado).
*/15 * * * * php /ruta/timeklee2/bin/guac-smoke --json >> /var/log/timeklee/guac-smoke.log 2>&1
```

### Cadencia y efecto de cada worker

| Worker | Cadencia | Qué pasa si no corre |
|---|---|---|
| `expire-citas` | cada 5 min | El no-show nunca se materializa; las pre-reservas vencidas siguen bloqueando cupo; no hay sanciones automáticas |
| `email-queue` | cada 5 min | El sistema es silencioso: ninguna notificación sale |
| `waitlist expire` | cada 10 min | Un cupo ofrecido y no reclamado bloquea la cola indefinidamente |
| `analytics-refresh` | nocturno | Los dashboards muestran ocupación obsoleta o vacía (`ocupacion_pct` en NULL) |
| `guac-smoke` | cada 15 min | Nadie detecta que el acceso remoto está caído antes que un estudiante |

### Verificación sin efectos secundarios

Todos aceptan `--dry-run` y `--json`, así que sirven como sonda de monitoreo:

```bash
php bin/expire-citas --dry-run --json      # pending_expired, confirmed_no_show, overdue_return_warned...
php bin/email-queue --dry-run --json       # pendientes en cola sin enviar nada
php bin/analytics-refresh --dry-run --json
```

`email-queue` y `analytics-refresh` devuelven **exit code distinto de 0 en fallo**
(2 = error de ejecución; `analytics-refresh` usa además 3 = otra corrida en curso),
de modo que se integran directamente con un monitor.

---

## Capa analítica

`bin/analytics-refresh` materializa lo que leen los dashboards. **Ninguna
pantalla debe recalcular ocupación en caliente sobre el OLTP.**

### Qué produce

| Tabla | Grano | Contenido |
|---|---|---|
| `dim_tiempo` | un día | Calendario con periodo académico, semana del periodo, festivos y días hábiles |
| `agenda_slots_ofertados` | agenda × fecha × franja | Capacidad ofertada, con el motivo de no disponibilidad (`festivo`, `bloqueo`, `agenda_inactiva`) |
| `agg_ocupacion_dia` | agenda × fecha | Oferta, demanda y `OcupacionPct` — lo que consumen los reportes |
| `analytics_refresh_log` | una corrida | Frescura y resultado de cada refresco |

### Opciones

```bash
php bin/analytics-refresh                                   # -180 a +90 días
php bin/analytics-refresh --desde=2026-01-01 --hasta=2026-12-31
php bin/analytics-refresh --dias-atras=365 --dias-adelante=120
php bin/analytics-refresh --only=dim_tiempo                  # o --only=capacidad
php bin/analytics-refresh --tenant=2
```

Variables de entorno: `ANALYTICS_DIAS_ATRAS` (180), `ANALYTICS_DIAS_ADELANTE`
(90), `ANALYTICS_TENANT_ID` (1). El horizonte **futuro** es el que habilita el
dashboard de disponibilidad, así que no conviene ponerlo en 0.

### Dos comportamientos deliberados

1. **`OcupacionPct` puede venir NULL.** Significa «no había capacidad ofertada»,
   que no es lo mismo que «nadie la ocupó». Debe mostrarse como *sin datos*,
   nunca como 0 %.
2. **`OcupacionPct` puede superar el 100 %.** No se recorta: indica reservas
   fuera del horario ofertado, típicamente préstamos inmediatos creados con la
   hora del reloj. Es un hallazgo operativo, no un error de cálculo.

### Frescura

```sql
SELECT Agregado, MAX(TerminadoEn) AS ultima_corrida, Estado
FROM analytics_refresh_log GROUP BY Agregado, Estado;
```

Toda pantalla que publique ocupación debería mostrar «datos al …» tomado de
`agg_ocupacion_dia.FechaCalculo`.

### Cuándo forzar un refresco manual

Tras editar el horario, el intervalo o los días de una agenda, tras cargar
festivos, y tras crear o levantar un bloqueo: esas tres cosas cambian el
denominador. El refresco nocturno lo recoge, pero si se necesita al instante:

```bash
php bin/analytics-refresh --only=capacidad --desde=$(date +%F) --hasta=$(date -d '+90 days' +%F)
```
