|
| 1 | +# Runbook operativo — Seafood Intelligence OS (Pescamar) |
| 2 | + |
| 3 | +Guía de operación para el equipo técnico de apoyo (no desarrollo diario). Todo lo descrito aquí está verificado contra el repositorio; ante duda, manda `db/README.md`, `PILOT_ACCEPTANCE.md` y este documento, en ese orden de autoridad de esquema. |
| 4 | + |
| 5 | +## 1. Arquitectura en 5 líneas |
| 6 | + |
| 7 | +1. Front-end: SPA Vite (React 19, entradas ES/EN) servida desde Vercel; enrutamiento SPA vía `vercel.json`. |
| 8 | +2. Back-end: ~111 funciones serverless en `api/*.ts` (TypeScript, Node) desplegadas como Vercel Functions; sesión por cookie firmada contra PostgreSQL. |
| 9 | +3. Datos: PostgreSQL 18 en Neon (proyecto `pescamar-control`, región aws-us-east-2, rama `main`), acceso único vía `DATABASE_URL`. |
| 10 | +4. Despliegue: cada push a `main` dispara CI (GitHub Actions, workflow `Quality`) y, en verde, Vercel despliega `main` automáticamente a Producción. |
| 11 | +5. Modelo operativo: capturar una vez → confirmar lo físico → automatizar lo repetitivo → escalar excepciones → decidir con evidencia. Ver `README.md` y `ROADMAP.md`. |
| 12 | + |
| 13 | +## 2. Verificación de salud |
| 14 | + |
| 15 | +Hay tres capas independientes. Verde total = las tres verdes. Ante rojo, alertar al responsable técnico. |
| 16 | + |
| 17 | +**(a) Monitor externo de liveness.** Corre cada hora desde fuera de Vercel (a la hora en punto ~:17, America/Santiago) y alerta ante caída; usa el header de bypass de protección de despliegue configurado a nivel proyecto Vercel (`x-vercel-protection-bypass`, valor solo en el proyecto, nunca en el repo). Complemento interno: la app expone un cron horario en Vercel (`/api/cron-operational-health`, minuto :17, autenticado con `Authorization: Bearer $CRON_SECRET`) que mide salud operacional desde dentro de la plataforma. Replica humana del probe: |
| 18 | + |
| 19 | +```bash |
| 20 | +curl -s https://<dominio-producción>/api/public-health |
| 21 | +# Esperado: HTTP 200 {"ok":true,"service":"pescamar","status":"live","healthVersion":"pescamar.public-health.v1","checkedAt":"..."} |
| 22 | +``` |
| 23 | + |
| 24 | +**(b) Vista en sitio `/admin/salud`** (solo admin, sin entrada de navegación; URL directa). Verde = panel "Build y entorno" con `Entorno: production` y `Commit` igual al SHA desplegado, y chip de liveness en **Operativa** (HTTP 200, latencia en ms, `checkedAt` fresco; se repolle cada 30 s con tu sesión). |
| 25 | + |
| 26 | +**(c) Telemetría de pilotaje `/admin/pilotaje`** (solo admin) + gate de esquema. Verde = los cuatro paneles cargan sin banner de error (rutas 7/30 d, operadores activos 14 d, últimos eventos, feedback Seafood AI) y `/api/schema-preflight` reporta `pilotGate: pass`. Un `hold` del gate indica esquema no reconciliado (ver §3). |
| 27 | + |
| 28 | +## 3. Aplicar migraciones |
| 29 | + |
| 30 | +**No existe endpoint que aplique migraciones.** El flujo canónico (`db/README.md`): ejecutar los archivos de `db/migrations/` en orden ascendente por prefijo, con parada inmediata ante error: |
| 31 | + |
| 32 | +```bash |
| 33 | +for migration in $(find db/migrations -maxdepth 1 -name '*.sql' | sort); do |
| 34 | + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$migration" || exit 1 |
| 35 | +done |
| 36 | +``` |
| 37 | + |
| 38 | +En Neon, la práctica segura es crear una rama efímera desde `main`, aplicar ahí, verificar y borrarla; para producción, aplicar solo el archivo nuevo contra `main` (los archivos son idempotentes: `if not exists` / `on conflict do nothing`). Registros innegociables: |
| 39 | + |
| 40 | +- `baseline` / `reconciled` / `applied` tienen semántica distinta; **nunca** inventar `applied_at`: `applied` exige timestamp real del momento de ejecución. |
| 41 | +- Cada migración nueva se registra sola vía el trailer `insert into schema_migrations ...` del archivo. |
| 42 | +- Después de aplicar: verificar `select migration_name,evidence_kind,applied_at from schema_migrations order by 1` y el `pilotGate` de `/api/schema-preflight` (debe pasar a `pass`). |
| 43 | +- CI protege el SQL de los endpoints de solo lectura con `scripts/endpoint-sql-contracts.mjs` (rama efímera + migraciones + consultas canónicas); un SQL inválido falla CI antes de desplegar, pero **aplicar la migración en Neon sigue siendo manual**. |
| 44 | + |
| 45 | +## 4. CI/CD |
| 46 | + |
| 47 | +Workflow `Quality` (`.github/workflows/quality.yml`), corre en cada push a `main` y en PRs hacia `main`, `feat/**` y `docs/**`: |
| 48 | + |
| 49 | +1. `npm ci` → `npm run quality` (lint 0 warnings + typecheck + build). |
| 50 | +2. ~39 contratos en `scripts/*.mjs` (inventario de migraciones, contratos de UI, contrato SQL de endpoints contra Neon efímero). |
| 51 | +3. Playwright desktop + mobile (Chromium) con artefactos de evidencia adjuntos al run. |
| 52 | + |
| 53 | +**Regla same-SHA:** un cambio solo se considera desplegado cuando CI está en `success` **y** Vercel reporta `Deployment has completed` **sobre el mismo SHA**. Comprobar: API de GitHub Actions (`actions/runs?branch=main`) y Deployments del repo. |
| 54 | + |
| 55 | +**Rollback:** `git revert <sha> && git push` — Vercel despliega `main` automáticamente. Ojo: el rollback de código no revierte datos; las migraciones aplicadas en Neon no tienen rollback automático. |
| 56 | + |
| 57 | +**Política de flakes:** hay specs conocidos como flaky ocasional (p. ej. rollforward, browser-smoke, supplier-reception-decision); CI reintenta una vez (`retries: 1`) y un flake que pasa en retry es aceptable. Si un test falla dos veces seguidas, tratarlo como fallo real e investigar **antes** de volver a pushear. Nunca dejar `main` rojo a propósito; un push rojo se corrige de inmediato o se revierte. |
| 58 | + |
| 59 | +**Logs:** GitHub Actions → run → job `quality` (steps y artefactos `browser-release-evidence`). Logs de runtime de funciones: dashboard Vercel → proyecto → Logs. |
| 60 | + |
| 61 | +## 5. Secretos y variables de entorno |
| 62 | + |
| 63 | +Nombres exactos referenciados en el código/config. **Nunca** pegar valores en docs, tickets ni chats. |
| 64 | + |
| 65 | +| Nombre | Dónde | Propósito | Rotación | |
| 66 | +|---|---|---|---| |
| 67 | +| `DATABASE_URL` | Vercel (producción) | Conexión única a Neon | Desde Neon (reset password de rol); actualizar en Vercel | |
| 68 | +| `ADMIN_SETUP_TOKEN` | Vercel | Activación del primer admin (`/api/bootstrap`); exige ≥24 chars | Rotar tras la activación inicial; luego altas solo con identidad admin individual | |
| 69 | +| `CRON_SECRET` | Vercel | Bearer del cron interno `/api/cron-operational-health` (minuto 17) | Anual o ante sospecha | |
| 70 | +| `OPENAI_API_KEY` | Vercel | Seafood AI (respuestas) | Según política del proveedor | |
| 71 | +| `OPENAI_VISION_MODEL` | Vercel | Modelo de visión usado por reception/Uni Vision provenance | Con el modelo | |
| 72 | +| `CEO_OPERATOR_EMAIL` | Vercel | Correo que identifica al operador CEO (experiencia ejecutiva); tiene default en código | Si cambia el CEO | |
| 73 | +| `PLANT_EXECUTION_WRITES_ENABLED` | Vercel | Kill-switch de escrituras Plant Execution (default activo solo en producción) | No aplica | |
| 74 | +| `COLD_SENSOR_INGEST_SECRET` | Vercel | Ingesta machine-to-machine de sensores de frío (fail-closed) | Anual o ante exposición | |
| 75 | +| `WHATSAPP_WEBHOOK_SECRET` | Vercel | Verificación del webhook de WhatsApp | Anual o ante exposición | |
| 76 | +| `VERCEL_GIT_COMMIT_SHA` | Vercel (automática) | SHA expuesto en `/api/status` (panel de despliegue) | Automática | |
| 77 | +| `NEON_API_KEY` | GitHub (secret, **pendiente**) | Contrato SQL de endpoints: crear/borrar ramas efímeras | Al configurar y anual | |
| 78 | +| `NEON_PROJECT_ID` | GitHub (variable, **pendiente**) | `icy-union-17389410` | Estable | |
| 79 | + |
| 80 | +Notas: no existen `AI_GATEWAY_API_KEY` ni `AUTH_BYPASS` en el código — cualquier referencia a ellos es inventada. La protección de despliegue de Vercel (bypass header) se configura a nivel proyecto, fuera del repo. |
| 81 | + |
| 82 | +## 6. Base de datos (Neon) |
| 83 | + |
| 84 | +- Proyecto `pescamar-control` (aws-us-east-2, PostgreSQL 18), plan **free** al momento de escribir. |
| 85 | +- Plan free: **sin backups programados** y ventana de restauración a punto-en-el-tiempo (PITR) de **6 horas** (`history_retention`). La rama `main` **no está protegida** (admite push/reset directo). |
| 86 | +- Recomendación: subir el plan de Neon para tener backups programados y protección de rama `main`; hasta entonces, como práctica manual: **exportar antes de cualquier cambio riesgoso** (`pg_dump` o consulta de evidencia previa) y usar **ramas efímeras para experimentos** (nunca probar contra `main`). |
| 87 | +- `pg_stat_statements` está instalado (v1.12): para consultas lentas, revisar la consola de Neon (Monitoring / query insights) o `select * from pg_stat_statements order by total_exec_time desc limit 20;` desde el SQL editor. |
| 88 | +- Consumo/límites del plan en la consola de Neon → proyecto → Usage. |
| 89 | + |
| 90 | +## 7. Incidentes — escalera de triage |
| 91 | + |
| 92 | +**App caída / usuarios no entran:** |
| 93 | +- [ ] ¿El monitor externo alertó? Replicar el probe humano de §2(a): ¿HTTP 200 + `status: live`? |
| 94 | +- [ ] Abrir `/admin/salud` con sesión admin: ¿chip Operativa? ¿`Commit` = SHA esperado? |
| 95 | +- [ ] ¿CI y Vercel en verde sobre el mismo SHA? (Deployments del repo). Si el último deploy es sospechoso → `git revert <sha> && git push`. |
| 96 | +- [ ] ¿Caída solo de la API con SPA viva? Logs de funciones en Vercel → Logs; buscar errores 500 recientes por ruta. |
| 97 | +- [ ] ¿Base de datos? `/api/schema-preflight` (¿DATABASE_URL caída da 503), consola Neon → estado del proyecto. |
| 98 | + |
| 99 | +**Datos / esquema:** |
| 100 | +- [ ] Verificar `schema_migrations` vs `db/migrations/` (deben coincidir exactamente; `db/README.md`). |
| 101 | +- [ ] Pérdida de datos: restauración solo dentro de la ventana PITR de 6 h (crear rama desde punto-en-el-tiempo en Neon y extraer; fuera de ventana, no hay copia). Por eso el export manual antes de cambios riesgosos. |
| 102 | + |
| 103 | +**Comunicación:** registrar hora (America/Santiago), SHA afectado, y avisar al responsable técnico con lo verificado arriba (hechos, no hipótesis). |
| 104 | + |
| 105 | +## 8. Gobernanza |
| 106 | + |
| 107 | +- **UX congelado post-piloto** (veredicto y evidencia en `docs/POST-PILOT.md`): no se iteran superficies de producto visibles por ahora. |
| 108 | +- **Criterio de reactivación UX:** la fricción medida con evidencia en `/admin/pilotaje` (rutas visitadas, operadores activos, feedback Seafood AI) es la fuente que dispara la reactivación; cuando baje sostenidamente, se reactiva la iteración. La telemetría registra solo la ruta (nunca query strings) y el rol siempre se toma de la sesión en el servidor; no contiene PII. |
| 109 | +- Cambios de esquema: solo vía migraciones versionadas en `db/migrations/` (§3). Cambios de código: siempre con la cadena CI completa en verde (§4). |
0 commit comments