El smoke-test contra prod revelo MongoDB 3.2.11 (EOL): el driver v6 exige >=4.2 y no conecta. Cambios: - mongodb ^3.7.4 + @types/mongodb; db.ts con useUnifiedTopology, sin retryWrites - sends.ts: deteccion de duplicate-key (11000) sin depender de MongoServerError - poller.ts: FilterQuery en vez de Filter - tests de integracion migrados de mongodb-memory-server a un fake en memoria (test/fake-repo.ts) porque no hay mongod 3.x ejecutable en hosts modernos; emula operadores ($ne/$in/$gte/$exists), $set/$unset e indice unico (11000) - docs: legacy-behavior.md §0 (entorno MongoDB 3.2, sin change streams -> fase 1b usara polling; recomendacion de upgrade de Mongo como fase propia) 47 tests en verde, typecheck y build OK. Datos reales del import: 413 pendientes FCM, 0 email, 8176 users con fireBaseToken.
179 lines
8.9 KiB
Markdown
179 lines
8.9 KiB
Markdown
# Comportamiento del sistema viejo (caracterización)
|
|
|
|
> Fuente: `todos-contra-el-fuego-web` (Meteor 1.6). Ficheros leídos:
|
|
> `imports/modules/server/notificationsProcess.js`,
|
|
> `imports/startup/server/notificationsObserver.js`,
|
|
> `imports/startup/server/cron.js`,
|
|
> `imports/api/Notifications/Notifications.js`,
|
|
> `imports/modules/server/send-email.js`,
|
|
> `private/email-templates/new-fire-{es,en}.{html,txt}`,
|
|
> `public/locales/{es,en,gl}/common.json`.
|
|
>
|
|
> Este documento fija el comportamiento que el nuevo servicio debe reproducir
|
|
> **campo a campo**. Los tests de caracterización (`test/legacy-behavior.test.ts`)
|
|
> lo verifican.
|
|
|
|
## 0. Entorno de datos (crítico)
|
|
|
|
- **Producción corre MongoDB 3.2.11** (verificado en shiva vía `buildInfo`,
|
|
jul-2026). Es EOL desde 2018. Implica:
|
|
- El servicio usa el **driver `mongodb` v3.7** (el v6 exige servidor ≥ 4.2 y
|
|
**no conecta** a 3.2). No subir el driver sin subir antes el servidor.
|
|
- **No hay change streams** (requieren 3.6+) → la fase 1b debe usar el
|
|
**polling** de respaldo, no change streams.
|
|
- Los tests de integración corren contra un **fake en memoria**
|
|
(`test/fake-repo.ts`), porque en hosts modernos no se puede
|
|
descargar/ejecutar un mongod tan antiguo (glibc/libssl).
|
|
- Cifras reales del import (jul-2026): ~413 docs `notifications` pendientes de
|
|
FCM, 0 de email, **8176 users con `fireBaseToken`** (muchos llevarán ~2 años
|
|
muertos → alta tasa de token inválido en el primer envío real).
|
|
- Recomendación de fondo (fuera de fase 1a): planificar un **upgrade de MongoDB**
|
|
como fase propia; 3.2 bloquea drivers modernos, change streams y es un riesgo
|
|
de seguridad. Afecta a Meteor 1.6 y node-red, que dependen de él.
|
|
|
|
## 1. Esquema del doc `notifications`
|
|
|
|
Colección `notifications` (db `fuegos`, `idGeneration: 'MONGO'` → `_id` es
|
|
`ObjectId`, no string).
|
|
|
|
| Campo | Tipo | Notas |
|
|
|---|---|---|
|
|
| `_id` | ObjectId | generado por Mongo; usado como `tag`/`collapse_key` de FCM |
|
|
| `userId` | String | id del user de Meteor (string, no ObjectId) |
|
|
| `subsId` | ObjectId | id de la suscripción (opcional, blackbox) |
|
|
| `content` | String | texto de la alerta (viene de node-red, con prefijo `🔥 ` y sufijo `:`) |
|
|
| `geo` | `{ type: 'Point', coordinates: [lng, lat] }` | GeoJSON — **coordinates[0]=lng, [1]=lat** |
|
|
| `type` | String | **`'mobile'`** → push FCM · **`'web'`** → email. Es el canal. |
|
|
| `notified` | Boolean? | `true` cuando la push se envió con éxito (canal mobile) |
|
|
| `notifiedAt` | Date? | timestamp del envío push |
|
|
| `emailNotified` | Boolean? | `true` cuando el email se envió (canal web) |
|
|
| `emailNotifiedAt` | Date? | timestamp del envío email |
|
|
| `when` | Date | momento de detección del fuego |
|
|
| `sealed` | String | id/slug del fuego; usado en la URL `fire/<sealed>` |
|
|
| `createdAt` / `updatedAt` | Date | timestamps de simpl-schema |
|
|
|
|
**El canal está determinado por `type`**: un doc es o mobile o web, nunca ambos.
|
|
|
|
## 2. Condiciones de envío
|
|
|
|
Un doc se procesa (`processNotif`) si `isMailServerMaster` (solo el proceso
|
|
master pm2 envía) **y**:
|
|
|
|
- **mobile**: `type === 'mobile'` && `notified !== true` && hay un
|
|
`fcmApiToken` válido en settings.
|
|
- **web**: `type === 'web'` && `emailNotified !== true`.
|
|
|
|
Disparadores:
|
|
|
|
1. **Observer** (`notificationsObserver.js`): `Notifications.find().observe()`
|
|
llama `processNotif` en `added` y en `changed`, inmediatamente, sin cola ni
|
|
batching. Es la vía principal (y la que satura).
|
|
2. **Cron** (`cron.js`, SyncedCron cada 15 min): reprocesa pendientes.
|
|
**⚠️ BUG en el sistema viejo**: la query usa campos **mal escritos**
|
|
(`nofitied` y `emailNofitied` en vez de `notified`/`emailNotified`), por lo
|
|
que **el cron nunca encuentra nada** — el reproceso de pendientes está roto
|
|
desde siempre. El nuevo poller usa los nombres correctos.
|
|
|
|
## 3. Push FCM (canal `mobile`)
|
|
|
|
Librería vieja: `node-gcm` contra la **API legacy GCM/FCM** (apagada por Google
|
|
en jun-2024 → todas las push devuelven `FCM error: 404` desde entonces; ver
|
|
`plan-modernizacion/ESTADO.md`). Token del user en `user.fireBaseToken`.
|
|
|
|
Mensaje construido (`gcm.Message`):
|
|
|
|
- **notification**:
|
|
- `title`: `i18n.t('Alerta de fuego')` → es `"Alerta de fuego"`, en `"Alert of fire"`.
|
|
- `body`: `trim(notif.content)` (ver §5).
|
|
- `click_action`: `'FLUTTER_NOTIFICATION_CLICK'` (lo espera la app Flutter).
|
|
- `tag`: `notif._id` (dedupe/colapso de la notificación en el dispositivo).
|
|
- `sound`: `'default'`.
|
|
- `icon`: `'launch_image'`.
|
|
- **data** (todo string en FCM v1):
|
|
- `id`: `notif._id._str` (hex de 24 chars del ObjectId).
|
|
- `description`: body (= `trim(content)`).
|
|
- `lat`: `notif.geo.coordinates[1]`.
|
|
- `lon`: `notif.geo.coordinates[0]`.
|
|
- `when`: `notif.when`.
|
|
- `subsId`: `notif.subsId._str`.
|
|
- `sealed`: `notif.sealed`.
|
|
|
|
Al recibir respuesta:
|
|
- éxito → `Notifications.update(_id, { $set: { notified: true, notifiedAt: new Date() } })`.
|
|
- error → `console.error('FCM error: ...')` + log a Sentry (raven). **No** marca
|
|
`notified` (se reintenta en el siguiente ciclo — que con el bug del cron no
|
|
llega, así que en la práctica solo lo reintenta el observer si el doc cambia).
|
|
- si el user **no** tiene `fireBaseToken` → warn, no envía, no marca.
|
|
|
|
### Equivalencia FCM v1 (nuevo servicio)
|
|
|
|
FCM HTTP v1 (`firebase-admin`) no tiene `notification.tag`/`icon`/`click_action`
|
|
en la raíz; el mapeo Android equivalente:
|
|
|
|
- `message.android.collapseKey` = `notif._id` (equivale a `tag`).
|
|
- `message.android.notification.clickAction` = `'FLUTTER_NOTIFICATION_CLICK'`.
|
|
- `message.android.notification.sound` = `'default'`.
|
|
- `message.android.notification.icon` = `'launch_image'`.
|
|
- `message.notification.{title,body}` = title/body de arriba.
|
|
- `message.data` = los mismos campos (todos como string).
|
|
- `message.token` = `user.fireBaseToken`.
|
|
|
|
Tokens inválidos (`messaging/registration-token-not-registered`,
|
|
`messaging/invalid-argument` sobre el token) → marcar el token como muerto en el
|
|
user (`fireBaseToken` a null / campo `fireBaseTokenDeadAt`), **no reintentar**.
|
|
|
|
## 4. Email (canal `web`)
|
|
|
|
`processNotif` (rama web) → `getEmailOf(user)` para `emailAddress` + `firstName`
|
|
(solo si el email está **verificado**; si no hay email válido, **borra el doc**
|
|
`notifications` para no reintentar).
|
|
|
|
Construcción:
|
|
|
|
- `img` = URL de Google Static Maps (`google-maps-image-api-url`): center
|
|
`lat,lng`, `size 640x480`, `zoom 16`, `maptype hybrid`, `language es`,
|
|
marker con `fireIconUrl`. Key = `Meteor.settings.gmaps.key`.
|
|
- `fireUrl` = `${ROOT_URL}fire/${notif.sealed}`.
|
|
- `message` = `` `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).` ``
|
|
- `fireDetectedAt`: es `"fuego detectado el {{when}}"`, en `"fire detected on {{when}}"`, gl `"lume detectado o {{when}}"`.
|
|
- `dateLongFormat(when)` = `moment.tz(when, tzGuess).format('LLLL (z)')` (fecha
|
|
larga localizada + zona). En el nuevo servicio se replica con Luxon:
|
|
`DateTime.fromJSDate(when).setZone(tz).setLocale(lang).toFormat("cccc, d 'de' LLLL 'de' yyyy HH:mm '('ZZZZ')'")` (aprox; el objetivo es una fecha larga legible con zona — no es un campo comparado byte a byte).
|
|
- `subject` = `subjectTruncate(message, 70)` (trunca a 70 chars por frontera de
|
|
palabra + `...`).
|
|
- Plantilla `new-fire` (`-es`/`-en`), Handlebars, con CSS inline vía `juice`.
|
|
Vars: `applicationName` (=`i18n.t('AppName')`), `firstName`, `message`,
|
|
`fireUrl`, `img`, `subsUrl` (=`${ROOT_URL}subscriptions`).
|
|
- Envío con nodemailer (`MAIL_URL` de settings). Plantillas txt + html.
|
|
|
|
Tras enviar → `Notifications.update(_id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } })`.
|
|
|
|
Las plantillas `new-fire-{es,en}.{html,txt}` se **portan tal cual** a
|
|
`src/templates/` (idénticas a las de `private/email-templates/` de la web).
|
|
|
|
## 5. `trim(content)` — normalización del texto
|
|
|
|
```js
|
|
message.replace(/^🔥 /, '').replace(/:$/, '')
|
|
```
|
|
Quita el prefijo `🔥 ` (emoji + espacio) y el `:` final que node-red añade para
|
|
Telegram pero que no queremos en push/email.
|
|
|
|
## 6. Dedupe existente (aguas arriba, en node-red)
|
|
|
|
node-red, al **crear** los docs, usa `cacheKey` = `lat,lon` con radio 500m para
|
|
no crear notificaciones duplicadas del mismo fuego. Eso es de la **fase 1b**
|
|
(matching); el servicio de la 1a **consume** los docs ya creados. La 1a añade su
|
|
propia idempotencia **de envío** (`notification_sends`, único por
|
|
`(notificationId, channel)`) para no reenviar el mismo doc dos veces.
|
|
|
|
## 7. Cohabitación durante la migración
|
|
|
|
- El observer viejo procesa `notifications` en tiempo real. El nuevo poller
|
|
también. **Exclusión mutua por canal** (obligatoria): un flag en Meteor
|
|
settings `private.notifDisabledChannels: ['mobile','web']` hace que el
|
|
observer/cron viejos **ignoren** esos `type`. El servicio nuevo solo debe
|
|
tener activado (`OWNED_CHANNELS`) un canal **después** de deshabilitarlo en
|
|
Meteor. Ver `docs/rollout.md`.
|
|
- Ambos escriben `notified`/`emailNotified` con la misma semántica, de modo que
|
|
el modo `shadow` puede comparar decisiones.
|