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.
6.3 KiB
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ástodos-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
notificationsque 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.geotiene índice 2dsphere → las geo-queries$near/$geoWithinfuncionan 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)
- Candidatas (geo grueso): query sobre
subscriptionscongeo $near { $geometry: Point(fireLon,fireLat), $minDistance:0, $maxDistance:1000000 }(1000 km de prefiltro), filtrando la audiencia:- Telegram:
{ telegramBot: <bot> }. - Si el flag global
notifyWebUsersestá 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.
- Telegram:
- Filtro fino por distancia: por cada candidata,
dist = geolib.getDistance(sub.location, fire)(metros, great-circle). Sidist/1000 <= sub.distance(km) → hit.km = Math.round(dist/1000 * 10) / 10(1 decimal) para el texto.
- Dedupe 500 m: antes de insertar, buscar en
notificationsdel mismouserIduna notif congeo $near { $maxDistance: 500 }(0,5 km). Si existe alguna → NO se crea (ya notificado en esa zona). node-red guarda ademáscacheKey = "lat,lon"(informativo; el dedupe real es el geo-query 500 m). - Contenido (
content), i18n por lang del owner de la sub:typefuegomodis/viirs→ clavekmnasa.typefuegovecinal→ clavekmvecinal.- Interpolando
{{km}}. ⚠️ Las cadenas llevan literalmenteкм(cirílico, nokm) — es un error histórico de las traducciones; se replica verbatim.
sealed:Iron.seal(firePayload, ironPassword, Iron.defaults)con el paqueteiron(Hapi Iron).firePayload= el doc del fuego conourversion:"1"y sin_id; contiene al menos{lon,lat,when,type}. La web lo descifra enimports/modules/url-encode.js(Iron.unseal) para la rutafire/<sealed>y la app.- Nota: Iron usa IV/salt aleatorios →
sealedno 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 mismoironPassword. El shadow comparasealeddescifrándolo, no como string.
- Nota: Iron usa IV/salt aleatorios →
- 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
activefiresporcreatedAt > 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
updatedAtpara no re-notificar. - "Union of fires":
activefirestienefireUnion(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
createdAtnuevos, 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.