Commit graph

679 commits

Author SHA1 Message Date
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
a1a1b9a801 deps: react-bootstrap 0.31 -> 2 (Bootstrap 5 CSS deferred as debt)
Removes the largest batch of React-19-blocking warnings: the 0.31
components (Grid, FormGroup, ControlLabel, Navbar.Header/Brand, Checkbox,
SafeAnchor) all leaned on legacy context / defaultProps.

29 files converted: Grid->Container, FormGroup/ControlLabel/FormControl/
HelpBlock -> Form.Group/Label/Control/Text, Checkbox -> Form.Check (label
as prop), bsStyle->variant (default->secondary), bsSize->size,
pull-right->float-end. Custom Col.js now re-exports v2's Col; custom
NavItem.js rewritten self-contained (no SafeAnchor/createChainedFunction);
Navigation.js drops react-bootstrap Navbar (raw markup anyway).

CSS stays on Bootstrap 4 (alexwine:bootstrap-4) for now: the BS4->5 jump
is entangled with the jQuery carousel/swipe + navbar-collapse and is
tracked as separate debt in UPGRADE.md. Only .form-label needed a shim
(forms.scss). Browser-verified home carousel+navbar, signup form + terms
checkbox, login form; REST smoke byte-identical.
2026-07-21 12:44:46 +02:00
12bdbe8fed deps: i18next 10 -> 23, react-i18next 7 -> 14
Modernizes the whole i18n stack:
- 48 translate([], {wait:true}) / translate() HOCs -> withTranslation()
- react.wait:true -> react.useSuspense:false
- whitelist -> supportedLngs; added compatibilityJSON: 'v3' so our
  natural-language keys + v3 _plural layout keep working (custom
  separators ss/eth/dj preserved)
- backends: xhr -> i18next-http-backend (client), sync-fs ->
  i18next-fs-backend (server); dropped i18next-localstorage-cache
  (was enabled:false); languagedetector 2 -> 8
- ReSendEmail: <Interpolate> (removed in v10) -> <Trans values={{email}}/>,
  dropped the removed bare t export
- saveMissing handler signature (lngs, ns, key, fallbackValue)

Silences the per-component Translate/I18n legacy-context warnings.
Browser-verified es/en switch and <Trans> interpolation on /fires;
REST smoke byte-identical.
2026-07-18 06:28:41 +02:00
f1eae84e25 deps: react-helmet -> react-helmet-async 2
react-helmet 5 is unmaintained and breaks under React 18 StrictMode.
Same <Helmet> API in the 15 pages; HelmetProvider wraps <App/> in the
client entry. Browser-verified: per-page titles, meta description and
hreflang alternates all inject. REST smoke byte-identical.
2026-07-18 06:14:14 +02:00
f51acf4d78 deps: drop dead npm packages (reactstrap, sidebarv2, hash-link, pure-render-mixin)
None of them is imported anywhere: reactstrap 5.0.0-alpha.3 was never
adopted (react-bootstrap covers the UI), react-leaflet-sidebarv2 and
react-addons-pure-render-mixin were never used, and react-router-hash-link
only survived as a commented-out import in Index.js.
2026-07-18 06:02:49 +02:00
6e9c3a5570 docs: PENDIENTE.md — outstanding work beyond the Meteor 3 migration 2026-07-18 05:47:44 +02:00
a997aa7cae cleanup: silence our own React dev-console warnings
UNSAFE_ rename of our 4 componentWillReceiveProps (FireStats, Fires, FromNow,
SelectionMap) and Authenticated/Public component propType func -> elementType.
Rest of the console noise is legacy react-* dev-mode warnings, absent in prod.
2026-07-18 05:47:44 +02:00
9feaca9d0b fix: fire ids leaked into URLs as ObjectID("...") on Mongo 7
Fire collections use idGeneration:'MONGO', so _id is a Mongo.ObjectID whose
toString() is ObjectID("<hex>"), not the bare hex. Interpolating _id into a
URL produced /fire/archive/ObjectID("c0..."), which no route matches -> the
fire-detail page 404'd. New hexId() helper returns the 24-char hex for an
ObjectID (and passes plain strings through); applied at the three string-context
sites: the map-marker click URL (MarkListeners), the active->archive redirect,
and the comments referenceId (Fires.js). Left falsePositives.insert untouched —
its check() expects a Meteor.Collection.ObjectID, so it takes the object.

