diff --git a/.meteor/packages b/.meteor/packages index 8f1d390..b651f13 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -30,7 +30,6 @@ 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 @@ -45,9 +44,15 @@ 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 76a1110..a70d819 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -8,7 +8,6 @@ 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 @@ -60,6 +59,9 @@ 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 c013f6f..eeec0b9 100644 --- a/.meteorignore +++ b/.meteorignore @@ -3,3 +3,5 @@ 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 86ca6a5..144e6a7 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/1.9 | ⏸️ **diferida** (mapa = feature central; 4 plugins v1-only sin equivalente v4; ver UPGRADE.md) | +| 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) | | **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,40 +28,64 @@ 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`). **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.) +(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). --- ## 2. Deuda funcional / de calidad de la web (independiente de prod) -- **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`. +- **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. - **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 baa7e63..f71fdbb 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). **Sole remaining console warnings are react-leaflet + its 4 plugins** — nothing else. | +| 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-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.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). | +| 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. | | 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 new file mode 100644 index 0000000..c19ae46 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,163 @@ +# 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 d98096f..dd88ce3 100644 --- a/imports/modules/rate-limit.js +++ b/imports/modules/rate-limit.js @@ -9,3 +9,15 @@ 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 0a1f83e..7a3848e 100644 --- a/imports/startup/client/Gkeys.js +++ b/imports/startup/client/Gkeys.js @@ -13,6 +13,11 @@ 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 b4c5202..1ddf4ed 100644 --- a/imports/startup/client/i18n.js +++ b/imports/startup/client/i18n.js @@ -51,6 +51,8 @@ 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 f5dd1e2..4b474be 100644 --- a/imports/startup/client/index.js +++ b/imports/startup/client/index.js @@ -1,4 +1,8 @@ /* 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 a0615dc..e9bb761 100644 --- a/imports/startup/common/i18n.js +++ b/imports/startup/common/i18n.js @@ -3,7 +3,9 @@ import moment from 'moment'; // Load the js langs import es from 'meteor-accounts-t9n/build/es'; import en from 'meteor-accounts-t9n/build/en'; -// TODO ask for translation of this +// 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. // 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 a6044b4..88b68b6 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -15,8 +15,6 @@ 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'; @@ -28,3 +26,26 @@ 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 8e47f71..d4da8e2 100644 --- a/imports/ui/components/Feedback/Feedback.js +++ b/imports/ui/components/Feedback/Feedback.js @@ -18,6 +18,7 @@ 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); } @@ -50,7 +51,9 @@ class Feedback extends Component { } onTabClick() { - $('#feedback-form').toggle('slide'); + // 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 })); } handleSubmit() { @@ -76,7 +79,7 @@ class Feedback extends Component {
{ !this.props.isHome &&
-
(this.formdiv = formdiv)} style={{ display: 'none' }} className="card"> +
(this.formdiv = formdiv)} style={{ display: this.state.open ? 'block' : 'none' }} className="card">
(this.form = form)} className="form card-body" @@ -109,7 +112,7 @@ class Feedback extends Component { /> - diff --git a/imports/ui/components/FromNow/FromNow.js b/imports/ui/components/FromNow/FromNow.js index 68afa23..dd385e2 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 edc4f8e..be596d2 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: 'form-group', + root: 'mb-3', // was BS4 `.form-group` (removed in BS5); mb-3 keeps the bottom margin input: 'form-control', autocompleteContainer: 'autocomplete-container' }} diff --git a/imports/ui/components/Maps/DefMapLayers.js b/imports/ui/components/Maps/DefMapLayers.js index 0355394..022f298 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 { GoogleLayer } from 'react-leaflet-google/lib/'; +import GoogleMutantLayer from '/imports/ui/components/Maps/GoogleMutantLayer'; 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 80b9826..7d0d6e0 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 d63b1e7..034c1fe 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,23 +43,20 @@ 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 da78185..d09213a 100644 --- a/imports/ui/components/Maps/FirePixel.js +++ b/imports/ui/components/Maps/FirePixel.js @@ -12,11 +12,8 @@ const FirePixel = ({ }) => ( diff --git a/imports/ui/components/Maps/FirePolygonMark.js b/imports/ui/components/Maps/FirePolygonMark.js index 92e3640..1b76bde 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 8738d19..543fd2c 100644 --- a/imports/ui/components/Maps/FullScreenMap.js +++ b/imports/ui/components/Maps/FullScreenMap.js @@ -1,25 +1,27 @@ /* eslint-disable react/jsx-indent-props */ /* eslint-disable import/no-absolute-path */ -/* eslint-disable import/no-absolute-path */ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { withTranslation } from 'react-i18next'; -import FullscreenControl from 'react-leaflet-fullscreen'; -import 'react-leaflet-fullscreen/dist/styles.css'; +import { createControlComponent } from '@react-leaflet/core'; +import L from 'leaflet'; +import 'leaflet.fullscreen'; +import 'leaflet.fullscreen/Control.FullScreen.css'; -class FullScreenMap extends Component { - render() { - const { t } = this.props; - return ( - - ); - } -} +// 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 }) => ( + +); FullScreenMap.propTypes = { t: PropTypes.func.isRequired diff --git a/imports/ui/components/Maps/GoogleMutantLayer.js b/imports/ui/components/Maps/GoogleMutantLayer.js new file mode 100644 index 0000000..d2b07e7 --- /dev/null +++ b/imports/ui/components/Maps/GoogleMutantLayer.js @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..ea44ea2 --- /dev/null +++ b/imports/ui/components/Maps/MapBridge.js @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..ce69571 --- /dev/null +++ b/imports/ui/components/Maps/MapControl.js @@ -0,0 +1,32 @@ +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 41a4d81..c2f4216 100644 --- a/imports/ui/components/Maps/SubsUnion/SubsUnion.js +++ b/imports/ui/components/Maps/SubsUnion/SubsUnion.js @@ -12,7 +12,8 @@ const subsUnion = (union, options) => { const interactive = options.interactive || false; if (options.subs) { - const lmap = options.map.leafletElement; + // v4: options.map is the Leaflet map instance directly (no .leafletElement) + const lmap = options.map; if (union) { lmap.removeLayer(union); } @@ -30,7 +31,7 @@ const subsUnion = (union, options) => { if (options.fit && options.bounds) { // console.log(options.bounds); const bounds = JSON.parse(options.bounds); - options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); + lmap.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); } } else if (options.subs.length > 0) { const result = calcUnion(L, options.subs, sub => sub, true); @@ -43,7 +44,7 @@ const subsUnion = (union, options) => { }); union.addTo(lmap); if (options.fit) { - options.map.leafletElement.fitBounds(bounds); + lmap.fitBounds(bounds); } } } diff --git a/imports/ui/components/NavItem/NavItem.js b/imports/ui/components/NavItem/NavItem.js index a3bbad0..9467868 100644 --- a/imports/ui/components/NavItem/NavItem.js +++ b/imports/ui/components/NavItem/NavItem.js @@ -4,7 +4,9 @@ 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
  • that toggles the collapsed menu on click. +// navbar needs is just an
  • . The mobile menu is now collapsed from React +// state in Navigation (the parent
      ), so the old jQuery +// `data-toggle="collapse"` / `data-target` hooks were dropped here. const propTypes = { active: PropTypes.bool, disabled: PropTypes.bool, @@ -57,8 +59,6 @@ class NavItem extends React.Component { return (
    • diff --git a/imports/ui/components/Navigation/Navigation.js b/imports/ui/components/Navigation/Navigation.js index 15b1c11..606f74a 100644 --- a/imports/ui/components/Navigation/Navigation.js +++ b/imports/ui/components/Navigation/Navigation.js @@ -15,44 +15,51 @@ import './Navigation.scss'; // removed class: fixed-top // md instead of lg: no menu in medium -const Navigation = ({ name = '', ...props }) => ( -
  • - -); + + ); +}; Navigation.propTypes = { t: PropTypes.func.isRequired, diff --git a/imports/ui/components/OAuthLoginButton/OAuthLoginButton.js b/imports/ui/components/OAuthLoginButton/OAuthLoginButton.js index 81abfb2..1b74779 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]} + )) } -
    -
    + +
    } @@ -292,7 +285,22 @@ 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(); - const notfound = !loading && FiresCollection.find().count() === 0; + // 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; /* console.log(`loading fire: ${loading}`); * console.log(`Not found fire: ${notfound}`); */ const falsePositives = FalsePositivesCollection.find().fetch().map(falsePositivesRemap); @@ -304,9 +312,9 @@ const FireContainer = withTracker(({ match }) => { fromHash, falsePositives, industries, - fire: FiresCollection.findOne(), + fire, notfound, - when: subscription.ready() && FiresCollection.findOne() ? FiresCollection.findOne().when : null + when: fire ? fire.when : null }; })(Fire); diff --git a/imports/ui/pages/FiresMap/FiresMap.js b/imports/ui/pages/FiresMap/FiresMap.js index ab5acd7..04d9130 100644 --- a/imports/ui/pages/FiresMap/FiresMap.js +++ b/imports/ui/pages/FiresMap/FiresMap.js @@ -11,8 +11,9 @@ 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 { Map } from 'react-leaflet'; -import Control from 'react-leaflet-control'; +import { MapContainer } from 'react-leaflet'; +import MapControl from '/imports/ui/components/Maps/MapControl'; +import { MapReady, MapEvents } from '/imports/ui/components/Maps/MapBridge'; import LoadingBar from '/imports/ui/components/Loading/LoadingBar'; import _ from 'lodash'; import store from 'store'; @@ -76,6 +77,24 @@ 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() { @@ -110,7 +129,7 @@ class FiresMap extends React.Component { } getMap() { - return this.fireMap.leafletElement; + return this.map; } setShowSubsUnion(showSubsUnion) { @@ -124,7 +143,7 @@ class FiresMap extends React.Component { handleViewportChange(viewport) { console.log(`Viewport changed: ${JSON.stringify(viewport)}`); - if (this.fireMap) { + if (this.map) { const bounds = this.getMap().getBounds(); // console.log(bounds); mapSize.set([bounds.getNorthEast(), bounds.getSouthWest()]); @@ -143,6 +162,8 @@ 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) { @@ -161,8 +182,8 @@ class FiresMap extends React.Component { } handleLeafletLoad(map) { - if (map && map.leafletElement && !this.state.moving) { - const lmap = map.leafletElement; + if (map && !this.state.moving) { + const lmap = map; try { const bounds = lmap.getBounds(); mapSize.set([bounds.getNorthEast(), bounds.getSouthWest()]); @@ -266,24 +287,13 @@ class FiresMap extends React.Component { : ''} {/* https://github.com/CliffCloud/Leaflet.Sleep */} - { - this.fireMap = map; - this.handleLeafletLoad(map); - }} + + + {/* http://wiki.openstreetmap.org/wiki/Tile_servers */} {!this.props.loading && @@ -349,13 +367,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 7c7b95a..2a204f3 100644 --- a/imports/ui/pages/Index/Index-custom.scss +++ b/imports/ui/pages/Index/Index-custom.scss @@ -40,10 +40,47 @@ height: 40px; } +// react-bootstrap v2 renders indicators as
    -
    + + {enabledLangs.map(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 964db13..a02f61b 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 &&