- config: bloque matcher (MATCHER_MODE off/shadow/full, IRON_PASSWORD, audiencia, poll, batch) + validacion - db: colecciones subscriptions/activefires/notifications_shadow/matcher_state + ensureMatcherIndexes (2dsphere en notifications_shadow para el dedupe 500m) - matcher/context: MatchContext sobre Mongo real (candidatas geo $near 1000km + audiencia; dedupe geo $near 500m; lang del owner) - matcher/ingest: polling de activefires por createdAt>checkpoint (Mongo 3.2 sin change streams), checkpoint persistido en matcher_state - matcher/runner: loop shadow(->notifications_shadow)/full(->notifications), idempotente por dedupe+checkpoint - matcher/compare: comparador de shadow (shadowOnly=spam, realOnly=faltantes, contentMismatches) — el gate antes del cutover - index: arranca el matcher; fake-repo ampliado (upsert, sort, $gt/$lt) - docs/env/README actualizados 73 tests en verde, typecheck y build OK.
6.9 KiB
Plan de puesta en marcha (rollout) — fase 1a
Regla de oro: nunca se salta un modo, y ningún canal pasa a
fullsin confirmación explícita del usuario. El riesgo nº1 es spamear a los usuarios.
Modos (env MODE)
| Modo | Envía | Para qué |
|---|---|---|
dry-run |
no | calcula el payload, resuelve destinatario, loguea. Detecta errores de datos/plantillas sin tocar a nadie. No reserva en notification_sends. |
shadow |
no | igual que dry-run pero pensado para correr ≥1 día en paralelo con el sistema viejo y comparar decisiones (logs estructurados). |
canary |
solo cohorte | envía únicamente a CANARY_USER_IDS / CANARY_SUBS_IDS (dispositivos/suscripciones propias). |
full |
todos | operación normal. |
Salvaguardas activas en todos los modos: KILL_SWITCH=1 (parada total), límite
LIMIT_PER_USER_DAY, circuit breaker LIMIT_MAX_PER_BATCH, idempotencia
persistente notification_sends (único por notificationId+channel).
Exclusión mutua por canal (obligatoria)
El sistema viejo (Meteor) y el nuevo jamás deben enviar por el mismo canal a la vez. Se controla con:
- Meteor (
todos-contra-el-fuego-web):settings.private.notifDisabledChannels, un array con"mobile"y/o"web". Si un canal está ahí, el observer/cron viejos lo ignoran (guard ennotificationsProcess.js). Cambio reversible sin desplegar código: editar settings y reiniciar el proceso Meteor. - Servicio nuevo (
OWNED_CHANNELS):fcmy/oemail. Solo procesa los que posee.
Mapa de canales: mobile ↔ fcm, web ↔ email.
Orden seguro de cutover de un canal: (1) añadir el canal a
notifDisabledChannelsen Meteor y reiniciar → el viejo deja de enviarlo; (2) confirmar en logs que el viejo ya no lo toca; (3) añadir el canal aOWNED_CHANNELSdel servicio y subir el modo. Nunca al revés.
Secuencia (por canal, empezando por FCM)
FCM es el primer cutover y el más seguro: el canal FCM del viejo está muerto
desde jun-2024 (API legacy GCM apagada → 404), así que no hay envío real que
solapar. Ver plan-modernizacion/ESTADO.md.
Paso 0 — Infra en shiva (una vez)
- Node 22 standalone (nvm o binario), no el Node 8 del sistema.
- Redis local (
apt install redis-servero binario/contenedor). - Clonar el repo,
npm ci && npm run build. - Config desde el repo privado
tcef-private-config:MONGO_URL(rsmain, dbfuegos),MAIL_URL,GMAPS_KEY,FIRE_ICON_URL,ROOT_URL.- Service account de Firebase (
FCM_SERVICE_ACCOUNT=/ruta/al.json) — descargar de la consola Firebase del proyectoorg.comunes.fires(angular-cosmos-108908) → Configuración → Cuentas de servicio → Generar clave privada. Guardar solo en el repo privado / disco de shiva.
Paso 1 — dry-run FCM (unos ciclos)
MODE=dry-run OWNED_CHANNELS=fcm KILL_SWITCH=0
- Corre varios imports NASA (cada 15 min). Verifica en logs: payloads bien formados, cuántos users sin token, cuántos con token. No se envía nada.
- Criterio de avance: sin errores de datos/plantilla; volumen esperado.
Paso 2 — shadow FCM (≥1 día)
MODE=shadow OWNED_CHANNELS=fcm
- Compara decisiones con el sistema viejo (que en FCM no envía nada real igual).
- Criterio de avance: sin discrepancias inesperadas; sin duplicados en
notification_sends.
Paso 3 — cutover + canary FCM (dispositivos propios)
- En Meteor:
notifDisabledChannels: ["mobile"], reiniciartcef_web*. - Verificar que el viejo ya no procesa mobile.
- Servicio:
MODE=canary OWNED_CHANNELS=fcm CANARY_USER_IDS=<tu userId> FCM_SERVICE_ACCOUNT=... - Instala la app Flutter en un dispositivo propio con sesión iniciada, genera
un fuego sintético cerca de tu suscripción → debe llegar la push (formato e
idioma correctos). Verifica
notification_sends(statussent) y logs de firebase-admin. - Simulacro de idempotencia: mata el worker a mitad de batch y reinícialo →
cero reenvíos (comprobar
notification_sends).
- Criterio de avance: push llega bien a la cohorte varios días, sin duplicados.
Paso 4 — full FCM (requiere confirmación explícita del usuario)
MODE=full OWNED_CHANNELS=fcm
- Los
fireBaseTokenllevan ~2 años sin uso → alta tasa de token muerto en el primer envío. Es esperado: se purgan (fireBaseToken=null), no se reintenta. - Vigilar el circuit breaker y el límite por usuario/día.
Paso 5 — email (repetir la secuencia, solo tras FCM estable)
- dry-run → shadow (aquí el viejo SÍ envía email, así que el shadow es
crítico: comparar a quién marcó el viejo como
emailNotifiedvs. a quién enviaríamos, volcar discrepancias) → cutover (notifDisabledChannels: ["mobile","web"]en Meteor) → canary (buzón de test víaMAIL_REDIRECT_TO) → full (con confirmación).
Rollback (por canal)
- Servicio:
KILL_SWITCH=1(parada inmediata) o quitar el canal deOWNED_CHANNELS. - Meteor: quitar el canal de
notifDisabledChannels, reiniciar → el viejo reasume ese canal. - Nada se pierde: los docs pendientes siguen en
notificationsconnotified/emailNotifiedsin marcar.
Fase 1b — matcher geoespacial (rollout)
El matcher genera los docs notifications (fuego → subs 2dsphere) que hoy
crea node-red. Config: MATCHER_MODE (off/shadow/full), IRON_PASSWORD
(sella sealed; = settings.private.ironPassword), MATCHER_AUDIENCE
(web,mobile). Ingesta por polling de activefires (Mongo 3.2 no tiene
change streams). Ver docs/legacy-matching.md.
Secuencia (empezar solo con la fase 1a estable):
MATCHER_MODE=shadow≥3 días / varios ciclos NASA reales. Escribe ennotifications_shadow(no tocanotifications, node-red sigue generando).- Comparar con
src/matcher/compare.ts(compareShadow): correr el comparador sobre la ventana y exigirshadowOnlyvacío (0 notifs de más = 0 spam) — cadarealOnly(faltante) se analiza uno a uno;contentMismatchesrevisados. - Cutover (con confirmación explícita): desactivar el subflow de matching en
node-red (exportar el flow antes como respaldo; documentar qué nodos) y poner
MATCHER_MODE=full. node-red deja de insertar ennotifications; el matcher pasa a ser el único origen. Los workers de 1a consumen igual. - Rollback: reimportar el flow de node-red y
MATCHER_MODE=shadow/off. Los workers de 1a siguen consumiendonotificationsvenga de donde venga.
Checklist de verificación (por paso)
- Tests en verde (
npm test). - dry-run: payloads OK, sin errores, volumen esperado.
- shadow ≥1 ciclo NASA real sin discrepancias ni duplicados.
- Exclusión por canal confirmada (viejo no toca el canal migrado).
- Canary: entrega correcta a cohorte propia, formato/idioma OK.
- Simulacro kill-a-mitad-de-batch → cero reenvíos.
- Confirmación explícita del usuario antes de
full.