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
| Pieza | Ubicación | Rol |
|---|---|---|
Módulo printing | apps/api/src/modules/printing/ | Bridges, impresoras y la cola de trabajos |
| Render ESC/POS | apps/api/src/lib/escpos/ | Construye los bytes que el agente imprime |
Plugin bridge-auth | apps/api/src/plugins/bridge-auth.ts | Autentica al agente por token de dispositivo |
| ixiclinic Connect | carpeta 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 ◀──────────────┘- Encolar —
enqueueJob(apps/api/src/modules/printing/service.ts) resuelve la impresora destino (porprinterIdexplícito o por rol, ver abajo) y su bridge dueño, dedupe poridempotencyKey, persiste el trabajo comopendingenprint_jobsy avisa al agente conemitBridgeJob(bridgeId, { jobId })por Socket.IO. - 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.getPendingJobsmarca los trabajos comosental entregarlos y adjunta el destino resuelto (ip:port o nombre del sistema) para que el agente sepa exactamente dónde imprimir. - Resultado — el agente reporta con
POST /api/bridge/jobs/:id/result(status:printing/done/failed). El servidor maneja los reintentos: unfailedcon intentos restantes vuelve apendingy reaparece en el próximo poll; al agotarmaxAttempts(default 3) se asienta comofailed.
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) osystemName(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,erroreidempotencyKey(único por tenant para deduplicar reprints).
Render ESC/POS (apps/api/src/lib/escpos/)
builder.ts—EscPos, 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 elBuffero subase64.documents.ts—renderQueueTicket(ticket de turno) yrenderInvoiceReceipt(recibo).invoice-svg.ts—buildInvoiceSvg: el comprobante se diseña como SVG para control total de fuentes, logo y layout.svg-raster.ts—renderSvgToEscpos: 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):
| Ruta | Para qué |
|---|---|
POST /api/bridge/register | El agente se registra / reporta plataforma y versión |
POST /api/bridge/heartbeat | Latido periódico |
POST /api/bridge/printers | Reporta las impresoras descubiertas |
GET /api/bridge/jobs | Recoge trabajos pendientes (fuente de verdad) |
POST /api/bridge/jobs/:id/result | Reporta 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/
lpstaten macOS/Linux,wmicen 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.