ixiclinicDocs
Desarrolladores

Impresión + Bridge

Arquitectura del sistema de impresión cloud→LAN — módulo printing del API, render ESC/POS, plugin bridge-auth y el agente ixiclinic Connect.

El API vive en la nube, pero las impresoras del laboratorio están en la LAN del cliente y el cloud no las alcanza. La solución es ixiclinic Connect (carpeta bridge/): un agente que se instala en una máquina dentro de la sucursal, es la única pieza que toca el hardware (impresoras de red :9100 y USB, cajón de dinero) y se conecta saliente a la nube — no hay que abrir puertos ni exponer nada.

Las piezas

PiezaUbicaciónRol
Módulo printingapps/api/src/modules/printing/Bridges, impresoras y la cola de trabajos
Render ESC/POSapps/api/src/lib/escpos/Construye los bytes que el agente imprime
Plugin bridge-authapps/api/src/plugins/bridge-auth.tsAutentica al agente por token de dispositivo
ixiclinic Connectcarpeta bridge/ (corre en Bun en la LAN)Ejecuta los trabajos contra el hardware

Flujo cloud → LAN

El render ocurre en la nube: el API construye los bytes ESC/POS y los manda en base64; el agente solo los escupe a la impresora. Así los formatos pueden cambiar sin tocar los agentes instalados.

Cloud (api.ixiclinic.com)            LAN de la sucursal
  enqueue job  ─────socket─────▶  ixiclinic Connect ──TCP 9100──▶ impresora de red
  (push "hay trabajo")                  │         ──CUPS/spooler──▶ impresora USB
  GET /api/bridge/jobs  ◀──poll──       │
  POST .../result       ◀──────────────┘
  1. EncolarenqueueJob (apps/api/src/modules/printing/service.ts) resuelve la impresora destino (por printerId explícito o por rol, ver abajo) y su bridge dueño, dedupe por idempotencyKey, persiste el trabajo como pending en print_jobs y avisa al agente con emitBridgeJob(bridgeId, { jobId }) por Socket.IO.
  2. Despertar vs fuente de verdad — el push por socket es solo un "despierta". La fuente de verdad es el poll GET /api/bridge/jobs: si el socket se cae, el agente igual recupera sus trabajos en el próximo poll. getPendingJobs marca los trabajos como sent al entregarlos y adjunta el destino resuelto (ip:port o nombre del sistema) para que el agente sepa exactamente dónde imprimir.
  3. Resultado — el agente reporta con POST /api/bridge/jobs/:id/result (status: printing / done / failed). El servidor maneja los reintentos: un failed con intentos restantes vuelve a pending y reaparece en el próximo poll; al agotar maxAttempts (default 3) se asienta como failed.

Enrutamiento por rol

Un trabajo pide un rol ('invoice', 'kiosk:<id>', 'cashbox:<id>', 'lab_labels', …) y resolvePrinter busca la impresora activa cuyo array roles lo incluya, prefiriendo la de la sucursal del trabajo. Así, p. ej., dos kioscos son simplemente dos filas de printers con roles distintos. También se puede apuntar a una impresora concreta por printerId.

Modelo de datos (apps/api/src/db/schema/printing.ts)

  • bridges — el agente: deviceToken (secreto único), status (online/offline), platform, capabilities (p. ej. escpos, zpl, cash_drawer, network_scan, usb), lastSeenAt.
  • printers — impresora física: kind (thermal_80mm, thermal_58mm, label_zpl, laser), connection (network / usb / system), ipAddress:port (red, ESC/POS 9100) o systemName (spooler del SO), roles (clave de enrutamiento), hasCashDrawer, y estado en vivo reportado por el agente.
  • print_jobs — cola durable: documentType (ticket/invoice/receipt/label/test/ raw), format (escpos/zpl/text/raw), payload (doc estructurado o bytes pre-renderizados base64), copies, openCashDrawer, priority, status, attempts/ maxAttempts, error e idempotencyKey (único por tenant para deduplicar reprints).

Render ESC/POS (apps/api/src/lib/escpos/)

  • builder.tsEscPos, un constructor de comandos sin dependencias: texto, alineación, negrita, tamaños, QR (qr), Code128 (barcode), corte (cut), pulso del cajón (drawer) e imagen raster (raster). Produce el Buffer o su base64.
  • documents.tsrenderQueueTicket (ticket de turno) y renderInvoiceReceipt (recibo).
  • invoice-svg.tsbuildInvoiceSvg: el comprobante se diseña como SVG para control total de fuentes, logo y layout.
  • svg-raster.tsrenderSvgToEscpos: rasteriza el SVG a un bitmap 1-bpp con @resvg/resvg-js (fuentes DejaVu Sans empaquetadas) y lo emite como bandas raster ESC/POS (GS v 0), listas para imprimir verbatim — sin cambios en el agente.

Autenticación del agente (bridge-auth)

Los endpoints /api/bridge/* no usan JWT: son públicos para el plugin de auth y se guardan con requireBridgeAuth (apps/api/src/plugins/bridge-auth.ts). El agente presenta su deviceToken en el header X-Bridge-Token; el plugin lo busca en la tabla bridges, valida que esté activo e inyecta request.bridge. En el socket, el namespace /bridge valida el mismo token en el handshake y voltea el estado online/offline.

Endpoints (apps/api/src/modules/printing/routes.ts)

Lado admin (gateado con requireModule('settings')): gestión de bridges, impresoras y trabajos — GET/POST/PATCH/DELETE /api/printing/bridges, .../rotate-token, .../rescan, .../installer, los equivalentes de printers, y la cola GET/POST /api/printing/jobs, .../retry, .../cancel, test-label, test-template.

Lado agente (device-token, sin JWT):

RutaPara qué
POST /api/bridge/registerEl agente se registra / reporta plataforma y versión
POST /api/bridge/heartbeatLatido periódico
POST /api/bridge/printersReporta las impresoras descubiertas
GET /api/bridge/jobsRecoge trabajos pendientes (fuente de verdad)
POST /api/bridge/jobs/:id/resultReporta el resultado de un trabajo

Y rutas públicas sin token para el onboarding/instaladores: GET /api/bridge/latest, GET /api/bridge/desktop/latest, GET /api/bridge/download/:kind/:platform (proxy de assets de release privados), GET /api/bridge/install.sh y GET /api/bridge/install.ps1.

El agente ixiclinic Connect (bridge/)

Corre en Bun ≥ 1.1 (compila a un binario único) o Node ≥ 18 en desarrollo. Se conecta saliente por Socket.IO (atraviesa NAT) y:

  • Descubre impresoras: escaneo de la subred al puerto 9100 + las instaladas en el SO (CUPS/lpstat en macOS/Linux, wmic en Windows).
  • Hace poll de trabajos (IXICLINIC_POLL_MS, default 15 s), heartbeat (default 30 s) y re-descubrimiento periódico (default 10 min), configurables por variables de entorno.
  • Se enrola con su token: en el admin se crea el agente (Hardware → Nuevo agente), se copia su token y el instalador lo registra como servicio (systemd / launchd / tarea de Windows). El agente avisa en logs cuando hay una versión más nueva (GET /api/bridge/latest).

Los binarios multi-SO los produce GitHub Actions al pushear un tag connect-v* (Bun cross-compila Linux / Windows / macOS).

Para el detalle de la app, ver Apps del monorepo → Connect.

On this page