- name
- devops-operations
- description
- Patrones operativos de DevOps: deploy en NaN.builders (502, cache, env vars), cron jobs con scripts (no_agent=True), pipeline de digest estático, y GitHub Pages deployment. Todo para mantener apps en producción.
- version
- 1.2.0
- author
- Hermes Agent
- tags
- ["devops","nan","deploy","cron","scripts","automation","production","github-pages"]
# DevOps Operations — Patrones de Producción
Patrones operativos para mantener aplicaciones en producción.
## Tabla de Contenidos
0. [Código → NaN: Push y Verificación](#0-código--nan-flujo-de-push-y-verificación) — edit → commit → pull --rebase → push → verify
1. [NaN Deploy Troubleshooting](#1-nan-deploy-troubleshooting) — 502, cache, TDZ, OOM
2. [Cron Jobs con Scripts](#2-cron-jobs-con-scripts) — no_agent=True, Python scripts
3. [Static Digest Pipeline](#3-static-digest-pipeline) — Fetch API → scoring → JSON → HTML → Pages
4. [GitHub Pages + Vite](#4-github-pages--vite) — base path, crossorigin, deploys
---
## 0. Código → NaN: Flujo de Push y Verificación
**⚠️ Regla de oro: Después de modificar código de un app desplegada en NaN, el trabajo NO está completo hasta que el push y la verificación en producción están hechos.**
El flujo completo para cualquier cambio de código en apps NaN:
```
[1] Hacer cambios en local (dashboard.html, server.js, etc.)
[2] Verificar que funcionan localmente (curl localhost, revisar sintaxis)
[3] git add -A && git commit -m "fix: descripción clara"
[4] git pull --rebase origin main ← CRÍTICO: la app auto-commitea database.json vía syncGitHub
[5] git push origin main
[6] Esperar ~2 min a que NaN detecte el push y redeployee
[7] Verificar en producción: curl -s https://<app>.apps.nan.builders/healthz
[8] Verificar el cambio específico: curl endpoint, grep en HTML, etc.
```
**Pitfall: `git push` rechazado por cambios remotos**
- La app tiene `syncGitHub()` que auto-commitea `data/database.json` tras cada mutación
- El repo local se queda detrás del remoto
- **Siempre hacer `git pull --rebase` antes de push** (nunca merge — mantiene historia limpia)
- Síntoma: `! [rejected] main -> main (fetch first)`
**Pitfall: No verificar en producción**
- NaN tiene polling de ~1-5 min y cache de Cloudflare
- Un push exitoso no significa que el cambio esté servido
- **Siempre verificar con curl** antes de decir "está listo":
```bash
curl -s https://<app>.apps.nan.builders/ | grep "cambio esperado"
# O para APIs:
curl -s https://<app>.apps.nan.builders/api/entrenamientos/0 | python3 -c "import sys,json; print(json.load(sys.stdin))"
```
- Si el cambio no está vivo tras 3 min, forzar con `git commit --allow-empty -m "chore: trigger redeploy" && git push`
**Pitfall: Asumir que el HTML se actualiza automáticamente**
- NaN reconstruye el contenedor completo con Kaniko
- Los archivos estáticos (HTML, CSS, JS) están dentro de la imagen Docker
- No hay hot-reload — cada cambio requiere nuevo build
- Tiempo típico build+deploy: 1-5 min
**🔥 Health check que miente: key existe pero no funciona**
**Patrón común:** El endpoint `/healthz` verifica que la variable de entorno `ORS_API_KEY` exista en `.env` (string truthy check), pero NO hace una llamada real a la API para verificar que la key sea válida.
**Síntoma:** `curl /healthz` devuelve `{"ors_api": true}` pero las llamadas reales a la API retornan `403 Access disallowed` o `401 Unauthorized`.
**Fix:** El healthcheck debe hacer una llamada real (o al menos validar el formato de la key) además de verificar que exista:
```javascript
// ❌ Miente: solo verifica que exista
checks.ors_api = !!process.env.ORS_API_KEY;
// ✅ Real: verifica que la key funcione (llamada mínima)
const testResp = await fetch('https://api.openrouteservice.org/v2/isochrones/driving-car', {
method: 'POST',
headers: { 'Authorization': process.env.ORS_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ locations: [[0,0]], range: [1] })
});
checks.ors_api = testResp.ok;
```
**Pitfall:** Hacer la llamada real en cada request de healthcheck es lento (200-500ms). Mejor cachear el resultado y re-validar cada 5 minutos.
**🔥 .env loader manual en Node.js (sin dotenv)**
**Patrón:** Node.js NO carga `.env` automáticamente (salvo `--env-file=.env` en Node 20.6+). Si no quieres dependencia de dotenv, añade un loader manual al inicio de `server.mjs`:
```javascript
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf-8');
for (const line of envContent.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx > 0) {
const key = trimmed.slice(0, eqIdx).trim();
const val = trimmed.slice(eqIdx + 1).trim();
if (!process.env[key]) process.env[key] = val; // NO sobreescribe
}
}
}
```
**Pitfall:** El loader SOLO carga variables que NO existen en `process.env`. Si haces `source .env` en bash ANTES de lanzar el servidor, las variables ya existen y el loader las ignora. Para testing: simplemente `node server.mjs` (el loader hace el trabajo).
**🔥 Git push falla por archivos grandes (OOM)**
**Patrón:** Proyectos con datos generados (GTFS, JSON grandes, caches) pueden acumular cientos de MB en `data/`. Git intenta hacer push de todo el history y muere con `pack-objects died of signal 9` (OOM en VMs con 2GB RAM).
**Síntoma:** `git push` falla con `pack-objects died of signal 9` o `remote end hung up unexpectedly`.
**Fix inmediato:**
```bash
# 1. Añadir al .gitignore
echo -e "\ndata/gtfs/\ndata/gtfs-cache/\n*.json\n!data/ciudades-*.json\n!data/codigos-postales-spain.json" >> .gitignore
# 2. Quitar del tracking (mantiene archivos locales)
git rm -r --cached data/gtfs/ data/gtfs-cache/
git rm --cached data/poblacion-cp.json data/salarios-*.json data/precios-vivienda.json
# 3. Commit y push
git add -A && git commit -m "chore: remove large data files from tracking"
git push origin main
```
**Si el history es demasiado grande (>100MB):** Crear repo fresco con solo código fuente:
```bash
cd /tmp && mkdir fresh-repo && cd fresh-repo && git init
cp -r /path/to/original/js . && cp -r /path/to/original/css .
cp /path/to/original/server.mjs /path/to/original/index.html .
# ... copiar solo archivos necesarios
git remote add origin <url> && git push --force origin main
```
**Prevención:** SIEMPRE añadir `data/` grande a `.gitignore` ANTES del primer commit. Los archivos GTFS raw pueden ocupar 750MB+.
## 1. NaN Deploy Troubleshooting
**TDZ (Temporal Dead Zone) — #1 causa de 502:**
- `const`/`let` usada antes de declaración → crash silencioso → Cloudflare 502
- Prevention: ordenar todas las `const` al inicio de la función
**NaN cachea contenedor Docker:**
- Después de cambios JS/CSS, NaN puede servir versión antigua durante horas
- Soluciones: cambiar Dockerfile → renombrar repo → eliminar/recrear espacio
**Scripts de verificación:**
- `verify-nan-deploy.sh <base-url>` — compara hashes MD5 locales vs remotos
- `git commit --allow-empty -m "chore: trigger redeploy" && git push` — trigger de redeploy
**Server crash silencioso:**
- Container "Running" pero endpoints devuelven 502 con ~3-4s
- Fix: `process.on('uncaughtException')` + fallback en route handlers
**🔥 Browser tool cache agresivo — HTML nuevo no se sirve**
**Error real (2026-06-11):** Tras hacer commit+push y redeploy en NaN, el browser tool seguía sirviendo HTML con JS antiguo. `typeof THREE === 'undefined'` aunque el CDN estaba en el HTML nuevo. El browser tool cachea el HTML y los scripts inline agresivamente.
**Síntoma:** `curl` desde terminal muestra HTML nuevo, pero `browser_console(expression='typeof THREE')` devuelve `undefined`. Los scripts CDN aparecen en el HTML pero no se ejecutan.
**Soluciones (en orden de efectividad):**
1. **Forzar redeploy** con un commit mínimo en `database.json` (o cualquier archivo servido por el server) → invalida cache de NaN
2. **Navegar con timestamp**: `browser_navigate(url + '?t=' + Date.now())` — fuerza recarga del HTML
3. **Esperar 2-3 minutos** — el cache de Cloudflare/NaN se expira
4. **Verificar con curl** antes de confiar en el browser tool: `curl -s https://app.apps.nan.builders/ | grep 'three.min.js'`
**Regla:** Si el HTML sirve correctamente (verificado con curl) pero el browser tool muestra comportamiento antiguo → es cache. No buscar bugs donde no los hay.
**Puerto del Dockerfile ≠ Container port de NaN — causa común de 502:**
- NaN tiene un campo **Container port** en la config del espacio (Settings > Container port)
- Si el servidor escucha en otro puerto (ej. 3000) pero NaN espera 7070 → 502 inmediato
- Fix: sincronizar ambos. Opción A: cambiar `ENV PORT=7070` + `EXPOSE 7070` en Dockerfile. Opción B: cambiar Container port en NaN a 3000
- **Recomendado:** Opción A (Dockerfile), así el build es autónomo y no depende de config manual de NaN
- Verificar: `curl -s -o /dev/null -w "%{http_code}" https://<app>.apps.nan.builders/` debe dar 200 tras el build
**Dockerfile faltante — error silencioso de Kaniko:**
- NaN usa Kaniko para construir imágenes Docker. Si el repo no tiene `Dockerfile` en la raíz, el build falla con: `Error: error resolving dockerfile path: please provide a valid path to a Dockerfile within the build context with --dockerfile`
- **Síntoma:** el build history muestra "failed" sin mensaje claro de error, solo el usage de Kaniko
- **Fix:** crear `Dockerfile` en la raíz del repo antes de conectar NaN. Mínimo viable:
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 4040
CMD ["node", "server.js"]
```
- **Pitfall:** si el repo se conectó a NaN antes de tener Dockerfile, NaN no lo detecta retroactivamente. Hay que hacer un push nuevo para triggerear el build.
- **Pitfall:** `npm ci` requiere `package-lock.json`. Si no existe, usar `npm install` en su lugar.
- **Pitfall:** no incluir `node_modules/` en `.gitignore` hace que el build suba 600+ archivos innecesarios (como pasó con el primer push del Mastermind Dashboard). Añadir `.gitignore` con `node_modules/` y hacer `git rm -r --cached node_modules` para limpiar.
**NaN build succeeded pero deployment stuck en "pending":**
- A veces el build de Kaniko termina con éxito (imagen creada en registry) pero NaN no despliega el contenedor y se queda en estado "pending" indefinidamente.
- **Síntoma:** build history muestra "succeeded" con imagen, pero la URL pública da 404 y el status de la app es "pending".
- **Causa probable:** NaN no asigna recursos al contenedor (problema de orquestación interna) o el webhook de deploy no se dispara tras el build.
- **Fixes:**
1. Desde la UI de NaN, darle a **"Deploy"** o **"Restart"** manualmente
2. Cambiar el **Container port** en Settings y hacer deploy de nuevo (fuerza re-asignación)
3. Cambiar el puerto en el código (`server.js` + `Dockerfile`), pushear, y esperar nuevo build+deploy
4. Si nada funciona, borrar la app y crearla de nuevo desde cero
- **Prevención:** no hay forma segura de evitarlo — es un problema de la plataforma NaN, no del código.
**Dashboard dual: local backend + NaN frontend:**
- El dashboard de control (monitorización del sistema) tiene dos caras:
- **Local** (microVM, puerto 4040): backend con datos reales del sistema (CPU, RAM, procesos, ChromaDB, crons). Usa `execSync`, `os`, `fs` para datos en vivo.
- **NaN** (contenedor): versión visual que consume APIs del local. Como el contenedor no ve el sistema real, los endpoints deben tener fallbacks graceful (try/catch con datos de ejemplo).
- **Flujo de creación:**
1. Desarrollar y testear localmente primero (el microVM tiene todos los datos reales)
2. Crear Dockerfile y subir a GitHub
3. Conectar repo a NaN como app
4. El contenedor de NaN no tiene acceso a ChromaDB local → el endpoint `/api/skills` debe devolver `{ status: 'disconnected' }` gracefulmente
5. El contenedor de NaN no ve procesos del host → `/api/processes` debe tener fallback
- **Puerto:** elegir uno que no choque con otras apps. El puerto 4000 está ocupado por el ESIOS Dashboard (u otro proyecto de David). Usar 4040 o 6060 para nuevos dashboards.
- **Auth:** Basic Auth con contraseña vía env var `DASH_PASSWORD`
- **Auto-refresh:** frontend con `setInterval(fetch, 5000)` para datos en vivo
- **Repo privado:** `github.com/Ntizar/Mastermind-Dashboard`
- **Flujo de creación de dashboard desde cero:**
1. Crear repo privado en GitHub via API REST (`curl -X POST -H "Authorization: token $GITHUB_TOKEN" ...`)
2. Inicializar git local, hacer primer commit, pushear
3. Desarrollar backend (Express) y frontend (HTML+CSS+JS) localmente
4. Testear en localhost con datos reales del microVM
5. Crear Dockerfile y entrypoint.sh
6. Pushear todo → NaN detecta el push y construye automáticamente
7. **Cuidado:** el primer push NO debe incluir `node_modules/` — añadir `.gitignore` antes del primer commit o limpiar con `git rm -r --cached node_modules` después
8. **Cuidado:** el Dockerfile debe estar en el repo ANTES de que NaN intente construir, o el build fallará con "Dockerfile not found"
**Infinite recursion → OOM:**
- 4GB RAM hard limit, sin swap
- Fix: reemplazar recursión con fetch único
**🔥 Verificar que `patch` realmente modificó el archivo**
El tool `patch` puede reportar éxito sin modificar el archivo (fuzzy matching no encontró el string exacto, o el archivo fue leído parcialmente con offset/limit). Siempre verificar después de cada patch:
```bash
# Verificar que el archivo cambió
git diff --stat
# O verificar contenido específico
grep "nuevo_contenido" js/archivo.js
```
**Síntoma:** `patch` dice `success: true` pero el archivo en disco sigue igual. El commit pusha código viejo. El deploy sirve versión obsoleta. Bugs "fantasma" que no se explican.
**Fix:** Si el patch no aplicó, usar `write_file` para reescribir el archivo completo en vez de intentar otro patch. Es más seguro para archivos pequeños (<500 líneas).
## 8. Cron Jobs con Scripts
**⚠️ `cronjob` tool no disponible en esta VM:**
- El `cronjob` tool no existe en el entorno actual — no hay `crontab`, no hay daemon cron, no hay systemd timers
- Los scripts de mantenimiento se guardan en `/hermes-home/scripts/` y se ejecutan manualmente o desde un cron externo (SSH desde otra máquina)
- Para automatizar: configurar cron en máquina local que SSH al VM, o usar systemd timer en el VM
- Ejemplo: script `/hermes-home/scripts/mastermind-weekly-maintenance.sh` (Domingo 05:00 UTC)
**Patrón de scripts de mantenimiento:**
- Script en `/hermes-home/scripts/` con shebang `#!/bin/bash`
- Script usa `set -e` y loguea a `/var/log/<name>.log`
- Script incluye health checks antes y después de cada paso
- Script hace `git add -A && git commit && git push` al final
**Pitfalls:**
- Script path: SOLO nombre de archivo (el scheduler añade el prefix)
- Schedule usa UTC
- Scripts ejecutan en sesión aislada → no tienen contexto de chat
- Scripts deben incluir retry logic para APIs externas (3 intentos, 2s delay)
- **No usar `cronjob` tool** — no existe en esta VM. Crear scripts bash ejecutables manualmente.
## 3. Static Digest Pipeline
Pipeline para feeds periódicos:
1. Fetch API externa
2. Normalización y scoring heurístico
3. Generar JSON + HTML
4. Deploy a GitHub Pages
Ver skill `devops/static-digest-pipeline` para la implementación completa.
## 4. Deploy Audit — Verificación de despliegues
Procedimiento sistemático para auditar y verificar despliegues en NaN.builders.
### Pasos
1. Descargar deploy remoto y comparar con local (`diff`)
2. `grep -c` por elementos clave en ambos archivos
3. Verificar git status y últimos commits
4. Verificar endpoints secundarios
### Checklist de integridad HTML
- [ ] `loadData()` definida y llamada
- [ ] `renderDashboard()` definida
- [ ] Hero quick status (heroPeso, heroPerdido, heroRitmo)
- [ ] Botones de acción rápida con `switchTab`
- [ ] Tabs con `display:none` excepto tab activo
- [ ] Sin código de dark mode residual
### Pitfalls
- NaN bloquea curl desde ciertas IPs (403) → usar `curl -A "Mozilla"`
- Tamaños iguales ≠ contenido idéntico → usar `diff`
GitHubで見る