Commit graph

691 commits

Author SHA1 Message Date
56f53e2c23 perf(subsUnion): unión en árbol, menos vértices, timeout y cota de memoria
All checks were successful
build-image / test (push) Successful in 2m49s
build-image / build (push) Successful in 13m4s
Cuatro endurecimientos salidos del banco de carga (bench/union-bench.js), cada
uno con su número:

1. Unión en ÁRBOL en vez de en cadena. La cadena unía siempre contra un polígono
   acumulado que no paraba de crecer. Con 1.000 suscripciones repartidas por el
   mundo —el peor caso, círculos que no se solapan— eran 238 s; en árbol son
   3,6 s, con un documento idéntico. Con datos realistas (España) 10.000
   suscripciones bajan de 39,3 s a 23,5 s. test/server/calcUnionAsync.test.js
   comprueba la equivalencia contra una referencia calculada en cadena.

2. 64 vértices por círculo en vez de 144, configurable con
   `private.unionCircleSteps`. Con 10.000 suscripciones: 23,5 s → 10,0 s y el
   pico de RSS de 777 MB → 421 MB. En pantalla un círculo de 64 lados sigue
   siendo un círculo, y la capa de zonas es un adorno difuso; el host de
   despliegue tiene 5,9 GB para todo el stack.

3. Timeout del worker (10 min por defecto, `private.unionWorkerTimeoutMs`). Un
   worker que no contestaba dejaba la promesa colgada y con ella la cola entera:
   `busy` se quedaba en true y no se volvía a calcular ninguna unión hasta
   reiniciar, sin que nada fallase. Se reintenta una vez ante un error real,
   nunca ante un timeout.

4. Cota de heap del worker (1 GB, `private.unionWorkerMaxHeapMb`), para que un
   caso patológico muera con ERR_WORKER_OUT_OF_MEMORY —que esta capa convierte
   en un rechazo, dejando la unión anterior intacta— en vez de que el kernel se
   lleve por delante el proceso de Meteor.

Además unionTelemetry.js manda a GlitchTip las uniones que pasan de un minuto y
las que hay que degradar por tamaño, y registra duración/tamaño de cada una. El
único aviso de que la unión iba mal era, hasta ahora, que el mapa se quedaba
viejo.

Smoke REST byte-idéntico. 90 tests en verde.
2026-08-01 22:32:37 +02:00
072e76aaed bench(union): banco de carga de la unión de suscripciones
Ejecuta el worker de producción contra suscripciones sintéticas realistas
(península, Canarias, Baleares; radios de 5 a 100 km) y mide lo que la fase 12
pide: duración del recreate completo y del incrementalAdd, retraso máximo del
event loop del proceso padre —que es donde vive DDP y por tanto la pregunta
real de "¿se congela la web?"—, tamaño del documento frente al límite de 16 MiB
y pico de RSS.

`--spread world` reparte las suscripciones por todo el mundo: es el peor caso
para el tamaño, porque casi no se solapan y la unión conserva un polígono por
suscripción en vez de fundirlos.

treeUnionWorker.js es una variante solo del banco (une en árbol en vez de en
cadena) para poder comparar estrategias con números antes de tocar el worker
de verdad. El worker de producción solo cambia en que ahora acepta `steps` por
workerData, con el mismo 144 de siempre por defecto.
2026-08-01 21:51:27 +02:00
c92f25a9a6 fix(ci): run playwright from e2e/, npx --prefix does not change the cwd
All checks were successful
build-image / test (push) Successful in 2m52s
build-image / build (push) Successful in 12m12s
`npx --prefix e2e playwright test` resolves the binary but keeps the working
directory, so playwright would look for playwright.config.js in the app root,
find none, and run with defaults against no tests. Caught before tonight's first
scheduled run.
2026-08-01 19:07:11 +02:00
19b76a570c fix(client): render the map pages when no Google Maps key is configured
Some checks failed
build-image / test (push) Successful in 3m31s
build-image / build (push) Has been cancelled
Gkeys waited on the key VALUE to decide the script had loaded. With an empty
gmaps key (any deployment that has not got one, and settings-ci.json) the
ReactiveVar stayed falsy forever, so every component mounting after the script
had already loaded — i.e. anything reached by client-side navigation — sat on
"Waiting for the gkey" and rendered an empty <div>. /subscriptions/new was
completely blank, with no error in the console: you simply could not add a zone.

Tracking "loaded" separately from the key fixes it. Found while writing the e2e
suite, which could not get past the add-zone page.
2026-08-01 18:44:32 +02:00
a3f2929ff7 test(e2e): browser suite for the flows the server tests cannot see
Playwright under e2e/, its own npm project so Meteor never sees the dependency
(.meteorignore keeps the directory out of the bundle — a config file at the app
root is otherwise eagerly loaded into the server and crashes it).

