Protocolo LAN de sala (Bar 0.1)
Contrato entre Personal Comander (cliente) y Personal Bar (nodo). Puerto fijo 8787. HTTP en claro solo en LAN de confianza.
No confundir con Identity: el servicio camareros (VPS https://camareros.siberia.solutions, Docker en el servidor; HTTPS) es el DNI del camarero y la fuente de verdad de cuentas y membresías. El servicio negocio (:8082) es de Bar / Identity Web. El nodo de sala sigue siendo Bar LAN. Commander no apunta Identity a Docker del host (10.0.2.2:8080).
Rutas Identity que Commander llama: identity-contract-paths.txt.
Rutas Bar que Commander llama: bar-contract-paths.txt. El job CI Family contracts las contrasta con Identity main (OpenAPI camareros y negocio) y Bar main (BarLanModule.kt); el informe queda en el summary del job. Aún no es check requerido del ruleset.
Glosario
| Término | Qué es | Qué no es |
|---|---|---|
| Establecimiento | Negocio / local. Registro canónico en Identity. En turno, el camarero se liga al nodo Bar (ModoSesion.Establecimiento). |
Una sala del mapa. |
| Sala | Zona del mapa del establecimiento (barra, interior, terraza…). En Comander: entidad Room Sala + Mesa.salaId (antes «zona»). |
El modo de sesión ni el host LAN. |
| idZona | Identidad de mesa en red: prefijo del nombre de la sala + indiceZona (T3 = Terraza 3). |
El id autoincrement de Room ni el alias visible. |
| Ronda | Lo que Comander envía al Bar al «enviar a cocina» ligado. Bar la parte en tickets BARRA / COCINA. | El pedido Room completo como modelo de red. |
| Preparado | Ticket listo en expo (Bar). Evento SSE ticket.preparado. En Comander: líneas LISTA («para recoger»). |
Servido en mesa. |
| Recogido | El camarero de barra sacó el ticket de la cola (POST /recogido). Evento ticket.recogido. |
Servido en mesa (eso es Comander, LineaEstado.SERVIDA). |
GET /health trae establecimiento (nombre del negocio), establecimiento_id (UUID Identity; se omite si el nodo no está vinculado) y sala como alias deprecado del nombre. sala en health no es una sala del mapa. Commander empareja el libro de oficio por UUID; el nombre es fallback si el id no viene.
Endpoints (Bar)
| Método | Ruta | Uso en Comander |
|---|---|---|
GET /health |
liveness {ok, role:"bar", establecimiento, establecimiento_id?, sala, version} |
Sí (ligar; UUID para el libro de oficio) |
POST /v1/sesion |
{ "qr": "phid1:…" } → { admitido, camareroId, nombre } |
Sí (candado carta/mapa/TPV al ligar; no inicia jornada) |
POST /v1/sesion/iniciar |
{ "qr": "phid1:…" } → estado de jornada |
Sí (gesto Empiezo; 404 = nodo viejo) |
POST /v1/sesion/cortar |
{ "qr": "phid1:…" } |
Sí (terminar jornada o salir del nodo) |
POST /v1/heartbeat |
{ "camareroId" } |
Sí (~10 s mientras hay jornada) |
POST /v1/rondas |
recibe una ronda → 201/200 + lista de tickets; 403 sin jornada | Sí (enviar; se guarda ticketId por línea) |
POST /v1/tickets/{id}/preparado |
ticket preparado | No (UI de expo en Bar) |
POST /v1/tickets/{id}/recogido |
ticket recogido en expo | No (UI de expo en Bar) |
GET /v1/estado |
establecimiento, salas, colas, mesas | Sí (realinear tickets al conectar SSE; réplica de layout al ligar si admitido) |
GET /v1/carta |
catálogo {schema?, productos:[{id,nombre,categoria,precio,disponible,subfamilia?,permiteNota?,grupos?}], gruposModificador?}. id es slug (cana) en schema 0/omitido, UUID cuando Bar migra el catálogo. Campos extra (subfamilia, grupos) son forward-compat: un nodo viejo no los manda y Commander no borra grupos locales. schema distinto (o ids UUID contra codigoBar slug) reconstruye el espejo por nombre, sin borrar filas locales. |
Sí (espejo al ligar) |
SSE /v1/eventos |
ticket.preparado / ticket.recogido / sesion.cortada |
Sí (aviso recoger y corte de jornada) |
| UDP 8788 | Beacon de presencia {ph, role, establecimiento, puerto, activo} |
Sí (radar en Resumen; no es HTTP) |
Handshake al ligar: POST /v1/sesion con el QR. Si el camarero está ACTIVA en la lista blanca, admitido=true y candan carta, mapa y TPV. 404 (nodo viejo) o no admitido: el ligue sigue; admitido=false.
Contratado ≠ jornada. Ligar al nodo no autoriza comandas. El camarero pide POST /v1/sesion/iniciar (gesto Empiezo). Bar concede sesionActiva. Commander manda POST /v1/heartbeat ~10 s. Corte: POST /v1/sesion/cortar, SSE sesion.cortada, 403 de ronda/heartbeat, o pérdida de lista blanca. 404 en iniciar = Bar 0.1: se trata como jornada implícita.
SSE
event = tipo. data = JSON SalaEvent v1 (Bar #37 / PR #38).
mesaId va en la raíz. destino, numeroCola y rondaId van dentro de ticket. Commander los hidrata a la raíz al parsear. Si faltan, no inventa la mesa.
{
"version": 1,
"tipo": "ticket.preparado",
"ticketId": "p42-t1730000000000-barra",
"preparadoPor": "Anita",
"mesaId": "T3",
"camarero": "Lucía García",
"resumen": "2× Caña",
"ticket": {
"id": "p42-t1730000000000-barra",
"rondaId": "p42-t1730000000000",
"destino": "BARRA",
"estado": "PREPARADO",
"preparadoPor": "Anita",
"numeroCola": 1,
"lineas": [
{ "productoId": "12", "nombreProducto": "Caña", "cantidad": 2 }
]
}
}
Aviso en sala: T3 · Cola 1 Bebida lista (destino BARRA → Bebida, COCINA → Comida).
Al reconectar: GET /v1/estado y marcar PREPARADO (y servidos) como LISTA en líneas con ese ticketId. El bucle SSE no reescribe el mapa.
Réplica de mapa
Bar es la fuente de verdad del layout. Si el camarero está admitido, al ligar Commander lee salas y mesas de GET /v1/estado y hace upsert por codigoBar (el id string de Bar). Conserva id Room, estado, comandaActivaId y reservaActivaId locales. No borra el seed ni salas/mesas sin código. Si no está admitido, 404 o el nodo no manda mapa, el layout local no se toca. idZona sigue siendo función (zonaPrefijo(nombreSala)+indiceZona), no un campo JSON.
Flujo de usuario
- El camarero inicia sesión contra Identity (servicio camareros) desde Entrar. La cuenta puede estar registrada en varios establecimientos; eso no activa un turno.
- Standalone (Local o Identidad): carta y mapa locales. Home no pinta la etiqueta; el radar de Resumen y Gestión → Locales lo indican. Ligarse a un nodo no es jornada.
- En Resumen, el radar sondea la Wi‑Fi (
GET /health+POST /v1/sesion) y escucha el beacon UDP 8788 de Bar (activar / latido / adiós). Si Bar no admite, el local se pinta apagado y no se persiste Establecimiento. Si Identity lista locales y elhealthno coincide, se avisa al pedir jornada pero no se bloquea. En emulador el probe10.0.2.2cubre eladb forward(el UDP no cruza los AVD). - Si está en la lista blanca, carta, mapa y TPV pasan a solo lectura y se replica el layout. El header dice En nodo hasta Empezar jornada (
POST /v1/sesion/iniciar); entonces dice Activo. Al volver a Home se revalidaadmitido. Si se pierde la lista blanca, se suelta el nodo. - Sin jornada no se llama a
POST /v1/rondas. Si Bar corta (SSEsesion.cortada, 403 o latido), el nodo puede seguir ligado. - Al enviar con jornada, Comander manda solo líneas
PENDIENTE, las marcaENVIADAy guarda losticketIddel body. - Bar marca preparado → SSE → líneas
LISTA+ snackbar/notificación. - El camarero marca servido en mesa (
SERVIDA). Recogido de bandeja sigue en Bar.
Pendiente de lista blanca no es una invitación de cuenta. Las invitaciones (Bar invita, camarero acepta o rechaza) se gestionan en Identity; en Commander la bandeja está en Gestión → Invitaciones.
Si Bar falla el POST, la comanda local permanece enviada y el tablet muestra un aviso.
Este flujo está en el APK v1.7 (radar en Resumen). La v1.6 ligaba el turno desde Ajustes; la v1.5 pública funcionaba en modo Local y no incluía la integración completa de establecimiento.
Presencia LAN (UDP 8788)
No es HTTP ni SSE. Bar, mientras Local activo, envía un datagrama de broadcast cada ~2 s y un adiós al parar:
{"ph":"phbar1","role":"bar","establecimiento":"Casa Pepe","puerto":8787,"activo":true}
activo:false es el corte. Commander en Resumen oye el puerto 8788, toma el host del origen (nunca lo pinta) y confirma con GET /health. Sin beacon durante 6 s el local desaparece. Un puerto 8787 abierto que no sea Bar no se lista. El scan TCP /24 queda de respaldo al entrar (y se reutilizan solo los nodos ya confirmados mientras Resumen sigue visible). Dos emuladores no comparten UDP: Commander sigue probando 10.0.2.2 mientras Resumen está visible.
Payload de ronda
{
"id": "p42-t1730000000000",
"mesaId": "T3",
"numero": 1,
"camarero": "Lucía García",
"creadoEn": 1730000000000,
"lineas": [
{ "productoId": "12", "nombreProducto": "Caña", "cantidad": 2, "modificadores": [] },
{ "productoId": "3", "nombreProducto": "Hamburguesa", "cantidad": 1, "nota": "sin cebolla",
"modificadores": [{ "grupo": "Punto", "opcion": "Al punto", "delta": 0 }] }
]
}
id: único por envío. Si se repite, Bar responde 200 y no duplica.mesaId: idZona, p. ej.T3. Nunca el id Room.productoId: id de red de Bar (slugcanao UUID del catálogo) si Commander espejóGET /v1/carta(codigoBar); si no, el Long de Room en string. Sin match, Bar manda la línea a BARRA.notaymodificadores(grupo/opción/delta en claro): snapshot de la línea. Un Bar que aún no los lee los ignora (Gson); la expo sigue ciega hasta el ítem de Bar.- Respuesta: array de tickets (
id={rondaId}-barra/-cocina).
Comportamiento en Comander
| Modo | Al enviar a cocina |
|---|---|
| Local / Identidad | Solo Room: pedido ENVIADA, mesa EN_COCINA, líneas LISTA (para recoger sin expo). |
| Establecimiento con jornada | Delta PENDIENTE + POST /v1/rondas → ENVIADA + ticketId. SSE + /estado. Servido solo desde LISTA. |
| Establecimiento sin jornada | No se llama a /v1/rondas. El camarero ve el aviso y empieza la jornada en Ajustes. |
No se recorta el un-tablet. esActivo en código sigue significando «ligado al nodo»; el permiso de comandar es sesionTrabajo.