Skip to content

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

  1. El camarero inicia sesión contra Identity (servicio camareros) desde Entrar. La cuenta puede estar registrada en varios establecimientos; eso no activa un turno.
  2. 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.
  3. 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 el health no coincide, se avisa al pedir jornada pero no se bloquea. En emulador el probe 10.0.2.2 cubre el adb forward (el UDP no cruza los AVD).
  4. 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 revalida admitido. Si se pierde la lista blanca, se suelta el nodo.
  5. Sin jornada no se llama a POST /v1/rondas. Si Bar corta (SSE sesion.cortada, 403 o latido), el nodo puede seguir ligado.
  6. Al enviar con jornada, Comander manda solo líneas PENDIENTE, las marca ENVIADA y guarda los ticketId del body.
  7. Bar marca preparado → SSE → líneas LISTA + snackbar/notificación.
  8. 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 (slug cana o 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.
  • nota y modificadores (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/rondasENVIADA + 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.