# Parte 1 — Mapa funcional del sistema (FASE 1)

> Objetivo de esta parte: entender el negocio como si hubiera que venderlo y operarlo. Qué hace cada módulo, cómo se relacionan, y cuáles son los flujos reales de punta a punta.

---

## 1.1 Arquitectura en una imagen

```
                        ┌──────────────────────────────────────────┐
   ESTUDIANTE /         │  PORTAL  (public/index.php → portal)     │
   DOCENTE       ──────►│  autoservicio: reservar, práctica libre,  │
                        │  cancelar, lista de espera, tickets,      │
                        │  abrir escritorio remoto                  │
                        └────────────────┬─────────────────────────┘
                                         │  clientes.Usuario / Contrasena
                                         │  sesión: $_SESSION['Timeklee2']['ClientPortal']
   ─────────────────────────────────────────────────────────────────────────
                                         │
   STAFF /              ┌────────────────▼─────────────────────────┐
   MONITOR /     ──────►│  BACKOFFICE  (public/admin/index.php)     │
   COORDINADOR          │  45 módulos · permisos rol × controlador  │
                        │  × acción · filtros de sesión sede/área   │
                        └────────────────┬─────────────────────────┘
                                         │  usuarios.Usuario / Contrasena
   ─────────────────────────────────────────────────────────────────────────
                                         │
        ┌────────────────────────────────┼────────────────────────────────┐
        │                                │                                │
   ┌────▼─────────┐            ┌─────────▼────────┐          ┌───────────▼──────────┐
   │  CATÁLOGO    │            │   OPERACIÓN      │          │  ACCESO REMOTO       │
   │  DE OFERTA   │            │                  │          │                      │
   │              │            │  citas           │          │  GuacamoleClient     │
   │  agendas     │◄───────────┤  citas_canceladas│          │  (REST, 100%)        │
   │  (~70 params)│  IdAgenda  │  citas_activos   │          │       │              │
   │  agendas_    │            │                  │          │       ▼              │
   │  recursos    │◄───────────┤  IdAgendaRecurso │──────────► Apache Guacamole     │
   │  agendas_    │            │                  │  Codigo   │  guacd → RDP/VNC/SSH │
   │  categoria   │            │  EstadoEntrega   │  Guacamole│       │              │
   │  agendas_    │            │  aprobación      │          │       ▼              │
   │  bloqueo     │            │                  │          │  PC físico del       │
   │  festivos    │            └────────┬─────────┘          │  laboratorio         │
   │  software_   │                     │                     └──────────────────────┘
   │  catalogo    │                     │
   │  activos (0) │            ┌────────▼─────────┐
   └──────────────┘            │  CONSECUENCIAS   │
                               │  clientes_bloqueos│  ← sanción automática por no-show
                               │  alertas_email    │  ← cola de correo (sin worker activo)
                               │  citas_lista_espera│ ← TABLA NO EXISTE
                               └──────────────────┘
                                         │
        ┌────────────────────────────────┼────────────────────────────────┐
        │                                │                                │
   ┌────▼──────────┐          ┌──────────▼───────┐          ┌────────────▼─────────┐
   │  REPORTES     │          │  DASHBOARDS      │          │  ASISTENTE IA        │
   │  16 Excel     │          │  3 gráficas      │          │  Ollama / Groq       │
   │  6 familias   │          │  1 Guacamole     │          │  10 herramientas     │
   │  ExcelReport  │          │  3 vistas home   │          │  propuesta→confirmar │
   └───────────────┘          └──────────────────┘          └──────────────────────┘
```

**Naturaleza técnica.** Framework propio («Klee Core»), PHP 8.1+, sin Composer framework: routing por `?c=controlador&a=accion` ([core/Router.php](../../core/Router.php)), MVC con modelo activo, plantillas Metronic 8, MySQL/InnoDB. Dos *entry points* físicamente distintos: `public/index.php` (portal del cliente, controlador por defecto `portal`) y `public/admin/index.php` (backoffice, controlador por defecto `login`).

---

## 1.2 Inventario completo de módulos (45 registrados, todos habilitados)

Fuente: [app/config/Modules.php](../../app/config/Modules.php). Ninguno está desactivado (`ConfigEnv::$MODULES_OVERRIDE = array()`).

### Categoría General (1)
| Módulo | Controlador | Qué hace |
|---|---|---|
| `home` | `home` | Dashboard de inicio con tres vistas: operacional, técnica y de activos |

