todos-contra-el-fuego-web/UPGRADE.md
vjrj 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

30 KiB
Raw Permalink Blame History

Meteor upgrade — 1.6.1.1 → 2.5 (Mongo 3.2) → 3.1 (Mongo 7, on meteor3-wip)

Incremental, verified upgrade of todos-contra-el-fuego-web. Branch: meteor3-upgrade (base tcef-master). Local commits only — no push until authorized.

⚠️ Hard blocker for the final 3.x jump: MongoDB 3.2

Production runs against the shared replica set rsmain on MongoDB 3.2.11. Meteor 3 ships the modern mongodb Node driver, which requires a server ≥ 4.2/4.4. Meteor 2.x still works against Mongo 3.2.

Therefore this branch lands on Meteor 2.5 — empirically the highest release whose bundled mongodb driver still connects to Mongo 3.2 (2.6 bumps the driver to 4.x and fails; see "Escala ceiling" below) — with everything green. The meteor update --release 3.x step (and even 2.6+) is deliberately not taken until the Comunes infra team upgrades rsmain (shared infra — coordinated in a separate phase, not unilaterally). Server code should still be migrated to async/await before the final 3.x jump so it is mechanical (tracked as debt).

⚠️ Deploy-ordering dependency: notifications cutover

The old push/email notification code was removed from the web (see the notifications commit) because it now lives in the tcef-notifications microservice. Production must not run this build until tcef-notifications is emitting in prod — otherwise users stop receiving notifications. The web deploy and the notifications cutover must be coordinated.

Safety net: REST smoke test

smoke/ is a standalone Node harness that calls every REST endpoint the Flutter app consumes, against a seeded local dev server, and diffs the responses against committed snapshots (smoke/snapshots/). See smoke/README.md.

Run after every escala — it must stay byte-identical:

docker run -d --name tcef-mongo32 -p 27018:27017 mongo:3.2   # once
export PATH="$HOME/.meteor:$PATH" MONGO_URL="mongodb://localhost:27018/fuegos"
meteor --settings settings-development.json --port 3100 &     # dev server
./smoke/smoke.sh                                              # seed + compare

Escalas

Baseline — Meteor 1.6.1.1 (starting point)

  • Boots against Mongo 3.2 with settings-development.json.
  • Dev-enabling changes (do not affect production behavior):
    • IPGeocoder.js: degrade gracefully when the MaxMind GeoLite2 DB is absent (provisioned by cron in prod) instead of crashing at boot.
    • settings-development.json: added dev-only private.internalApiToken (dev-smoke-token) — the REST API only registers its routes when this is set.
  • Notifications code removed (migrated to tcef-notifications): deleted notificationsObserver.js, notificationsProcess.js, the "Process pending notif" SyncedCron job, and the node-gcm dependency. subsUnion.js kept (it feeds subs-public-union, consumed by the app).
  • REST smoke baseline captured here. green.

Escala 1 — 1.6.1.1 → 1.8.3 green

meteor update --release 1.8.3
meteor npm install --save @babel/runtime@^7.26.0

Core packages bumped (highlights): meteor 1.8.6→1.9.3, ecmascript 0.10.7→0.13.2, modules 0.11.6→0.14.0, mongo 1.4.7→1.7.0, npm-mongo 2.2.34→3.2.0 (mongodb driver 3.x — still fine against Mongo 3.2 server), webapp 1.5.0→1.7.5, standard-minifier-js →2.5.2. underscore auto-added (1.7 dropped it from meteor-base), fetch + modern-browsers added.

Breaking change — Babel beta → stable. Boot failed with Cannot find module '@babel/runtime/helpers/objectSpread2': the pinned @babel/runtime@7.0.0-beta.44 predates helpers that 1.8's ecmascript emits. Fixed by upgrading to stable 7.x (@babel/runtime@^7.29.7). Note: @latest resolves to 8.x, which is too new for Meteor's 7-style helper layout — pin to ^7.

REST smoke test: byte-identical to baseline.

Escala 2 — 1.8.3 → 1.11 (version resolution only)

meteor update --release 1.11

