# Permisos de identificación en el kiosko — huella / documento

Plan para implementación directa (mismo patrón que Fase 8: diseñado con
el usuario en conversación — sesión `/grill-me` 2026-10-01 — antes de
tocar código).

## Objetivo

Cerrar un hueco real encontrado al revisar el código: hoy
`Kiosk::startIdentifying()`/`Kiosk::identify()` **no tienen ningún
candado del lado del servidor**. El botón "Identificarse con documento"
solo se oculta en el navegador cuando `agentAvailable` (Alpine) es
falso — pero eso es client-side puro. Cualquiera con las devtools
abiertas, o simplemente un fallo de permiso de cámara (que también
muestra el botón), puede identificarse por documento aunque el lector
esté conectado y funcionando. El admin necesita poder declarar qué
métodos están permitidos, y que el servidor lo haga cumplir de verdad.

## Decisiones acordadas (grilled 2026-10-01)

1. **Sin detección de hardware en el servidor.** Laravel no intenta
   saber si el lector físico sigue conectado (eso requeriría que el
   agente le reportara estado en tiempo real, agrega latencia/staleness
   y sigue siendo manipulable por quien tenga acceso al navegador). La
   política del admin (`allow_fingerprint`/`allow_document`) es la
   única fuente de verdad: el servidor rechaza `identify()` por un
   método no permitido, **sin importar la razón** (lector apagado,
   desconectado, o alguien tratando de saltárselo a propósito). Mismo
   resultado práctico que "detectar la desconexión", sin la complejidad
   ni el punto débil de depender de una señal del cliente.
2. **`agentAvailable` se queda como ayuda de UX, nunca como candado.**
   Con ambos métodos permitidos, el comportamiento no cambia: se sigue
   intentando huella automático primero, documento aparece como
   alternativa cuando no hay lector. El toggle decide **si** documento
   está permitido, no **cuándo** se ofrece.
3. **No se puede guardar la configuración con los dos desactivados** —
   al menos uno debe quedar activo. Para cerrar el autoservicio del
   todo ya existe `is_open`; mezclar dos mecanismos para el mismo
   resultado final es confuso.
4. **Config global, una sola fila** (mismo patrón que `is_open`) — sin
   pensar en multisede todavía (eso se diseña aparte, al final).

## Fuera de alcance (ruling, no se construye en esta tarea)

Verificar criptográficamente que un `identificationMethod = fingerprint`
realmente vino de un match 1:N real en el lector (y no de alguien
forzando el valor por devtools) **no se resuelve acá** — es una
limitación conocida y ya aceptada del diseño actual
(`CLAUDE.md` §10: "respaldo detectivo, no preventivo", el registro
queda marcado y es auditable). Esta tarea cierra específicamente:
*documento deshabilitado ⇒ documento rechazado, siempre, sin
excepción* — que es el hueco real que se encontró y lo que se pidió.

## Modelo de datos

Migración sobre `system_configuration` (mismo patrón que `is_open`):
- `allow_fingerprint` — boolean, default `true`.
- `allow_document` — boolean, default `true`.

## Servidor — el candado real

- `SystemConfiguration`: fillable + cast para las 2 columnas nuevas.
- `Kiosk::startIdentifying()`: si `! allow_document`, no entra a
  `step = 'identifying'` (mismo patrón que ya usa `ensureOpen()`).
- `Kiosk::identify()`: rechazar según `$this->identificationMethod` —
  `manual` requiere `allow_document`, `fingerprint` requiere
  `allow_fingerprint`. Esto es la defensa real, no solo esconder un
  botón — cubre tanto el camino normal como cualquier intento de
  llamar al método directo sin pasar por la UI.
- Flujo automático de huella (Alpine, `maybeAutoIdentify()`): si
  `! allow_fingerprint`, no intenta conectarse al agente ni muestra la
  animación de "coloque su huella" — evita que un docente lea bien su
  huella y el servidor se la rechace igual, sin explicación.

## Panel admin

Dos toggles nuevos en el mismo componente donde ya viven `is_open` y
`registration_window_minutes` (`SystemConfiguration` Livewire + su
blade) — mismo patrón visual. Validación: bloquear guardar si ambos
quedan en `false`, con mensaje claro de por qué.

## Mensajes al docente

Si `startIdentifying()`/`identify()` terminan bloqueados por política
(caso borde: alguien fuerza el camino), mostrar un mensaje explícito
("Identificación por documento no disponible — contacta a
administración") en vez de fallar en silencio — mismo criterio que
`not_found`/`no_assignments_today`.

## Testing

- `identify()` rechaza método `manual` con `allow_document = false`,
  llamado directo (sin pasar por `startIdentifying()`), confirmando que
  el candado es real y no solo de la UI.
- `identify()` rechaza método `fingerprint` con `allow_fingerprint =
  false`.
- `startIdentifying()` no avanza a `identifying` con `allow_document =
  false`.
- Validación: no se puede guardar `system_configuration` con ambos en
  `false` (ni desde el toggle que apaga el segundo que queda activo).
- Regresión: con ambos permitidos (default), el comportamiento actual
  del kiosko no cambia — mismos tests de `RegistrationWindowTest`/
  `KioskIdentityPanelTest`/etc. siguen en verde.
- UI: con `allow_fingerprint = false`, el kiosko no muestra la interfaz
  de huella.

## Riesgos / pendientes de decidir en la implementación

- Texto exacto del mensaje de bloqueo al docente — se define al
  implementar, no bloqueante para el diseño.
- Si Alpine debe omitir directamente el polling a `/status` del agente
  cuando `allow_fingerprint = false` (ahorra una llamada inútil) —
  detalle de implementación.
