· 15 min de lectura
Calculadora 3D: el proyecto que OpenCode server construyó para el emprendimiento de mi esposa
Una mini-app de gestión para el taller de impresión 3D de mi esposa. Calculadora de costos, ventas con estado, dashboard y un módulo de IA que tira sugerencias. Todo el código lo escribió un agente corriendo con OpenCode server en mi host.
Mi esposa tiene una Bambu Lab A1 en el cuarto de las máquinas y un pequeño emprendimiento de impresión 3D: vende piezas por encargo a conocidos, vecinos y algún cliente que llega por Instagram. Cuando arrancó, la gestión de costos y ventas vivía en una hoja de cálculo de Google que se desactualizaba cada dos semanas y que, en el mejor de los casos, mostraba un número que no coincidía con lo que había cobrado.
Le construí una PWA para reemplazar eso. La app se llama Calculadora 3D, vive entera en el navegador del dispositivo donde la abras, y todo el código lo escribió un agente de IA corriendo en mi propio host con OpenCode server. Yo no escribí una sola línea de TypeScript para este proyecto: fui el dueño del producto, el revisor de cada cambio y el que decide cuándo algo está listo.
Este post es la historia de ese proyecto: qué necesitaba el emprendimiento, qué terminó construido, y qué sale bien — y qué no — cuando le pedís a una IA que te arme una app de mediana complejidad de principio a fin.
El taller antes de la app
Las cosas que escuché decir a mi esposa en los primeros meses:
- "Quiero saber exactamente cuánto me sale cada impresión y cuánto tengo que cobrar".
- "Si me piden 5 piezas iguales, no quiero volver a calcular todo de cero".
- "Al final del mes necesito ver si gané plata o no, y por qué".
- "Si hay algo raro — un material que no vende, un cliente que repite, una venta que viene cayendo — avisame antes de que sea tarde".
- "Y no quiero otra cuenta más con usuario y contraseña".
Traducido a features concretas, la lista fue:
- Calculadora de costo por impresión con material, peso, tiempo, gastos extras y multiplicador de margen. Calculada en vivo a cada tecla.
- Plantillas de impresión guardadas para reusar el cálculo cuando alguien pide varias unidades de algo que ya imprimí.
- Registro de ventas con cliente, fecha, monto cobrado, y un estado que va de borrador a finalizado pasando por cobrado y entregado.
- Dashboard mensual con KPIs (cobrado, ganancia, margen) y al menos un par de gráficos de tendencia.
- Backup exportable a JSON, porque sus datos son de ella y no iban a vivir en mi servidor.
- Un módulo de análisis con IA que mire las ventas y le tire sugerencias accionables.
Es decir: una mini-app de gestión + BI para una sola persona. Suficientemente chica para no necesitar backend, suficientemente rica para que no entre en una spreadsheet.
La podés probar en vivo en calculadora.jfaramburu.com — funciona offline después de la primera carga. A lo largo del post vas a ver links directos a cada pantalla para que la recorrida sea de un click.
Por qué OpenCode server (y no yo, ni un freelance)
Lo conté en detalle en OpenCode en producción: corro OpenCode como un container más del host, con acceso a bash, filesystem, docker socket y la red interna web. Es lo que tengo para escribir software de infra; no es la única manera de usar agentes pero es la que mejor me funciona.
Para este proyecto, OpenCode fue el desarrollador. Yo fui el dueño del producto y el revisor. El flujo se pareció bastante a esto:
- Le describí la necesidad en lenguaje natural: "necesito una app para que mi esposa lleve los costos y ventas de su taller de impresión 3D, offline, con IA".
- Él arrancó a preguntar cosas concretas: cómo se mide el costo, qué moneda, si necesito backend, qué materiales usa, qué hace cuando alguien le pide 5 piezas iguales a una que ya hizo.
- Yo le respondía y él iba proponiendo arquitectura, modelo de datos, librerías. A veces proponía cosas razonables, a veces cosas que mostraban que no había entendido bien — y ahí intervenía.
- Cuando la cosa tenía forma, le decía "implementá" y él escribía el código, levantaba el dev server, lo probaba con un par de datos de muestra, y volvía con un screenshot del resultado.
- Yo miraba el resultado y le decía qué cambiar. Él refactorizaba. Iteración hasta que la app hacía lo que queríamos.
Lo que quiero dejar claro antes de meterme en el detalle técnico: OpenCode no reemplazó mi criterio. El producto es mío, la decisión de qué construir es mía, la decisión de qué descartar es mía, y la responsabilidad de lo que termine publicado también. Pero la ejecución mecánica — escribir el código, elegir la librería, debuggear el ReferenceError ese de las 3 AM, hacer el deploy — la hizo él. Y bien.
Las expectativas (lo que la IA hace, lo que no)
Antes de mostrar lo que se construyó, vale poner las cartas sobre la mesa. Esto es lo que aprendí sobre qué se puede esperar cuando delegás un proyecto así:
Lo que OpenCode hace muy bien:
- Andamiaje: armar la estructura de un proyecto React+Vite+Dexie con TanStack Router, configurar Tailwind, levantar el dev server, configurar el Dockerfile. Todo eso lo hizo en minutos y bien.
- Implementación de features con especificación clara: si le describís con precisión qué tiene que hacer un componente, lo implementa y lo itera siguiendo feedback. La calculadora, el formulario de ventas, el backup: cada uno necesitó entre 3 y 6 rondas de revisión.
- Migraciones de schema: cada vez que la base de datos cambió de estructura (versión 2, 3, 4, 5), OpenCode escribió la migración de Dexie con su
upgrade()y la lógica de recuperación. Eso es de las cosas más fáciles de romper a mano. - Testing de lógica pura: las funciones de cálculo (
computePrint,computeSaleProfit) tienen tests porque OpenCode las escribió y testeó en el momento en que las creó. No fueron un después. - Documentación: el
README.md, elAGENTS.mddel workspace, y este post son outputs del mismo flujo. La documentación se hizo junto con el código.
Lo que OpenCode hace regular:
- Decisiones de producto: la primera propuesta de "ventas" tenía un solo ítem por venta. Yo le dije que necesitaba multi-ítem desde el día uno porque a mi esposa le piden combos. Sin esa directiva, la app habría salido subdimensionada. El producto soy yo, no la IA.
- UX fina: detalles como el autocompletar de clientes o el snap de la sugerencia de cobrado cuando agregás ítems fueron bugs que aparecieron durante uso real y que la IA no había pensado.
- Edge cases de IndexedDB: las migraciones tolerantes a fallos, la lógica de "merge vs replace" en backup, la reparación de fotos huérfanas — todo eso necesitó de mi conocimiento del dominio (yo sabía qué podía romperse porque vi las cosas romperse).
- Naming y tono: nombres de variables en español mezclados con inglés, comentarios sin acentos, mensajes de toast a veces un poco secos. Cosma menor que se arregla con una pasada de revisión.
Lo que OpenCode no hace solo:
- Decir "esto no se hace". Si le pedís algo que no tiene sentido, te lo construye igual y lo defiende. Tuve que vetar features dos veces (sync entre dispositivos vía algo casero, autenticación con email) y proponer las alternativas correctas (export/import JSON, no auth porque es single-user).
- Saber cuándo parar. La primera versión de
/analisistenía cuatro pantallas con cuatro tipos de gráficos distintos. Era demasiado. Le dije "recortá a lo que un humano realmente va a mirar" y quedó en tres vistas que se usan. El criterio de "menos es más" sigue siendo humano. - Cuestionar el stack. Si yo le decía "usá MongoDB", usaba MongoDB. La decisión de no usar backend fue mía, no suya.
Esa es la parte que quería dejar por escrito antes de mostrar la app. Es totalmente legítimo que un proyecto así lo escriba un agente de punta a punta, pero solo si vos estás del otro lado decidiendo qué construir, qué descartar, y qué se rompe si cambia el contexto.
Lo que se construyó: recorrida
OK, ahora la recorrida funcional. Es breve — el detalle técnico está en el repo, acá va lo que necesita saber alguien que evalúa si le sirve.
Calculadora de impresión
La pantalla /prints/new es el corazón. Es un formulario con material, peso, tiempo, gastos extras, y un panel de totales pegado al borde derecho que se actualiza en cada keystroke.