### Categoría Configuración (16)
| Módulo | Controlador | Qué hace |
|---|---|---|
| `configuracion` | `configuraciones` | Contenedor de menú «Sistema» |
| `configuraciones` | `configuraciones` | Parámetros globales: token de API, whitelist de IP, políticas de aprobación. **6 filas en total** |
| `roles` | `roles` | CRUD de roles de staff. 2 roles vivos: Administrador, Operador |
| `permisos` | `permisos` | Matriz rol × módulo × acción, serializada como JSON en `permisos.Permission` |
| `alertas` | `alertas` | Notificaciones in-app (campanita). **0 filas; nadie las dispara automáticamente** |
| `alertasemail` | `alertasemail` | Cola de correo transaccional + envío manual. Tope `CORREOS_DIARIOS=20` por corrida |
| `usuarios` | `usuarios` | CRUD de staff: contraseña, foto, asignación de sedes/áreas (CSV en `varchar(500)`) |
| `academico` | `sedes` | Contenedor de menú «Espacios y horarios» |
| `personas` | `usuarios` | Contenedor de menú «Personas» |
| `areasuniversitarias` | `areasUniversitarias` | Catálogo de facultades/áreas. **Tabla vacía — dimensión rota** |
| `clientes` | `clientes` | CRUD de solicitantes (estudiantes/docentes/monitores/administrativos/externos). **Sin gestión de contraseña en la UI** |
| `agendas` | `agendas` | Configuración del recurso reservable: 9 pestañas, ~70 columnas de política |
| `agendascategoria` | `agendasCategoria` | Categorías de agenda (8 vivas). Gobiernan qué ve cada cliente |
| `festivos` | `festivos` | Calendario de no laborables. 13 filas, **todas de 2026** |
| `agendasbloqueo` | `agendasBloqueo` | Bloqueos puntuales de disponibilidad. Valor especial `IdAgenda=-1` = todas |
| `agendasrecurrencias` | `agendasRecurrencias` | Series de reservas recurrentes. **Falla en runtime: la tabla no existe** |

### Categoría Operación (9)
| Módulo | Controlador | Qué hace |
|---|---|---|
| `prestamos` | `citas` | Contenedor de menú «Reservas» |
| `citas` | `citas` | Calendario operativo de reservas (FullCalendar) |
| `citasaprobacion` | `citas` | Bandeja de pre-reservas pendientes (`pendingApprovals`, `reservationDecision`) |
| `prestamosinmediatos` | `prestamosInmediatos` | Tablero de mostrador: entregar/recibir sin reserva previa |
| `prestamosinmediatosmenu` | `prestamosInmediatos` | Contenedor de menú |
| `inventario` | `activos` | Contenedor de menú |
| `activos` | `activos` | Inventario patrimonial con serial y valor. **0 filas** |
| `activosTipos` | `activosTipos` | Catálogo de tipos de activo. **0 filas** |
| `sedes` | `sedes` | Catálogo de sedes. 3 filas: Bogotá, Medellín, Cali |

### Categoría Herramientas (9)
| Módulo | Controlador | Qué hace |
|---|---|---|
| `reportes` | `reportes` | **16 reportes Excel en 6 familias**, catálogo formal con código |
| `analitica` | `graficas` | Contenedor de menú |
| `graficas` | `graficas` | Hub de dashboards |
| `graficasEspacios` | `graficas` | Dashboard «Uso de espacios»: 4 KPIs, heatmap, top/bottom, tendencia |
| `graficasReservas` | `graficas` | Dashboard «Gestión de reservas»: embudo, no-shows, cancelaciones |
| `graficasActivos` | `graficas` | Dashboard «Inventario de activos». **Corre sobre tabla vacía** |
| `asistenteIa` | `asistenteIa` | Chat administrativo con LLM y *tool calling* |
| `sincronizacion` | `sincronizacion` | Sincronización de clientes. **Es un stub: no importa nada** |
| `test` | `test` | **Módulo de diagnóstico expuesto en el menú de producción** |

### Categoría Administración remota — Guacamole (10)
| Módulo | Controlador | Qué hace |
|---|---|---|
| `guacAdmin` | `guacUsuarios` | Contenedor de menú |
| `guacDashboard` | `guacDashboard` | 12 tarjetas + 5 gráficas de uso remoto |
| `guacUsuarios` | `guacUsuarios` | CRUD de usuarios de Guacamole y sus permisos |
| `guacGruposUsuario` | `guacGruposUsuario` | Grupos de usuario |
| `guacConexiones` | `guacConexiones` | Conexiones RDP/VNC/SSH a equipos. Incluye `testJson` (sonda `guacd`) **sin UI** |
| `guacGruposConexion` | `guacGruposConexion` | Agrupación de conexiones (salas) |
| `guacPerfiles` | `guacPerfiles` | Perfiles de compartición de pantalla |
| `guacSesiones` | `guacSesiones` | Sesiones activas, con capacidad de corte |
| `guacHistorial` | `guacHistorial` | Historial de conexiones + export CSV/XLSX multi-hoja |
| `guacHistorialUsuarios` | `guacHistorial` | Historial de logins en Guacamole |

### Controladores no registrados en el menú (6)
`AjaxController`, `ApiController` (API pública v1 con token único), `ElFinderController` (gestor de archivos), `GeneralController`, `LoginController`, `PortalController`. No son apagables por configuración.

---

