caso de estudio · 2026

Aplicación de gestión / Trazabilidad
Ortho Control

Ortho Control

01

el reto

El laboratorio de ortodoncia organizaba su producción en Monday.com. Como tablero funcionaba, pero arrastraba tres problemas que no se arreglan con más configuración: se paga por usuario y por mes, indefinidamente; los datos —ligados a pacientes— viven en un servidor ajeno; y nada de lo específico del oficio existe en una herramienta genérica. Lotes, caducidades, escaneo de códigos médicos, consumo de material por trabajo: o se construye a mano encima con automatizaciones frágiles, o no está. En paralelo corría una obligación legal. Un laboratorio que fabrica dispositivos médicos a medida debe, bajo el reglamento europeo MDR, poder vincular cada aparato entregado al lote concreto de material con el que se hizo: si un lote de resina sale defectuoso, hay que saber en qué pacientes se usó. Eso se llevaba en una hoja de cálculo y en la memoria de quien fabricaba. El encargo tenía entonces dos mitades: sustituir la suscripción por algo propio, y que ese algo resolviera lo que la suscripción no resolvía.

02

la solución

Una aplicación de escritorio diseñada, construida y desplegada de cero, instalada en los ordenadores de la clínica. La decisión de producto que marca toda la interfaz: copiar deliberadamente la ergonomía de Monday —tableros, columnas de estado con etiquetas de color, edición en línea— para que nadie tuviera que reaprender a trabajar. Cambiar de herramienta no debía sentirse como cambiar de herramienta. Debajo, en cambio, todo lo que faltaba: lotes y caducidades, escaneo GS1 DataMatrix que trae código, lote y fecha en un solo disparo del lector, descuento automático de stock, etapas de fabricación fechadas, y la cadena lote → consumo → caso cerrada sin añadir trabajo manual. La decisión estructural: el núcleo de la aplicación nunca habla con una base de datos, habla con una interfaz única, con dos implementaciones intercambiables —SQLite en local, PostgreSQL en compartido—. Eso permitió arrancar en un solo puesto y pasar meses después a base común sin reescribir la aplicación. El salto a toda la clínica va escrito, no improvisado: PostgreSQL en Docker sobre el NAS del gabinete y deliberadamente no sobre el servidor de dominio, puestos apuntando a un nombre DNS y nunca a una IP, instalación por usuario sin permisos de administrador, migración del histórico en una sola transacción verificada con vuelta atrás, y un manual de puesta en servicio en francés: alojamiento y alternativas, red y cortafuegos, copias de seguridad y prueba de restauración, síntomas frecuentes.

03

el resultado

En producción, seis puestos, base compartida y actualización en tiempo real entre ellos. La suscripción está eliminada y la clínica es propietaria del software que usa; los datos de pacientes se quedaron en casa; el requisito regulatorio está cerrado: de un lote defectuoso se llega a los pacientes, y de un paciente a sus lotes. Hasta hoy: 293 casos trazados, 1 738 etapas de producción fechadas, 8 tablas y 20 807 líneas de TypeScript. Lo que me llevo cabe en tres averías. Un stock en −6 porque dos puestos leían antes de escribir, resuelto con bloqueo de fila y verificado con dos instancias contra un PostgreSQL real. Un corte de red que cerraba la aplicación entera en lugar de degradarla, convertido en una reconexión con espera creciente que se resincroniza al volver. Y previsiones de ruptura absurdas —diecinueve años de autonomía en una placa— nacidas de la misma fórmula copiada tres veces y divergida. Desde entonces, cuando la base de cálculo es demasiado fina, los paneles se niegan a responder y dicen por qué: un número inventado sobre el que alguien va a comprar material es peor que un guion.

el encargo

salir de monday sin que nadie lo note

antes · Monday.com

después · la aplicación

Columna por columna, es el mismo tablero — y ese parecido es deliberado: cambiar de herramienta no debía sentirse como cambiar de herramienta. Lo que cambia no se ve aquí: debajo de la tabla de la derecha hay lotes, caducidades, consumo por trabajo y escaneo de códigos médicos. Nombres de pacientes pixelados; la captura de Monday conserva su estructura real.

lo que cambia

coste recurrente

Eliminado. Sin licencias por usuario ni renovación anual: la clínica es propietaria del software que usa.

