Caracterizacion del matching viejo de node-red en docs/legacy-matching.md (reglas: candidatas geo $near 1000km, filtro fino geolib dist/1000<=sub.distance, dedupe 500m, content i18n kmnasa/kmvecinal con 'км' cirilico verbatim, sealed via Iron). Modulos: - matcher/content.ts: i18n kmnasa/kmvecinal + km redondeado (Math.round dist/1000*10/10) - matcher/geo.ts: geolib.getDistance (identico al viejo) + isHit - matcher/seal.ts: @hapi/iron (Fe26.2) — COMPATIBLE con el iron@5 de la web (verificado: sello en servicio -> unseal en web round-trip OK) - matcher/matcher.ts: matchFire(fire, ctx) -> docs notifications identicos a node-red; dedupe 500m + 1 notif/usuario/fuego; solo audiencia web/mobile - deps: @hapi/iron, geolib 63 tests en verde (incluye interop CJS/ESM en dist). MongoDB 3.2 -> ingesta por polling (sin change streams), pendiente de cablear.
122 lines
6.3 KiB
Markdown
122 lines
6.3 KiB
Markdown
# 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: <bot> }`.
|
|
- 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/<sealed>` 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.
|