Embalo es una aplicación web (PWA) para conductores profesionales — Uber, 99, iFood, taxi y particulares — que quieren ver la ganancia del día, no solo la facturación de la plataforma. Reúne ingresos y gastos, vehículo, jornada, rutas, recibos, vouchers e informes. Funciona en el navegador del celular o de la computadora, en portugués, en el dominio embalomotorista.com.br. El código es JavaScript vanilla con Supabase (PostgreSQL, Auth y Storage) y despliegue en Vercel.

Este caso técnico muestra lo que hace el código, las decisiones que lo moldearon y las fallas reales que se volvieron regla. Los números se revisaron en el repositorio en la versión del 28/10/2026 (commit c07ce82).

Transparencia: Embalo fue diseñado y es desarrollado por Jeremias Cardoso, autor de este blog. El producto está en implementación; este texto no afirma flota de pago ni métricas de usuarios.

Nota de actualización (02/11/2026): este post se reescribió como caso técnico. La versión del 01/10/2026 era una presentación de producto. Esta trae arquitectura, números, incidentes, límites y fuentes revisados en el repositorio.

El problema: el extracto de la plataforma no es la ganancia

El valor de un viaje no es lo que le queda al conductor. Combustible, mantenimiento, peaje y el tiempo detenido entre llamadas salen de la misma cuenta, pero no aparecen en el extracto de Uber o de 99. Sin esos números lado a lado, la semana termina con la facturación conocida y el resultado desconocido.

La hoja de cálculo resuelve mientras cabe en una persona y en un archivo que no desaparece. El problema empieza cuando el registro tiene que ocurrir en el celular, con mala red, foto del comprobante, recibo para la empresa e historial que sobreviva al cambio de aparato. Es la misma frontera en la que las hojas de cálculo de control dejan de bastar y un sistema con inicio de sesión y reglas pasa a tener sentido — la misma lógica que aparece en EscalaOper para la escala de turnos.

El sistema en números

Medición local el 02/11/2026 sobre el commit c07ce82 (último commit del repositorio el 28/10/2026).

El sistema en números
MedidaValor
Aplicación (JS/CSS/HTML principales, sin tests/docs/assets)28 archivos; cerca de 22,7 mil líneas de código (no en blanco y sin comentario)
API serverless (api/)12 archivos; cerca de 1,5 mil líneas de código
Carpeta tests/232 archivos (incluye fixtures e informes; no usar el total bruto de líneas como “código de prueba”)
Historial en Gitcerca de 681 commits
Dominio públicoembalomotorista.com.br

El conteo excluye node_modules, documentación, estado, mockups y assets. El archivo central app.js por sí solo supera los 500 KB en el disco.

Una cuenta, dos accesos

La decisión de producto es tratar la aplicación y el portal como el mismo producto: la misma cuenta, los mismos datos, las mismas reglas. El celular no es una versión reducida. La pila elegida fue deliberadamente delgada en el front: HTML, CSS con tokens claro/oscuro y JavaScript sin framework, para que quepa en el bolsillo y en el 4G.

Los datos viven en Supabase. La autenticación, las tablas y las fotos del comprobante pasan por allí. Parte de las reglas de plan y de uso mensual se migró a funciones y rutas serverless en api/, porque dejar el navegador como única fuente de verdad del plan abre un agujero: se puede pasar a Pro desde la consola del navegador. La auditoría de junio de 2026 ya lo marcaba como bloqueante antes de cobrar.

Embalo no importa automáticamente las ganancias de las plataformas. El conductor registra a mano, por voz o con ayuda de la lectura del valor en la foto del comprobante. La ventaja está en reunir, en el mismo lugar, lo que cada plataforma paga y lo que el auto consume.

Lo que entrega el código

Financiero. Ingresos, gastos y resultado neto por período (mes, últimos 30 días, año). Meta diaria, filtros por categoría y anexo de foto. Los totales salen de los registros del propio conductor.

Mi vehículo. Registro, kilometraje, mantenimientos con previsión del próximo cambio y checklist diario. La previsión parte del último registro de cada tipo de servicio.

Jornada y productividad. Inicio y fin de turno con kilometraje; ganancia por hora, costo por kilómetro, horas trabajadas y la categoría de ingreso que más rindió en el mes. Gráficos de los últimos siete días y por turno.

