Hub Streamlit — hosting compartido de reportes

Decomisionado el 2026-08-26. Confirmado por Esteban que ya no se usa, pero la baja real (borrar trivasa-bi-dev/exploracion/layout-gastos-ctunlinux/streamlit_venv/) había pasado sin detener streamlit-hub.service ni quitar la ruta del túnel — el unit quedó en crash-loop indefinido (Restart=on-failure, RestartSec=5s) desde que el venv dejó de existir, acumulando 30,344 reinicios (status=203/EXEC, “Unable to locate executable”) para cuando se detectó. Mismo síntoma externo que el incidente de 2026-08-09 (502 en explore.frento.com.mx) pero causa distinta: esa vez era un shebang con ruta vieja (venv seguía existiendo), esta vez el venv completo ya no está en disco. Cierre real aplicado el 2026-08-26 con acceso SSH directo: systemctl stop+disable streamlit-hub.service, más la línea de ingress quitada de /etc/cloudflared/config.yml (ver ctunlinux § túnel) — explore.frento.com.mx pasó de 502 a 404 limpio, mismo estado final que Superset. El resto de esta página describe el servicio como corría mientras estuvo activo.

explore.frento.com.mx expone, en un solo túnel cloudflared y un solo puerto (localhost:8501), todos los reportes Streamlit de Trivasa como páginas de una misma app — en vez de abrir un hostname/puerto nuevo por reporte. El servicio systemd streamlit-hub.service corre hub_app.py (repo layout-gastos-ctunlinux), que usa st.navigation/st.Page de Streamlit para despachar a cada reporte por ruta.

cloudflared (explore.frento.com.mx) -> localhost:8501 -> streamlit-hub.service -> hub_app.py
                                                                                    ├── Inicio
                                                                                    ├── Layout de Gastos   (layout-gastos-ctunlinux/streamlit_app.py)
                                                                                    └── Consumo Interno    (consumo_interno_trazabilidad/streamlit_app.py)

Por qué un hub y no un servicio por reporte

Decisión explícita: agregar un reporte nuevo no debía abrir un puerto/hostname nuevo. Antes de este hito, el proceso en 8501 corría directo consumo_interno_trazabilidad/streamlit_app.py; al publicar Layout de Gastos se reemplazó el entrypoint por hub_app.py, que expone ambos reportes como páginas de la misma app en el mismo puerto — el patrón a seguir para cualquier reporte Streamlit nuevo de Trivasa.

Cómo agregar un reporte nuevo al hub

  1. Referenciar el streamlit_app.py del reporte nuevo desde hub_app.py (ruta absoluta, vía st.Page).
  2. Envolver cualquier st.set_page_config(...) del reporte nuevo en try/except st.errors.StreamlitAPIException — solo puede llamarse una vez por sesión, y el hub ya lo llama antes de despachar a cualquier página. Es el único cambio de código que necesita un reporte existente para integrarse al hub.
  3. El venv que usa el servicio (layout-gastos-ctunlinux/streamlit_venv) necesita las dependencias de todos los reportes servidos (streamlit, pandas, sqlalchemy, pymssql, openpyxl, …) — en producción no hay un venv por reporte.
  4. Validar con streamlit.testing.v1.AppTest cada reporte standalone, luego el servidor real en un puerto alterno (ej. 8511) con curl a cada ruta, antes de tocar el servicio real en 8501.

Rollback

El unit file anterior a la migración al hub se conserva como backup (no se borró): /etc/systemd/system/streamlit-consumo-interno.service.bak-pre-hub.

sudo systemctl stop streamlit-hub.service
sudo systemctl disable streamlit-hub.service
sudo cp /etc/systemd/system/streamlit-consumo-interno.service.bak-pre-hub \
        /etc/systemd/system/streamlit-consumo-interno.service
sudo systemctl daemon-reload
sudo systemctl enable --now streamlit-consumo-interno.service

Incidente: crash-loop de ~40 horas por ruta desactualizada (2026-08-09)

streamlit-hub.service estuvo en crash-loop (Restart=on-failure, RestartSec=5) durante ~40 horas sin que nadie lo notara — explore.frento.com.mx devolvía 502 todo ese tiempo. Se detectó recién en una revisión de disco no relacionada, con el contador de reinicios de systemd ya en 30,624.

Causa: en algún punto la carpeta del repo se renombró de layout-gastos a layout-gastos-ctunlinux (nombre actual, correcto) sin actualizar dos cosas que quedaron con la ruta vieja grabada:

  1. El propio .service — WorkingDirectory y ExecStart apuntaban a .../exploracion/layout-gastos/streamlit_venv/bin/streamlit, un path que ya no existe (status=203/EXEC, “Unable to locate executable”).
  2. El shebang de cada script en streamlit_venv/bin/ (streamlit, pip, pip3, etc.) — venv graba la ruta absoluta al intérprete del venv como primera línea de cada script generado (#!/ruta/completa/al/venv/bin/python3) en el momento de crearlo. Renombrar la carpeta que contiene el venv no actualiza esos shebangs — corregir solo el .service no bastaba, seguía fallando con el mismo error porque el propio streamlit binario apuntaba a un intérprete inexistente.

Fix: corregido el path en /etc/systemd/system/streamlit-hub.service (WorkingDirectory + ExecStart) y sed sobre todos los scripts de streamlit_venv/bin/ para reescribir el shebang a la ruta actual — alternativa más rápida que recrear el venv entero, funciona porque el intérprete real (streamlit_venv/bin/python3) es un symlink a /usr/bin/python3, no una instalación propia con dependencias distintas. Cero pérdida de datos — la ruta vieja siempre había sido un nombre stale, no una carpeta faltante.

Lección para cualquier futura reorganización de carpetas de este repo: mover o renombrar un directorio que contiene un venv de Python rompe todos sus scripts en bin/ en silencio (siguen ahí, con permisos de ejecución, pero apuntan a un intérprete que ya no existe) — revisar grep -rl "<ruta-vieja>" venv/bin/ después de cualquier rename, no solo los .service/.env que referencian la carpeta desde afuera.

Incidente sin causa raíz confirmada (2026-07-26)

Durante la validación de la migración, streamlit-consumo-interno.service se detuvo solo (log limpio: “Stopping…” → “Deactivated successfully”, exit code 0 — no fue un crash) sin que apareciera ningún systemctl stop en el journal en ese rango. Se restauró de inmediato y quedó sano. No se identificó causa concluyente — si vuelve a pasar, vale la pena revisar si hay un límite de recursos o watchdog externo no documentado en ninguno de los repos.

Véase también