los datos

En la clínica. La base vive en su propio NAS, no en un servidor de terceros. Con información ligada a pacientes, eso deja de ser un detalle.

funcionalidad

A medida. Escaneo GS1, trazabilidad lote → caso, etapas de fabricación fechadas, alertas de desgaste. Nada de esto cabía en la herramienta genérica.

dimensión

el proyecto en cifras

20 807

líneas TypeScript / TSX en src/

2 334

líneas de scripts: AutoHotkey, PowerShell, Node

98

commits en 3 meses

8

tablas, con 2 backends intercambiables

293

casos de laboratorio trazados

1 738

eventos de etapa de producción fechados

stack

qué hay debajo

Runtime / UI

Electron 39 · React 19 · TypeScript 5.9 en strict · electron-vite 5 con HMR · Vibe, el design system de Monday (@vibe/core 4)

Datos

SQLite (better-sqlite3 12) · PostgreSQL (pg 8) · tiempo real por LISTEN/NOTIFY · migraciones propias idempotentes · visualización en SVG escrita a mano

Integraciones

Códigos GS1 DataMatrix, parser propio · lector 2D en modo HID (Inateck BCST-52) · lectura de .xlsx sin dependencias · etiquetas Brother QL-810W · automatización de escritorio con AutoHotkey

Build / despliegue

electron-builder (NSIS, Windows) · ESLint y Prettier en flat config · instalador por usuario, sin administrador · icono y AppUserModelID vía rcedit y PowerShell · migración SQLite → PostgreSQL con script transaccional

arquitectura

una capa de datos enchufable

La decisión estructural del proyecto: el IPC nunca habla con una base de datos, habla con una interfaz DataStore. Hay dos implementaciones con el mismo contrato, y cambiar de una a otra no requiere recompilar — se elige por variable de entorno o por un fichero de configuración. Eso permitió arrancar en local, en un solo PC, y pasar meses después a base compartida sin reescribir la aplicación.

Renderer

React 19 · un componente por board

Tablas editables inline, paneles de estadísticas, primitivas SVG propias. Cero acceso a datos.

↕ window.api — contextBridge, sin nodeIntegration

Main

Proceso Node · IPC, impresión y vigilancia de carpeta

Mapeo fino sobre el store; ámbito de los ajustes (comunes o por puesto); difusión de cambios a las ventanas.

↕ DataStore — una sola interfaz, un solo contrato

Persistencia

SqliteStore

Local, por defecto. Fichero en el perfil del usuario, WAL, migraciones aditivas.

PgStore

PostgreSQL central en el NAS del laboratorio. Carga por import dinámico, transacciones con bloqueo de fila y notificaciones en tiempo real.

La lógica que ambos lados comparten —como la regla de descuento automático de stock— vive en src/shared/ con tipos estructurales propios, para no depender ni de los tipos del renderer ni de los del store.

despliegue

de un puesto a toda la clínica

La aplicación arrancó en un solo ordenador con base local. En cuanto pasó a ser la herramienta de trabajo del laboratorio, el encargo creció: ponerla en todos los ordenadores de la clínica contra una única base compartida, con actualización en tiempo real entre puestos.