Covers what the phase asked for: sign up / log in, creating a zone from the map
and watching the union get painted on /subscriptions and /zones, removing it
again, commenting on a fire, and switching language. The zone spec is the flow
that froze staging on 2026-07-30 — it passes only if the server is still
answering DDP while the union is recomputed.

docker-compose.e2e.yml is a throwaway stack (Mongo on tmpfs, no named volume) so
the destructive seeds cannot be pointed at anything real by accident;
playwright.config.js refuses to start against the staging or production domains.

i18n.spec.js leaves a test.fixme on a real bug found while writing it: the
language chosen in the profile does not survive a page reload.
2026-08-01 18:44:09 +02:00
3b04ebceb3 ci(web): run the server suite before building the image
The build job now needs: test, so an image cannot be published with the suite in
red — which is the whole point of this phase. Both jobs live in the same file
because needs: only links jobs within one workflow.

settings-ci.json is committed and contains no secrets: settings-development.json
is gitignored, so CI had nothing to pass to --settings. The one test that cannot
work without a secret is the node-red iron fixture, sealed with the deployment's
own ironPassword; it now reports itself as skipped instead of failing on an hmac
mismatch, and still runs locally against settings-development.json.

Measured locally: 22s and a ~2.0 GB peak for the whole `meteor test` run (its own
mongod included), hence --memory=3g. The meteor-tool download is not cached yet;
the note in the workflow says what that would take.
2026-08-01 18:02:31 +02:00
e61efc681c test(publications): cover the geo publications the map calls on every pan
activefiresmyloc (bounding box, published fields, out-of-range coordinates),
activefiresunionmyloc (geoIntersects, including a shape larger than the viewport)
and mysubscriptions (own subscriptions only, nothing for anonymous visitors).
2026-08-01 18:02:20 +02:00
755afccf90 test(calcUnionAsync): pin the worker plumbing and the event-loop guarantee
Runs the real worker against the real @turf modules, because what broke twice was
never the geometry: the worker script and the turf packages sit in sibling
directories of the bundle that a bare require() cannot reach (3249362), and the
first fix doubled the programs/server prefix so nothing resolved (570cb49).

The last test is the reason the worker exists at all: timers keep firing while a
120-circle union computes. Revert fbb746f and it fails.
2026-08-01 18:02:20 +02:00
6e448bda55 test(subsUnion): cover the queue, the fast path and the size guard
The cases are the incidents: a worker failure must leave the stored union alone
instead of overwriting it with "null" (50ca9cc), an incremental merge must refuse
to run on state that is not exactly one behind, recomputes must coalesce while
adds must not be dropped, and legacy telegram subscriptions (no radius, no
location) must be skipped without throwing.

Also the 16 MiB case the FIXME asked for: a 20 MB union comes back under the cap,
still parseable, with the degradation recorded.
2026-08-01 18:02:20 +02:00
124ed4e135 refactor(subsUnion): extract the logic out of Meteor.startup, cap the union size
Everything the union does lived inside a Meteor.startup callback with no exports:
the queue, the incremental fast path, the worker-failure handling. None of it was
reachable from a test, which is how three separate incidents shipped — a failed
worker stored as an empty union (50ca9cc), turf deps unresolvable from the worker
(3249362/570cb49) and a recompute that blocked the event loop and froze DDP
(b4e5511/fbb746f). subsUnionLogic.js now takes its collaborators as arguments and
subsUnion.js is only the wiring.

The FIXME at the old subsUnion.js:82 goes with it, because it lives inside the
extracted storeUnion: a union over 16 MiB is simply rejected by Mongo, so the map
would freeze at the last union that happened to fit, silently. unionSizeGuard.js
degrades the geometry instead — coordinate precision, then vertex decimation,
then holes, then the smallest polygons — until it fits, and reports what it did
so the setting document records it. The cap is 8 MiB rather than 16: the same
string is pushed to every browser over DDP.

addNoisy no longer mutates the document it is given, so the public pass cannot
leak its fuzzing into the private one.
2026-08-01 18:02:04 +02:00
e68340931c fix(meteor): pin session@1.2.2, 1.2.3 needs a newer Meteor core
session@1.2.3 requires meteor@2.3.1 (Meteor 3.3+) and this app is on 3.1, so
every `meteor test` and `meteor build` since 685af89 died at version selection
with "Constraint meteor@2.3.1 is not satisfied by meteor 2.0.2". 1.2.2 is the
release that ships with Meteor 3.1 and gives Bert.alert() the same Session var.

