diff --git a/.meteor/packages b/.meteor/packages index b651f13..8f1d390 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -30,6 +30,7 @@ audit-argument-checks@1.0.8 ddp-rate-limiter@1.2.2 dynamic-import@0.7.4 static-html@1.4.0 +alexwine:bootstrap-4 gadicc:blaze-react-component # Add braze to react 255kb:meteor-status # Connect status @@ -44,15 +45,9 @@ ostrio:meteor-root aldeed:schema-deny dburles:collection-helpers reywood:publish-composite -# Bootstrap CSS/JS now comes from the `bootstrap` npm package (BS5), imported in -# imports/startup/client/index.js. The old alexwine:bootstrap-4 package (BS4 CSS + -# jQuery + BS4 JS) was removed after the navbar/carousel/dropdown/feedback widgets -# were migrated off jQuery. facts-base@1.0.2 nspangler:autoreconnect quave:synced-cron # lmachens:kadira nimble:restivus@0.8.12 underscore@1.6.4 - -meteortesting:mocha # test driver (see `npm test` / `npm run test-watch`) diff --git a/.meteor/versions b/.meteor/versions index a70d819..76a1110 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -8,6 +8,7 @@ accounts-password@3.0.3 aldeed:collection2@4.2.0 aldeed:schema-deny@4.0.2 aldeed:simple-schema@1.13.1 +alexwine:bootstrap-4@4.1.0 allow-deny@2.0.0 audit-argument-checks@1.0.8 autoupdate@2.0.0 @@ -59,9 +60,6 @@ logging@1.3.5 mdg:geolocation@1.3.0 meteor@2.0.2 meteor-base@1.5.2 -meteortesting:browser-tests@1.8.0 -meteortesting:mocha@3.3.0 -meteortesting:mocha-core@8.2.0 minifier-css@2.0.0 minifier-js@3.0.1 minimongo@2.0.2 diff --git a/.meteorignore b/.meteorignore index eeec0b9..c013f6f 100644 --- a/.meteorignore +++ b/.meteorignore @@ -3,5 +3,3 @@ cucumber output.json # REST smoke-test harness runs standalone (Node), never as Meteor server code smoke -# Ops/deploy scripts (p.ej. scripts/*.mongo.js usan el global `db` de mongosh, no de Meteor) -scripts diff --git a/PENDIENTE.md b/PENDIENTE.md index 144e6a7..86ca6a5 100644 --- a/PENDIENTE.md +++ b/PENDIENTE.md @@ -18,7 +18,7 @@ desarrollo (no en producción) y **bloquean el salto a React 19**. Estado: | react-bootstrap | 0.31 → 2 | ✅ hecho (CSS sigue en Bootstrap 4) | | reactstrap + 3 deps muertas | — | ✅ eliminadas (0 usos) | | react-router-dom | 4.2 → 6.30 | ✅ hecho (history 5, HOC `withRouterCompat`) | -| react-leaflet + leaflet | 1.8/1.3 → 4.2/1.9 | ✅ hecho (Map→MapContainer, refs directas, useMap/useMapEvents; 4 plugins reimplementados: MapControl portal, GoogleMutantLayer, fullscreen, sleep/graphicscale vanilla; ver UPGRADE.md) | +| **react-leaflet + leaflet** | 1.8/1.3 → 4/1.9 | ⏸️ **diferida** (mapa = feature central; 4 plugins v1-only sin equivalente v4; ver UPGRADE.md) | | **Bootstrap CSS/JS** | 4.1 → 5 | ⏸️ **diferida** (carrusel/navbar jQuery BS4; ver UPGRADE.md) | Lo verificado en navegador tras cada lib: `/`, `/fires`, `/fire/archive/`, @@ -28,64 +28,40 @@ parámetros por defecto; `UNSAFE_componentWillReceiveProps` (FromNow, FireStats, Fires, SelectionMap) → `getDerivedStateFromProps`/`componentDidUpdate`; Reconnect (banner Blaze `meteorStatus` con `findDOMNode`) → React nativo con `useTracker(Meteor.status)`; react-share 2→5 y react-progress-bar.js → progressbar.js -(ambos tiraban `findDOMNode`/`defaultProps`); y react-leaflet 1.8→4.2 (mapa central, -4 plugins reimplementados). **Tras esto la consola tiene 0 warnings de React en las -rutas principales** (verificado forzando re-render en / y /fires). Único resto para -React 19: el `` de Status.js (`findDOMNode`), solo en `/status` (admin). +(ambos tiraban `findDOMNode`/`defaultProps`). **Tras esto, la ÚNICA fuente de +warnings en consola es react-leaflet + sus 4 plugins (diferida).** (Queda un +`findDOMNode` menor solo en `/status`: el `` de Status.js, página admin.) --- ## 2. Deuda funcional / de calidad de la web (independiente de prod) -- **Comentarios (feature React nueva): verificación manual en staging.** El smoke - no llega a la UI. Los **métodos** (insert/edit/remove/like/dislike) y el embed - (`mediaAnalyzers`) ya están cubiertos por mocha (`test/server/comments.test.js`), - pero quedan por probar a mano en una página de fuego: - - publicar / editar / borrar un comentario (UI React) - - like / dislike (toggle) - - render del embed de imagen y de YouTube - - email a otros comentaristas (`onCommentAdd.js`, entrega real de correo) -- ✅ **`FireContainer` endurecido** (`imports/ui/pages/Fires/Fires.js`): el - `findOne()` sin selector ahora se acota por el `_id` de la URL en la ruta - `archive` (`new Meteor.Collection.ObjectID(id)`); en active/alert/hash cada - publicación deja un único fire en minimongo, así que el read vacío es correcto. -- ✅ **Suite de tests con runner (`meteortesting:mocha`).** `npm test` corre - `meteor test … --driver-package meteortesting:mocha` (watch: `npm run test-watch`). - Tests reescritos a `chai` + APIs async de Meteor 3 en `test/server/*.test.js` - (server-only), incluyendo cobertura nueva de los métodos de comentarios. Jest y - `rest.test.js` (ya cubierto por `smoke/`) eliminados. **36 passing.** Pendiente - menor: portar los casos de token inválido (401/400) de `rest.test.js` al smoke. -- ✅ **Rate-limit de publications** (`imports/startup/server/api.js`): reglas - `type: 'subscription'` vía `rateLimitSubscriptions` — 5/1000ms en `fireFrom*` y - `comments.forReference`; 10/1000ms en las subs geo (map pan/zoom). -- ✅ **Google Maps con `loading=async`** (`imports/startup/client/Gkeys.js`): - se envuelve `GoogleMapsLoader.createUrl` para añadir el parámetro (el paquete - `google-maps` npm no expone hook). *Verificar en staging que el mapa carga y el - aviso de consola desaparece.* -- ✅ **`meteor-accounts-t9n` actualizado** a `^2.6.0` (es/en OK). El paquete sigue - sin build gallego (`gl`), así que se mantiene el fallback gl→es documentado en - `i18n.js`. Traducción gallega de la app (`gl/common.json`): 0 claves faltantes. -- ✅ **Salto Bootstrap 4→5 (CSS/JS) hecho** — react-bootstrap v2 ahora corre sobre - su CSS nativo (BS5). **Compila y arranca; falta SOLO verificación visual en - staging** (BS4→5 cambia sutilezas de grid/gutters/tipografía en todas las páginas). - - Widgets jQuery/BS4 migrados a React (los blockers): navbar collapse - (`Navigation.js`/`NavItem.js`), carrusel del home (`Index.js` → ``, - conserva `.lazy` vía `onSlide`), dropdowns de idioma/tipo (`Profile.js`/ - `Fires.js` → ``), y toggle del feedback (`Feedback.js` → estado React). - Deps `bootstrap-carousel-swipe` y `alexwine:bootstrap-4` eliminadas; jQuery - global ya no se usa (solo `jquery`+`jquery-validation` vía npm en `validate.js`). - - CSS: `bootstrap@5` npm importado en `imports/startup/client/index.js` (antes - que `app.scss`, para que los overrides ganen). Renombres de utilidades: - `ml-auto`→`ms-auto`, `float-right`→`float-end`, `btn-block`→`w-100`, - `data-toggle`→`data-bs-toggle`, `sr-only`→`.visually-hidden` (shim). - - **QA visual pendiente en staging (todas las páginas):** formularios (react- - bootstrap ya emite markup BS5 → deberían mejorar), navbar, botones, modales, - cards, grid/gutters, y los widgets migrados (menú móvil, carrusel, dropdowns, - feedback). Alertas de `themeteorchef:bert` (comprobar que siguen bien sin el - jQuery de alexwine). - - Menor: `popper.js@1` en `package.json` es legacy sin uso (react-bootstrap trae - `@popperjs/core@2`) → se puede quitar. El bloque `.form-label` de `forms.scss` - ya es redundante (BS5 lo trae) salvo por el `display:block` explícito. +- **Comentarios (feature React nueva): verificación manual en staging.** No la + cubre el smoke. Probar en una página de fuego: publicar/editar/borrar, + like/dislike, embed de imagen y YouTube, y el email a otros comentaristas. +- **`FireContainer` usa `FiresCollection.findOne()` sin selector** + (`imports/ui/pages/Fires/Fires.js`): puede devolver un doc de una suscripción + previa. Endurecer filtrando por el `_id` de la URL. (Preexistente, no bloquea, + pero conviene antes de prod.) +- **Suite de tests sin runner.** El stack de test (meteortesting:mocha, chai…) + se eliminó en la migración 2.x (arrastraba coffeescript que crasheaba el build). + Quedan tests Jest en `test/` (`rest.test.js`, `email.test.js`, …) y + `npm test → jest`, pero hay que **verificar/actualizar** que pasan sobre el + código async de Meteor 3. Hoy la ÚNICA red automática es `smoke/`. +- **Rate-limit de publications** pendiente (`imports/startup/server/api.js:18`). +- **Google Maps** se carga sin `loading=async` (aviso de rendimiento); el loader + `google-maps` npm es viejo → valorar el loader async moderno. +- **i18n de cuentas (`meteor-accounts-t9n`, T9n):** paquete antiguo aún usado + (Login, VerifyEmail, i18n). Verificar que funciona en Meteor 3; traducción + gallega (gl) incompleta (varios TODO en `i18n.js`). +- **Salto Bootstrap 4→5 (CSS/JS) pendiente:** react-bootstrap ya está en v2 y + corre sobre el CSS Bootstrap 4 actual (`alexwine:bootstrap-4`) con un único + shim `.form-label` en `forms.scss`. El salto a Bootstrap 5 npm está diferido + porque el carrusel del home (`bootstrap-carousel-swipe`, jQuery/BS4) y el + collapse del navbar dependen del JS jQuery de BS4 (`data-toggle`, `data-slide`). + Al hacerlo: importar `bootstrap@5` SCSS+JS, reescribir el carrusel (BS5 trae + swipe nativo → quitar el plugin), `data-toggle`→`data-bs-toggle`, + `ml-auto`→`ms-auto`, revisar `new-age.scss`/`custom.scss`/`bootstrap-overrides.scss`. - **Limpieza menor:** migración 217 `// TODO remove falsepositives lowercase collection`; `prerender.js` gating comentado; sección per-fire del sitemap eliminada (era código muerto tras `firesMapEnabled=false`). diff --git a/UPGRADE.md b/UPGRADE.md index f71fdbb..baa7e63 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -308,7 +308,7 @@ MONGO_CONTAINER=tcef-mongo7 MONGO_SHELL=mongosh MONGO_PORT=27019 ./smoke/smoke.s | 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_componentWillReceiveProps` → `getDerivedStateFromProps`/`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 / 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_componentWillReceiveProps` → `getDerivedStateFromProps`/`componentDidUpdate`; the Blaze/findDOMNode `Reconnect` → native React; react-share 2→5 and react-progress-bar.js → progressbar.js (both dropped their findDOMNode/defaultProps). **Sole remaining console warnings are react-leaflet + its 4 plugins** — nothing else. | | 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.) | @@ -320,7 +320,7 @@ MONGO_CONTAINER=tcef-mongo7 MONGO_SHELL=mongosh MONGO_PORT=27019 ./smoke/smoke.s | 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. `whitelist`→`supportedLngs`, added `compatibilityJSON: 'v3'` (our locale JSON uses natural-language keys + v3 `_plural` layout, not the v4 `_one`/`_other` suffixes), custom separators `ß`/`ð`/`đ` kept. `sendMissing`→`saveMissing`, 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:true`→`react.useSuspense:false`. `` unchanged (locale JSON already stores the indexed-tag format `<1><0>{{x}}`). ReSendEmail: removed `` (deleted in v10) → ``, 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, `` interpolation (`{{countTotal}}`+``) 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. ``→``; `.leafletElement` refs (6 files/13 sites) → the map/layer instances directly (via a `` child using `useMap`, and plain refs on Marker/Circle/GeoJSON); the controlled-viewport pattern (`onViewportChanged`+`state.center/zoom`) → `` (`useMapEvents` moveend/zoomend) + imperative `setView`; `onClick`→`eventHandlers={{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-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 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. | +| react-leaflet + leaflet | 1.8 / 1.3.1 | 4.x / 1.9 | ⏸️ **deferred as debt** (the one lib not modernized this pass). react-leaflet v4 is a ground-up hooks rewrite of the app's core feature (the fire map), and the migration is deeply entangled: (a) **4 unmaintained v1-only plugins with no v4 equivalent** — `react-leaflet-control` (custom map buttons, 3 maps), `react-leaflet-fullscreen`, `react-leaflet-google` (`GoogleLayer` base layers in DefMapLayers), `leaflet-sleep` (`sleep`/`wakeTime`… props on ``) — each needs a bespoke `createControlComponent`/`createLayerComponent`/googlemutant reimplementation; (b) the **controlled-viewport** pattern (`onViewportChanged` + `state.center/zoom` in FiresMap/SelectionMap) is gone in v4 (`MapContainer` center/zoom are initial-only; you drive it via a `useMap()` child); (c) `.leafletElement` refs in 6 files/13 sites (FiresMap, SelectionMap, SubscriptionsMap, Fires, SubsUnion) — v4 hands you the `L.Map`/layer directly with different timing; (d) `Leaflet.control.graphicScale().addTo(map)` + `subsUnion` reach into the raw map via the ref. **v1.8 works on React 18** (verified: all maps render), so this only blocks the eventual React 19 jump. Do it as its own focused project with heavy in-browser verification (drag markers, viewport-driven fire reload, fitBounds, layer switch, fullscreen, sleep, custom controls). Stays pinned at 1.8 until then. **After the rest of the front-end modernization + warning cleanup, react-leaflet + these 4 plugins are the SOLE remaining source of dev-console warnings** (legacy context on Map/LayersControl/TileLayer/Marker/CircleMarker/Circle/Tooltip and the plugins, plus `ReactDOM.render` from react-leaflet-control). | | 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) | diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml deleted file mode 100644 index c19ae46..0000000 --- a/docker-compose.staging.yml +++ /dev/null @@ -1,163 +0,0 @@ -# Stack de STAGING de "Todos contra el fuego" para testfuegos.comunes.org. -# Derivado de docker-compose.yml (validación local). Diferencias clave: -# - web sirve en https://testfuegos.comunes.org (tras el proxy assange), puerto host 8004 -# - Mongo 7 AISLADO, sembrado con un volcado real de `fuegos` y contactos neutralizados -# (NUNCA apunta al rsmain de producción) -# - notificaciones en MODE=shadow / MATCHER_MODE=shadow: calcula y loguea, NO envía -# - mailhog captura TODO el correo (nada sale a un SMTP real) -# - node-red solo conversacional; si se prueba el bot, con un BOT DE PRUEBAS, nunca los reales -# -# Despliegue real: vía el role ansible roles/tcef_staging/ en el repo de Comunes -# (~/proyectos/sync/comunes/shared-l2/ansible/). Este compose es la fuente que ese role monta. -# Secretos: ./secrets/*.staging.* (gitignored). Uso: RUNBOOK.md. - -name: tcef-staging - -x-logging: &default-logging - driver: json-file - options: - max-size: "10m" - max-file: "3" - -services: - mongo: - image: mongo:7 - container_name: tcef-staging-mongo - command: ["--replSet", "rs0", "--bind_ip_all"] - volumes: - - mongo-data:/data/db - healthcheck: - test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] - interval: 10s - timeout: 5s - retries: 12 - start_period: 20s - restart: unless-stopped - logging: *default-logging - - # One-shot: inicia el replica set de un nodo y marca las migraciones a v18 - # (los datos reales llegan pre-migrados). Idéntico al compose de validación. - mongo-init: - image: mongo:7 - depends_on: - mongo: - condition: service_healthy - restart: "no" - entrypoint: - - bash - - -ec - - | - mongosh --host mongo --quiet --eval 'try { rs.status().ok } catch (e) { rs.initiate({_id: "rs0", members: [{_id: 0, host: "mongo:27017"}]}) }' - for i in $$(seq 1 30); do - state=$$(mongosh --host mongo --quiet --eval 'try { rs.isMaster().ismaster } catch (e) { false }') - [ "$$state" = "true" ] && break - sleep 1 - done - mongosh --host mongo --quiet fuegos --eval 'db.migrations.replaceOne({_id: "control"}, {_id: "control", version: 18, locked: false}, {upsert: true})' - echo "mongo-init: replica set + migrations OK" - logging: *default-logging - - web: - build: - context: . - dockerfile: Dockerfile - image: tcef-web:meteor3 - container_name: tcef-staging-web - depends_on: - mongo-init: - condition: service_completed_successfully - environment: - PORT: "3000" - ROOT_URL: https://testfuegos.comunes.org - MONGO_URL: mongodb://mongo:27017/fuegos?replicaSet=rs0 - METEOR_SETTINGS_FILE: /run/secrets/meteor-settings.json - volumes: - - ./secrets/meteor-settings.staging.json:/run/secrets/meteor-settings.json:ro - # Solo el proxy (assange) llega a este puerto; se publica en la red interna del host. - ports: - - "8004:3000" - restart: unless-stopped - logging: *default-logging - - redis: - image: redis:7-alpine - container_name: tcef-staging-redis - command: ["redis-server", "--appendonly", "yes"] - volumes: - - redis-data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 6 - restart: unless-stopped - logging: *default-logging - - notifications: - build: - context: ../tcef-notifications - image: tcef-notifications:local - container_name: tcef-staging-notifications - depends_on: - mongo-init: - condition: service_completed_successfully - redis: - condition: service_healthy - # secrets/notifications.staging.env → MODE=shadow, MATCHER_MODE=shadow, sin FCM, - # MAIL_URL=smtp://mailhog:1025. No envía nada a usuarios reales. - env_file: - - ./secrets/notifications.staging.env - restart: unless-stopped - logging: *default-logging - - # Buzón SMTP de captura: TODO el correo (verificación/reset/notifs) aterriza aquí, nunca fuera. - # UI web en 127.0.0.1:8025 (acceder por túnel SSH). SMTP interno en mailhog:1025. - mailhog: - image: mailhog/mailhog:latest - container_name: tcef-staging-mailhog - environment: - MH_STORAGE: memory - ports: - - "127.0.0.1:8025:8025" - restart: unless-stopped - logging: *default-logging - - node-red: - image: nodered/node-red:4.0 - container_name: tcef-staging-node-red - volumes: - - nodered-data:/data - ports: - - "127.0.0.1:1880:1880" - healthcheck: - test: ["CMD", "wget", "-qO", "/dev/null", "http://127.0.0.1:1880/"] - interval: 30s - timeout: 5s - retries: 5 - start_period: 30s - restart: unless-stopped - logging: *default-logging - - # Importador NASA FIRMS → escribe activefires en ESTE mongo (no en prod), alimentando el - # matcher en shadow. No arranca con `up` (profile "importer"): lo dispara un systemd timer - # del host cada 15 min con `docker compose run --rm importer`. - # NOTA: requiere crear su Dockerfile en fires-csv-mongo-import/ (fase-3 §2). Hasta entonces, - # este servicio queda declarado pero su build context debe completarse. - importer: - profiles: ["importer"] - build: - context: ../todos-contra-el-fuego/fires-csv-mongo-import - image: tcef-nasa-importer:local - container_name: tcef-staging-importer - depends_on: - mongo-init: - condition: service_completed_successfully - environment: - MONGO_URL: mongodb://mongo:27017/fuegos?replicaSet=rs0 - restart: "no" - logging: *default-logging - -volumes: - mongo-data: - redis-data: - nodered-data: diff --git a/imports/modules/rate-limit.js b/imports/modules/rate-limit.js index dd88ce3..d98096f 100644 --- a/imports/modules/rate-limit.js +++ b/imports/modules/rate-limit.js @@ -9,15 +9,3 @@ export default ({ methods, limit, timeRange }) => { }, limit, timeRange); } }; - -// Same idea for subscriptions. DDPRateLimiter matches subscribe calls only when -// the rule pins `type: 'subscription'`, so this is a separate helper. -export const rateLimitSubscriptions = ({ subscriptions, limit, timeRange }) => { - if (Meteor.isServer) { - DDPRateLimiter.addRule({ - type: 'subscription', - name(name) { return subscriptions.indexOf(name) > -1; }, - connectionId() { return true; }, - }, limit, timeRange); - } -}; diff --git a/imports/startup/client/Gkeys.js b/imports/startup/client/Gkeys.js index 7a3848e..0a1f83e 100644 --- a/imports/startup/client/Gkeys.js +++ b/imports/startup/client/Gkeys.js @@ -13,11 +13,6 @@ class GkeysC { // console.log(google.maps); GoogleMapsLoader.KEY = key; GoogleMapsLoader.LIBRARIES = ['places']; - // The google-maps npm package (v3.2.1) builds the script URL without - // `loading=async`, which Google now warns about. It exposes no hook for - // extra params, so wrap createUrl to append it (no node_modules edit). - const origCreateUrl = GoogleMapsLoader.createUrl; - GoogleMapsLoader.createUrl = () => `${origCreateUrl()}&loading=async`; GoogleMapsLoader.load(() => { self.gmapkey.set(key); console.log('GMaps script just loaded'); diff --git a/imports/startup/client/i18n.js b/imports/startup/client/i18n.js index 1ddf4ed..b4c5202 100644 --- a/imports/startup/client/i18n.js +++ b/imports/startup/client/i18n.js @@ -51,8 +51,6 @@ if (sendMissing && Meteor.isDevelopment) { } function setT9(lang) { - // meteor-accounts-t9n has no Galician build, so use Spanish for gl account - // error messages (the closest shipped language). See common/i18n.js. if (lang === 'gl') { T9n.setLanguage('es'); } else { diff --git a/imports/startup/client/index.js b/imports/startup/client/index.js index 4b474be..f5dd1e2 100644 --- a/imports/startup/client/index.js +++ b/imports/startup/client/index.js @@ -1,8 +1,4 @@ /* global */ -// Bootstrap 5 CSS (npm) replaces the old `alexwine:bootstrap-4` meteor package. -// Import it first so the app + component stylesheets below (and react-bootstrap, -// which targets BS5) override it. -import 'bootstrap/dist/css/bootstrap.css'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { HelmetProvider } from 'react-helmet-async'; diff --git a/imports/startup/common/i18n.js b/imports/startup/common/i18n.js index e9bb761..a0615dc 100644 --- a/imports/startup/common/i18n.js +++ b/imports/startup/common/i18n.js @@ -3,9 +3,7 @@ import moment from 'moment'; // Load the js langs import es from 'meteor-accounts-t9n/build/es'; import en from 'meteor-accounts-t9n/build/en'; -// meteor-accounts-t9n (2.6.0) ships no Galician build (build/gl.js), so account -// error messages fall back to Spanish for gl — see setT9() in client/i18n.js. -// Re-enable this import if the upstream package ever adds a gl translation. +// TODO ask for translation of this // import gl from 'meteor-accounts-t9n/build/gl'; const backOpts = { diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index 88b68b6..a6044b4 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -15,6 +15,8 @@ import '../../api/FireAlerts/server/publications'; import '../../api/Subscriptions/methods'; import '../../api/Subscriptions/server/publications'; +// TODO add rate-limit to these publications + import '../../api/Notifications/methods'; import '../../api/Notifications/server/publications'; @@ -26,26 +28,3 @@ import '../../api/SiteSettings/server/publications'; import '../../api/FalsePositives/methods'; import '../../api/FalsePositives/server/publications'; - -import { rateLimitSubscriptions } from '../../modules/rate-limit'; - -// Rate-limit the abusable publications (the old TODO above). Single-fire -// lookups and the comments feed are cheap but brute-forceable, so keep them -// tight; the geo subs fire on every map pan/zoom, so give them more headroom. -rateLimitSubscriptions({ - subscriptions: [ - 'fireFromHash', 'fireFromAlertId', 'fireFromActiveId', 'fireFromId', - 'comments.forReference' - ], - limit: 5, - timeRange: 1000 -}); - -rateLimitSubscriptions({ - subscriptions: [ - 'activefiresmyloc', 'activefiresunionmyloc', 'fireAlerts', - 'falsePositivesMyloc', 'industriesMyloc' - ], - limit: 10, - timeRange: 1000 -}); diff --git a/imports/ui/components/Feedback/Feedback.js b/imports/ui/components/Feedback/Feedback.js index d4da8e2..8e47f71 100644 --- a/imports/ui/components/Feedback/Feedback.js +++ b/imports/ui/components/Feedback/Feedback.js @@ -18,7 +18,6 @@ import './Feedback.scss'; class Feedback extends Component { constructor(props) { super(props); - this.state = { open: false }; this.handleSubmit = this.handleSubmit.bind(this); this.onTabClick = this.onTabClick.bind(this); } @@ -51,9 +50,7 @@ class Feedback extends Component { } onTabClick() { - // Was `$('#feedback-form').toggle('slide')` — global jQuery from the BS4 - // meteor package. React state instead, so nothing depends on that global. - this.setState(s => ({ open: !s.open })); + $('#feedback-form').toggle('slide'); } handleSubmit() { @@ -79,7 +76,7 @@ class Feedback extends Component {
{ !this.props.isHome &&
-
(this.formdiv = formdiv)} style={{ display: this.state.open ? 'block' : 'none' }} className="card"> +
(this.formdiv = formdiv)} style={{ display: 'none' }} className="card">
(this.form = form)} className="form card-body" @@ -112,7 +109,7 @@ class Feedback extends Component { /> - diff --git a/imports/ui/components/FromNow/FromNow.js b/imports/ui/components/FromNow/FromNow.js index dd385e2..68afa23 100644 --- a/imports/ui/components/FromNow/FromNow.js +++ b/imports/ui/components/FromNow/FromNow.js @@ -33,7 +33,7 @@ class FromNow extends Component { render() { return ( - {this.state.when} + {this.state.when} ); } } diff --git a/imports/ui/components/LocationAutocomplete/LocationAutocomplete.js b/imports/ui/components/LocationAutocomplete/LocationAutocomplete.js index be596d2..edc4f8e 100644 --- a/imports/ui/components/LocationAutocomplete/LocationAutocomplete.js +++ b/imports/ui/components/LocationAutocomplete/LocationAutocomplete.js @@ -88,7 +88,7 @@ class LocationAutocomplete extends React.Component { styles={myStyles} autocompleteItem={AutocompleteItem} classNames={{ - root: 'mb-3', // was BS4 `.form-group` (removed in BS5); mb-3 keeps the bottom margin + root: 'form-group', input: 'form-control', autocompleteContainer: 'autocomplete-container' }} diff --git a/imports/ui/components/Maps/DefMapLayers.js b/imports/ui/components/Maps/DefMapLayers.js index 022f298..0355394 100644 --- a/imports/ui/components/Maps/DefMapLayers.js +++ b/imports/ui/components/Maps/DefMapLayers.js @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { withTranslation } from 'react-i18next'; -import GoogleMutantLayer from '/imports/ui/components/Maps/GoogleMutantLayer'; +import { GoogleLayer } from 'react-leaflet-google/lib/'; import Gkeys from '/imports/startup/client/Gkeys'; import { TileLayer, LayersControl } from 'react-leaflet'; @@ -54,15 +54,15 @@ class DefMapLayers extends Component { {/* React.Fragment does not work here */} { this.state.gkey && - + } { this.state.gkey && - + } { this.state.gkey && - + } ); diff --git a/imports/ui/components/Maps/FireCircleMark.js b/imports/ui/components/Maps/FireCircleMark.js index 7d0d6e0..80b9826 100644 --- a/imports/ui/components/Maps/FireCircleMark.js +++ b/imports/ui/components/Maps/FireCircleMark.js @@ -31,7 +31,7 @@ class FireCircleMark extends Component { } = this.props; const rect = rectangleAround({ lat, lon }, track, track); return ( - + ); diff --git a/imports/ui/components/Maps/FireIconMark.js b/imports/ui/components/Maps/FireIconMark.js index 034c1fe..d63b1e7 100644 --- a/imports/ui/components/Maps/FireIconMark.js +++ b/imports/ui/components/Maps/FireIconMark.js @@ -33,9 +33,9 @@ class FireIconMark extends Component { lat, lon, scan, track, nasa, id, history, falsePositives, industries, neighbour, when, t } = this.props; return ( - +
{ !falsePositives && !industries && - + } { industries && @@ -43,20 +43,23 @@ class FireIconMark extends Component { } { /* disabled */ industries && false && } { falsePositives && !industries && - + {t('Es una industria (fuente: nuestros usuarios/as)')} { /* disabled because was a past fire (and can be marked multiple times) */ false && } } { !falsePositives && !industries && - + } - +
); } } diff --git a/imports/ui/components/Maps/FirePixel.js b/imports/ui/components/Maps/FirePixel.js index d09213a..da78185 100644 --- a/imports/ui/components/Maps/FirePixel.js +++ b/imports/ui/components/Maps/FirePixel.js @@ -12,8 +12,11 @@ const FirePixel = ({ }) => ( diff --git a/imports/ui/components/Maps/FirePolygonMark.js b/imports/ui/components/Maps/FirePolygonMark.js index 1b76bde..92e3640 100644 --- a/imports/ui/components/Maps/FirePolygonMark.js +++ b/imports/ui/components/Maps/FirePolygonMark.js @@ -29,7 +29,7 @@ class FirePolygonMark extends Component { const lon = centerid.coordinates[0]; const lat = centerid.coordinates[1]; return ( - + ); /* */ } diff --git a/imports/ui/components/Maps/FullScreenMap.js b/imports/ui/components/Maps/FullScreenMap.js index 543fd2c..8738d19 100644 --- a/imports/ui/components/Maps/FullScreenMap.js +++ b/imports/ui/components/Maps/FullScreenMap.js @@ -1,27 +1,25 @@ /* eslint-disable react/jsx-indent-props */ /* eslint-disable import/no-absolute-path */ +/* eslint-disable import/no-absolute-path */ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { withTranslation } from 'react-i18next'; -import { createControlComponent } from '@react-leaflet/core'; -import L from 'leaflet'; -import 'leaflet.fullscreen'; -import 'leaflet.fullscreen/Control.FullScreen.css'; +import FullscreenControl from 'react-leaflet-fullscreen'; +import 'react-leaflet-fullscreen/dist/styles.css'; -// Replacement for react-leaflet-fullscreen (v1-only): wrap the vanilla -// leaflet.fullscreen control with react-leaflet v4's control factory. -const FullscreenControl = createControlComponent( - ({ position = 'topleft', title, titleCancel }) => L.control.fullscreen({ position, title, titleCancel }) -); - -const FullScreenMap = ({ t }) => ( - -); +class FullScreenMap extends Component { + render() { + const { t } = this.props; + return ( + + ); + } +} FullScreenMap.propTypes = { t: PropTypes.func.isRequired diff --git a/imports/ui/components/Maps/GoogleMutantLayer.js b/imports/ui/components/Maps/GoogleMutantLayer.js deleted file mode 100644 index d2b07e7..0000000 --- a/imports/ui/components/Maps/GoogleMutantLayer.js +++ /dev/null @@ -1,25 +0,0 @@ -import { createLayerComponent } from '@react-leaflet/core'; -import L from 'leaflet'; -import 'leaflet.gridlayer.googlemutant'; - -// Replacement for react-leaflet-google's GoogleLayer (v1-only). The Google Maps -// JS API is already loaded (with the places library) by Gkeys before any Google -// BaseLayer renders, so googlemutant can use window.google.maps directly. -function createGoogleMutant({ - maptype = 'roadmap', opacity, googlekey, ...options -}, ctx) { - const instance = L.gridLayer.googleMutant({ - type: String(maptype).toLowerCase(), - ...(opacity != null ? { opacity } : {}), - ...options - }); - return { instance, context: { ...ctx } }; -} - -function updateGoogleMutant(instance, props, prevProps) { - if (props.opacity != null && props.opacity !== prevProps.opacity) { - instance.setOpacity(props.opacity); - } -} - -export default createLayerComponent(createGoogleMutant, updateGoogleMutant); diff --git a/imports/ui/components/Maps/MapBridge.js b/imports/ui/components/Maps/MapBridge.js deleted file mode 100644 index ea44ea2..0000000 --- a/imports/ui/components/Maps/MapBridge.js +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from 'react'; -import { useMap, useMapEvents } from 'react-leaflet'; - -// v4 replaces the v1 `ref`/`.leafletElement` + handleLeafletLoad pattern: this -// child runs inside and hands the ready Leaflet map to a class -// parent once, so the parent can drive it imperatively (setView, fitBounds, -// graphicScale, subsUnion, …). -export const MapReady = ({ onReady }) => { - const map = useMap(); - useEffect(() => { - if (onReady) onReady(map); - // run once per map instance - }, [map]); // eslint-disable-line react-hooks/exhaustive-deps - return null; -}; - -// Bridges Leaflet map events to callbacks (replaces the v1 onMoveend / -// onViewportChanged / onZoomend props on ). -export const MapEvents = ({ handlers }) => { - useMapEvents(handlers || {}); - return null; -}; diff --git a/imports/ui/components/Maps/MapControl.js b/imports/ui/components/Maps/MapControl.js deleted file mode 100644 index ce69571..0000000 --- a/imports/ui/components/Maps/MapControl.js +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { useMap } from 'react-leaflet'; -import L from 'leaflet'; - -// Replacement for react-leaflet-control (v1-only): mounts a Leaflet control at -// the given corner and portals arbitrary React children into it. Click/scroll -// on the control no longer pans/zooms the map underneath. -const MapControl = ({ position = 'topright', children }) => { - const map = useMap(); - const [container, setContainer] = useState(null); - - useEffect(() => { - const ReactControl = L.Control.extend({ - onAdd: () => { - const div = L.DomUtil.create('div', 'leaflet-control leaflet-control-react'); - L.DomEvent.disableClickPropagation(div); - L.DomEvent.disableScrollPropagation(div); - setContainer(div); - return div; - }, - onRemove: () => setContainer(null) - }); - const control = new ReactControl({ position }); - control.addTo(map); - return () => control.remove(); - }, [map, position]); - - return container ? createPortal(children, container) : null; -}; - -export default MapControl; diff --git a/imports/ui/components/Maps/SubsUnion/SubsUnion.js b/imports/ui/components/Maps/SubsUnion/SubsUnion.js index c2f4216..41a4d81 100644 --- a/imports/ui/components/Maps/SubsUnion/SubsUnion.js +++ b/imports/ui/components/Maps/SubsUnion/SubsUnion.js @@ -12,8 +12,7 @@ const subsUnion = (union, options) => { const interactive = options.interactive || false; if (options.subs) { - // v4: options.map is the Leaflet map instance directly (no .leafletElement) - const lmap = options.map; + const lmap = options.map.leafletElement; if (union) { lmap.removeLayer(union); } @@ -31,7 +30,7 @@ const subsUnion = (union, options) => { if (options.fit && options.bounds) { // console.log(options.bounds); const bounds = JSON.parse(options.bounds); - lmap.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); + options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); } } else if (options.subs.length > 0) { const result = calcUnion(L, options.subs, sub => sub, true); @@ -44,7 +43,7 @@ const subsUnion = (union, options) => { }); union.addTo(lmap); if (options.fit) { - lmap.fitBounds(bounds); + options.map.leafletElement.fitBounds(bounds); } } } diff --git a/imports/ui/components/NavItem/NavItem.js b/imports/ui/components/NavItem/NavItem.js index 9467868..a3bbad0 100644 --- a/imports/ui/components/NavItem/NavItem.js +++ b/imports/ui/components/NavItem/NavItem.js @@ -4,9 +4,7 @@ import PropTypes from 'prop-types'; // Self-contained NavItem. The old one wrapped react-bootstrap 0.31's // `SafeAnchor` + `createChainedFunction` (both gone in v2); the markup this -// navbar needs is just an
  • . The mobile menu is now collapsed from React -// state in Navigation (the parent
  • + +); Navigation.propTypes = { t: PropTypes.func.isRequired, diff --git a/imports/ui/components/OAuthLoginButton/OAuthLoginButton.js b/imports/ui/components/OAuthLoginButton/OAuthLoginButton.js index 1b74779..81abfb2 100644 --- a/imports/ui/components/OAuthLoginButton/OAuthLoginButton.js +++ b/imports/ui/components/OAuthLoginButton/OAuthLoginButton.js @@ -45,7 +45,7 @@ const defaultCallback = (error) => { const OAuthLoginButton = ({ service, callback = defaultCallback }) => (
    - - +
    + +
    {Object.keys(FalsePositiveTypes).map(key => ( - this.onTypeSelect(key)}> - {FalsePositiveTypes[key]} - + )) } - - +
    +
    } @@ -285,22 +292,7 @@ const FireContainer = withTracker(({ match }) => { // console.log(`Type of '${fireType}' fire, active: ${active}, archive: ${archive}, fromHash: ${fromHash}`); // console.log(`Subs ready: ${subscription.ready()}, fire: ${JSON.stringify(FiresCollection.findOne())}`); const loading = !subscription.ready(); - // Scope the client-side read to the fire named in the URL. Only the archive - // route's param is a Fires `_id` (as 24-char hex, per hexId()); active/alert - // params are ActiveFire/Alert ids and hash is an encrypted blob, so their - // publications each put exactly one Fires doc in minimongo and an empty - // selector is the correct read. Scoping archive by _id stops a lingering doc - // from a previously-viewed fire being returned by a selector-less findOne(). - let selector = {}; - if (archive && id) { - try { - selector = new Meteor.Collection.ObjectID(id); - } catch (e) { - selector = {}; - } - } - const fire = FiresCollection.findOne(selector); - const notfound = !loading && !fire; + const notfound = !loading && FiresCollection.find().count() === 0; /* console.log(`loading fire: ${loading}`); * console.log(`Not found fire: ${notfound}`); */ const falsePositives = FalsePositivesCollection.find().fetch().map(falsePositivesRemap); @@ -312,9 +304,9 @@ const FireContainer = withTracker(({ match }) => { fromHash, falsePositives, industries, - fire, + fire: FiresCollection.findOne(), notfound, - when: fire ? fire.when : null + when: subscription.ready() && FiresCollection.findOne() ? FiresCollection.findOne().when : null }; })(Fire); diff --git a/imports/ui/pages/FiresMap/FiresMap.js b/imports/ui/pages/FiresMap/FiresMap.js index 04d9130..ab5acd7 100644 --- a/imports/ui/pages/FiresMap/FiresMap.js +++ b/imports/ui/pages/FiresMap/FiresMap.js @@ -11,9 +11,8 @@ import { withTracker } from 'meteor/react-meteor-data'; import { Tracker } from 'meteor/tracker'; import { Helmet } from 'react-helmet-async'; import { Trans, withTranslation } from 'react-i18next'; -import { MapContainer } from 'react-leaflet'; -import MapControl from '/imports/ui/components/Maps/MapControl'; -import { MapReady, MapEvents } from '/imports/ui/components/Maps/MapBridge'; +import { Map } from 'react-leaflet'; +import Control from 'react-leaflet-control'; import LoadingBar from '/imports/ui/components/Loading/LoadingBar'; import _ from 'lodash'; import store from 'store'; @@ -77,24 +76,6 @@ class FiresMap extends React.Component { this.onViewportChanged = this.onViewportChanged.bind(this); this.onMoveEnd = this.onMoveEnd.bind(this); this.onMoveStart = this.onMoveStart.bind(this); - this.handleMoveEnd = this.handleMoveEnd.bind(this); - this.handleMapReady = this.handleMapReady.bind(this); - } - - // v4: combined moveend/zoomend handler fed by — clears the - // moving flag and reports the new viewport (was the v1 onMoveend + - // onViewportChanged props on ). - handleMoveEnd() { - this.onMoveEnd(); - if (!this.map) return; - const c = this.map.getCenter(); - this.onViewportChanged({ center: [c.lat, c.lng], zoom: this.map.getZoom() }); - } - - // v4: MapReady hands us the ready Leaflet map (replaces ref/.leafletElement) - handleMapReady(map) { - this.map = map; - this.handleLeafletLoad(map); } componentDidMount() { @@ -129,7 +110,7 @@ class FiresMap extends React.Component { } getMap() { - return this.map; + return this.fireMap.leafletElement; } setShowSubsUnion(showSubsUnion) { @@ -143,7 +124,7 @@ class FiresMap extends React.Component { handleViewportChange(viewport) { console.log(`Viewport changed: ${JSON.stringify(viewport)}`); - if (this.map) { + if (this.fireMap) { const bounds = this.getMap().getBounds(); // console.log(bounds); mapSize.set([bounds.getNorthEast(), bounds.getSouthWest()]); @@ -162,8 +143,6 @@ class FiresMap extends React.Component { centerOnUserLocation(viewport) { this.setState({ viewport: { center: viewport.center, zoom: 10 } }); - // v4 map is uncontrolled: move it imperatively - if (this.map) this.map.setView(viewport.center, 10); } useMarkers(use) { @@ -182,8 +161,8 @@ class FiresMap extends React.Component { } handleLeafletLoad(map) { - if (map && !this.state.moving) { - const lmap = map; + if (map && map.leafletElement && !this.state.moving) { + const lmap = map.leafletElement; try { const bounds = lmap.getBounds(); mapSize.set([bounds.getNorthEast(), bounds.getSouthWest()]); @@ -287,13 +266,24 @@ class FiresMap extends React.Component { : ''} {/* https://github.com/CliffCloud/Leaflet.Sleep */} - { + this.fireMap = map; + this.handleLeafletLoad(map); + }} className="firesmap-leaflet-container" + animate minZoom={5} center={this.state.viewport.center} zoom={this.state.viewport.zoom} preferCanvas + viewport={this.state.viewport} + onViewportChanged={this.onViewportChanged} sleep={isHome() && !isChrome} + onMoveend={this.onMoveEnd} + onMovestart={this.onMoveStart} + onZoomend={this.onMoveEnd} + onZoomstart={this.onMoveStart} sleepTime={10750} wakeTime={750} sleepNote @@ -302,14 +292,6 @@ class FiresMap extends React.Component { wakeMessageTouch={this.props.t('Pulsa para activar')} sleepOpacity={0.6} > - - {/* http://wiki.openstreetmap.org/wiki/Tile_servers */} {!this.props.loading && @@ -367,13 +349,13 @@ class FiresMap extends React.Component { /> } - + this.centerOnUserLocation(viewport)} onlyIcon {... this.props} /> - + - +

    (*)Para preservar la privacidad de nuestros usuarios/as, los datos reflejados están aleatoriamente alterados y son solo orientativos.

    diff --git a/imports/ui/pages/Index/Index-custom.scss b/imports/ui/pages/Index/Index-custom.scss index 2a204f3..7c7b95a 100644 --- a/imports/ui/pages/Index/Index-custom.scss +++ b/imports/ui/pages/Index/Index-custom.scss @@ -40,47 +40,10 @@ height: 40px; } -// react-bootstrap v2 renders indicators as
    +
    {enabledLangs.map(lang => ( - this.onLangSelect(lang)}> + )) } - - +
    +
    {this.t('Puedes participar en las traducciones')} diff --git a/imports/ui/pages/Signup/Signup.js b/imports/ui/pages/Signup/Signup.js index a02f61b..964db13 100644 --- a/imports/ui/pages/Signup/Signup.js +++ b/imports/ui/pages/Signup/Signup.js @@ -132,7 +132,7 @@ class Signup extends React.Component { { Meteor.settings.public.telegramAuth &&