Esa es la parte que separa un proyecto personal de un sistema que otros usan, y la que más decisiones obligó a tomar por escrito antes de tocar nada — sobre infraestructura que ya está en producción para otra cosa.

  1. 01

    PostgreSQL en Docker, sobre el NAS de la clínica

    Y deliberadamente no sobre el servidor principal, que es a la vez controlador de dominio y servidor de ficheros de todo el gabinete. Si la base falla, se para la aplicación — no las sesiones, ni los recursos compartidos, ni el software de pacientes. El NAS además ya está en RAID, encendido 24 h y dentro del plan de copias.

  2. 02

    Los puestos apuntan a un nombre DNS, nunca a una IP

    Para poder mover la base de máquina el día que haga falta sin tocar uno por uno los ordenadores del gabinete.

  3. 03

    Instalación sin permisos de administrador

    El instalador es por usuario y la configuración de cada puesto es un fichero en su propio perfil: montar un ordenador nuevo no necesita credenciales de dominio ni tocar el registro. Cambiar de base local a compartida —o volver atrás— es editar ese fichero, sin recompilar. La opción por directiva de dominio existe en el código, pero se descartó por no aportar nada aquí a cambio de exigir un administrador.

  4. 04

    El histórico se migró con un script transaccional

    Conserva los identificadores originales —lotes, consumos y eventos se referencian entre sí— y reposiciona las secuencias al terminar; convierte las fechas a zona horaria explícita, porque sin eso el histórico entero se desplazaba una hora y la producción por día cambiaba de casilla. Todo en una sola transacción, con los recuentos verificados dentro: si algo no cuadra, rollback. Tiene modo de ensayo.

  5. 05

    Qué es de todos y qué es de cada puesto

    Al pasar a base compartida, los ajustes de interfaz dejaron de ser «los míos». Las etiquetas de estado y sus colores se comparten —todos deben ver lo mismo—; los anchos de columna no, porque dependen de la pantalla que uno tenga delante y compartirlos movería la tabla de los otros cinco al arrastrar un borde. El reparto se resuelve en el servidor y el interfaz ni se entera.

  6. 06

    El despliegue va escrito, no improvisado

    Un manual de puesta en servicio en francés, con su PDF: decisión de alojamiento y sus alternativas si el NAS no sirve, verificaciones previas, red y cortafuegos, copias de seguridad y prueba de restauración, síntomas frecuentes y checklist final. Incluye lo que no se toca — añadir una regla al cortafuegos, nunca activarlo desde cero con una sola, que cortaría el resto de servicios de la clínica. Un software a medida sin manual de operación no es un ahorro, es una dependencia disfrazada.

funcionalidad

tres superficies de trabajo

inventario

« Inventaire »
  • Entrada por escaneo GS1 DataMatrix: un solo disparo del lector trae GTIN, lote y caducidad, y el parser los separa. Si el GTIN ya está en el catálogo, la app lo reconoce y evita el duplicado.
  • Todo se teclea en envases, nunca en unidades. «Recibí 12 botellas», y la app multiplica por el contenido. Es la regla que más errores de stock ha evitado.
  • Diario por artículo: cada entrada, salida y corrección deja una línea fechada, ligada a su lote. La trazabilidad del producto se lee de un tirón.
  • Panel de estadísticas con tres vistas: lo esencial (qué hay que pedir), la visión general y la ficha por artículo — valor de stock, rotación, caducidades y autonomía.
El stock se lee en las dos unidades a la vez — «123.3 sachets / 1225 ud», «3.2 boîtes / 1520 ud». Es la regla que evitó que «12 botellas» volvieran a entrar como 144 ml.

seguimiento de producción

« Suivi Labo »
  • Tabla estilo Monday: edición inline, columnas redimensionables y renombrables, columnas de estado con etiquetas y colores creados por el usuario, agrupación automática por estado.
  • Etapas de fabricación fechadas: cada cambio de etapa escribe un evento datado, lo que convierte una etiqueta de color en un histórico medible (duraciones, cuellos de botella, carga por semana).
  • Una tarea terminada se bloquea. La trazabilidad por lote solo se sostiene si una tarea equivale a un trabajo entregado; el aviso de bloqueo, además de prohibir, abre el siguiente pase del paciente.
  • Alerta activa de desgaste de la fresa de corte, deducida de un diario de eventos en vez de un contador almacenado — para que seis puestos sumando a la vez no se pisen.
El sub-proceso de fabricación de un trabajo terminado: las seis etapas fechadas al minuto, 11,5 días de duración total, la etapa más lenta señalada en naranja y la comparación contra las otras 124 tareas del mismo tratamiento. De aquí sale la mediana de producción.

puente de entrada de citas

« Ortholeader »
  • El software de pacientes del laboratorio es cerrado y sin API. La vía elegida es legítima por diseño: se usa su propia función de exportar la tabla a Excel. Nada de leer su base de datos, interceptar red ni raspar pantallas.
  • El único dato que cruza los dos sistemas es el número de orden, la clave compartida. Acoplamiento mínimo y estable.
  • Captación automática con AutoHotkey: exporta cada dos horas en franja laboral, solo si el puesto lleva 90 segundos inactivo, y devuelve el ratón y la ventana activa donde estaban.
  • Nada pasa a producción sin aprobación humana, y el puente se limpia solo de las citas que el seguimiento ya cubre — su función es verificar que no se queda ninguna sin pasar.