La fórmula es la que uno esperaría:
costoBase =
(peso / 1000) * precioPorKg # material
+ horas * costoPorHoraMaterial # mano de obra / overhead
+ horas * electricidadPorHora # UTE prorrateado
+ horas * desgastePorHora # prorrateo del precio de la impresora
+ Σ (extras.cantidad * extras.unitCost)
precioFinal = costoBase * multiplicador
Lo que me importa destacar acá es una decisión que quedó muy clara apenas empezamos: los totales se guardan como snapshot inmutable al momento de imprimir. Cambiar el precio del filamento tres meses después no reescribe la historia. Esto es lo que hace que la app sobreviva a "actualizo la tarifa UTE" sin mentirle al usuario sobre ventas pasadas.
El multiplicador es bidireccional con el precio final: si edito el precio, el multiplicador se recalcula, y viceversa. Esto es porque en la práctica uno no piensa "qué multiplicador quiero", piensa "cuánto quiero cobrar". El link se ve con un icono Link2 arriba del campo.
Plantillas de impresión
Cada cálculo se puede guardar como "impresión". El nombre se lo pone ella: "Engranaje helicoidal M3", "Soporte para celular X", lo que sea. La próxima vez que alguien le pida una igual, abre la plantilla desde /prints, cambia la cantidad, y la registra como venta sin volver a tipear todo.

