# apps/cerebrito/ — Puente de impresión + POS local

Reglas al trabajar en `apps/cerebrito/**`. Se carga solo al tocar este directorio.

## Qué es y su lema

El cerebrito corre en la laptop del local (exe de Windows, Node/TS). Es local-first:
funciona sin internet porque todo lo que importa persiste en disco con escritura
atómica, un apagón a media comanda no puede dejar nada corrupto. Imprime las
comandas por ESC/POS (`src/comanda.ts` + `src/escpos.ts` + `src/impresoras.ts`),
levanta un POS para las tablets de los meseros en `0.0.0.0:8738` (`src/pos.ts`,
`PUERTO_POS`) que anda con o sin internet contra el espejo local del menú, entrega
cada venta cerrada directo a la Estación Fiscal en `127.0.0.1:8739`
(`src/puenteFiscal.ts`) y sincroniza con Supabase cuando hay internet
(`src/sincronizador.ts`, `src/nube.ts`).

## Patrones canónicos

- Todo lo que se guarda en disco va por `escribirAtomico` (`src/espejo.ts`): escribe a
  `ruta + '.tmp'` y luego `renameSync` — nunca `writeFileSync` directo sobre el
  archivo final. Lo usan tanto `espejo.ts` (`local/espejo.json`) como `salon.ts`
  (`local/salon.json`, vía `persistir()`).
- Las operaciones de creación del POS (`/pos/pedido`, `/pos/venta`, `/pos/cobrar`)
  llevan idempotencia por `opId`: el cliente genera uno con `uid()` en el POS
  (línea ~155) y lo reusa en los reintentos; el servidor guarda la respuesta en
  `respuestasPorOp` (ver `recordarOp` en `pos.ts` ~línea 413) y si el mismo `opId`
  vuelve a llegar, devuelve la respuesta guardada sin reejecutar nada.
- El POS tiene freno anti fuerza bruta del PIN (`pos.ts`, sección "EL FRENO DE LA
  PUERTA", auditoría 2026-07-30): 5 intentos por IP (`deQuien`) y backoff
  exponencial 30s → 1m → 2m... hasta 30 min (`anotarFallo`/`trancadoHasta`), y la
  comparación del PIN es timing-safe (`mismoPin`, con `timingSafeEqual`).
- El puente fiscal deduplica por `origen_id` (`pedido:<uuid>` / `cuenta:<uuid>` vía
  `cargasDe` en `puenteFiscal.ts`) — la misma venta puede llegarle a la Estación
  por esta vía directa Y por la cola de la nube, y el libro fiscal la deduplica
  porque `sincronizar_venta_local` sube a la nube con los MISMOS `uuid` que generó
  la laptop. Nunca generar un id distinto para la copia que sube a la nube.
- Los `total_usd` de pedidos y renglones van explícitos en `salon.ts`
  (`cobrarMesa`, `ventaDirecta`): si no viajan, la nube los deja en $0 por
  `coalesce`. No confiar en que la nube los recalcule.

## Anti-patrones (NO hacer)

| No hacer | Hacer | Por qué |
|---|---|---|
| Aceptar `/pos/pedido`, `/pos/venta` o `/pos/cobrar` sin `opId` | Exigir `opId` y chequear `respuestasPorOp` antes de ejecutar | Un doble-tap en la tablet o un reintento por wifi lento duplica la comanda o el cobro |
| Confiar en `extra_usd` o precios que manda la tablet tal cual | Revalidar contra el espejo local (`armarItems` en `salon.ts` recalcula `precio_usd` desde `e.productos`) | La tablet es un cliente no confiable en la red del local |
| `writeFileSync` directo sobre `espejo.json` / `salon.json` | `escribirAtomico` de `espejo.ts` | Un apagón a mitad de escritura deja el archivo corrupto e ilegible al reiniciar |
| Editar `parsearOpciones` solo en `src/comanda.ts` (o solo en shared) | Editar las DOS copias igualitas: la canónica en `packages/shared/src/opciones.ts` y la copia en `apps/cerebrito/src/comanda.ts` | El exe no puede importar `shared`; `test/test_copia_opciones.mjs` compara el cuerpo de la función byte a byte y revienta si divergen |

## Al cambiar comportamiento

- Correr las suites de `test/*.mjs` relevantes (`npm run prueba` corre
  `prueba_local.mjs` + `test_freno_pos.mjs`; hay además `test_local_first.mjs`,
  `test_offline.mjs`, `test_asistente.mjs`, `test_caja_y_copia.mjs`,
  `test_puente_fiscal.mjs`, `test_copia_opciones.mjs`). Varias piden
  `SUPABASE_URL`/`SUPABASE_ANON_KEY`/`SUPABASE_SERVICE_KEY` de la base viva y se
  saltan solas si faltan.
- Nunca correr una suite de pruebas si hay un cerebrito de producción arriba en
  esta máquina: varias suites llaman primero a `exigirPuertosLibres` (en
  `test/puerto_libre.mjs`) para los puertos 8737/8738 y abortan si están
  ocupados. Es una guarda real: el 2026-07-25 una suite le tomó el mando al
  cerebrito de un cliente real por correr sin esta protección.