Dos columnas de texto que nunca se mezclan: lo que escribe el gabinete al exportar la cita y lo que anota el laboratorio. Solo la segunda pasa a la tarea. Cada fila se envía o se anula a mano.
La tesis del proyecto, en una tabla. El material que salió del stock para este trabajo concreto, lote por lote, con su número de lote real y su fecha. Es el requisito regulatorio resuelto: de un lote defectuoso se llega a los pacientes, y de un paciente a sus lotes.

ingeniería

cinco problemas reales y cómo se cerraron

Se detectaron contra los datos y el material reales del laboratorio, no en un entorno de pruebas: unos en uso diario, otros reproducidos con dos puestos trabajando a la vez sobre la base compartida. Es la parte del proyecto de la que más he aprendido, así que la documento con el síntoma medido delante.

un lote con stock negativo

concurrencia
síntoma
Con dos puestos trabajando a la vez, dos salidas de 8 unidades sobre un lote de 10 lo dejaban en −6.
causa
Los dos puestos leyeron 10, los dos concluyeron que había bastante, y como el UPDATE era relativo se aplicaron las dos restas. Con un solo PC esto no existe; con seis, cada «leer y luego escribir» es una carrera.
solución
Toda operación de leer-y-escribir pasa a transacción con SELECT … FOR UPDATE. Verificado con dos instancias simultáneas contra un PostgreSQL real.

una conexión ociosa que mataba la aplicación entera

resiliencia
síntoma
Al cortar la conexión con el servidor —un reinicio nocturno, la red que cae, un cortafuegos que corta por inactividad— la aplicación no se degradaba: se cerraba entera, sin aviso, en cada puesto conectado.
causa
Un error en una conexión ociosa del pool se emite como evento; sin un oyente pool.on('error'), Node lo convierte en excepción no capturada.
solución
Manejador en el pool, y la escucha de notificaciones engancha su error antes de conectar. La reconexión reintenta siempre con espera creciente (2 s → 30 s) y re-sincroniza al recuperarse: lo que pasó durante el corte no dejó ninguna notificación para ese puesto.

«ruptura de stock en 2 días» en un material que nadie había tocado

modelo de cálculo
síntoma
El panel anunciaba rupturas falsas, y la misma resina daba 26 días de autonomía en un filtro y 316 en otro. Una placa llegó a 7 171 días — diecinueve años.
causa
Tres copias de la misma fórmula ingenua (salidas ÷ días), mal por tres razones distintas: contaba como consumo las correcciones de inventario, dividía por la duración nominal del filtro en vez de por la del histórico real, y trataba un movimiento suelto como si fuera un ritmo.
solución
Una definición única, con media ponderada de vida media 30 días (los días sin salida cuentan en el divisor, que es lo que distingue «12 salidas en 12 días» de «12 en 12 meses»). Por debajo de 7 días observados y 2 salidas el panel se niega a responder y dice por qué, en vez de dar una cifra sobre la que alguien acabaría comprando.

la búsqueda dejaba de aceptar teclas, sin nada que mirar

diagnóstico
síntoma
Intermitente, cada pocos días: el campo de búsqueda dejaba de escribir y había que cerrar la aplicación. En pantalla todo parecía normal.
causa
Imposible de reproducir a voluntad, así que construí una sonda dentro de la app (un atajo que vuelca el estado del DOM a la base de datos, sin registrar jamás lo tecleado — por ese campo pasan nombres de pacientes). El primer volcado descartó las hipótesis obvias. El segundo fue concluyente: las teclas llegaban al DOM, sin anular, con el campo vacío — el contenido había perdido el «focus de página» de Chromium, estado en el que las teclas se entregan pero el texto no se inserta.
solución
El único gesto que lo repara es webContents.focus() desde el proceso principal; desde la página no hay forma. Ahora se dispara solo en cuanto llega una tecla sin foco, así que el usuario no llega a notarlo.

12 botellas que entraron como 144 ml

