Todos contra el fuego — microservicio de notificaciones (FCM, email, Telegram)
Find a file
vjrj 6d1ba439a2 fix(poller): stop infinite re-enqueue of unsendable notifications
In shadow/dry-run/kill-switch modes, when the target has no token/recipient/
chat, or when not in the canary cohort, the handlers returned without marking
the notification, so the poller (pendingFilter on notified:{$ne:true})
re-selected the same batch every cycle forever (observed: fcm:500 every 10s).

Add a service-owned per-channel marker processedBy.<channel>, set in every
terminal 'won't/can't send' branch, and exclude marked docs in pendingFilter.
Unlike the shared notified/emailNotified/telegramNotified flags, processedBy is
invisible to the old system, so it never suppresses a real send in prod shadow.

- types.ts: NotificationDoc.processedBy?: Partial<Record<Channel, Date>>
- poller.ts: pendingFilter excludes processedBy.<channel>
- handlers.ts: markProcessed() in no-token/no-recipient/no-chat, decideSend=false,
  dead-token and telegram-blocked branches
- fake-repo: dotted-path support in matching and $set/$unset
- tests: regression test + processedBy assertions (86 passing)
2026-07-23 23:47:13 +02:00
deploy Fase 1a: microservicio tcef-notifications (workers FCM v1 + email) 2026-07-13 00:10:29 +02:00
docs Fase 1c: canal Telegram (telegram-worker) — codigo migrado, inerte 2026-07-13 18:49:52 +02:00
scripts Fase 1a: microservicio tcef-notifications (workers FCM v1 + email) 2026-07-13 00:10:29 +02:00
src fix(poller): stop infinite re-enqueue of unsendable notifications 2026-07-23 23:47:13 +02:00
test fix(poller): stop infinite re-enqueue of unsendable notifications 2026-07-23 23:47:13 +02:00
.dockerignore fase 3: Dockerfile multi-stage (node:22-alpine) 2026-07-14 11:39:31 +02:00
.env.example Fase 1c: canal Telegram (telegram-worker) — codigo migrado, inerte 2026-07-13 18:49:52 +02:00
.gitignore Fase 1a: microservicio tcef-notifications (workers FCM v1 + email) 2026-07-13 00:10:29 +02:00
config.example.json Fase 1a: microservicio tcef-notifications (workers FCM v1 + email) 2026-07-13 00:10:29 +02:00
Dockerfile fase 3: Dockerfile multi-stage (node:22-alpine) 2026-07-14 11:39:31 +02:00
package-lock.json Fase 1b: nucleo del matcher geoespacial (caracterizado + testeado) 2026-07-13 17:58:43 +02:00
package.json Fase 1b: nucleo del matcher geoespacial (caracterizado + testeado) 2026-07-13 17:58:43 +02:00
README.md Fase 1c: canal Telegram (telegram-worker) — codigo migrado, inerte 2026-07-13 18:49:52 +02:00
tsconfig.json Fase 1a: microservicio tcef-notifications (workers FCM v1 + email) 2026-07-13 00:10:29 +02:00
vitest.config.ts Fase 1a: microservicio tcef-notifications (workers FCM v1 + email) 2026-07-13 00:10:29 +02:00

tcef-notifications

Microservicio de envío de notificaciones para Todos contra el Fuego (alertas tempranas de incendios NASA FIRMS). Sustituye el envío de push que hacía la web Meteor con la API legacy de GCM (apagada por Google en jun-2024) y saca el envío del observer de Meteor a una cola controlada.

Fases 1a (workers de envío) y 1b (matching geoespacial) del plan de modernización. La 1a consume la colección notifications; la 1b la genera (fuego → subs 2dsphere), sustituyendo el matching O(fuegos×subs) de node-red. Telegram (1c) viene después.

Qué hace

  • poller: cada N s busca docs pendientes en notifications (con los nombres de campo correctos — el cron viejo tenía un typo, ver docs/legacy-behavior.md §2) y encola un job por canal.
  • fcm-worker: envía push con firebase-admin (FCM HTTP v1), preservando el payload que espera la app Flutter (FLUTTER_NOTIFICATION_CLICK, collapseKey = _id, data). Purga tokens muertos sin reintentar.
  • email-worker: nodemailer + las plantillas new-fire portadas verbatim.
  • idempotencia persistente: colección notification_sends, índice único (notificationId, channel) — sobrevive reinicios y replays de cola.

Salvaguardas anti-spam

El riesgo nº1 es spamear a los usuarios. Por eso:

  • Modos graduales MODE: dry-runshadowcanaryfull. Nunca se salta un modo; full solo con confirmación explícita del usuario.
  • Exclusión mutua por canal: OWNED_CHANNELS aquí + notifDisabledChannels en Meteor. El viejo y el nuevo jamás envían por el mismo canal a la vez.
  • Kill switch: KILL_SWITCH=1 detiene todo envío al instante.
  • Límites: máx envíos/usuario/día y circuit breaker por batch (para y alerta).

Detalle del despliegue y la secuencia de cutover: docs/rollout.md.

Desarrollo

npm ci
npm test            # 47 tests (vitest + fake-repo en memoria, sin infra externa)
npm run typecheck
npm run build       # -> dist/ (+ plantillas)

Nota MongoDB: producción corre MongoDB 3.2 (EOL), así que el servicio usa el driver mongodb v3.7 (el moderno no conecta a 3.2) y no hay change streams (fase 1b usará polling). Los tests van contra un fake en memoria porque no hay mongod tan antiguo ejecutable en hosts modernos. Detalle en docs/legacy-behavior.md §0.

Ejecución

Config por env (ver .env.example) o config.json (ver config.example.json); env gana. Secretos (Mongo, MAIL_URL, service account de Firebase) desde el repo privado tcef-private-config — nunca en este repo.

MODE=dry-run OWNED_CHANNELS=fcm MONGO_URL=... npm start

Despliegue en shiva: deploy/tcef-notifications.service (systemd) o deploy/pm2-tcef-notifications.json. Node 22 standalone (no el Node 8 del sistema) + Redis local. En fase 3 pasa a Docker Compose.

Estado

  • 1a: workers FCM v1 + email, poller, idempotencia, salvaguardas, tests.
  • 1a: flag de cutover en Meteor (notifDisabledChannels).
  • 1a: canary FCM verificado (push real entregada a dispositivos propios).
  • 1b: matcher geoespacial (content i18n, geolib, sealed vía Iron — compatible con la web —, dedupe 500 m), ingesta por polling, modos shadow/full, comparador de shadow. Tests en verde.
  • 1c: telegram-worker (texto + ubicación, firelink, rate limiting global 25/s + 1/s por chat, 429/bloqueo, idempotencia). Tests en verde. Código migrado; sin tokens configurados → inerte.
  • 1a full: envío masivo FCM (~8176 users) — con el usuario, vigilando.
  • 1b: rollout shadow ≥3 días → comparar → cutover node-red → full.
  • 1c: extraer tokens de bot → rollout shadow/canary → cutover node-red.