- 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.
79 lines
3.6 KiB
Markdown
79 lines
3.6 KiB
Markdown
# 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](../plan-modernizacion/README.md). 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`](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-run` → `shadow` → `canary` → `full`. 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`](docs/rollout.md).
|
||
|
||
## Desarrollo
|
||
|
||
```bash
|
||
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`](docs/legacy-behavior.md) §0.
|
||
|
||
## Ejecución
|
||
|
||
Config por env (ver [`.env.example`](.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.
|
||
|
||
```bash
|
||
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
|
||
|
||
- [x] **1a**: workers FCM v1 + email, poller, idempotencia, salvaguardas, tests.
|
||
- [x] **1a**: flag de cutover en Meteor (`notifDisabledChannels`).
|
||
- [x] **1a**: canary FCM verificado (push real entregada a dispositivos propios).
|
||
- [x] **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.
|
||
- [ ] **1a full**: envío masivo FCM (~8176 users) — con el usuario, vigilando.
|
||
- [ ] **1b**: rollout `shadow` ≥3 días → comparar → cutover node-red → `full`.
|
||
- [ ] **1c**: envío Telegram.
|