Arquitectura del ecosistema WorkDone¶
WorkDone no es una aplicación monolítica: es un backend que oficia de sistema de registro y tres clientes que lo consumen, cada uno en su propio repositorio. Esta página explica por qué está dividido así, por qué cada cliente trabaja offline-first, cómo se comunican las piezas y cómo se autentica cada actor. La idea no es que memorices endpoints — eso vive en la referencia de API — sino que entiendas el modelo mental: dónde está la verdad, quién manda y por qué.
La arquitectura 3+1¶
El ecosistema tiene cuatro componentes en cuatro repos separados: un backend central y tres clientes.
graph LR
subgraph back["workdone-backend (fuente de verdad)"]
API["API REST<br/>Spring Boot 4 + JHipster<br/>Java 25"]
DB[("MSSQL Server 2019+<br/>catálogos · trabajos<br/>opiniones · alertas")]
SCHED["Scheduler SLA"]
API --> DB
SCHED --> DB
end
MOBILE["workdone-mobile<br/>App operarios<br/>Kotlin + Compose + Room"]
KIOSK["workdone-smiley-kiosk<br/>Terminal Smiley<br/>Compose modo kiosko"]
BO["BackOffice<br/>Supervisión<br/>React · en el backend"]
MOBILE <-->|HTTPS| API
KIOSK <-->|HTTPS| API
BO <-->|HTTPS| API
| Componente | Repo | Rol |
|---|---|---|
| Backend | workdone-backend |
Sistema de registro: la única fuente de verdad. Expone la API, procesa los taps, sincroniza, corre el scheduler de SLA. |
| App móvil | workdone-mobile |
Cliente de operarios. Lee NFC, registra trabajos, sincroniza. |
| Kiosk Smiley | workdone-smiley-kiosk |
Cliente de opinión pública. Registra caritas, muestra frescura del último servicio. |
| BackOffice | dentro de workdone-backend (src/main/webapp, Vite + React) |
Cliente de supervisión y administración: ABM, dashboard, reportes, alertas y portal del cliente. Servido por el backend en :8080. |
Por qué repos distintos¶
No es una decisión cosmética. Cada cliente tiene un stack, un ciclo de vida y restricciones propias:
- Stacks incompatibles entre sí. El backend es Java/Spring, el móvil y el kiosk son Kotlin/Android. Mezclar esos builds en un repo único acoplaría cosas que no tienen nada que ver.
- Releases independientes. Una corrección en la app móvil no debería forzar un redeploy del backend, ni viceversa.
- El BackOffice es la excepción: vive con el backend. Es un frontend Vite + React + shadcn/ui + Tanstack Query que reside en
workdone-backend/src/main/webapp: se construye y se sirve junto con el backend (en:8080), pero deliberadamente no usa el scaffolding de JHipster.
El backend manda sobre los contratos
Los clientes no inventan endpoints ni DTOs: los consumen. La fuente de verdad de cada contrato REST vive en workdone-backend/docs/. El kiosk y el móvil mantienen copias gobernadas del protocolo de sync, generadas automáticamente desde el canónico del backend (scripts/contracts.ps1), y un hook de git aborta el push si la copia divergió. Si documento y realidad no coinciden, gana el backend: se reporta, no se "arregla" en el cliente.
El patrón offline-first y por qué¶
Acá está la decisión arquitectónica más importante del ecosistema. Los clientes no asumen que hay red: asumen que no la hay y tratan la conexión como un lujo eventual.
El contexto lo explica todo: los sanitarios están en subsuelos, terminales y zonas de aeropuertos/shoppings con conectividad pésima o nula. Si el registro de una limpieza dependiera de tener señal en el momento del tap, se perderían limpiezas reales todos los días. Eso es inaceptable: la regla operativa de Corpal es que ninguna limpieza real se pierde.
sequenceDiagram
participant Op as Operario
participant App as App móvil (Room)
participant API as Backend
Op->>App: Acerca el teléfono al tag NFC
Note over App: Genera client_uuid v4<br/>captura ts del teléfono
App->>App: Persiste en Room (cola offline)
App-->>Op: UI optimista → VERDE (timer corriendo)
Note over App,API: Si hay red, sincroniza ahora<br/>si no, queda en cola
App->>API: POST /sync (cuando hay red)
API-->>App: acción_real + deltas de catálogos
App->>App: Reconcilia UI si difiere (raro)
Las reglas que hacen que esto funcione sin perder ni duplicar nada:
client_uuidse genera AL toque, antes de cualquier I/O. Es la identidad del evento. Persiste en disco local antes de cualquier intento de red, así que sobrevive a crashes, reinicios y a que el sync falle 100 veces.- Idempotencia por
client_uuid. El mismo evento reenviado N veces produce el mismo resultado: el backend deduplica. Reenviar es seguro. - El timestamp del teléfono al momento del tap es la verdad (
inicio_ts/fin_ts). El backend solo registra aparte cuándo recibió el evento (server_received_ts). Esto vale aunque el evento llegue horas después. - UI optimista. Al tapear, la app asume éxito y pinta verde/gris al instante. Si el backend después rechaza, se reconcilia. El operario nunca espera a la red.
- La cola sobrevive a todo. Reinicios de app, de device y actualizaciones: los trabajos pendientes en Room quedan intactos.
Mismo patrón en el kiosk
La terminal Smiley aplica las mismas reglas: opera 100% sin red (solo el "hace X min" del idle pierde frescura), genera client_uuid al toque y deduplica por idempotencia. El cliente cambia, el principio no.
Cómo se comunican: backend como sistema de registro¶
El backend es el sistema de registro (system of record): el lugar donde la verdad se consolida. Los clientes son fuentes de eventos que empujan lo que pasó y reciben de vuelta el estado consolidado.
El mecanismo central es la sincronización (/sync), que dispara periódicamente (cada ~15 min vía WorkManager), al recuperar red, al abrir la app o al loguearse:
- El cliente empaqueta su cola de trabajos pendientes más un watermark (
last_sync) por cada catálogo. POST /synccon ese payload.- El backend procesa los trabajos en orden cronológico (
ts_local_device ASC— el cliente garantiza el orden) y calcula los deltas de catálogos: las filas conupdated_at > last_sync. - La respuesta trae los deltas a aplicar más el estado de cada trabajo procesado.
- El cliente aplica los deltas a su cache local (Room) y marca los trabajos como sincronizados.
sequenceDiagram
participant App as Cliente (Room)
participant API as Backend
participant DB as MSSQL
App->>API: POST /sync<br/>{ last_sync por tabla, trabajos_pendientes }
API->>DB: Aplica trabajos en orden cronológico
API->>DB: SELECT catálogos WHERE updated_at > last_sync
DB-->>API: deltas (sanitario, tag, operario, eventos…)
API-->>App: { deltas, estado por trabajo }
App->>App: Aplica deltas a Room · marca sincronizados
Las consultas en tiempo real (dashboard del BackOffice, frescura del kiosk) son lecturas directas contra el backend; el dashboard hace polling periódico. Pero el flujo que define la arquitectura es el de sync: los clientes acumulan eventos localmente y el backend los consolida.
El corazón del negocio
El motor de taps y el protocolo de sync (/sync, /tap) son la pieza crítica del sistema: ahí viven la idempotencia, el cierre automático de trabajos colgados, el anti doble-tap y los casos especiales del tap. En el backend, leer docs/SYNC_PROTOCOL.md entero es obligatorio antes de tocar nada que los roce. Esta página da el modelo mental; el contrato exacto vive ahí.
Autenticación a alto nivel¶
WorkDone separa tajantemente personas de dispositivos. Son dos planos de identidad distintos.
graph TD
subgraph Personas["Personas — autenticación con JWT"]
OP["Operario / Supervisor / Admin<br/>usuario + PIN/password<br/>/api/v1/app/auth/*"]
WEB["Admin / Supervisor web<br/>usuario + password<br/>/api/authenticate (jhi_user)"]
CLI["Cliente (portal transparencia)<br/>ROLE_CLIENTE"]
end
subgraph Devices["Dispositivos — autenticación con api_key"]
PHONE["Teléfono operario<br/>api_key por device"]
TERM["Terminal Smiley<br/>api_key por device"]
end
OP -->|JWT| API[Backend]
WEB -->|JWT web| API
CLI -->|JWT| API
PHONE -->|api_key| API
TERM -->|api_key| API
- Personas → JWT. Los operarios se loguean con
usuario + PIN(o password) y reciben un JWT corto más un refresh token ligado aldevice_uuid. El BackOffice web tiene su propio plano de login (jhi_user), separado del móvil: el rol web (ROLE_USER/ROLE_ADMIN) no es el rol móvil (ROLE_OPERARIO). Hay además unROLE_CLIENTEde solo lectura para el portal de transparencia. - Dispositivos → api_key. Cada teléfono y cada terminal Smiley se identifica con una api_key propia, usada para rate limiting, trazabilidad y para poder deshabilitar un device (si se pierde o roban un teléfono, administración lo deshabilita y el siguiente sync lo rechaza).
- Endpoints sin auth. El QR público (
/api/v1/publico/*) es anónimo, rate-limited por IP — la cara pública de transparencia para el visitante.
Separación de prefijos
Cada audiencia tiene su prefijo de endpoints: /api/v1/app/* (móvil), /api/v1/admin/* (backoffice), /api/v1/cliente/* (portal cliente) y /api/v1/publico/* (QR público, sin auth). Los detalles de cada contrato — payloads, códigos, errores — están en la referencia de API del backend, no acá.
Deploy¶
El backend corre como un JAR de Spring Boot (servicio Windows vía NSSM) detrás de un reverse proxy (IIS ARR / nginx, 443 → 8080), sobre una EC2 Windows compartida con otros productos de LubecaTech, con MSSQL Server en la misma instancia. HTTPS obligatorio (TLS 1.2 mínimo). Es una topología deliberadamente simple: las cargas son chicas (~240 trabajos/día, ~5 devices sincronizando cada 15 min) y no justifican infraestructura distribuida ni cache compartido.
Para entender las entidades que viajan en estos flujos — sanitario, trabajo, tag_nfc, operario, dispositivo, alerta — pasá por el Glosario.