diseño de interfaz
síntoma
Teclear «12» en una entrada de resina daba 144 ml en lugar de 12 000. El stock apenas se movía y parecía que el panel no se refrescaba.
causa
El campo pedía envases — bien — pero se abría con un contenido por defecto de 1 unidad. Pedir la unidad correcta no basta si el valor por defecto es el equivocado. El mismo fallo de fondo tenía el «stock mínimo», que se leía en mililitros cuando el usuario pensaba en botellas: la alerta no saltaba nunca.
solución
Todo campo de cantidad se abre con el envase de referencia del artículo y muestra el equivalente en las dos unidades a la vez — «12 botellas = 12 000 ml» — para que un valor mal puesto salte a la vista antes de confirmar. La aritmética de envases se unificó en un módulo compartido.
El resultado del tercer incidente. La app no se limita a dar un número de días: dice sobre qué lo ha calculado — «136 j · rupture vers le 31/01/27 · estimation fiable sur 24 j (40 sorties)». Cuando no tiene base suficiente, escribe un guion y el motivo en lugar de una cifra.

código

dónde vive la complejidad

SuiviLaboBoard.tsx
5 295
InventaireBoard.tsx
3 020
InventaireStats.tsx
1 595
SuiviLaboStats.tsx
1 492
store/sqlite.ts
1 274
store/postgres.ts
1 247
lib/viz.tsx
781
store/types.ts
758
main/print.ts
600
lib/fraise.ts
463
27 archivos restantes
4 282

Líneas por archivo en src/, de 20 807 en total. Los dos boards concentran el 40 % del código y son el objetivo declarado del próximo refactor: crecieron por acumulación de columnas y modales, y ahí es donde cuesta más entrar. Los dos backends pesan casi lo mismo (1 274 y 1 247) — es la señal de que el contrato DataStore se ha mantenido simétrico.

pantallas

el resto de la aplicación

Producción: cinco contadores en un eje común, comparación con el período de referencia y el estado del tablero.
Visión general del inventario. Solo se suman euros y movimientos: sumar mililitros con placas no significaría nada.
Ficha por artículo: curva de stock reconstruida hacia atrás, estado de cada lote y ritmo de consumo ponderado.
La alerta de desgaste: contador desde el último cambio, total del laboratorio y la vida útil de las fresas anteriores.
Las tareas terminadas se bloquean. Una tarea equivale a un trabajo entregado, y reescribirla falsearía su trazabilidad.
Alta de artículo por escaneo: un disparo del lector trae código, lote y caducidad en un solo código bidimensional.
El diario de un artículo: entradas de lote, salidas y correcciones mezcladas por fecha, cada una ligada a su lote.
Acciones en masa. Es uno de los cuatro caminos de escritura del tablero, y por eso también pasa por el bloqueo.
Ajustes compartidos: los colores y las etiquetas los ven todos igual; el ancho de las columnas es de cada puesto.

Todas las capturas proceden de una copia de la base real con los nombres de pacientes sustituidos por otros ficticios. Los volúmenes, lotes, fechas y etapas son los del laboratorio.

criterio

lo que me llevo

negarse a responder es una función

Un número inventado sobre el que alguien va a comprar material es peor que un guion y un motivo. Varias partes de la app dicen «no tengo base suficiente» y explican por qué.

la vía legítima también es la robusta

Se descartó leer la base del software cerrado y se usó su propia exportación. Además de respetar la licencia, es lo único que no se rompe cuando el proveedor actualiza.

instrumentar antes que adivinar

El fallo de foco llevaba semanas sin explicación. Dos volcados de una sonda escrita para el caso lo cerraron; ninguna cantidad de lectura del código lo habría hecho.

una sola definición por concepto

Casi todos los errores de cálculo venían de la misma fórmula copiada tres veces y divergida. Ahora cada concepto —autonomía, estado de stock, número de placas— tiene un único sitio.

Y una honestidad que el proyecto me obliga a añadir: dejar una suscripción no sale gratis, cambia la factura de sitio. La clínica ya no paga una cuota, pero sí depende de que esto siga mantenido — por eso la puesta en servicio va escrita paso a paso, la instalación no pide administrador y los dos backends comparten contrato. Un software a medida que solo su autor sabe operar no es un ahorro, es otra dependencia. Software propietario, repositorio privado; los detalles de infraestructura del cliente van omitidos.

Trazabilidad de lotes Escaneo GS1 Base compartida en tiempo real Software a medida Fin del abono mensual
tecnologías

Electron·React·TypeScript·SQLite·PostgreSQL·Docker·electron-vite·AutoHotkey

¿un proyecto
parecido?

hablemos