Nothing caught it because CI only builds the image and never ran the suite —
which is what the rest of this phase is about.
2026-08-01 17:37:19 +02:00
685af89f20 fix(client): add missing session package for Bert.alert()
Some checks failed
build-image / build (push) Failing after 3m10s
themeteorchef:bert's Bert.alert() calls Session.set('bertAlert', ...)
internally, but the session package was never declared in
.meteor/packages — only reactive-var/tracker were. Every Bert.alert()
call (subscribe/unsubscribe success and error paths, ViewSubscription,
SubscriptionEditor) threw "ReferenceError: Session is not defined",
which crashed the DDP method result callback and silently ate whatever
UI update was supposed to happen next.
2026-07-30 11:31:53 +02:00
775461f7b2 fix(users): use updateAsync for Meteor 3 server-side user writes
All checks were successful
build-image / build (push) Successful in 12m38s
Meteor.users.update() is sync-only and no longer exists server-side
under Meteor 3, so it threw on every call, surfacing as a generic
500 to the client. users.setLang runs on every page load for logged-in
users, which is what broke /zones; the same bug in edit-profile.js
was silently breaking /profile too.
2026-07-30 10:13:21 +02:00
570cb49764 fix(subsUnion): drop duplicated programs/server prefix in npm path
All checks were successful
build-image / build (push) Successful in 11m53s
process.cwd() for this process is already /app/programs/server (same
basis WORKER_PATH already relied on), so prefixing 'programs/server/'
again produced .../programs/server/programs/server/npm/node_modules —
still Cannot find module '@turf/circle', just with a longer wrong path.
2026-07-30 09:04:11 +02:00
324936242b fix(subsUnion): resolve turf deps by absolute path, not bare specifier
All checks were successful
build-image / build (push) Successful in 12m9s
The previous side-effect-import fix didn't work: the app's own npm deps
(installed by `meteor build`) live in programs/server/npm/node_modules,
a SIBLING of programs/server/assets, not an ancestor of it. Node's
require() only walks up parent directories looking for node_modules,
so require('@turf/circle') from the worker asset script could never
find it regardless of what gets imported elsewhere in the app — it
kept failing with "Cannot find module '@turf/circle'" even after that
fix was deployed.