Ventas (la agenda comercial)
La pantalla está en /sales y la lista es lo primero que abre mi esposa cuando entra a la app. El formulario de ventas es multi-ítem: una venta puede tener varias impresiones, cada una con su cantidad. Esto fue una de las cosas que yo corregí en la primera propuesta de OpenCode — había salido como single-ítem y eso no servía para combos.
Los estados siguen un flujo semi-lineal con seis valores:
borrador → presupuestado → cobrado → entregado → finalizado
└────────────→ cancelado
En la lista, los seis estados se agrupan en cuatro fases (chips arriba, siempre visibles): En pipeline, Pendiente de cobro, Cobradas, Cerradas. Eso permite filtrar "todas las pendientes de cobrar" con un click.

Dos detalles de UX que ella usa todo el tiempo y que no eran obvios al principio:
- Autocompletar cliente: cuando escribe un nombre, la app le sugiere clientes existentes. Si ya existía, autocompleta también el teléfono. Implementación: un
<datalist>HTML puro, sin librería de combos. - Compartir por WhatsApp: cuando una venta está presupuestada, hay un botón verde que arma un mensaje con el detalle y abre WhatsApp con la conversación prearmada. En el celular funciona vía Web Share API (adjunta las fotos de las piezas automáticamente). En desktop abre WhatsApp Web con el texto y ella adjunta las fotos a mano — la UI le avisa.
Dashboard y gráficos
El dashboard es la home, en /. Arriba tiene los KPIs del mes (ventas, cobrado, ganancia, margen) con un selector de rango custom (Desde / Hasta). Abajo hay una grilla de gráficos que el sistema elige automáticamente según los datos — si no hay ventas de un tipo, no muestra ese gráfico. Mi esposa no configuró nunca qué gráficos ver; la app los va eligiendo sola.

La librería de charts es SVG puro. Donut, líneas, gauges, sparklines, h-bars — todo escrito a mano en el bundle. Cero recharts ni apexcharts. Esto lo decidió OpenCode en función de "bundle chico + control fino sobre la animación". Le pedí que no me agregara dependencias pesadas y cumplió.
Módulo de análisis con IA
La pantalla está en /analisis. Esta es la sección más nueva y la más experimental. Tiene dos vistas:
- Ventas generales sobre un rango de fechas.
- Un producto filtrando por nombre y rango.
Ambas invocan una IA con los datos del período en formato JSON pre-calculado en el cliente, y le piden devolver otro JSON estructurado. El provider por defecto es Puter usando gpt-5-nano, que tiene una capa gratuita razonable y no requiere API key.
Si la IA falla (red caída, JSON malformado, respuesta sin sentido), se ejecuta un fallback local que arma una proyección lineal de los próximos 3 meses y recomienda mix por ganancia. El chip amarillo de Modo local aparece cuando estamos en fallback, así el usuario sabe que ese insight no vino del modelo. Esto es importante: la app no se rompe si la IA se cae.