Rutas. Paradas ordenables; apertura en Google Maps o en Waze; optimización del orden en versión beta, que puede fallar al geocodificar una dirección.

Recibos, clientes y vouchers. Recibos numerados en PDF, compartibles en WhatsApp; vouchers con situación (pendiente, enviado, en análisis, pagado). El mismo eje de “artefacto listo, relación humana” aparece en ZapOrca y en el post sobre el costo de WhatsApp con IA.

Planes Free, Pro y Max. Límites de registros, fotos, recibos, vouchers y vehículos; mensaje en portugués cuando se alcanza el techo; trial y suscripción por Pix en el diseño actual.

Arquitectura bajo restricción: lo que cabe en el celular

El front no usa React ni Vue a propósito. La lógica de negocio vive en app.js y en módulos satélite (voz-parser.js, assistente-voz.js, business-ui.js, report-ui.js). El service worker y el manifiesto cierran el PWA. La elección reduce el payload y evita una capa de build en el camino crítico del conductor; el precio es un archivo monolítico grande y la disciplina de no mezclar el estado de sesión entre cuentas.

En el servidor, las rutas en api/ concentran lo que no puede confiar en el cliente: consumir cuota de comando de voz, conciliar el uso mensual y volver estricto el plan. El SQL versionado en sql/ documenta la reconstitución de la base y los rollbacks de las migraciones de plan. La auditoría de junio ya separaba “fase de prueba gratuita” (aprobable con salvedades) de “empezar a cobrar” (reprobado hasta que el servidor sea la fuente de verdad y la eliminación de la cuenta exista en la app).

São Paulo aparece en el diseño del producto porque el huso UTC−3 y el conductor nocturno son el público típico del bug de fecha — no como marketing de ciudad. El dominio público embalomotorista.com.br es la vitrina y el manual; este caso describe el repositorio, no un dictamen de lo que está desplegado en cada momento en el panel de Vercel.

Incidentes que se volvieron regla

1. El límite del plan que se volvía “Algo salió mal”

Cuando el plan Free llegaba al techo, la app lanzaba un error con texto claro en portugués (“Límite alcanzado… haga el upgrade”). El catch pasaba por traduzirErro(), hecho para mensajes en inglés de Supabase. Ningún match coincidía, y el conductor veía el fallback genérico. Quien más necesitaba ver la invitación de upgrade no la veía. La corrección marcó los errores de la propia app con jaAmigavel y pasó a devolver el mensaje original. En app.js, las claves LIMITE_PLANO:* y la rama err.jaAmigavel siguen ahí.

2. Doble toque, segundo vehículo, jornada fantasma

En el celular, un segundo toque en los 700–1500 ms entre el éxito y el cambio de pantalla reenviaba el formulario. En vehículo, el segundo insert rompía la pestaña: carregarVeiculo() usa .maybeSingle() y la pantalla quedaba en error permanente. En jornada, nacía una jornada abierta “fantasma”. La regla que quedó: no rehabilitar el botón en el finally del éxito mientras la navegación no haya terminado, y establecer el modo edición en cuanto se crea el vehículo.

3. Después de las 21 h en São Paulo, el día se volvía mañana

Las fechas de calendario usaban toISOString().slice(0,10) (UTC). En SP (UTC−3), después de las 21 h eso ya es el día siguiente — exactamente cuando el conductor nocturno cierra el día. El gráfico de siete días y los informes de jornada se desplazaban. El helper dataISOLocal / dataHojeISO pasó a usar el huso del aparato. El comentario en app.js registra el bug clásico de SP a propósito.

4. Mil líneas sin aviso

Las consultas que suman dinero pedían “todo” sin .limit(). PostgREST devuelve como máximo 1.000 líneas y no avisa el corte. Un conductor que registra cada viaje pasa de ese techo en cerca de un año y el informe anual miente en silencio. Lo mínimo es detectar el techo y avisar; paginar de verdad quedó como deuda.

5. Copiloto de voz: offline, período y cuota

Tres lecciones revisadas en el código en octubre de 2026: (a) la “resiliencia offline” solo vale para la interpretación del texto — el reconocimiento de habla y la grabación en Supabase necesitan red, y no hay fila de reenvío; (b) el período de la consulta por lista de exclusión aceptaba “en 2025” o “sábado” y caía en el día equivocado — la guarda segura es por inclusión de lo que la app sabe calcular; (c) la cuota de comandos en localStorage no es la fuente de verdad — el costo de la IA y la cuota pasan por el servidor.