Verified in browser: /fire/archive/<hex> and /fire/active/<hex> render the
detail page (map + comments), URL stays clean hex, no ObjectID( anywhere.
2026-07-17 18:31:59 +02:00
c6c22643c1 scss: migrate @import -> @use, lighten/darken -> color.adjust (dart-sass)
Silences the dart-sass deprecation warnings that flooded every build:
- All @import of local partials (colors, mixins, and the CSS-emitting
  partials) -> @use '...' as * (partials are pure defs, so 'as *' keeps the
  global names and the ~25 call sites unchanged). Added explicit @use './colors'
  to bootstrap-overrides.scss and cookies-eu.scss, which used -palette*
  transitively (broke under @use's scoping).
- lighten(c, x%) -> color.adjust(c, $lightness: x%) and darken -> negative,
  with @use 'sass:color' in the 6 affected files.
Compiles clean (byte-for-byte CSS behavior preserved).
2026-07-17 18:18:04 +02:00
62ac2fbd9c sentry: tunnel client events through the app to dodge Cloudflare's 503
GlitchTip is behind Cloudflare, which answers the browser's cross-site ingest
POSTs with 503 (no CORS headers -> 'Failed to fetch'); server-side requests
pass. The browser SDK now posts envelopes same-origin to /sentry-tunnel, and
the server forwards them to GlitchTip:
- imports/startup/server/sentryTunnel.js: WebApp handler that relays the raw
  envelope to <dsn>/api/<project>/envelope/?sentry_key=<key> over IPv4
  (family:4 avoids Happy-Eyeballs picking Cloudflare's unroutable IPv6 on
  IPv4-only hosts). GlitchTip authenticates by the sentry_key query, so the
  key is derived from the DSN and passed explicitly.
- client ravenLogger: tunnel: '/sentry-tunnel'.
Verified: POST /sentry-tunnel -> 200 (envelope reaches GlitchTip).
2026-07-17 18:17:47 +02:00
696789e2b1 meteor3: react-meteor-data 0.2.16 -> 3.0.6 (fixes blank fire-detail pages)
0.2.16 (React-15/16-era withTracker) looped forever on fire-detail pages
under React 18: the withTracker subscription never reached ready() so the
page stayed blank and pegged a CPU core. Publications were already correct
(subscribing standalone from the console readied instantly). 3.0.6 (the
React-18 line for Meteor 3.1) fixes it with zero call-site changes. Also
drops tmeasday:check-npm-versions (the constraint that pinned 0.2.16).

Browser-verified at /fire/archive/<id>: full render (map, NASA/satellite,
FalsePositives widget, Comments box), zero console errors. REST smoke
byte-identical (12/12).
2026-07-17 13:03:54 +02:00
efa1e30bcb docs: record fire-detail page bug (react-meteor-data 0.2.16 vs React 18)
Browser verification of the Comments feature surfaced a pre-existing client
bug: fire-detail pages (/fire/*/:id) stay blank because the page's withTracker
subscription never reaches ready() and re-subscribes in a loop. Isolated it:
the publications ready fine when subscribed standalone, so it's client-side —
react-meteor-data@0.2.16 (React-15/16 era) misbehaving under React 18 for a
component that gates render on subscription.ready(). Fix = upgrade
react-meteor-data to 2.x (useTracker). Documented in UPGRADE.md; not yet done.
2026-07-16 15:57:20 +02:00
0711416300 meteor3: nodemailer 4 -> 6.10
Drop-in for our usage: email.js only does createTransport(MAIL_URL) and reads
transport.options.auth.user (production-only from() branch) — both verified
identical in v6 (createTransport from a URL still populates options.auth.user).
Actual sending goes through ostrio:mailer (MailTime 2.5), v6-compatible. Mail
server inits clean ('I'm the mail server'); REST smoke byte-identical (12/12).
2026-07-16 12:29:10 +02:00