Lo que devuelve el modelo se valida con un parser tolerante: si viene envuelto en fences de markdown o con texto alrededor, lo extrae igual. El prompt está tuneado para pedir JSON estricto, pero el parser no se confía.
Mi esposa mira esta sección una vez por semana, antes de decidir qué imprimir la semana siguiente. No es la sección que más usa — la que más usa es la calculadora y la lista de ventas — pero cuando la mira, le cambia el comportamiento. El mes pasado le dijo "el PLA blanco tiene margen 18%, ¿por qué no subo un poco el precio?" y la IA le contestó "porque estás cobrando lo mismo que el mercado local"; ajustó el precio y mejoró el margen. Eso justifica el módulo entero.
Backup y diagnóstico
La pantalla está en /backup y es la que más tranquilidad da. Cuatro bloques:
- Exportar descarga un JSON con todo: settings, materiales, impresiones, ventas y fotos (en base64).
- Importar restaura ese JSON. Al importar, vuelve a pasar las fotos por el compresor — si el backup viene con blobs sin optimizar, los normaliza.
- Estandarizar corre reglas idempotentes para limpiar datos viejos (hoy: comprimir fotos grandes y reparar links huérfanos).
- Diagnóstico lee directo de IndexedDB y reporta conteos + última venta guardada. Esto sirve para responder "¿la UI dice algo distinto a lo que realmente está guardado?" sin tener que abrir DevTools.

Ella exporta un backup cada domingo a la noche y lo guarda en su Drive. Si el celular se rompe, restaura desde el último backup y sigue donde estaba. Esa tranquilidad la paga sola.
El stack final
Lo que terminó construido, en una línea: Vite 8 + React 19 + TypeScript + TanStack Router + Tailwind v4 + Dexie 4 + zustand + zod + sonner + Vitest, todo client-side, servido por nginx 1.27-alpine en el host. PWA con service worker y manifest, sin backend, sin auth, sin cookies.
El bundle final gzipped pesa ~220 KB. El deploy es un docker compose up -d --build desde el host — Traefik descubre el servicio y Let's Encrypt emite el certificado. Es la misma mecánica que el resto de los servicios del blog.
Lo que la app no tiene, deliberadamente:
- Sync entre dispositivos: sí, sería útil. Pero sync = backend. La fricción es mayor que el beneficio. Por ahora, un export/import JSON los domingos.
- Multi-usuario / roles: es una herramienta de una persona. Si algún día hace falta sumar a un colaborador, lo construyo separado. Meter usuarios en una app single-user es el tipo de decisión que después cuesta revertir.
- Facturación electrónica / UTE: territorio de UTE-fact o similar. No me meto.
- Notificaciones push: no las necesita. La app se abre cuando se abre.
¿Y el código, está "bien"?
Es una pregunta que me hacen seguido cuando digo que esto lo hizo una IA. La respuesta corta: sí, está bien para el alcance que tiene. La respuesta larga:
- Linting limpio: oxlint corre sin warnings.
- Tipos consistentes: TypeScript estricto, sin
anysalvo en dos o tres lugares documentados. - Tests: Vitest con happy-dom, coverage de las funciones puras de cálculo.
- Migraciones de schema: probadas con un script de seed que recrea la DB desde cero en cada cambio.
- Sin secretos en el repo: no hay claves, no hay tokens. La única "auth" es que la app corre en el navegador del usuario.
- Sin código muerto: lo que no se usa se borra. El agente suele dejar helpers viejos; los borré manualmente.
Pero no está perfecta. Lo que yo sé que la IA no cuida tan bien y que revisé a mano:
- Mensajes de error en español rioplatense consistentes (a veces quedaba "Ocurrió un error" en vez de "No se pudo guardar").
- Comentarios útiles sólo donde suman (saco los que son ruido).
- Nombres de variables en español cuando el dominio lo amerita (
cliente,montoCobrado) y en inglés cuando son técnicos (useEffect,computePrint). - Tests que prueban cosas que un humano probaría, no sólo lo que sale gratis.
Esa pasada final la hago yo. Es el equivalente al git commit --amend que también es mío.
¿Cómo se usa?
Abrí calculadora.jfaramburu.com desde el navegador. La primera carga es la única que necesita conexión: instala el service worker, baja el bundle, y después anda offline. Todo se guarda en el dispositivo.
Si querés ver el código, está todo en el host. El blog donde está publicado este post también está armado con la misma filosofía: cero JavaScript de tracking, cero servidor custom, todo Markdown + nginx.
Y si te interesa cómo trabaja el agente que construyó Calculadora 3D, el detalle está en OpenCode en producción.
Nota: las capturas de este post no fueron generadas automáticamente — la app no se puede renderizar dentro del flujo de trabajo sin un browser real. Para agregarlas, capturá cada sección descrita corriendo npm run dev en el repo de calculadora3d o apuntando a calculadora.jfaramburu.com con datos de prueba.