6. “Completada” y “100% de las pruebas” sin prueba

La tarea del servidor como fuente de verdad de los planes llegó marcada como completada sin cumplir el criterio. “100% de las pruebas” valía solo para una batería del copiloto; la línea de base de la batería entera dio 18 PASS y 21 FAIL ya existentes. La regla que quedó en la memoria del proyecto: una afirmación sin evidencia que se pueda señalar es una hipótesis.

7. Keep-alive que consultaba una columna inexistente

El robot que calentaba Supabase cada tres días consultaba configuracoes?select=id, pero la clave de la tabla es chave, y el rol anon no lee esa tabla. La respuesta 4xx se trataba como éxito. La consulta se cambió; la actividad en el panel no se prueba con un estado HTTP engañoso.

Pruebas: la máquina importa

En la revisión R19 (28/10/2026), run-all --fechamento en la máquina del dueño con Edge dio 43/44 (1.060 casos PASS; un FAIL por TEMPO_CASO / Edge que no cerró a tiempo; el mismo caso pasó en la repetición 16/16). En la nube, la misma ronda dio 38/44: la nube no tiene Edge y lo cambia por Chromium con calce. La memoria del proyecto registra: la batería en la nube no es la batería de la máquina para pantalla y zoom.

Eso no prueba que Embalo esté “listo para cobrar”. Prueba que la prueba de interfaz depende del entorno, y que un timeout de navegador no se vuelve defecto de producto sin repetir el caso.

Lo que todavía falta / lo que este caso no prueba

  • Adopción. El repositorio y este texto no afirman cuántos conductores usan el producto en producción. Reclutar testers y el acceso a la tienda aparecen como tareas bloqueadas en el estado del proyecto, no como hechos concluidos.
  • Cobro pleno. Antes de cobrar de hecho, la auditoría y las tareas todavía tratan el servidor como fuente de verdad de los planes, la eliminación de la cuenta por la app (LGPD) y la revisión jurídica de la política y de los términos como pendientes de go-live. La fase de prueba gratuita y la fase de cobrar son veredictos distintos en el informe de junio.
  • Importación de las plataformas. Sigue siendo manual. No hay conector Uber/99/iFood en el código de esta versión.
  • Offline real. No hay fila de registros para cuando vuelve la red. El copiloto interpreta el texto en forma local; grabar y oír dependen de internet.
  • Paginación de los totales. El techo de 1.000 líneas de PostgREST sigue siendo un riesgo estructural si el uso crece sin .range() o una suma en la base.
  • Secretos y paneles. Los detalles de claves, las políticas RLS completas y la configuración de SMTP quedan fuera de este texto a propósito.
  • Bibliotecas de PDF y de hoja de cálculo. La memoria registra versiones y avisos conocidos (jsPDF, SheetJS); este caso no vuelve a leer la cadena de supply de cada dependencia de front.

Lecciones que valen fuera de la app del conductor

  1. El mensaje amable que el catch se traga no existe. Marque el error de la propia app o el usuario ve el genérico.
  2. El huso es un requisito de producto para quien trabaja de noche. toISOString en una fecha de calendario miente después de las 21 h en SP.
  3. El techo silencioso de una API es una mentira contable. Si la suma puede cortar, el corte tiene que aparecer.
  4. “Offline” y “100% de las pruebas” piden una definición escrita. Sin ella, se vuelven marketing interno.
  5. La prueba de pantalla en la nube ≠ la prueba en la máquina del usuario. El entorno forma parte de la evidencia.

Fuentes

  • Código, documentación, auditoría de preproducción, memoria del proyecto e historial: repositorio motorista-pro (Embalo) en el commit c07ce82 (28/10/2026), leído por el autor; extracto con fragmentos, método del conteo y límites, verificado el 2026-11-02. La suite completa no se volvió a ejecutar en esta redacción; los resultados de batería citados vienen del informe R19 del 28/10/2026 versionado en el repositorio.
  • Manual público del producto en embalomotorista.com.br (presentación y capturas de demostración), revisado en la versión de lanzamiento del post original.