Resolution reached 1.11 (ecmascript 0.14.3, mongo 1.10.0, npm-mongo 3.8.0 — still Mongo-3.2-compatible, accounts-* 1.x, email 2.0). But this release surfaced the two big blockers below. The app was ultimately driven to 2.3 (see next section); 1.11 was a transit point, not a landing.

Escala 3 — → Meteor 2.3 green (builds + runs against Mongo 3.2, smoke byte-identical)

.meteor/release = METEOR@2.3. npm-mongo 3.9.0 (mongodb driver 3.x) — the last driver line that still speaks to a Mongo 3.2 server. Core bumps: accounts-* 2.0, ecmascript 0.15.2, mongo 1.12.0, HMR added.

This escala was dominated by dead Atmosphere packages (the plan's #1 risk). Root causes and fixes, in the order they were hit:

  1. arkham:comments-ui — no Meteor 2.x build. The vendored local package used Npm.depends, and rebuilding a local package's npm deps crashes meteor-tool ≥1.11 on this host (node_contextify.cc … Assertion args[1]->IsString() → SIGABRT; reproducible with any Npm.depends, even is-number). Switching to the Atmosphere isopack avoided the local rebuild but the package is abandoned: its newest 2.x-solvable version (0.2.15) pins accounts-password@1.0.1, conflicting with Meteor 2.0's accounts-password 2.0. Decision (user-approved): reimplement comments as a small React feature (imports/api/Comments/*, imports/ui/components/Comments/CommentsBox.js), dropping the package entirely. Fire-page comments only (the sole usage), with likes/dislikes, image/YouTube embed, owner edit/remove, and the "email other commenters" side-effect preserved. Comment text is now rendered as safe plain text (+ media embed) instead of markdown-to-HTML — a small scope reduction and a security improvement (no raw-HTML injection of user content). The old local fork is preserved in its nested git repo (commit 4761da9).

  2. coffeescript@1.0.17 build plugin — crashes the build. The same node_contextify assertion fired at build start on this host whenever the coffeescript compiler plugin loaded. It was pulled transitively by the dead test stack (meteortesting:mocha, practicalmeteor:chai, xolvio:cleaner) and by the old auto-downgraded nimble:restivus@0.6.6 (which needs iron:router). Removing those removed coffeescript and unblocked the build. A trivial meteor create app builds fine on the same tool, confirming the crash is package-specific, not a broken tool.

  3. nimble:restivus — the REST API package — pins accounts-password@1.3.3 (incompatible with 2.x) and is CoffeeScript (needs the crashing plugin). Vendored as packages/nimble-restivus: the .coffee sources were precompiled to plain JS (lib/restivus-all.js, single translation unit so the Auth/Route/Restivus classes share scope; Restivus left un-var'd so Meteor's api.export picks it up), and package.js loosens accounts-password to 2.x and drops the coffeescript dependency. This also let restivus resolve to 0.8.12 (json-routes based), which removed the whole iron:router stack. REST behavior unchanged (smoke byte-identical).

  4. maximum:server-transform (unused) removal broke Meteor.publishTransformed in FalsePositives/server/publications.js; no transform was actually configured, so replaced with plain Meteor.publish.

  5. fourseven:scss 4.5.4 → 4.14.1node-sass@4.5.3 won't build on node 12 (Meteor ≥1.9); 4.14.1 uses a node-12-compatible node-sass with prebuilt binaries. (Meteor 1.8 built it on node 8.) 4.15.0 needs ecmascript ≥ 0.15.1 → too new for 1.11.

  6. less, markdown removed — no .less/.md source files; their build plugins were dead weight.

⚠️ Local build-host caveat

Meteor-tool's bundled node (12.x/14.x) SIGABRTs (node_contextify assertion) on this machine (kernel 6.12) when certain old build plugins load — this is an environment interaction, not a code defect (the deploy host built these fine historically). It was fully worked around by removing the dead build plugins above. If a future escala hits it again, build in a container matching the deploy target (Debian) rather than on this host.

Escalas 2.4 & 2.5 — green

meteor update --release 2.4, then --release 2.5. Both trivial; npm-mongo stays at 3.9.1 (mongodb driver 3.x). REST smoke byte-identical against mongo:3.2 on both.

Escala ceiling vs Mongo 3.2 — landing on Meteor 2.5 (empirically verified)

Meteor bumps the bundled mongodb Node driver to 4.3.1 at Meteor 2.6, which requires server wire version ≥ 6 (MongoDB ≥ 3.6). Production's rsmain is Mongo 3.2 (wire version 4). Verified directly by booting 2.6 against a real mongo:3.2:

MongoCompatibilityError: Server at localhost:27018 reports maximum wire
version 4, but this version of the Node.js Driver requires at least 6
(MongoDB 3.6)
=> Exited with code: 1

So Meteor 2.5 is the highest release that runs against the production Mongo 3.2 (npm-mongo 3.9.1). That is where this branch lands: everything green, REST smoke byte-identical. Going past 2.5 (to 2.16 or 3.x) is gated on upgrading the shared rsmain replica set to Mongo ≥ 4.4 first — the same DB blocker as 3.x.

Meteor 3.1 + MongoDB 7 — REST smoke GREEN (branch meteor3-wip)

The 3.x jump works end-to-end for the REST/Flutter contract. On branch meteor3-wip, the web runs on Meteor 3.1 (Node 22, async/await, no Fibers) against a dockerized MongoDB 7 (npm-mongo 6.10 / mongodb driver 6), and the REST smoke test is byte-identical to the 1.6.1.1 baseline (all 12 endpoints). This is the "mongo superior" decision realized: give fuegos its own Mongo 7 instead of the shared rsmain 3.2 — which is what unblocks Meteor 3's driver.

meteor3-upgrade stays at the deployable Meteor 2.5 (Mongo 3.2). The 3.x work lives on meteor3-wip until (a) production Mongo is migrated to 7 and (b) the web-UI async migration below is finished.

Reproduce the 3.1 dev stack

docker run -d --name tcef-mongo7 -p 27019:27019 mongo:7 --replSet rs0 --port 27019 --bind_ip_all
docker exec tcef-mongo7 mongosh --port 27019 --quiet --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"localhost:27019"}]})'
# (migrations run from scratch on an empty DB — all up() bodies are async now,
#  no need to pre-mark db.migrations at version 18)
export MONGO_URL="mongodb://localhost:27019/fuegos?replicaSet=rs0"
export NODE_OPTIONS="--dns-result-order=ipv4first --no-network-family-autoselection"  # reach warehouse.meteor.com on Node 22
meteor --settings settings-development.json --port 3100
MONGO_CONTAINER=tcef-mongo7 MONGO_SHELL=mongosh MONGO_PORT=27019 ./smoke/smoke.sh

Key build/runtime unblocks (Meteor 3.1)

  • underscore linker crash (Runtime is not available … underscore): resolved underscore@1.6.2 had a runtime-less web.browser unibuild → pin underscore@1.6.4 (diagnosed by instrumenting the tool's linker.js).
  • collection2 v4 is fully lazy with no main module → import its eager entry meteor/aldeed:collection2/static.js from server|client/00-collection2-init.js so attachSchema/autoValues are patched before any collection loads.
  • Fibers gone: removed fibers.js; server sync Mongo → *Async throughout (Rest.js + helpers + methods + startup: oauth/subsUnion/migrations/email/facts).
  • vendored restivus patched to await async endpoint handlers.
  • $near + countAsync(): driver 6 runs count via aggregation where $near is illegal → derive total from fetchAsync().length.
  • json-routes 3.0 matches routes in registration order → reordered the mobile/subscriptions/all/... route before .../:subsId in Rest.js.
  • fixtures.js / sitemaps.js disabled (sync-only @cleverbeagle/seeder / pre-0.9 gadicohen:sitemaps) — not on the REST path. Debt.

Web-UI async migration (beyond the REST contract)

  • Fires publications → async (fireFromId, fireFromAlertId, fireFromActiveId, fireFromHash): findOnefindOneAsync, count()countAsync(), firesUnion awaited, handlers async so the try/catch actually catches rejections. Verified over raw DDP (all subs ready, docs flow; fire + falsePositives docs delivered for the seeded fire). falsePositivesMyloc / industriesMyloc / comments.forReference needed no change — they return bare find() cursors (still sync-safe on Meteor 3). subscriptions.* methods were already *Async.
  • Note: the module-level new Counter(...) (natestrauser:publish-performant-counts) behind falsePositivesTotal boots fine; the publication itself is unused by the current UI — watch it if re-enabled.
  • Comments methods → async (comments.insert/edit/remove/like/dislike): findOne/insert/update/remove*Async, helpers (requireOwner, toggle) async, users.findOneAsync for the username. onCommentAdd mail fan-out: users.find().forEachforEachAsync. Still fire-and-forget from the insert. Comments UI staging verification pending (pre-existing note below).
  • fixtures.js re-enabled@cleverbeagle/seeder (sync Mongo, no Meteor 3 support) removed from package.json and replaced with a small in-repo async seeder: idempotent, dev-only (staging opts in with TCEF_SEED=1); same accounts as before (admin@admin.com / 5 test users / 5 Documents each). Verified: fixtures: 6 dev users ensured on boot.
  • sitemaps.js re-enabledgadicohen:sitemaps (pre-0.9 API, dead on Meteor 3) removed from .meteor/packages; /sitemap.xml is now a small WebApp.connectHandlers handler serving the same static page list (the per-fire section of the old handler was behind firesMapEnabled = false, i.e. dead code, and was dropped). Verified serving on dev.
  • Dead client packages purged — the web UI now RENDERS on Meteor 3. First-ever browser verification of this branch (the smoke only covers REST) found a chain of ancient Blaze-era packages whose client code threw at bundle load, killing the whole app (meteorInstall is not defined):
    • alanning:roles 1.2.10 (client crash, and its server Roles.userIsInRole is sync) → package removed; the app barely used it: facts.js now checks the plain user.roles field (admin set cached at startup) and App.js takes roles from the user doc (prop was unused). Upgrading to roles v4 was rejected: needs a prod data migration.
    • selaias:cookie-consent (its Cookies global is gone) → removed; replaced by in-repo React imports/ui/components/CookieConsent reusing the same i18n keys; CookieConsent.init dropped from i18n.js.
    • natestrauser:publish-performant-counts server Counter._publishCursor uses sync cursor.count()vendored as packages/publish-performant-counts (tcef:publish-performant-counts) with countAsync and an async-aware interval; same public API.
    • Meteor.autorun (removed in Meteor 3) → dropped (App.js debug) / Tracker.autorun (FiresMap).
    • Map/server publications not exercised by the REST smoke converted to async: activefiresmyloc, activefiresunionmyloc, fireAlerts (countAsync/fetchAsync/awaited firesUnion), and oauth.verifyConfiguration (findOneAsync). migrations.js: all 18 historical up() bodies ported to async (fetchAsync+for..of, *Async writes, createIndexAsync, Accounts.createUserAsync; percolate:migrations 2.0.1 awaits async up()). Verified: empty Mongo 7 DB migrates 0→18 with no errors (indexes + industry registries created); pre-marking version=18 is no longer needed. Verified in a real browser: / and /fires render (map, search, layers, cookie banner), zero uncaught exceptions; REST smoke byte-identical.
  • React 16 → 18.3react/react-dom bumped to ^18.3.1 (--legacy-peer-deps: the ancient react-* libs below declare 15/16 peers), client entry migrated to createRoot (imports/startup/client/index.js). The legacy UI libs (react-bootstrap 0.31, reactstrap 5-alpha, react-leaflet 1.8, react-router-dom 4, react-i18next 7) stay pinned and WORK on 18, but spam the console with legacy-context/defaultProps deprecation warnings and will die on React 19. Debt: own front-end refresh project. ⚠️ npm gotcha: running meteor npm install/uninstall while the dev server watches node_modules can crash the meteor-tool watcher mid-prune (ENOENT on a watched file). Stop the server (or restart it after) when touching deps.

Still TODO before the web (not just REST) is fully deployable on 3.x

  • React 16 → 18 render root + ancient react-* libs modernized: react-helmet→ react-helmet-async, i18next 10→23 + react-i18next 7→14, react-bootstrap 0.31→2, react-router-dom 4→6. Dead deps (reactstrap, react-leaflet-sidebarv2, react-router-hash-link, react-addons-pure-render-mixin) dropped. All browser-verified, REST smoke byte-identical. Remaining front debt: react-leaflet 1.8→4 (deferred — core map, 4 v1-only plugins) and the Bootstrap 4→5 CSS/JS jump (deferred — jQuery carousel/navbar). Both are what's left before a React 19 jump.
  • Re-enabled dev fixtures / sitemaps / email default template with async ports.
  • SCSS @import@use, lighten/darkencolor.adjust (dart-sass deprecations).
  • Production cutover still needs: Mongo data migrated 3.2→7 (mongodump/restore), the notifications service emitting in prod, and the Docker deployment (fase 3).

Dependency debt (tracked, to resolve in the noted escala)

Dep From Target Status
React / react-dom 16.0 18 done — 18.3.1 + createRoot. The peripheral react-* libs that only worked on 18 via legacy context are now all modernized (rows below) except react-leaflet (deferred). Our own React-19 blockers are also cleared: defaultProps on function components → default params; UNSAFE_componentWillReceivePropsgetDerivedStateFromProps/componentDidUpdate; the Blaze/findDOMNode Reconnect → native React; react-share 2→5 and react-progress-bar.js → progressbar.js (both dropped their findDOMNode/defaultProps). react-leaflet 1.8→4.2 done too (row below). Dev console is now 0 React warnings on the main routes. The only React-19 leftover is the Blaze serverFacts on the /status admin page (findDOMNode via gadicc:blaze-react-component).
react-share 2.0 5.3 done — v2 shipped function-component defaultProps (React-19 blocker). v5 is clean. Dropped the dead GooglePlus button (removed upstream in v4+); the other 6 share buttons are API-compatible.
react-progress-bar.js 0.2.3 — (progressbar.js 1.1) removed — the wrapper used the deprecated findDOMNode. LoadingBar now drives progressbar.js (its own underlying dep, promoted to direct) through a ref.
gadicc:blaze-react-component (in Reconnect) native React Reconnect's Blaze meteorStatus bridge used findDOMNode and was always mounted → rewritten with useTracker(Meteor.status) + countdown effect, reusing 255kb:meteor-status's CSS. (Status.js's Blaze serverFacts kept — /status admin page only.)
Server async/await Fibers *Async APIs done — REST, publications, methods, startup and migrations all on *Async.
bcrypt 1.0.3 (broken binding) 5.x done — 1.0.3 didn't load on Node 22 (accounts-password silently fell back to pure JS) and wouldn't compile in the Docker build; 5.1.1 ships Node-22 prebuilds.
raven / flowkey:raven 2.4 @sentry/node + @sentry/browser 8.x done — flowkey:raven (Sentry legacy SDK, dead on 3.x, spammed the boot log with 502s against the dead sentry.comunes.org) replaced by the modern SDKs behind the same ravenLogger.log() facade (6 call sites unchanged). Backend: GlitchTip self-hosted on aaron (Sentry-compatible DSN/API; Sentry proper needs 16 GB RAM, didn't fit), deployed via the Comunes ansible (glitchtip.yml, org "comunes" / project "tcef-web"), fronted at https://sentry.comunes.org (nginx on assangeaaron:8000). DSN set in settings-development.json; end-to-end verified (a test exception sent from the dev server showed up in GlitchTip within seconds).
nodemailer 4 6.10 done — drop-in for our usage: email.js only calls nodemailer.createTransport(MAIL_URL) and reads transport.options.auth.user (in the production-only from() branch); both verified unchanged in v6. Sending goes through ostrio:mailer (MailTime 2.5), which is v6-compatible.
Babel 7 beta 7 stable done (escala 1)
react-meteor-data 0.2.16 3.0.6 done — 0.2.16 (React-15/16-era) looped withTracker on fire-detail pages under React 18 (never-ready → blank). meteor add react-meteor-data@3.0.6; no call-site changes (HOC API kept). Also dropped tmeasday:check-npm-versions (the constraint pinning 0.2.16). See section above.
i18next 10.5 23.16 done — with react-i18next 14. whitelistsupportedLngs, added compatibilityJSON: 'v3' (our locale JSON uses natural-language keys + v3 _plural layout, not the v4 _one/_other suffixes), custom separators ß/ð/đ kept. sendMissingsaveMissing, missingKeyHandler signature is (lngs, ns, key, fallbackValue) now (array first). See react-i18next row.
react-i18next 7.4 14.1 done — 48 translate([], {wait:true})/translate() HOCs → withTranslation(); react.wait:truereact.useSuspense:false. <Trans> unchanged (locale JSON already stores the indexed-tag format <1><0>{{x}}</0></1>). ReSendEmail: removed <Interpolate> (deleted in v10) → <Trans i18nKey values={{email}}/>, dropped the removed bare t export. This is what silenced the per-component Translate/I18n legacy-context console spam. Backends: xhr→i18next-http-backend (client), sync-fs→i18next-fs-backend (server); i18next-localstorage-cache dropped (was enabled:false); languagedetector 2→8. Browser-verified: es/en switch, <Trans> interpolation ({{countTotal}}+<strong>) renders on /fires.
react-leaflet + leaflet 1.8 / 1.3.1 4.2.1 / 1.9.4 done — the last and hardest lib; a ground-up hooks rewrite of the app's core map. <Map><MapContainer>; .leafletElement refs (6 files/13 sites) → the map/layer instances directly (via a <MapReady> child using useMap, and plain refs on Marker/Circle/GeoJSON); the controlled-viewport pattern (onViewportChanged+state.center/zoom) → <MapEvents> (useMapEvents moveend/zoomend) + imperative setView; onClickeventHandlers={{click}}, path styling → pathOptions/style; subsUnion takes the L.Map directly. The 4 v1-only plugins were reimplemented with react-leaflet v4's factories / vanilla Leaflet plugins: react-leaflet-control→ in-repo MapControl (L.Control + createPortal), react-leaflet-googleGoogleMutantLayer (createLayerComponent + leaflet.gridlayer.googlemutant; Google Maps API already loaded by Gkeys), react-leaflet-fullscreencreateControlComponent + 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 pipeline, no NaN), fire detail (GeoJSON rect + fitBounds), home (all 3 maps coexist; SelectionMap draggable marker→updatePosition + distance circle). Console now 0 warnings across home//fires (forced full re-render). REST smoke byte-identical.
arkham:comments-ui 1.4.x replaced with in-repo React feature
nimble:restivus Atmosphere vendored local precompiled package, accounts-password 2.x
maximum:server-transform, meteorhacks:zones, less, markdown, test stack removed (dead/unused)
fourseven:scss 4.5.4 4.14.1 node-12 compatible
node-gcm 1.0.2 removed (dead API, moved to microservice)
react-router-dom 4.2.2 6.30 done — with history 4→5 and react-router-bootstrap 0.24→0.26. Kills the Router/Switch/Route/Link/LinkContainer legacy-context warnings. Strategy: keep the shared history singleton (used outside React by NotificationsObserver and Utils/location) via unstable_HistoryRouter, and DON'T rewrite the ~20 class pages — a new withRouterCompat HOC bridges v6 hooks back to v4-shaped history/match/location props. <Switch><Routes>, component=element=, Authenticated/Public become guard components rendering children or <Navigate replace>, LocationListener is now a useLocation+useEffect function. The regex route /fire/:type(active|archive|alert)/:id/fire/:type/:id (type validated in the component; the only behavior delta is an invalid two-segment type now hits Fires instead of 404 — negligible). history.listen callback is ({location}) in v5. Browser-verified: SPA nav, /subscriptions/login auth redirect, deep-link /fire/archive/:id (params flow), browser back/forward; REST smoke byte-identical. Gotcha hit & fixed: the rewritten guard files need import React for their <Navigate> JSX.
react-helmet 5.2.0 react-helmet-async 2.0.5 done — react-helmet is unmaintained and misbehaves under React 18 StrictMode. Drop-in: same <Helmet> children API across the 15 pages; <HelmetProvider> wraps <App/> in imports/startup/client/index.js. Verified in browser: per-page titles, meta description and hreflang alternates injected (data-rh). Note: head updates are rAF-deferred (like react-helmet), so hidden/background tabs don't flush — irrelevant for real users.
react-bootstrap 0.31.5 2.10 done — kills the biggest batch of React-19-blocking warnings (Grid/FormGroup/ControlLabel/Navbar.Header/Navbar.Brand/Checkbox/SafeAnchor all used legacy context/defaultProps). 29 files: GridContainer, FormGroup/ControlLabel/FormControl/HelpBlockForm.Group/Label/Control/Text, CheckboxForm.Check (label moves to a prop), bsStylevariant (×23, "default""secondary"), bsSizesize (×2), pull-rightfloat-end (×7). Custom Col.js (used v0.31 bootstrapUtils/StyleConfig internals) now re-exports v2's Col; custom NavItem.js rewritten self-contained (dropped SafeAnchor+createChainedFunction); Navigation.js drops the react-bootstrap Navbar (was hand-rolled raw markup anyway). CSS kept at Bootstrap 4 (see next row) — the only BS5-only class v2 emits that BS4 lacks is .form-label, shimmed in forms.scss. Browser-verified: home carousel+navbar, signup form + terms Form.Check (toggles submit), login form; REST smoke byte-identical.
Bootstrap CSS/JS 4.1 (alexwine:bootstrap-4) 5.x ⏸️ deferred as debt. The app's CSS+JS still comes from the Atmosphere alexwine:bootstrap-4 (Bootstrap 4 + jQuery). The BS4→5 jump is a separate invasive sub-project: the home carousel uses the jQuery bootstrap-carousel-swipe plugin (BS4-only), the navbar collapse and ~17 data-toggle/data-slide attributes across 6 files are BS4 jQuery behaviors, and BS5 is vanilla-JS with data-bs-*. react-bootstrap v2 runs fine on BS4 CSS for every component we use (Button/Alert/Row/Col/Table/ButtonGroup/Modal render identical classes; only .form-label needed a shim). Do this jump on its own: npm bootstrap@5 SCSS+JS, rewrite carousel (BS5 has native swipe → drop the plugin), data-toggledata-bs-toggle, ml-automs-auto, etc.
reactstrap 5.0.0-alpha.3 removed — zero imports in the codebase (react-bootstrap covers all UI).
react-leaflet-sidebarv2 0.5.1 removed — never imported.
react-router-hash-link 1.1.1 removed — only a commented-out import in Index.js.
react-addons-pure-render-mixin 15.6.2 removed — never imported (React-15 era leftover).
chimp 0.51.1 removed — dead e2e test runner (Selenium 2 / cucumber), sole remaining puller of native fibers@1.x, which cannot compile on Node 22 and broke the clean Docker build. Not imported anywhere.

Comments React feature — verification note

The server builds and the REST smoke test is green, and the client bundle compiles, but the comments UI itself is not exercised by the smoke test (it's not part of the Flutter REST contract). It needs manual staging verification: on a fire archive page — logged-in post/edit/remove, like/dislike, and image/YouTube embed.

Fixed: fire-detail pages now render (react-meteor-data 0.2.16 → 3.0.6)

Browser-verifying the Comments feature surfaced a pre-existing client bug: the fire-detail pages (/fire/{active,archive,alert}/:id, component imports/ui/pages/Fires/Fires.js) stayed blank — the page's withTracker subscription (fireFromActiveId / fireFromId) never reached ready(), so loading stayed true and nothing rendered, pegging a CPU core in a re-subscribe loop.

Diagnosis (isolated, not guessed):

  • The publications were fine. Subscribing to fireFromActiveId / fireFromId standalone from the browser console readied immediately and delivered the doc — not a server/publication bug.
  • The bug was react-meteor-data@0.2.16 — a React-15/16-era version (componentWillMount + legacy context, the source of the deprecation warnings flooding the console) that misbehaves under React 18 for a component gating its whole render on subscription.ready().

Fix: meteor add react-meteor-data@3.0.6 (the React-18-compatible line for Meteor 3.1; 4.x targets Meteor 3.2+). No call-site changes were needed — 3.x keeps the withTracker HOC API, and all 19 call sites work unchanged. The old constraint that pinned 0.2.16 was tmeasday:check-npm-versions, pulled in by 0.2.16 itself; the upgrade dropped it, which also silenced the "npm peer requirements not installed (react@15-16)" boot/console warning.

Verified in a real browser at /fire/archive/c0000000000000000000000c: the page renders fully (title, Leaflet + NASA/satellite map, the "not a wildfire" FalsePositives widget, and the Comments box — all withTracker-driven), zero console errors. REST smoke byte-identical (12/12).