calcUnionAsync.js (Meteor-compiled code, which already knows how to
locate the app's npm deps) now resolves the three turf package paths
itself and hands them to the worker via workerData; the worker
requires them by absolute path instead of bare specifier.
2026-07-30 08:48:50 +02:00
50ca9cc6cf fix(subsUnion): stop treating a failed worker call as a valid empty union
All checks were successful
build-image / build (push) Successful in 12m16s
storeUnion checked `typeof union === 'object'` as its "did this work"
guard, but typeof null is also 'object' — so a worker call that failed
(caught upstream, leaving union as its initial null) looked identical
to a legitimate "zero valid subscriptions" result, and got silently
stored as the string "null" instead of being treated as a failure.
This is exactly what corrupted subs-public-union/subs-private-union on
staging just now, while chasing the missing-turf-deps bug.

storeUnion is now only called once calcUnionAsync has actually
resolved; process() and incrementalAdd() catch failures themselves and
skip storing anything, leaving the previous (still valid) union alone.
2026-07-30 08:29:21 +02:00
4879d3482d fix(subsUnion): force-bundle turf deps used only by the worker thread
Some checks failed
build-image / build (push) Has been cancelled
Meteor's build only includes npm packages it sees imported/required
somewhere in its own traced module graph. private/workers/unionWorker.js
is a plain asset (loaded at runtime via new Worker(), never
import/require'd from Meteor-compiled code), so @turf/circle,
@turf/union and @turf/truncate got silently dropped from
programs/server/node_modules — the worker crashed on every call with
"Cannot find module '@turf/circle'", which incrementalAdd/process
swallowed and stored as a "null" union (typeof null === 'object', so
the null-check that guards against failed unions doesn't catch it).
Side-effect imports here force Meteor to bundle them.
2026-07-30 08:27:40 +02:00
110ad66e86 feat(subsUnion): incremental union merge for new subscriptions
All checks were successful
build-image / build (push) Successful in 13m24s
A full recreate() over 7k+ subscriptions takes ~20 minutes even in a
worker thread — fine for not blocking the site, but means a new zone
takes ages to show up on the map. Since union only grows when adding a
circle, a new subscription can just be merged into the union already
stored (one turf.union call) instead of rebuilding the whole chain.

changed/removed still trigger a full recreate() (union can't be
"subtracted" from safely), and incrementalAdd falls back to a full
recreate whenever the stored count isn't exactly one behind what's
expected, rather than risk merging into stale state. Both paths now
share one serialized queue (FIFO for adds, coalesced for recomputes)
so they never race on the same stored union.
2026-07-30 08:07:00 +02:00
fbb746fba2 fix(subsUnion): move geo-union math to a worker thread, not just yields
All checks were successful
build-image / build (push) Successful in 13m13s
The previous fix (yielding to the event loop between turf.union calls)
was not enough: once the merged polygon gets complex with thousands of
subscriptions, a single turf.union call can itself take seconds, and
yielding between iterations doesn't help when one iteration alone
blocks that long. Confirmed in staging: the site was still fully
unresponsive (Cloudflare 524, curl hanging 2+ minutes, healthcheck
failing) while a recompute ran.

Validation/decoration (addNoisy/noNoisy, cheap) stays on the main
thread; the actual circle+union chain now runs in a worker_thread
(private/workers/unionWorker.js, plain CommonJS so meteor build copies
it verbatim instead of compiling it) so the main event loop serving
DDP/HTTP is never blocked by it, regardless of how slow any single
turf call gets.
2026-07-30 07:48:17 +02:00
3d52e2f709 docs(subsUnion): note why legacy telegram subs are skipped, not errors
All checks were successful
build-image / build (push) Successful in 11m55s
2026-07-30 06:58:09 +02:00
3ac580f246 chore: retrigger build-image CI (forgejo queue reset) 2026-07-30 06:56:25 +02:00
f0be6652e8 chore: retry CI trigger 2026-07-30 06:45:44 +02:00
e18e00f298 chore: retrigger build-image CI (aaron ran out of disk on the previous push) 2026-07-30 06:44:00 +02:00
b4e5511cd0 fix(subsUnion): stop blocking DDP on subscription union recompute
Every subscription add/change/remove recomputed the full geo union
over all 7k+ subscriptions with a synchronous turf.union chain,
freezing the single Node event loop (and thus DDP/HTTP) for minutes.
The same recompute also runs at startup ("Subs union outdated"),
so every restart froze the site too.

calcUnionAsync yields to the event loop periodically during the
union chain, and subsUnion.js now fires recomputes without blocking
Meteor.startup or the observer callbacks, serializing overlapping
triggers instead of stacking them.
2026-07-30 06:36:37 +02:00
23e5f26239 staging: seguir la etiqueta de rama que publica el CI (meteor3-wip)
All checks were successful
build-image / build (push) Successful in 11m34s
El workflow etiqueta con GITHUB_REF_NAME, o sea `meteor3-wip`, pero el compose apuntaba a
`meteor3` (la etiqueta de la subida manual). Se alinean para que un build del CI se
despliegue con un `pull` sin tocar nada.
2026-07-28 15:12:01 +02:00
f3ea93fc6f ci: bajar el codigo con wget+tar; la imagen de kaniko no trae git
All checks were successful
build-image / build (push) Successful in 11m21s
Primera pasada con secretos: el guard paso en verde y reventó el checkout con
"/var/run/act/workflow/1.sh: line 4: git: not found". La imagen
kaniko-project/executor:debug solo trae busybox y los binarios de kaniko: ni git ni apk
para instalarlo (por eso tampoco valen las actions basadas en Node).

Se baja el tarball del commit por el API de Forgejo (wget + tar de busybox) a ./src y el
contexto de kaniko pasa a apuntar ahi.
2026-07-28 14:55:33 +02:00
6aa057f739 staging: la web se descarga del registry, no se compila en groucho
Some checks failed
build-image / build (push) Failing after 8s
Cloudflare ya no esta delante de git.comunes.org, asi que el registry de Forgejo acepta
la capa de 508 MB del bundle (comprobado: subida completa desde groucho, la imagen esta
en comunes/-/packages/container/tcef-web con los tags meteor3 y a24c7a567c).

- compose: `build:` -> `image: git.comunes.org/comunes/tcef-web:${TCEF_WEB_TAG:-meteor3}`.
  groucho pasa de 30 min de `meteor build` (que lo dejaron sin ssh) a un pull de segundos.
- workflow: se activa el disparo por push, y se anade una comprobacion de secretos como
  primer paso. Sin ella, faltar REGISTRY_TOKEN se descubria a los ~30 min, al intentar
  el push; ahora falla en 2 segundos diciendo exactamente que crear y donde.
2026-07-28 13:50:23 +02:00
72975e44ae ci: construir la imagen de la web en el runner, no en el host de despliegue
groucho (5,9 GB, compartidos con Mongo 7 + web + notifications + redis + node-red) se
quedo sin ssh compilando un `meteor build` y hubo que reiniciarlo. El host de despliegue
no debe compilar: debe recibir una imagen.

Workflow para el runner de Forgejo en aaron. Dos decisiones que conviene no deshacer sin
leer fase-9-ci-imagenes.md:

- Kaniko en vez de `docker build`: aaron es host COMPARTIDO (git.comunes.org, Jenkins,
  GlitchTip). `docker build` obligaria a montar /var/run/docker.sock en el job (root
  equivalente sobre aaron) y ademas el limite de memoria del job no serviria de nada,
  porque quien construye es el demonio, fuera del contenedor. Kaniko construye DENTRO
  del job: sin socket y con `--memory` que si acota al que come la RAM.
- `--memory=4g --memory-swap=4g`: swap a cero adrede. Preferimos que muera el build a
  que aaron pagine y se lleve la forja por delante.

De momento solo `workflow_dispatch`. El disparo por push queda comentado hasta que
git.comunes.org salga de detras de Cloudflare (la capa del bundle son 508 MB contra un
tope de 100 MB del plan gratuito: el push se queda en Retrying eterno) y existan los
secretos REGISTRY_USER/REGISTRY_TOKEN.
2026-07-28 12:28:29 +02:00
a24c7a567c fix(fires): "En rojo, 0 fuegos activos" con el mapa lleno de fuegos
La leyenda de /fires decidia por `activefires` pero contaba `activefiresunion`:

  (activefires.length + firealerts.length) === 0 ? "No hay fuegos..." :
     "En rojo, {activefiresunion.length + firealerts.length} fuegos activos"

`activefiresunion` no la escribe nadie. La union de fuegos es un WIP de
fires-csv-mongo-import que nunca llego a produccion (usa thelpers/tsimplify/tbuffer
sin require), y de hecho el <FireListUnion> que la dibujaria solo se renderiza bajo
`Meteor.isDevelopment`. Verificado en la base de datos de PRODUCCION: activefiresunion = 0
documentos, con 3608 en activefires. O sea que el contador lleva anos diciendo 0.

Ahora cuenta `activefires`, que es justo lo que pinta en rojo el FireList `nasa`.

No es una regresion de la modernizacion: produccion tiene el mismo fallo. Si algun dia se
reimplementa la union de fuegos habra que revisar este contador (y sacar FireListUnion de
isDevelopment).
2026-07-28 09:01:05 +02:00
03082a8d55 staging: conecta el importador NASA reimplementado al stack
El servicio `importer` apuntaba a fires-csv-mongo-import/ (el de 2018, sin Dockerfile) y
vivia tras el profile "importer", a la espera de un systemd timer que nunca se creo. Ahora
apunta a todos-contra-el-fuego/nasa-importer/, arranca con el stack y hace su ciclo cada
15 minutos por si mismo.

El JWT de NASA entra por secrets/importer.staging.env (gitignorado); se anade el .example.
Volumen importer-data para los CSV descargados: sin el, cada ciclo se creeria que NASA ha
publicado datos nuevos y reimportaria siempre.
2026-07-28 07:49:05 +02:00
e5f7a0f4be fix(maps): entregar el mapa solo cuando tiene tamano real (MapReady)
MapReady entregaba el mapa en el useEffect de montaje, cuando el contenedor
puede no tener tamano todavia. En FiresMap eso hacia que getBounds() lanzara;
el catch solo avisaba ("Failed to set map bounds and scale") y mapSize nunca
se seteaba, con lo que la suscripcion de fuegos por viewport NI SE CREABA:
loading eterno y tile-pane vacio (0 capas) pese a haber 7470 fuegos.

Evidencia: en Meteor.connection._subscriptions solo aparecian activefirestotal,
activefiresuniontotal, settings y userData — ninguna por localizacion.

Ahora se entrega via map.whenReady() + invalidateSize(), solo cuando getSize()
no es 0, con reintento en el evento resize y entrega unica (flag delivered).

Toca el componente compartido por todos los mapas: al desplegar hay que
re-verificar /fires, /zones, /subscriptions, home y detalle de fuego.
SIN DESPLEGAR todavia (la imagen viva es la de 01:33, sin este cambio).
2026-07-28 07:46:19 +02:00
c50690cc8a fix(maps): evitar carrera MapReady/DDP que dejaba el mapa en gris
En react-leaflet v4 la capa union no se dibujaba (mundo gris a zoom 0) en
/zones, /subscriptions y el bloque Participa cuando los datos DDP llegaban
despues de montar el mapa. Se anade reintento en componentDidUpdate y se
endurece la guarda, con flag de instancia para el ajuste de bounds.
2026-07-28 00:35:11 +02:00
07993ffb91 staging: neutralizar credenciales OAuth vivas, nombres reales y tokens de sesion
La primera version solo invalidaba emails, fireBaseToken, campos telegram* y
services.google.email. Auditando el staging aparecieron, sin neutralizar:
- 42 users con services.google.accessToken/refreshToken/idToken -> credenciales
  OAuth VIVAS que dan acceso a la cuenta de Google REAL del usuario
- 42 users con services.google.name/given_name/family_name/picture
- 99 users con profile.name.first/last (nombre y apellidos reales)
- tokens de sesion (services.resume.loginTokens) y verificationTokens

Pasaron desapercibidos porque el bloque de verificacion tampoco los comprobaba.
Se anaden los $unset y se amplia la verificacion para que falle si reaparecen.

services.password.bcrypt se CONSERVA a proposito (hashes, no reversibles
directamente, y son la unica forma de hacer QA de login: el volcado real no
trae la cuenta de fixtures). Documentado en el propio script.
2026-07-28 00:35:11 +02:00
2e298f9ee5 staging: artefactos de despliegue dockerizado + neutralizacion de datos
- .meteorignore excluye scripts/ del bundle Meteor (mongosh usa global db); docker-compose.staging.yml (mongo7/redis/notif-shadow/mailhog); scripts/neutralize-contacts.mongo.js (contactos + cola de correo); secrets/*.staging.*.example
2026-07-23 10:46:09 +02:00
1ef0012af9 bootstrap: BS5 sweep follow-ups (form-group, drop popper.js@1)
Post-swap hardening after a full BS4-removed-class sweep of the codebase:
- LocationAutocomplete passed `root: 'form-group'` to react-places-autocomplete;
  BS5 removed `.form-group` (it gave the bottom margin) -> use `mb-3`.
- Drop the unused `popper.js@1` dependency (react-bootstrap bundles
  @popperjs/core@2). No source imports it.

Sweep otherwise clean: no other BS4-only utility/component classes remain in JSX
or SCSS. Full-app build boots clean.
2026-07-22 10:12:49 +02:00
bddfb392c1 bootstrap: swap BS4 (alexwine) CSS for bootstrap@5 npm
Completes the Bootstrap 4->5 migration now that every jQuery/BS4 widget is React
(navbar, carousel, dropdowns) — plus the feedback toggle here (Feedback.js:
global `$('#feedback-form').toggle()` -> React state).

- Load Bootstrap 5 CSS from the `bootstrap` npm package in client/index.js
  (imported first so app + component styles and react-bootstrap override it).
- Remove the `alexwine:bootstrap-4` meteor package (BS4 CSS + jQuery + BS4 JS).
  jQuery for jquery-validation still comes from the npm `jquery` dep.
- Utility renames to BS5: ml-auto->ms-auto, float-right->float-end,
  btn-block->w-100, data-toggle->data-bs-toggle (FromNow tooltip).
- forms.scss `.form-label` is no longer a shim (BS5 ships it); comment updated.

Full-app build boots clean; server suite 36 passing. Needs a visual staging pass
across all pages (BS4->5 shifts grid gutters/typography); forms should improve
since react-bootstrap v2 already emitted BS5 markup.
2026-07-22 05:55:34 +02:00
9ff9abacc3 bootstrap: lang/type dropdowns jQuery -> react-bootstrap <Dropdown>
The false-positive-type selector (Fires.js) and the language selector
(Profile.js) used Bootstrap's jQuery dropdown (`data-toggle="dropdown"`), the
last widgets depending on the BS4 jQuery JS that goes away with the CSS swap.
Replace both with react-bootstrap <Dropdown>/<Dropdown.Toggle>/<Dropdown.Item>
(open/close in React, no jQuery). Keeps the `.lang-selector` hook and `.btn-group`
layout; works on the current BS4 CSS.

Full-app build boots clean.
2026-07-22 01:01:59 +02:00
f775d4ac26 bootstrap: home carousel jQuery plugin -> react-bootstrap <Carousel>
Second (last) jQuery/BS4 JS blocker to the BS5 CSS swap. Replaces the
`bootstrap-carousel-swipe` jQuery plugin + `$(...).carousel()` init with
react-bootstrap's <Carousel> (native swipe, no jQuery) for both home carousels.

- Preserves the progressive `.lazy` background mechanism (blur -> full image on
  slide-in): the old `slide.bs.carousel` handler becomes <Carousel onSlide>,
  which marks the incoming index in component state.
- Keeps all per-slide class hooks (carousel-item-N, carousel-snd-item-N).
- CSS ports in Index-custom.scss for react-bootstrap's BS5 markup: indicators
  render as <button> (not <li>), and prev/next labels use .visually-hidden
  (BS4 had .sr-only) -> add a shim so labels stay hidden on current BS4 CSS.
- Drops the now-unused bootstrap-carousel-swipe dependency.

Full-app build boots clean. Needs a visual staging check of the home page.
2026-07-22 00:13:10 +02:00
8dead27d22 bootstrap: navbar collapse jQuery -> React state (BS4->5 prep)
Bootstrap's jQuery collapse (`data-toggle="collapse"` in Navigation.js, plus the
per-NavItem `data-target=".navbar-collapse.show"` auto-close) disappears when we
drop `alexwine:bootstrap-4` for `bootstrap@5`. Replace it with a React `useState`
that toggles `.show`, and close the mobile menu via an onClick on the nav <ul>.

Works on the current BS4 CSS (`.collapse.show` is pure CSS; jQuery only added the
height animation) and removes one of the two jQuery/BS4 JS blockers to the CSS
swap (the home carousel is the other). Also drops the dead sr-only toggler button
that targeted a non-existent id. No CSS changes.
2026-07-21 23:04:02 +02:00
6b6f02d92d quality: web debt batch (tests runner, FireContainer, rate-limit, maps, i18n)
Independent-of-prod quality debt from PENDIENTE.md §2:

- test: migrate broken Jest -> meteortesting:mocha. Tests rewritten to chai +
  Meteor 3 async APIs, moved to test/server/ (server-only). test/server/
  00-setup.test.js re-runs the collection2/accounts init that `meteor test`
  skips (no server/main.js). New comments method + mediaAnalyzers coverage.
  Dropped rest.test.js (removed meteor/http; covered by smoke/). 36 passing.
- fix: scope FireContainer read by the URL _id on the archive route instead of
  a selector-less FiresCollection.findOne() (imports/ui/pages/Fires/Fires.js).
- feat: rate-limit abusable publications via rateLimitSubscriptions (fireFrom*
  and comments.forReference 5/1000ms; geo subs 10/1000ms).
- perf: append loading=async to the Google Maps loader URL (Gkeys.js).
- deps: meteor-accounts-t9n 2.0 -> 2.6 (no gl build -> keep gl->es fallback,
  documented); add 3 missing gl/common.json keys (0 missing now).
- deps: drop jest/babel/enzyme, add chai.
2026-07-21 22:59:06 +02:00
bc778bfd97 deps: react-leaflet 1.8 -> 4.2 (+ leaflet 1.9) — the last legacy react-* lib
Ground-up hooks rewrite of the app's core map. Removes the final batch of
React-19-blocking warnings (legacy context on Map/LayersControl/TileLayer/
Marker/CircleMarker/Circle/Tooltip + ReactDOM.render from the old controls).

Core: <Map> -> <MapContainer>; .leafletElement refs (6 files) -> the map/
layer instances directly, via a <MapReady> child (useMap) + plain refs on
Marker/Circle/GeoJSON; controlled viewport (onViewportChanged + state.center/
zoom) -> <MapEvents> (useMapEvents moveend/zoomend) + imperative setView;
onClick -> eventHandlers={{click}}; Path styling -> pathOptions/style;
subsUnion takes the L.Map directly.

The 4 v1-only plugins reimplemented (new in-repo helpers under Maps/):
- react-leaflet-control -> MapControl (L.Control + createPortal)
- react-leaflet-google  -> GoogleMutantLayer (createLayerComponent +
  leaflet.gridlayer.googlemutant; Google Maps API already loaded by Gkeys)
- react-leaflet-fullscreen -> createControlComponent + leaflet.fullscreen
- leaflet-sleep / leaflet-graphicscale kept as vanilla (work on leaflet 1.9)

Browser-verified: /fires (tiles, OSM+Google layer switch, custom control,
fullscreen, graphic scale, pan/zoom -> re-fetch, no NaN), fire detail
(GeoJSON rect + fitBounds), home (3 maps coexist; SelectionMap draggable
marker -> updatePosition + distance circle). Console now shows 0 React
warnings on home/fires. REST smoke byte-identical.
2026-07-21 18:45:15 +02:00
e61a3a9bcb docs: warning cleanup done — react-leaflet is the sole remaining source
Records the defaultProps/UNSAFE_/Reconnect-Blaze/react-share/progress-bar
cleanups and that, after them, react-leaflet + its 4 plugins are the only
dev-console warnings left (verified on the home page: 16 warnings, all
leaflet/plugins; zero from anything else).
2026-07-21 14:26:20 +02:00
b710388f50 refactor: LoadingBar via progressbar.js + ref (drops react-progress-bar.js findDOMNode)
react-progress-bar.js (unmaintained wrapper) rendered the progress line
through findDOMNode. Reimplemented LoadingBar with progressbar.js directly
(the wrapper's own underlying dep) via a ref + useEffect — same thin line
(strokeWidth 2, #5A7636), no findDOMNode. Promoted progressbar.js to a
direct dependency and removed react-progress-bar.js. Verified progressbar.js
draws its SVG in-browser; findDOMNode gone from /fires and home; REST smoke
byte-identical.
2026-07-21 14:24:32 +02:00
0abf5b4f02 deps: react-share 2 -> 5 (drops its defaultProps warnings)
react-share 2.0 shipped function components with defaultProps (CreatedButton,
Icon) -> React-19-blocking warnings on fire-detail pages. v5 is React-18-safe
and drops them. Removed the GooglePlusShareButton/Icon (Google+ is dead;
removed from react-share in v4+); the other 6 buttons (Facebook, Twitter,
Telegram, WhatsApp, Reddit, Email) are API-compatible. Browser-verified 6
buttons render, no defaultProps warnings. REST smoke byte-identical.
2026-07-21 14:14:48 +02:00
258723ea72 refactor: Reconnect banner Blaze -> native React (drops findDOMNode)
Reconnect rendered the Blaze meteorStatus template via
gadicc:blaze-react-component, whose bridge uses the deprecated findDOMNode
(warned on every page since it's always mounted). Reimplemented natively
with useTracker(Meteor.status) + a countdown effect, same behaviour and
reusing 255kb:meteor-status's .meteor-status CSS. Also dropped the dead
Blaze import from App.js.

Remaining findDOMNode warnings are third-party only: react-progress-bar.js
(LoadingBar) and Status.js's Blaze serverFacts (/status admin page).
Browser-verified home renders, no banner while connected, no new errors.
REST smoke byte-identical.
2026-07-21 14:08:52 +02:00
229b2cb233 cleanup: UNSAFE_componentWillReceiveProps -> modern lifecycles (React 19)
The 4 class components on UNSAFE_componentWillReceiveProps converted:
- FromNow, FireStats: state is a pure mirror of props -> getDerivedStateFromProps
- Fires: split the derived state (getDerivedStateFromProps) from the URL-
  canonicalization side effect (componentDidUpdate, guarded by prevProps) so
  the side effect never runs inside the pure gDSFP
- SelectionMap: marker is also locally draggable, so gDSFP would clobber a
  drag -> componentDidUpdate guarded on a real center/distance value change
  (no clobber, no setState loop), merged with the existing fit()

Browser-verified: /fires count, fire detail + FromNow, active-fire URL
canonicalizes to /fire/archive/:id, home SelectionMap renders stably (no
render loop). No UNSAFE_ warnings remain from our code. REST smoke
byte-identical.
2026-07-21 14:03:44 +02:00
4ddaa0d8ec cleanup: defaultProps -> JS default params on our function components
React 19 drops defaultProps on function components. Converted the 9 of
ours that used it (App, Navigation, ReSendEmail, Reconnect, OAuthLoginButton,
PageHeader, Page, EditDocument, EditSubscription) to destructured default
params; App defaults userId/emailAddress in its withTracker instead (it
spreads {...props} widely). Class components keep defaultProps (still
supported). The only defaultProps warnings left are from the react-share
npm package (CreatedButton/Icon), not our code. REST smoke byte-identical.
2026-07-21 13:54:02 +02:00
5661e129fb fix: NaN in /fires 'active fires in map' count (react-i18next 14 <Trans>)
react-i18next 14 reserves `count` as the pluralization variable and no
longer interpolates it from a <Trans> object child, so <strong>{{count:N}}</strong>
left {{count,number}} unresolved -> the number formatter ran on undefined ->
"NaN". (t() and the countTotal var were unaffected; regression from the
i18next 10->23 / react-i18next 7->14 bump.)

Renamed the variable count -> inMap in FiresMap.js and in the
activeFireInMapCount value of the es/en/gl locales (the string has no plural
forms, so count was only an unlucky name). Browser-verified: /fires now shows
the real count; home/zones/fire-detail render with no real errors. REST smoke
byte-identical.
2026-07-21 13:45:59 +02:00
6e28bf3545 docs: record react-* modernization (helmet/i18n/bootstrap/router) + deferred leaflet & BS5
UPGRADE.md dependency-debt table and PENDIENTE.md updated to reflect the
5 shipped phases and the two deferred sub-projects (react-leaflet 1.8->4
and the Bootstrap 4->5 CSS/JS jump), with rationale for each deferral.
Final state: helmet/i18next/react-bootstrap/react-router warnings all
gone from the console; only react-leaflet (deferred) and our own
defaultProps/findDOMNode remain. REST smoke byte-identical.
2026-07-21 13:05:05 +02:00
e2d13ca64c deps: react-router-dom 4 -> 6 (history 5, react-router-bootstrap 0.26)
Removes the Router/Switch/Route/Link/LinkContainer legacy-context
warnings. Keeps the shared history singleton (used outside React by
NotificationsObserver + Utils/location) via unstable_HistoryRouter, and
bridges v6 hooks back to v4-shaped history/match/location props with a
new withRouterCompat HOC so the ~20 class pages stay untouched.

- <Switch> -> <Routes>, component= -> element=
- Authenticated/Public -> guard components (children or <Navigate replace>)
- LocationListener -> useLocation + useEffect function
- regex route /fire/:type(active|archive|alert)/:id -> /fire/:type/:id
- history.listen callback is ({ location }) in history v5

Browser-verified: SPA nav, /subscriptions->/login auth redirect,
deep-link /fire/archive/:id (params), browser back/forward. REST smoke
byte-identical.
2026-07-21 13:00:15 +02:00