## 1.3 Servicios de dominio (9)

| Servicio | Responsabilidad de negocio |
|---|---|
[ClientReservationPolicyService](../../app/services/ClientReservationPolicyService.php) | Puerta de elegibilidad del cliente: bloqueos activos y cuota de reservas simultáneas (`CLIENT_QUOTA_ACTIVE_MAX=5`). Auto-bloqueo por reincidencia de no-show (3 en 30 días → 14 días de sanción) |
| [ReservationExpirationService](../../app/services/ReservationExpirationService.php) | Cierra el ciclo de vida: pendientes vencidas → Negada; confirmadas sin entrega → No asistió; entregadas sin devolver → sólo warning en log |
| [WaitlistService](../../app/services/WaitlistService.php) | Lista de espera FIFO por franja exacta. **La tabla no existe; y falta la acción de reclamar el cupo** |
| [RecurringReservationService](../../app/services/RecurringReservationService.php) | Series recurrentes (semanal/diaria/mensual), tope 90 instancias, política de conflicto `skip`/`fail`. **La tabla no existe** |
| [ClientesLookupService](../../app/services/ClientesLookupService.php) | Búsqueda y conciliación de clientes por documento normalizado (quita puntos, guiones, espacios) |
| [GuacHistorialStats](../../app/services/GuacHistorialStats.php) | 11 funciones puras de agregación del historial remoto. **El modelo a imitar para toda la analítica** |
| [AsistenteIaService](../../app/services/AsistenteIaService.php) | Loop agéntico con 10 herramientas, dos proveedores (Ollama/Groq), máx. 8 iteraciones por turno |
| [AsistenteIaAccionesService](../../app/services/AsistenteIaAccionesService.php) | Ejecución de las acciones de escritura confirmadas por el humano, con revalidación de estado |
| [AsistenteIaCitasBridge](../../app/services/AsistenteIaCitasBridge.php) | Permite al asistente cancelar reutilizando el flujo completo de Citas (archivado, correos, lista de espera) |

---

## 1.4 Automatizaciones y comandos CLI (14)

Fuente: [bin/](../../bin/). **Ninguno está programado: `crontab -l` está vacío.**

| Comando | Cadencia que necesita | Efecto de negocio si no corre |
|---|---|---|
| **`expire-citas`** | cada 5 min | **El no-show nunca se materializa.** No hay expiración de pendientes ni sanciones automáticas. El KPI de no-show mide un fenómeno inexistente |
| **`email-queue`** | cada 5 min | **El sistema es silencioso.** Nadie recibe confirmación, aprobación, negación ni cancelación. Hoy: 19 correos varados |
| **`waitlist expire`** | cada 10 min | Un cupo ofrecido y no reclamado bloquea la cola indefinidamente |
| `recurring` | manual | Alta masiva de clases de semestre |
| `clientes-bloqueo` | manual | Sanción/apelación desde soporte. **Escribe con `TenantId=0`**, fuera del tenant 1 |
| `guac-smoke` | cada 5–15 min como healthcheck | Nadie detecta que el acceso remoto está caído antes que el estudiante |
| `doctor`, `smoke`, `cache`, `migrate`, `seed`, `make`, `link-htdocs` | despliegue / desarrollo | — |

> **Hallazgo de operación:** [docs/05_OPERATIONS.md](../05_OPERATIONS.md) documenta `doctor`, `cache`, `smoke`, `migrate` y `seed`, pero **no menciona `email-queue`, `expire-citas`, `waitlist`, `recurring` ni `clientes-bloqueo`**. Los workers críticos del negocio no están en el runbook.

---

## 1.5 FLUJO 1 — Autenticación

Existen **dos poblaciones de identidad completamente separadas**, sin tabla puente:

| | `usuarios` (staff) | `clientes` (solicitantes) |
|---|---|---|
| Puerta | `/admin/` → `LoginController` | `/` → `PortalController` |
| Clave de sesión | `$_SESSION['Timeklee2']['User']` | `$_SESSION['Timeklee2']['ClientPortal']` |
| Volumen vivo | 1 | 20 |
| Modelo de permisos | rol × controlador × acción (JSON en `permisos`) | lista blanca de categorías (`clientes.CategoriasAgendas`, CSV) |
| Revalidación de estado | **No** — desactivar un usuario no expulsa su sesión | **Sí, en cada request** |

