# Comportamiento del matching viejo (fase 1b — caracterización) > Fuente: subflow "send notifications" de node-red en > `todos-contra-el-fuego/telegram-bot/flows.json` (copia rescatada, fase 0), más > `todos-contra-el-fuego-web`: `imports/api/Subscriptions/Subscriptions.js`, > `imports/api/ActiveFires/ActiveFires.js`, `imports/modules/url-encode.js`, > `imports/api/Fires/server/publications.js`, y locales node-red. > > Objetivo: reproducir **campo a campo** los docs `notifications` que node-red > crea, para que los workers de la fase 1a no noten el cambio de origen. ## 0. Entorno (recordatorio) - MongoDB **3.2** → **no hay change streams**. La ingesta de fuegos es por **polling** de `activefires` (marca de tiempo / checkpoint), no change stream. - Driver `mongodb@3.7` (ya en el servicio). `subscriptions.geo` tiene índice **2dsphere** → las geo-queries `$near`/`$geoWithin` funcionan en 3.2. ## 1. Colecciones implicadas ### `activefires` (fuego) ``` { _id, ourid:{type:'Point',coordinates:[lon,lat]}, lat, lon, when:Date, type:'modis'|'viirs'|'vecinal', acq_date, acq_time, confidence, frp, ..., createdAt:Date, updatedAt:Date, fireUnion?:ObjectId } ``` La importación NASA (cron de `incinera` cada 15 min) inserta/actualiza aquí. `createdAt` marca cuándo entró el fuego → base para el polling de fuegos nuevos. ### `subscriptions` ``` { _id:ObjectId, location:{lat,lon}, geo:{type:'Point',coordinates:[lon,lat]}, distance:Number, // RADIO de la sub en km owner:String, // userId type:String, // 'web' | 'mobile' | 'telegram' chatId?:Number, telegramBot?:String, // solo type 'telegram' createdAt, updatedAt } ``` ### `notifications` (lo que hay que generar — ver `legacy-behavior.md` §1) Campos escritos por node-red: `userId`, `subsId`, `content`, `geo`(Point), `type`, `when`, `sealed`, `createdAt`, `updatedAt`. Sin `notified`/`emailNotified` (pendiente → lo consumen los workers de 1a). ## 2. Reglas del matching (por cada fuego) 1. **Candidatas (geo grueso)**: query sobre `subscriptions` con `geo $near { $geometry: Point(fireLon,fireLat), $minDistance:0, $maxDistance:1000000 }` (**1000 km** de prefiltro), filtrando la audiencia: - Telegram: `{ telegramBot: }`. - Si el flag global `notifyWebUsers` está activo, además: `$or: [{telegramBot},{type:'web'},{type:'mobile'}]`. - **Fase 1b (workers 1a) solo genera para `type ∈ {web, mobile}`**; el envío Telegram es fase 1c. 2. **Filtro fino por distancia**: por cada candidata, `dist = geolib.getDistance(sub.location, fire)` (metros, great-circle). Si `dist/1000 <= sub.distance` (km) → **hit**. - `km = Math.round(dist/1000 * 10) / 10` (1 decimal) para el texto. 3. **Dedupe 500 m**: antes de insertar, buscar en `notifications` del mismo `userId` una notif con `geo $near { $maxDistance: 500 }` (0,5 km). **Si existe alguna → NO se crea** (ya notificado en esa zona). node-red guarda además `cacheKey = "lat,lon"` (informativo; el dedupe real es el geo-query 500 m). 4. **Contenido** (`content`), i18n por lang del **owner** de la sub: - `type` fuego `modis`/`viirs` → clave **`kmnasa`**. - `type` fuego `vecinal` → clave **`kmvecinal`**. - Interpolando `{{km}}`. ⚠️ Las cadenas llevan literalmente **`км`** (cirílico, no `km`) — es un error histórico de las traducciones; se replica verbatim. 5. **`sealed`**: `Iron.seal(firePayload, ironPassword, Iron.defaults)` con el paquete **`iron`** (Hapi Iron). `firePayload` = el doc del fuego con `ourversion:"1"` y **sin `_id`**; contiene al menos `{lon,lat,when,type}`. La web lo descifra en `imports/modules/url-encode.js` (`Iron.unseal`) para la ruta `fire/` y la app. - **Nota**: Iron usa IV/salt aleatorios → `sealed` **no es byte-idéntico** entre ejecuciones ni sistemas, y **no debe compararse por bytes**. Lo que importa es que **descifre al fuego correcto** con el mismo `ironPassword`. El shadow compara `sealed` **descifrándolo**, no como string. 6. **Doc insertado** en `notifications`: ``` { userId: sub.owner, subsId: sub._id, content, type: sub.type, geo: { type:'Point', coordinates:[fire.lon, fire.lat] }, when: fire.when, sealed, createdAt: now, updatedAt: now } ``` ## 3. Cadenas i18n (verbatim de los locales node-red) `kmnasa`: - es: `🔥 Alerta de posible fuego a {{km}} км de distancia (fuente NASA)` - en: `🔥 Alert of possible fire in {{km}} км of distance (source NASA)` - gl: `🔥 Alerta posible lume a {{km}} км de distancia (orixe NASA)` - ast: `🔥 Alerta de fueu posible a {{km}} км de distancia (fonte NASA)` - pt: `🔥 Alerta, possível fogo posto em {{km}} км de distância (origem NASA)` `kmvecinal`: - es: `🔥 Alerta vecinal de fuego a {{km}} км de distancia` - gl: `🔥 Alerta veciñal de lume a {{km}} км de distancia` - ast: `🔥 Alerta vecinal de fueu a {{km}} км de distancia` (El sufijo `:` y el prefijo `🔥 ` los quita `trim()` en los workers — ver `legacy-behavior.md` §5. Aquí el contenido NO lleva `:` final; node-red lo añade solo para el mensaje de Telegram, no para el doc `notifications`.) ## 4. Ingesta de fuegos (fase 1b, sin change streams) - **Polling** de `activefires` por `createdAt > checkpoint` (fuegos nuevos desde la última pasada), con checkpoint persistido (colección propia del servicio, p.ej. `matcher_state`). Alternativa/disparo: al final del import NASA. - Re-detecciones (mismo fuego re-visto): el **dedupe 500 m** evita duplicar; no hace falta tratar `updatedAt` para no re-notificar. - **"Union of fires"**: `activefires` tiene `fireUnion` (agrupación de fuegos cercanos, lógica de 2018). Pendiente de confirmar si afecta a qué fuego dispara notificación; por defecto se procesa por fuego individual y el dedupe 500 m agrupa la vecindad. **Anotar como punto a verificar en el shadow.** ## 5. Puntos abiertos a validar en shadow - ¿El matching viejo notifica solo fuegos `createdAt` nuevos, o re-procesa? (el cron NASA corre cada 15 min; el subflow procesa el batch del import). - Efecto exacto de `fireUnion` / "union of fires" en el disparo. - `notifyWebUsers`: ¿está activo en prod? (determina si se generan notifs web/mobile o solo telegram). Verificar el global en la config de node-red. - Lang usado para `content`: el del **owner** de la sub (users.lang). Confirmar con casos reales en shadow.