Plataforma para que COMBUSES registre sus alistamientos y mantenimientos, y para que GESMOVIL los consuma por API y los transmita al ecosistema de la Superintendencia de Transporte (SINST – VIGIA 2), como Aliado Tecnológico.
Son dos mitades:
| Quién | Qué | |
|---|---|---|
| Captura | conductor y taller | formularios web que alimentan las tablas sicov_* |
| Entrega | GESMOVIL | API de solo lectura con el contrato del manual |
El contrato de salida es el del Manual de apis requeridas para integración alistamiento-mantenimientos-despachos. Los dos PDF de origen (ese manual y la propuesta económica de GESMOVIL) están en la raíz del repo.
El checklist son las actividades oficiales de la Supertransporte, guardadas por su id oficial. No hay tabla de homologación en ningún punto del sistema, porque no hay nada que traducir: el id que el conductor marca es el mismo que viaja en el reporte.
Eso es lo que se gana al diseñar el modelo desde el manual en vez de adaptar tablas que ya existían para otra cosa. Cada campo que el manual exige tiene su columna, así que no hay huecos que rellenar al responder.
Cubre alistamientos y mantenimientos. El manual describe también despachos y llegadas; esos no están aquí, y pedirlos a la API devuelve un 404 que lo dice.
Lo que ya existía en el proyecto y no se toca:
sicov-alistamiento — el push a sicov.calisoftware.com.co. Sigue
funcionando; esta plataforma es independiente y no comparte nada con ella.preoperacionales — el checklist urbano interno, con su propia vida.flota_vehiculos y employees — se leen para validar placa y cédula,
nunca se escriben.api_clientes y api_accesos — credenciales y auditoría, se reutilizan.supabase/migrations/
20260921120000_sicov_plataforma.sql modelo de datos
20260922100000_actividades_propias.sql catálogo propio + homologación
supabase/seed-checklist.sql las 40 actividades de COMBUSES
supabase/configurar.sql plantilla de sicov_config
supabase/functions/
sicov-alistar/ registro de alistamientos (público, sin login)
sicov-mantenimiento/ registro de mantenimientos (con login)
sicov-gesmovil/ API de lectura para GESMOVIL
sicov-sync-catalogos/ trae el catálogo oficial de actividades
web/ la PWA
index.html portada y botón de instalar
alistamiento.html formulario del conductor
mantenimiento.html formulario del taller
manifest.webmanifest
sw.js service worker — aquí vive la versión
README.md portada del repo público
assets/ app.css, comun.js, los dos JS, iconos
scripts/
generar-api-key.mjs credencial de un consumidor
generar-iconos.ps1 dibuja los PNG del manifest
publicar-pages.ps1 replica web/ al repo público de GitHub Pages
publicar-web.ps1 sube la PWA con los Cache-Control correctos
servir-local.ps1 servidor de pruebas en localhost
Fuera del repo a propósito: los *.local.sql (cédula del responsable, hash de
credenciales) y los PDF fuente. Ambos están en .gitignore desde antes del
primer commit, porque el historial de git no se limpia después sin reescribirlo.
| Repo | Visibilidad | Qué lleva |
|---|---|---|
gesmovil-sicov |
privado | todo: migraciones, funciones, scripts, y web/ |
gesmovil-sicov-web |
público | solo el contenido de web/, para GitHub Pages |
GitHub Pages no sirve desde un repo privado sin plan de pago, y hacer público
el repo entero publicaría las migraciones, la lógica de las funciones y el
contrato con GESMOVIL. Así que solo web/ se replica, con
scripts/publicar-pages.ps1 (un git subtree push, que deja index.html en
la raíz del repo público, que es donde Pages lo busca).
El frontend es público de todos modos: es una página web, su código llega al
navegador. Lo que importa es que no lleve nada que no deba — de ahí la
comprobación del script, que falla si web/ vuelve a leer la lista completa
de conductores.
La app queda en https://desarrollocombuses.github.io/gesmovil-sicov-web/
sicov_cat_actividades catálogo oficial (la PK es el id de la Supertransporte)
sicov_config NIT, razón social, responsable del proceso
sicov_usuarios quién puede registrar mantenimientos
sicov_alistamientos uno por placa y día
└ sicov_alistamiento_actividades qué se verificó, y si quedó conforme
sicov_mantenimientos trabajos ejecutados
└ sicov_mantenimiento_actividades
Un par de cosas que no son obvias:
fecha y registrado_en son distintos: el día que se reporta y el
instante en que se grabó. Sin esa separación, un alistamiento hecho a las 11
de la noche se reportaría en el día siguiente.estado del alistamiento lo calcula un trigger, no el formulario. Si
se confiara en el cliente, un error de JavaScript podría marcar como OK un
alistamiento con fallas — justo lo que el registro debe evitar.registrado_por no es el responsable: un coordinador puede digitar un
mantenimiento que ejecutó un mecánico, y el manual pide el segundo.supabase link --project-ref cbplebkmxrkaafqdhiyi
supabase db push
# Registro
supabase functions deploy sicov-alistar --no-verify-jwt # link público
supabase functions deploy sicov-mantenimiento # requiere login
# Entrega y catálogo
supabase functions deploy sicov-gesmovil --no-verify-jwt # valida su X-API-Key
supabase secrets set SUPERTRANSPORTE_TOKEN=<token>
supabase functions deploy sicov-sync-catalogos # solo nosotros
Sobre los --no-verify-jwt: sicov-alistar no lleva JWT porque el conductor no
tiene cuenta, y sicov-gesmovil porque valida su propia X-API-Key (obligar
además a un JWT del proyecto significaría darle a GESMOVIL una segunda
credencial sin ganar nada).
1. Configurar la empresa.
update sicov_config set valor = '<nit sin dígito de verificación>' where clave = 'nit';
update sicov_config set valor = '<razón social exacta>' where clave = 'razon_social';
update sicov_config set valor = '<cédula del responsable>' where clave = 'responsable_num_id';
update sicov_config set valor = '<nombre del responsable>' where clave = 'responsable_nombre';
Sin el responsable, sicov-alistar rechaza los registros con un 503 que lo
dice. Sin el NIT, la API de mantenimientos responde lo mismo.
2. Sincronizar el catálogo de actividades. Ver la sección siguiente: hace falta un token que todavía no tenemos.
3. Autorizar al personal del taller. Crear el usuario en Supabase Auth (Authentication → Users) y luego:
insert into sicov_usuarios (user_id, nombre, cedula, rol) values
('<uuid del usuario>', 'Nombre Completo', '<cédula>', 'taller');
Roles: taller registra a su nombre; coordinador y admin pueden además
atribuir un trabajo a otra persona.
4. Publicar la PWA. Configurar SUPABASE_URL y SUPABASE_ANON_KEY en
web/assets/comun.js — están los dos en un solo sitio, y
la anon key es pública por diseño. Después crear el bucket en el panel (Storage
→ New bucket → sicov, marcado como público) y publicar:
powershell -ExecutionPolicy Bypass -File scripts/publicar-web.ps1
El link que se reparte a los conductores queda en
https://<ref>.supabase.co/storage/v1/object/public/sicov/alistamiento.html.
5. Dar de alta a GESMOVIL.
node scripts/generar-api-key.mjs "GESMOVIL S.A.S." 120 365
Imprime la key en claro (única vez que existe: la base solo guarda su SHA-256) y el SQL de alta. Entregar por canal cifrado, no por correo en texto plano.
Es HTML, CSS y JS sin build ni dependencias que compilar. Se instala en la pantalla de inicio y abre sin buscar el enlace, que es lo que cuenta cuando el conductor está de pie junto al vehículo.
VERSION en web/sw.js (primera línea de código).powershell -ExecutionPolicy Bypass -File scripts/publicar-web.ps1Eso es todo. VERSION es la fuente única: el nombre del caché la incluye,
así que cada versión estrena caché y los anteriores se borran al activar — no
hay forma de quedarse con una mezcla de archivos de dos versiones. La UI
muestra la versión en el pie, preguntándosela al worker en vez de guardar una
segunda copia del número que acabaría discrepando.
Funciona porque el navegador compara sw.js byte a byte con el instalado.
Por eso publicar-web.ps1 lo sube con max-age=0: si Storage lo sirviera con
una hora de caché, una versión nueva tardaría hasta una hora en detectarse. Los
assets sí van con caché de 7 días, porque el worker los pide con
{cache: "reload"} al instalar y salta el caché HTTP.
El script sube sw.js al final, después de los assets. Al revés, un
teléfono podría instalar la versión nueva y pedir archivos que aún no están
arriba.
Una versión nueva no se activa sola si hay un formulario a medio llenar. Activarla recarga la página, y el conductor perdería el checklist.
Cada formulario declara cómo sabe si tiene trabajo a medias
(SICOV.registrarGuardia), así que la lógica de versiones no necesita conocer
los campos de cada uno.
| Sin conexión | |
|---|---|
| Abrir la app y el formulario de alistamiento | ✅ |
| Ver el checklist y la lista de vehículos | ✅ del último catálogo guardado |
| Llenar el formulario | ✅ y no se pierde |
| Enviar el registro | ❌ requiere señal |
| Mantenimientos (login) | ❌ requiere señal |
El envío requiere red a propósito. Un alistamiento es una afirmación ante el regulador y tiene una restricción de uno por placa y día; guardarlo en el teléfono para mandarlo después acabaría en duplicados o en un «creí que lo había enviado». La cinta lo dice con esas palabras y lo escrito no se pierde: el conductor busca señal y vuelve a tocar Registrar.
Si más adelante hace falta, una cola con IndexedDB es el siguiente paso — pero hay que resolver primero qué pasa con el registro encolado de un día que ya pasó, y quién responde por él.
El formulario de mantenimientos no funciona offline porque importa supabase-js de un CDN, y el login necesita red de todas formas. El de alistamiento no usa ningún CDN precisamente por eso.
powershell -ExecutionPolicy Bypass -File scripts/servir-local.ps1
Hace falta un servidor porque un service worker no se registra desde file://;
localhost cuenta como origen seguro. En Chrome, F12 → Application → Manifest
y → Service Workers, y F12 → Network → Offline para comprobar que abre sin red.
powershell -ExecutionPolicy Bypass -File scripts/generar-iconos.ps1
Se dibujan con System.Drawing (viene con Windows) en vez de traer un binario
de imágenes, así que son reproducibles desde el repo y no hay un PNG suelto que
nadie sabe de dónde salió. El maskable deja el dibujo en el 58% central porque
Android recorta hasta un 20% por lado.
Esto es lo único que bloquea la puesta en marcha, y no es código.
Verificado el 21-sep-2026 contra los servicios reales:
| Servicio | Resultado |
|---|---|
listar-actividades |
❌ «Error en el token» — el token del manual no sirve aquí |
nivelservicio |
✅ responde (no lo necesitamos) |
listar-clase-vehiculo |
✅ responde (no lo necesitamos) |
La cabecera correcta es Authorization: Bearer — con token: o x-token: el
servicio responde «Falta el token de autenticación», así que reconoce la
primera y rechaza el valor.
Hay que pedirle a GESMOVIL un token de COMBUSES para esa maestra. Con él:
curl -X POST "https://<ref>.supabase.co/functions/v1/sicov-sync-catalogos" \
-H "Authorization: Bearer $SERVICE_ROLE_KEY"
Los formularios se pintan desde la tabla, así que sembrarla a mano funciona igual y el sync posterior la corrige. El manual solo muestra dos actividades oficiales en su ejemplo, y son las únicas que se pueden afirmar:
insert into sicov_cat_actividades (id, descripcion, grupo, orden) values
(1, 'Fugas del motor', 'Motor', 1),
(4, 'Niveles de aceite de motor', 'Motor', 2)
on conflict (id) do nothing;
No inventes los demás ids: un id equivocado hace que el reporte afirme que se verificó algo distinto de lo que se verificó.
El sincronizador desactiva (no borra) lo que ya no venga en la maestra: los alistamientos históricos apuntan a esos ids y su reporte debe seguir siendo reproducible.
GET /sicov-gesmovil/alistamientos?fechaInicio=AAAA-MM-DD&fechaFin=AAAA-MM-DD
GET /sicov-gesmovil/mantenimientos?fechaInicio=AAAA-MM-DD&fechaFin=AAAA-MM-DD
Autenticación: cabecera X-API-Key. Respuesta { success, data }.
Códigos: 200 exitosa · 400 parámetros inválidos · 401 no autorizado ·
404 sin registros o endpoint inexistente · 429 límite superado · 503
configuración incompleta.
Sobre el 404: el manual reserva ese código para «No se encontraron
registros», así que un rango sin actividad responde 404 y no un 200 con lista
vacía. No es lo habitual en REST, pero es el contrato que espera el cliente.
Límite de rango: 92 días. El manual no define paginación, y la alternativa sería truncar filas en silencio: GESMOVIL reportaría de menos sin enterarse.
El conductor sin login, el taller con login. El alistamiento se valida contra la flota, no contra un usuario: solo se puede escribir sobre una placa que existe y con actividades del catálogo. El mantenimiento es distinto — es la afirmación de que un trabajo se ejecutó, y el manual exige un responsable con nombre y cédula. Un formulario anónimo no puede sostener eso.
El formulario no descarga la nómina. La primera versión devolvía las 298
cédulas con nombre completo en GET /formulario, a cualquiera que pidiera la
URL, sin credencial. Cédula y nombre de una persona identificada son dato
personal bajo la Ley 1581 de 2012, y no había autorización ni finalidad que
amparara publicarlos.
Ahora el conductor digita su cédula y GET /conductor?cedula= resuelve esa
sola, devolviendo únicamente el nombre — nunca el cargo ni nada más de
employees: lo que no viaja no se filtra. Se pasa de regalar 298 registros a
responder uno, y hay que conocer una cédula válida de antemano para obtener
algo.
No es secreto perfecto: quien ya tenga la cédula de alguien puede confirmar si trabaja aquí. Pero deja de ser un volcado de la nómina, que era el problema. Encima van dos cosas más:
api_accesos
y no en memoria (cada petición cae en una instancia nueva de la función, así
que un contador en el proceso nace vacío y no frena nada). El número deja
holgado un cambio de turno entero desde el wifi del patio, donde todos salen
por la misma IP pública, y hace inviable recorrer un rango de cédulas.Las placas sí van completas en /formulario. Una placa está pintada en el
costado del bus y en el techo: no identifica a una persona ni es dato
reservado, y el desplegable evita que el conductor teclee mal la suya a las 4
de la mañana.
Tener cuenta no es estar autorizado. verify_jwt solo prueba que hay una
sesión del proyecto; quién puede registrar mantenimientos se comprueba además
contra sicov_usuarios.
Ningún punto del checklist viene premarcado. Hay que tocar OK o Falla en cada uno. Un formulario que llegara todo en «OK» se enviaría sin mirar el vehículo, y eso vaciaría de sentido el registro. Un toque por punto sigue siendo rápido.
El responsable del alistamiento sale de la configuración, no del formulario. El conductor no puede designar a un tercero como responsable de su propio alistamiento.
Sin CORS en sicov-gesmovil, con CORS en las de registro. La primera es
servidor-a-servidor: si se pudiera llamar desde un navegador, la API key
acabaría dentro de un frontend a la vista de cualquiera. Las de registro sí las
llama un navegador, por definición.
Credenciales compartidas con alcance separado. api_clientes ya existía
para api-externa (programación de turnos de CombuAsigna) y se reutiliza, con
una columna apis que acota qué API puede consumir cada credencial. Dos
barreras a propósito: esa columna, y que api-externa ya exige bases no
vacío — un cliente de SICOV (bases = '{}') recibe 403 de esa API aunque se
olvide el chequeo. Una sola barrera dependería de que nadie se equivoque una vez.
Rate limiting contra la base, no en memoria. Cada petición cae en una instancia nueva de la función, así que un contador en el proceso nace vacío siempre y no frena nada. El coste está donde toca: la consulta de intentos fallidos solo se hace cuando la credencial ya falló.
Nada se escribe desde el navegador. Todas las tablas tienen RLS sin políticas de escritura; los registros solo nacen por las edge functions, que validan placa, cédula y actividades. Un alistamiento es una afirmación ante el regulador.
Estado de salud fuera de las respuestas. La operación pega el estado al
nombre ("Juan Pérez [INCAPACITADO]"). Eso es dato de salud de una persona
identificada, sensible bajo la Ley 1581 de 2012. Se limpia antes de mostrarlo
en una lista pública y antes de reportarlo.
employees sirve mejor de lo que parecía. Un comentario en
preoperacional-publico dice que la tabla solo tiene unos pocos conductores
bien marcados, pero eso era filtrando por las dos rutas del preoperacional
urbano. Sin ese filtro, cargo = 'CONDUCTOR' y activo = true dan 298
conductores (verificado contra la función desplegada), y flota_vehiculos
trae 135 placas — con más rutas de las esperadas: AEROPUERTO, ZAMORA,
ARANJUEZ - GUADALUPE y TERMINAL 313. El formulario igual acepta una cédula
que no esté en la nómina: si está, usa el nombre oficial y evita el error de
tipeo.fechaAlistamiento se describe como
fecha y hora pero el ejemplo trae solo fecha (aquí se envían ambas).functions deploy es lo que lo confirmaría.servir-local.ps1.