**Pipeline por petición** ([core/Controller.php:383](../../core/Controller.php#L383)): tres middlewares en orden fijo — `AuthMiddleware` → `CsrfMiddleware` (sólo POST) → `RateLimitMiddleware` (120 req/60 s por IP). Los desenlaces `NO_LOG_IN`, `NO_CSRF` y `RATE_LIMIT` producen un **redirect silencioso indistinguible entre sí**, y **ninguno se persiste en ninguna parte**.

**Hallazgos relevantes para analítica:**
- **No existe registro de login de clientes al portal.** `log_acceso` sólo registra staff. Por tanto la tasa de conversión del embudo del portal (sesiones → reservas) **no es calculable**.
- `log_acceso.CierreSesion` está **NULL en el 100 % de las filas** → la duración de sesión no es calculable ni para el staff.
- **Superusuario cableado por número**: `usuarios.Id = 1` evade el sistema de permisos.
- Tres funcionalidades declaradas que no existen: `LOGIN_AUTOMATICO` (el botón lleva a un controlador inexistente), `CAMBIO_ROL` (flag sin consumidores), `PasswordPolicy` (clase sin llamadores → la política de contraseñas no se aplica).
- **Los clientes no tienen forma de obtener contraseña**: `Contrasena` no está en el formulario de administración ni existe autoservicio ni importación. Las 20 claves actuales son de seeder.

---

## 1.6 FLUJO 2 — Reserva de un espacio, de punta a punta

Este es **el proceso central del negocio**. 18 pasos verificados en código:

```
 1. Cliente se autentica en el portal            PortalController::authenticateAction
 2. PUERTA DE ELEGIBILIDAD                       ClientReservationPolicyService::canCreateReservation
      ├── ¿bloqueo activo en clientes_bloqueos?  → rechazo con motivo visible
      └── ¿supera 5 reservas activas?            → rechazo por cuota
 3. Catálogo visible                             cruza CategoriasAgendas × VisiblePortal × Estado × sede × área
 4. Selección de fecha y franja                  buildSlotAvailability → status por slot:
                                                 disponible | parcial | llena | bloqueada |
                                                 festivo | fuera_anticipacion | inhabilitada
 5. Selección de recurso                         obligatoria si la agenda tiene inventario activo
 6. ESTADO INICIAL (política de aprobación)      resolveInitialReservationState
      configuraciones.AprobacionReservaEspacio = 'agenda'  → delega a agendas.RequiereConfirmacion
      configuraciones.AprobacionReservaActivo   = 'pendiente' → los activos SIEMPRE nacen pendientes
 7. Normalización del payload                    genera CodigoQr 'AG-{IdAgenda}-{YmdHis}',
                                                 serializa el formulario dinámico a InformacionAdicional (JSON)
 8. VALIDACIONES (12, en este orden)             agenda activa → fecha/horas → día habilitado →
                                                 dentro del rango diario → encaje en la retícula →
                                                 anticipación máxima → campos obligatorios →
                                                 NO festivo → NO bloqueado → recurso libre →
                                                 cupo disponible → activos válidos
 9. PERSISTENCIA CON CANDADO                     transacción + SELECT ... FOR UPDATE
                                                 (re-verifica solape, recurso y cupo)
10. Notificación                                 encola res_creada_sol + res_creada_rsp
11. APROBACIÓN / RECHAZO                         bandeja pendingApprovals (ventana hoy→hoy+60d)
                                                 sella Aprobado*/Negado*; motivo obligatorio al negar
12. Recordatorio previo                          ✗ NO EXISTE
13. Check-in / llegada                           ✗ NO EXISTE
14. Uso                                          (a) físico: registro de entrega por staff
                                                 (b) remoto: reservationRemoteConnect (ver Flujo 5)
15. Entrega / devolución                         reservationHandover; autocompleta horas con NOW()
16. Cierre                                       EstadoEntrega=2 ⇒ Estado=3 Finalizada
17. Expiración automática (cron)                 pendiente vencida → Negada(5)
                                                 confirmada sin entrega → No asistió(4)
                                                 entregada sin devolver → sólo warning
18. Consecuencia                                 3 no-shows en 30 días → bloqueo automático 14 días
```

### La máquina de estados real

**No existe un único campo «estado». Hay cinco dimensiones independientes.**

`citas.Estado` (tinyint):

| Valor | Etiqueta | Significado de negocio |
|---|---|---|
| 0 | Pendiente de aprobación | Pre-reserva. Ocupa cupo y bloquea el recurso, pero **no habilita conexión remota** |
| 1 | Confirmada | Reserva válida y ejecutable |
| 2 | Cancelada | **Estado casi inalcanzable**: el flujo normal borra la fila. Sólo lo dejan la cancelación de series y los seeders |
| 3 | Finalizada | Uso cerrado con devolución registrada |
| 4 | No asistió | No-show |
| 5 | Negada | **Rechazo administrativo *o* expiración automática — mismo valor para dos hechos distintos** |

`citas.EstadoEntrega`: 0 Pendiente · 1 Entregado · 2 Devuelto · 3 No asistió.

**El estado comercial es derivado del logístico, no lo contrario**: `mapDeliveryStateToReservationState` fuerza `Estado` a partir de `EstadoEntrega` (1→1, 2→3, 3→4).

**Anomalías de nomenclatura documentadas** (afectan a cualquier reporte nuevo): el código `3` se llama «Completada» en el COMMENT de la columna y en `ReportesModel`, pero «Finalizada» en `CitasModel` y en `vista_citas`. El código `5` **no está documentado en el COMMENT** de la columna, así que cualquier consulta que asuma el rango 0–4 cuenta las reservas negadas como pendientes.

### Diagrama de transiciones

```
                        [creación: portal | staff | quick-create | préstamo inmediato | práctica libre]
                                              │
                    ┌─────────────────────────┴─────────────────────────┐
                    ▼ (RequiereConfirmacion=1)                          ▼ (=0)
             Estado=0 Pendiente                                  Estado=1 Confirmada
                    │                                                   │
      approve ──────┼───────────────────────────────────────────────────►│
      deny+motivo ──┼──► Estado=5 Negada [terminal]                      │
      cron expira ──┴──► Estado=5 Negada (NegadoPorUserId NULL)          │
                                                                         │
                          ┌──────────────────────────────────────────────┤
                          ▼ handover EstadoEntrega=1                     │
                   Estado=1 / entregado ──► EstadoEntrega=2 ──► Estado=3 Finalizada
                          │
                          ▼ handover EstadoEntrega=3  ó  cron
                   Estado=4 No asistió ──► hook auto-bloqueo (3 no-shows/30 días)

  DESDE CUALQUIER ESTADO:  cancelar ──► fila BORRADA de `citas` + INSERT en `citas_canceladas`
                                        con EstadoOriginal congelado  [terminal, fuera de `citas`]
```

### Estados que el negocio esperaría y no existen

Comparado con la generación anterior (`timeklee_tadeo.citas`, que sí los tenía): `Llegada`, `Asistencia`, `UsuarioAsistencia`, `FechaConfirmacion`, `InicioDesarrollada`/`FinDesarrollada` (hora real de uso), `Asistentes` (aforo real), `FechaReagendada`. **En Timeklee2 no hay check-in ni reagendamiento.**

---

## 1.7 FLUJO 3 — Las cinco variantes de creación

| Variante | Punto de entrada | Diferencia en los datos | Impacto analítico |
|---|---|---|---|
| **Autoservicio (portal)** | `PortalController::bookAction` | `IdCliente` **siempre poblado**; el nombre/documento se **sobreescriben** desde `clientes` (el cliente no puede falsear identidad) | El único canal con trazabilidad poblacional completa |
| **Staff / mostrador** | `reservationCreate`, `reservationQuickCreate`, `reservationActivoCreate` | Nombre y documento se **prellenan con el usuario del sistema**: si el operador no los cambia, la reserva queda atribuida al operador. `IdCliente` puede quedar NULL | **En la base viva ~12 % de reservas sin cliente vinculado.** Cada una es invisible para la cuota, la sanción y todo KPI por tipo de usuario |
| **Préstamo inmediato (walk-in)** | `PrestamosInmediatosController::startLoan` | Nace `Estado=1` + `EstadoEntrega=1` + `HoraEntrega=HoraInicio`. Marcador `InformacionAdicional.__origen='prestamo_inmediato'`. `IdCliente` normalmente **NULL** | La puntualidad es artificialmente perfecta por construcción |
| **Práctica libre (portal)** | `PortalController::practiceFreeStart` | Igual al anterior pero iniciado por el cliente. Ventana: de −5 a +15 min respecto a ahora. Máx. 2 franjas (constante hardcodeada) | **No tiene cierre propio**: en la base viva hay 4 sesiones con `EstadoEntrega=1` y `HoraDevolucion` NULL desde junio |
| **Serie recurrente** | `RecurringReservationService` | `InformacionAdicional='{}'`, **sin `CodigoQr`**, **sin correos**, validación más débil (no revisa festivos ni bloqueos) | **La tabla no existe → falla en runtime.** Y sin `IdRecurrencia` no se puede distinguir demanda institucional de espontánea |

> **Asimetría de política no documentada:** el staff **no pasa por `ClientReservationPolicyService`**. Un cliente sancionado obtiene la reserva igual si la pide en el mostrador. La sanción es un control de autoservicio, no una política institucional.

---

## 1.8 FLUJO 4 — Cancelación, rechazo y expiración: tres hechos, un solo campo

Esto es crítico para los dashboards 13 y 14 solicitados. La tabla de discriminación exacta:

| Hecho | Dónde vive | Discriminante exacto | Fecha del hecho |
|---|---|---|---|
| Cancelación por cliente | `citas_canceladas` | `CanceladaPorCanal='portal'` y `CanceladaPorClienteId IS NOT NULL` | `CanceladaFecha` |
| Cancelación por admin | `citas_canceladas` | `CanceladaPorCanal='admin'` y `CanceladaPorUserId IS NOT NULL` | `CanceladaFecha` |
| **Rechazo real (decisión humana)** | `citas` | `Estado=5` **y `NegadoPorUserId IS NOT NULL`** | `NegadoFecha` |
| **Expiración automática** | `citas` | `Estado=5` **y `NegadoPorUserId IS NULL`** | `NegadoFecha` |
| No-show manual | `citas` | `Estado=4` y `EstadoEntrega=3` | `FechaModificacion` |
| No-show automático | `citas` | `Estado=4` y `ComentariosEntrega LIKE '%Auto-marcada%'` | `FechaModificacion` |
| Cancelación de serie | `citas` (**no archivada**) | `Estado=2` con el motivo en `NegadoObservacion` | `NegadoFecha` (campo equivocado) |

**Defectos que esto produce en los reportes actuales:**
1. `ReportesModel::getRechazos` filtra sólo por `Estado=5` → **infla los rechazos con expiraciones automáticas del cron**, y su columna «Tiempo Respuesta (h)» mide, para las expiradas, «tiempo hasta que empezó la franja», no tiempo de decisión.
2. **No existe la métrica de incumplimiento del equipo de aprobación** (`Estado=5` con `NegadoPorUserId IS NULL`), que es la que mide si la administración responde o deja vencer.
3. `getCancelaciones` usa `INNER JOIN agendas`, y las reservas de activo tienen `IdAgenda=0` → **las cancelaciones de equipos desaparecen del reporte oficial de cancelaciones.**
4. `CanceladaMotivo` es texto libre sin catálogo, autorrellenado con literales genéricos. **La clasificación por causa no es posible**; el reporte RES002 clasifica sólo por temporalidad (Tardía <2 h / Mismo día <24 h / Anticipada ≥24 h).

**Ventanas de cancelación:** el cliente sólo puede cancelar en estados 0 y 1, con antelación `agendas.DiasCancelar × 86400 + agendas.HorasCancelar × 3600`. **El administrador puede cancelar sin ninguna restricción de estado ni de plazo** — incluso una reserva ya finalizada.

---

## 1.9 FLUJO 5 — Escritorio remoto, de punta a punta

Es el diferenciador comercial. **Integración 100 % REST API; no hay una sola lectura SQL contra `guacamole_*`** (contra lo que documentan [PLAN_CONSOLA_GUACAMOLE.md:98](../PLAN_CONSOLA_GUACAMOLE.md) y `DISEÑO_SISTEMA.md` §4.8.3).

| # | Paso | Estado real |
|---|---|---|
| 1 | Crear la conexión RDP/VNC/SSH en Guacamole | **MANUAL** desde la consola |
| 2 | Obtener el identificador de cliente | AUTOMÁTICO: `base64("{connection_id}\0c\0mysql")` |
| 3 | Enlazar conexión ↔ equipo de agenda | **MANUAL**: pegar ese valor en `agendas_recursos.CodigoGuacamole` |
| 4 | Habilitar la agenda para remoto | **MANUAL**: `agendas.PermiteConexionRemota=1`. Hoy: 3 agendas |
| 5 | Crear la reserva | Flujo normal. **No ocurre ningún aprovisionamiento en Guacamole** |
| 6 | Crear usuario Guacamole del reservante | ✗ **NO EXISTE** |
| 7 | Conceder permiso `READ` al aprobar | ✗ **NO EXISTE** — está especificado en `DISEÑO_SISTEMA.md` §4.13 y no se implementó |
| 8 | Validar la ventana de acceso | AUTOMÁTICO: exige `Estado=1`, agenda activa, `PermiteConexionRemota=1`, y `[HoraInicio − 5 min, HoraFin + 10 min]` |
| 9 | Lanzar el cliente | AUTOMÁTICO: pide token de servicio y hace **302 directo** a `{base}/#/client/{id}?token=…` |
| 10 | Sesión activa | Visible en el módulo de sesiones. **Sin vínculo con la reserva** |
| 11 | Cierre | **MANUAL/pasivo**: el usuario cierra la pestaña. **No hay corte automático al llegar a `HoraFin`** |
| 12 | Revocación de permisos | **Nada que revocar.** Una sesión ya abierta sobrevive indefinidamente al fin de la reserva |

### Los tres hallazgos que condicionan todo el módulo

1. **El usuario final navega con el token de la cuenta de servicio `guacadmin`.** Por tanto `guacamole_connection_history.username` siempre valdrá `guacadmin`: **la trazabilidad persona↔sesión se pierde en origen.**
2. **Hallazgo de seguridad (IDOR).** `canCurrentUserAccessReservationRemote()` devuelve `true` con sólo existir la reserva ([CitasController.php:2151](../../app/controllers/CitasController.php#L2151)); no comprueba propiedad. **Un cliente autenticado puede abrir el escritorio remoto de la reserva de otro cambiando el `Id` en la URL**, dentro de la ventana horaria.
3. **La cadena de llaves se rompe en seis puntos** (detalle en la Parte 2, §2.6). El resumen: el historial guarda un *nombre* de conexión, no el id; `CodigoGuacamole` es base64 opaco escrito a mano, poblado en 8 de 59 recursos y con 4 valores inválidos; y hay un *fallback* a `agendas.Codigo` que produce botones «Conectar» que siempre fallan.

### Estado de los datos hoy
`guacamole_connection_history`: **0 filas**. `guacamole_user_history`: 9 filas, todas de `guacadmin` desde `127.0.0.1`, **8 con `end_date` NULL** (se leerán como «En curso» para siempre). El servidor configurado (`192.168.31.197:8080`) es **inalcanzable desde este entorno**. Y la base de producción anterior **no contiene ni una sola columna relacionada con acceso remoto**: la serie histórica de uso remoto arranca en cero. No se puede prometer «traemos tus tres años de datos».

---

## 1.10 FLUJO 6 — Uso de laboratorios y práctica libre

**Definición operativa de «laboratorio» en el sistema: `agendas.PortalPracticaLibre = 1`.** No existe una columna «tipo de espacio»; los tres reportes de laboratorio usan esa bandera de configuración como si fuera una dimensión ([ReportesModel.php:864](../../app/models/ReportesModel.php#L864), `:894`, `:925`). Hoy: 1 de 15 agendas activas.

Flujo de práctica libre: el estudiante entra al portal, elige el laboratorio, el sistema le asigna (o él elige) un PC del inventario de `agendas_recursos`, opcionalmente declara el software que va a usar, y la sesión nace ya entregada. Si la agenda tiene `PermiteConexionRemota=1` y el recurso tiene `CodigoGuacamole`, aparece el botón de escritorio remoto.

**Lo que la generación anterior medía y esta no** (2.531 sesiones reales, 2022-03 a 2025-02, 941 estudiantes distintos): el **serial físico entregado**, el equipo concreto anclado a un salón, la duración almacenada, la fecha/hora de fin, y el parámetro de capacidad de la sala (`EquiposSala = 20`) — **el denominador sin el cual sólo se cuentan sesiones y nunca se calcula ocupación**.

Dato de comportamiento real que ningún dashboard actual revelaría: la curva horaria de la práctica libre es **plana durante toda la jornada laboral**, radicalmente distinta a la curva de picos 7 h / 9 h de las reservas de aula. **Son dos negocios distintos**: uno gobernado por la malla de horarios académicos, otro por la conveniencia del estudiante. Mezclarlos en un mismo «heatmap de demanda» destruye la información.

---

## 1.11 FLUJO 7 — Utilización de equipos: la bifurcación

**Existen dos inventarios paralelos y desconectados**, y esto define el techo de todo dashboard de equipos:

| | `agendas_recursos` | `activos` |
|---|---|---|
| Concepto | Unidades dentro de un espacio (los 30 PCs del laboratorio) | Inventario patrimonial prestable |
| Filas | **59** | **0** |
| Identidad | `Codigo`, `CodigoGuacamole`. **Sin serial, marca, modelo** | `Codigo`, `NumeroSerie`, `Valor` (única medida monetaria del sistema) |
| Quién lo usa | Calendario, tablero de préstamos, práctica libre, **escritorio remoto** | Reserva de sólo-activo, reportes de inventario, dashboard de activos |
| Auditoría | **Ninguna columna de fecha** → no se sabe cuándo entró o salió un equipo | `FechaRegistro`/`FechaModificacion` |
| Vínculo entre ambos | **NO EXISTE.** Ninguna columna, ninguna FK, ninguna vista | |

Tres mecanismos distintos ligan equipo y reserva: `citas.IdAgendaRecurso` (1:1), `citas.IdActivo` (1:1) y `citas_activos` (N:M). **Un mismo equipo físico puede existir a la vez en los dos inventarios, y reservar uno no bloquea el otro.**

**Ningún cambio de estado de activo es automático.** No hay una sola línea que escriba `activos.Estado` fuera del formulario CRUD: entregar o devolver una reserva escribe exclusivamente en `citas`. Por tanto `Estado=2 «En uso»` y `Estado=3 «Mantenimiento»` son **etiquetas manuales que nadie mantiene** — y el KPI «tasa de utilización» del dashboard de activos se calcula sobre ellas.

**El «mantenimiento» de una unidad se infiere buscando texto.** `isResourceMaintenance` busca las cadenas `'mantenimiento'` o `'mnt'` en `Tipo` + `Descripcion` ([PrestamosInmediatosController.php:829](../../app/controllers/PrestamosInmediatosController.php#L829)). Consecuencia ya medible: el recurso `PI-PC-05` tiene `Tipo='Mantenimiento preventivo'` y `Estado=1` → queda **permanentemente no prestable** por un falso positivo de texto.

**Mantenimiento como módulo: NO EXISTE.** Cero tablas con «mant» en el esquema; cero columnas `Marca`, `Modelo`, `Placa`, `Garantia`, `Inventario`. No hay órdenes de trabajo, técnicos, proveedores, repuestos, costos, downtime ni historial de fallas.

---

## 1.12 Cómo interactúan los módulos: las siete dependencias que importan

1. **`agendas` es el panel de control de todo el negocio.** ~70 columnas que gobiernan aforo, horario, anticipación, cancelación, aprobación, préstamo inmediato, práctica libre y remoto. Cambiar una regla institucional («cancelar con 24 h de antelación en toda la universidad») exige **editar cada agenda una por una**: no hay herencia de política por categoría ni por sede. En la generación anterior sí existía — `agendas_categoria` tenía **72 columnas** y era una plantilla completa de configuración; hoy quedó reducida a etiqueta y color.

2. **`configuraciones` gobierna la aprobación, pero sólo con 6 filas y sin historial.** `AprobacionReservaEspacio='agenda'` delega a cada agenda; `AprobacionReservaActivo='pendiente'` no delega nunca. **La tabla no tiene ninguna columna temporal**: si mañana cambia la política, no queda rastro de cuándo ni quién, y las series históricas de tasa de aprobación se vuelven incomparables sin explicación.

3. **`clientes.CategoriasAgendas` (CSV) decide qué ve cada solicitante.** En la generación anterior esto era una tabla de 10 perfiles (`clientes_permisos`); hoy es un texto replicado por persona. La política dejó de ser auditable.

4. **`agendas_blooqueo` es el único registro de tiempo no disponible** y por tanto un insumo obligatorio del denominador de ocupación. Tiene dos defectos de negocio: el bloqueo global (`IdAgenda=-1`) guarda la sede pero **no se filtra por sede** (un bloqueo de Bogotá bloquea Medellín y Cali), y el rango horario se aplica **igual a todos los días** del rango de fechas.

5. **`festivos` es global por tenant**: no tiene columna `Sede`, así que no se pueden declarar recesos regionales. Y sólo hay 13 filas, todas de 2026.

6. **La cola `alertas_email` es el único puente con el mundo exterior**, y no tiene `IdCita` ni `IdCliente`: es imposible atribuir un correo a una reserva o medir «% de reservas efectivamente notificadas». Además `Plantilla` es `varchar(15)` y el código hace `substr($template,0,15)`: `res_aprobada_sol` se **trunca** y rompe el agrupamiento por plantilla.

7. **El asistente IA es hoy el único consumidor «inteligente» de la capa de datos**, y es ciego a los dominios que más importan: no tiene ninguna herramienta sobre activos, lista de espera, recurrencias, bloqueos, tickets ni el historial de Guacamole. Tampoco puede invocar los 17 reportes formales del catálogo: genera su propio Excel plano.

---

## 1.13 Funcionalidad declarada que no existe (inventario para no prometerla)

Esta lista es material de venta: **nada de esto debe aparecer en una demo**.

| Elemento | Estado real |
|---|---|
`agendas.Recordatorio` / `RecordatorioAdmin` | Campos de formulario sin ningún consumidor. **No hay recordatorio previo a la reserva** |
| `agendas.BloqueoLlegadaTarde` / `TiempoLlegadaTarde` | Se capturan y se muestran; **nunca se evalúan**. La política anti-llegada-tarde es decorativa |
| `agendas.Invitacion` | Invitación de calendario (.ics): no implementada |
| `agendas.Externo` | «Reservable por externos»: no implementado |
| `agendas.AgendaMultiple`, `UsuariosDisponibilidad`, `DiasDisponibles`, `Qr` | Sin consumidores. `DiasDisponibles` está muerto |
| `agendas.MaximoFranjas` | **Sólo se valida en el navegador**; el servidor acepta cualquier número |
| Lista de espera | Código completo, **tabla inexistente**, y falta la acción de reclamar el cupo: el estado «Convertida» es inalcanzable |
| Reservas recurrentes | Código completo, **tabla inexistente** → el módulo falla con SQL error 1146 |
| Módulo de casilleros | Categoría creada, **las tres tablas no existen**; el seeder aborta |
| Sincronización de clientes | **Es un stub**: no importa nada, reporta «ok» con un mensaje hardcodeado. No hay LDAP, ni SIA, ni importación CSV, ni autorregistro |
| Atención de tickets | El cliente puede abrir tickets; **no existe controlador, vista ni modelo administrativo para responderlos**. Todos quedan en estado «Abierto» para siempre |
| Administración de bloqueos de clientes | La tabla existe y está bien diseñada; **no hay interfaz web**. Sólo CLI, y el CLI escribe con `TenantId=0` |
| `activos.PrestamoInmediatoAutoAsignar` | Se lee y **nunca se usa en ninguna decisión** |
| Historial y bloqueo por unidad en el tablero de préstamos | Backend implementado, URLs publicadas, **`board.js` no las consume**: no hay UI |
| `guacConexiones/testJson` (sonda de equipo) | Backend excelente, **cero referencias en las vistas** |
| Grabación de sesión remota | No configurada; el endpoint de logs de sesión no está implementado |

---

*Continúa en la [Parte 2 — Inventario de datos](02_INVENTARIO_DATOS.md).*
