- 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.
142 lines
6.9 KiB
Markdown
142 lines
6.9 KiB
Markdown
# Plan de puesta en marcha (rollout) — fase 1a
|
|
|
|
> **Regla de oro**: nunca se salta un modo, y ningún canal pasa a `full` sin
|
|
> 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 en `notificationsProcess.js`). Cambio reversible sin
|
|
desplegar código: editar settings y reiniciar el proceso Meteor.
|
|
- **Servicio nuevo** (`OWNED_CHANNELS`): `fcm` y/o `email`. 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
|
|
> `notifDisabledChannels` en Meteor y reiniciar → el viejo deja de enviarlo;
|
|
> (2) confirmar en logs que el viejo ya no lo toca; (3) añadir el canal a
|
|
> `OWNED_CHANNELS` del 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)
|
|
1. Node 22 standalone (nvm o binario), **no** el Node 8 del sistema.
|
|
2. Redis local (`apt install redis-server` o binario/contenedor).
|
|
3. Clonar el repo, `npm ci && npm run build`.
|
|
4. Config desde el repo privado `tcef-private-config`:
|
|
- `MONGO_URL` (rsmain, db `fuegos`), `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 proyecto `org.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)
|
|
1. En Meteor: `notifDisabledChannels: ["mobile"]`, reiniciar `tcef_web*`.
|
|
2. Verificar que el viejo ya no procesa mobile.
|
|
3. Servicio:
|
|
```
|
|
MODE=canary OWNED_CHANNELS=fcm CANARY_USER_IDS=<tu userId> FCM_SERVICE_ACCOUNT=...
|
|
```
|
|
4. 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` (status `sent`) y logs de
|
|
firebase-admin.
|
|
5. 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 `fireBaseToken` llevan ~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 `emailNotified` vs. a quién
|
|
enviaríamos, volcar discrepancias) → cutover (`notifDisabledChannels:
|
|
["mobile","web"]` en Meteor) → canary (buzón de test vía `MAIL_REDIRECT_TO`) →
|
|
full (con confirmación).
|
|
|
|
## Rollback (por canal)
|
|
|
|
1. Servicio: `KILL_SWITCH=1` (parada inmediata) o quitar el canal de
|
|
`OWNED_CHANNELS`.
|
|
2. Meteor: quitar el canal de `notifDisabledChannels`, reiniciar → el viejo
|
|
reasume ese canal.
|
|
3. Nada se pierde: los docs pendientes siguen en `notifications` con
|
|
`notified`/`emailNotified` sin 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):
|
|
|
|
1. **`MATCHER_MODE=shadow`** ≥3 días / varios ciclos NASA reales. Escribe en
|
|
`notifications_shadow` (no toca `notifications`, node-red sigue generando).
|
|
2. **Comparar** con `src/matcher/compare.ts` (`compareShadow`): correr el
|
|
comparador sobre la ventana y exigir **`shadowOnly` vacío** (0 notifs de más =
|
|
0 spam) — cada `realOnly` (faltante) se analiza uno a uno; `contentMismatches`
|
|
revisados.
|
|
3. **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 en `notifications`; el matcher
|
|
pasa a ser el único origen. Los workers de 1a consumen igual.
|
|
4. **Rollback**: reimportar el flow de node-red y `MATCHER_MODE=shadow`/`off`.
|
|
Los workers de 1a siguen consumiendo `notifications` venga 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`.
|