Gestor¶
El gestor es el panel de administración interno del Sistema de Premios: donde el operador de Caracol carga la campaña, arma y ejecuta los sorteos, gestiona ganadores y entregas, administra el padrón de clientes y consulta reportes. A diferencia de la landing, no es anónimo — requiere login — y no tiene presupuesto de peso: es una herramienta de uso interno, no una página que un cliente abre desde un QR.
Se sirve bajo /gestor/, empaquetado dentro del mismo jar que el backend (ver
Backend → build del jar único).
Stack¶
| Tecnología | Versión | Para qué |
|---|---|---|
| React | 18.3 | Runtime de UI (sin cambios respecto al stack original — ver ADR-019, que aclara por qué el gestor no migró a Preact) |
| Vite | ^5.4 | Build |
| Tailwind CSS | ^3.4 | Estilos, con componentes propios en el estilo shadcn (components/ui/) |
| React Router | ^6.26 | Ruteo del lado del cliente, basename="/gestor" |
| Zustand | ^5.0 | Store de autenticación |
| TanStack Query | ^5.59 | Fetching/caching contra la API del gestor |
| Axios | ^1.7 | Cliente HTTP, con interceptors de auth y manejo de 401 |
| React Hook Form + Zod | ^7.53 / ^3.23 | Formularios y validación |
vite-plugin-compression2 |
^2.5 | Precomprime .br/.gz en build (mismo tratamiento que la landing) |
PWA¶
El gestor es instalable como Progressive Web App — pensado para la tarea genuinamente móvil del panel: registrar la entrega de un premio en sucursal desde el celular. El resto del gestor sigue siendo mayormente de escritorio, pero el sidebar colapsa a un drawer por debajo de 768px y las tablas scrollean dentro de su propio contenedor (nunca el body).
Piezas de la PWA (public/manifest.webmanifest, public/sw.js, íconos 192/512 + maskable):
- Manifest: nombre "Sistema Premios",
scope/start_url=/gestor/,display: standalone, theme color = rojo de branding. - Service worker mínimo, a mano (sin Workbox ni
vite-plugin-pwa— sin librería nueva): precachea el shell de la app con stale-while-revalidate, pero nunca cachea respuestas de/api/**ni/bases/**— el gestor es una herramienta viva, sus datos siempre tienen que salir frescos de la red. El predicado que decide esto (debeUsarSoloRed) es una función pura, testeada evaluando el propio archivo del service worker (no una copia en TypeScript que se pueda desincronizar). - El backend sirve
manifest.webmanifestysw.jscon un controller dedicado (GestorPwaController), no por el resource handler estático genérico, porque necesita agregar el headerService-Worker-Allowed: /gestor/por request.
Detalle de la decisión (responsive + PWA, sin abrir un segundo codebase) en ADR-022.
Autenticación¶
HTTP Basic contra /api/gestor/** (ver Backend → seguridad). El gestor no
usa cookies ni sesión — cada request lleva el header Authorization armado en el cliente:
export function authHeader(credenciales: Credenciales | null): string | null {
if (!credenciales) return null;
return "Basic " + btoa(`${credenciales.usuario}:${credenciales.password}`);
}
- Las credenciales viven en un store de Zustand (
store/authStore.ts), persistidas ensessionStorage— a propósito, nolocalStorage: se pierden al cerrar la pestaña, no quedan indefinidamente en el disco del puesto de trabajo. - Un interceptor de request de Axios agrega el header
Authorizationa cada llamada. - Un interceptor de response detecta cualquier
401, desloguea (limpia el store) y muestra un toast de "Sesión vencida" — con un guard para no disparar el toast duplicado si llegan varios 401 concurrentes de una ráfaga de requests en vuelo. - El redirect a
/logines declarativo, no unlocation.assign:RequireAuthlee el store y renderiza<Navigate to="/login">en cuantocredencialesquedanull— un reload duro destruiría el toast recién mostrado antes de que React lo pinte.

Secciones y rutas¶
Todas las rutas cuelgan de basename="/gestor" (React Router) y quedan detrás de RequireAuth,
salvo /login:
| Ruta | Sección |
|---|---|
/panel |
Panel — resumen operativo de la campaña |
/campania |
Datos de la campaña (vigencia, bases, reglas de chances) |
/premios |
Alta y administración de premios |
/sorteos |
Generación y ejecución de sorteos |
/sorteos/actas/:id |
Detalle del acta de un sorteo |
/ganadores/:actaId |
Ganadores de un acta — gestión de entrega |
/clientes |
Padrón de clientes |
/clientes/:id |
Detalle de un cliente |
/chances |
Consulta de chances (soporte/trazabilidad) |
/claves |
Gestión de claves de firma (ClaveFirma) |
/modo-prueba |
Generador de vouchers de prueba (ver Backend → modo prueba) |
Cualquier ruta no reconocida (*) y la raíz (/) redirigen a /panel.
Cómo apunta al backend¶
Misma lógica que la landing: en producción, same-origin (VITE_API_BASE_URL ?? /api, servido
por el mismo jar bajo /gestor/); en desarrollo (vite dev, puerto 5174), el proxy de Vite
reenvía /api y /branding a http://localhost:8080.