Compare commits
No commits in common. "meteor3-upgrade" and "tcef-master" have entirely different histories.
meteor3-up
...
tcef-maste
49 changed files with 4438 additions and 6205 deletions
|
|
@ -15,5 +15,3 @@ notices-for-facebook-graph-api-2
|
||||||
1.4.1-add-shell-server-package
|
1.4.1-add-shell-server-package
|
||||||
1.4.3-split-account-service-packages
|
1.4.3-split-account-service-packages
|
||||||
1.5-add-dynamic-import-package
|
1.5-add-dynamic-import-package
|
||||||
1.7-split-underscore-from-meteor-base
|
|
||||||
1.8.3-split-jquery-from-blaze
|
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,34 @@
|
||||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||||
# but you can also edit it by hand.
|
# but you can also edit it by hand.
|
||||||
|
|
||||||
meteor-base@1.5.1 # Packages every Meteor app needs to have
|
meteor-base@1.3.0 # Packages every Meteor app needs to have
|
||||||
mobile-experience@1.1.0 # Packages for a great mobile UX
|
mobile-experience@1.0.5 # Packages for a great mobile UX
|
||||||
mongo@1.13.0 # The database Meteor supports right now
|
mongo@1.4.7 # The database Meteor supports right now
|
||||||
reactive-var@1.0.11 # Reactive variable for tracker
|
reactive-var@1.0.11 # Reactive variable for tracker
|
||||||
tracker@1.2.0 # Meteor's client-side reactive programming library
|
tracker@1.1.3 # Meteor's client-side reactive programming library
|
||||||
|
|
||||||
standard-minifier-css@1.7.4 # CSS minifier run for production mode
|
standard-minifier-css@1.4.0 # CSS minifier run for production mode
|
||||||
standard-minifier-js@2.7.1 # JS minifier run for production mode
|
standard-minifier-js@2.3.1 # JS minifier run for production mode
|
||||||
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers.
|
es5-shim@4.7.0 # ECMAScript 5 compatibility for older browsers.
|
||||||
ecmascript@0.16.0 # Enable ECMAScript2015+ syntax in app code
|
ecmascript@0.10.6 # Enable ECMAScript2015+ syntax in app code
|
||||||
shell-server@0.5.0 # Server-side component of the `meteor shell` command
|
shell-server@0.3.1 # Server-side component of the `meteor shell` command
|
||||||
|
|
||||||
react-meteor-data
|
react-meteor-data
|
||||||
alanning:roles
|
alanning:roles
|
||||||
fourseven:scss@4.14.1
|
fourseven:scss
|
||||||
accounts-base@2.2.0
|
accounts-base@1.4.2
|
||||||
accounts-password@2.2.0
|
accounts-password@1.5.0
|
||||||
service-configuration@1.3.0
|
service-configuration@1.0.11
|
||||||
accounts-facebook@1.3.3
|
accounts-facebook@1.3.1
|
||||||
accounts-github@1.5.0
|
accounts-github@1.4.1
|
||||||
accounts-google@1.4.0
|
accounts-google@1.3.1
|
||||||
themeteorchef:bert
|
themeteorchef:bert
|
||||||
fortawesome:fontawesome
|
fortawesome:fontawesome
|
||||||
aldeed:collection2-core@2.0.1
|
aldeed:collection2-core@2.0.1
|
||||||
audit-argument-checks@1.0.7
|
audit-argument-checks@1.0.7
|
||||||
ddp-rate-limiter@1.1.0
|
ddp-rate-limiter@1.0.7
|
||||||
dynamic-import@0.7.2
|
dynamic-import@0.3.0
|
||||||
static-html@1.3.2
|
static-html
|
||||||
alexwine:bootstrap-4
|
alexwine:bootstrap-4
|
||||||
|
|
||||||
selaias:cookie-consent # Cookie consent
|
selaias:cookie-consent # Cookie consent
|
||||||
|
|
@ -42,19 +42,28 @@ flowkey:raven # js errors
|
||||||
vjrj:piwik # Stats
|
vjrj:piwik # Stats
|
||||||
mdg:geolocation
|
mdg:geolocation
|
||||||
natestrauser:publish-performant-counts
|
natestrauser:publish-performant-counts
|
||||||
|
maximum:server-transform
|
||||||
mys:fonts # fonst in npm
|
mys:fonts # fonst in npm
|
||||||
ostrio:mailer # mailer
|
ostrio:mailer # mailer
|
||||||
# babrahams:constellation # dev utility
|
# babrahams:constellation # dev utility
|
||||||
percolate:migrations # db migrations
|
percolate:migrations # db migrations
|
||||||
ostrio:meteor-root
|
ostrio:meteor-root
|
||||||
|
meteortesting:mocha
|
||||||
|
practicalmeteor:chai
|
||||||
aldeed:schema-deny
|
aldeed:schema-deny
|
||||||
|
aldeed:template-extension
|
||||||
dburles:collection-helpers
|
dburles:collection-helpers
|
||||||
|
less@2.7.11
|
||||||
|
markdown@1.0.12
|
||||||
mongo-livedata@1.0.12
|
mongo-livedata@1.0.12
|
||||||
reywood:publish-composite
|
reywood:publish-composite
|
||||||
|
barbatus:stars-rating
|
||||||
|
arkham:comments-ui
|
||||||
facts@1.0.9
|
facts@1.0.9
|
||||||
gadicohen:sitemaps
|
gadicohen:sitemaps
|
||||||
nspangler:autoreconnect
|
nspangler:autoreconnect
|
||||||
saucecode:timezoned-synced-cron
|
saucecode:timezoned-synced-cron
|
||||||
# lmachens:kadira
|
# lmachens:kadira
|
||||||
nimble:restivus@0.8.12
|
meteorhacks:zones
|
||||||
underscore@1.0.10
|
nimble:restivus
|
||||||
|
xolvio:cleaner
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
METEOR@2.5
|
METEOR@1.6.1.1
|
||||||
|
|
|
||||||
193
.meteor/versions
193
.meteor/versions
|
|
@ -1,128 +1,155 @@
|
||||||
255kb:meteor-status@1.5.0
|
255kb:meteor-status@1.5.0
|
||||||
accounts-base@2.2.0
|
accounts-base@1.4.2
|
||||||
accounts-facebook@1.3.3
|
accounts-facebook@1.3.1
|
||||||
accounts-github@1.5.0
|
accounts-github@1.4.1
|
||||||
accounts-google@1.4.0
|
accounts-google@1.3.1
|
||||||
accounts-oauth@1.4.0
|
accounts-oauth@1.1.15
|
||||||
accounts-password@2.2.0
|
accounts-password@1.5.1
|
||||||
alanning:roles@1.3.0
|
alanning:roles@1.2.16
|
||||||
aldeed:collection2-core@2.1.2
|
aldeed:collection2-core@2.1.2
|
||||||
aldeed:schema-deny@2.0.1
|
aldeed:schema-deny@2.0.1
|
||||||
|
aldeed:template-extension@4.1.0
|
||||||
alexwine:bootstrap-4@4.1.0
|
alexwine:bootstrap-4@4.1.0
|
||||||
allow-deny@1.1.0
|
allow-deny@1.1.0
|
||||||
|
arkham:comments-ui@1.4.4
|
||||||
audit-argument-checks@1.0.7
|
audit-argument-checks@1.0.7
|
||||||
autoupdate@1.8.0
|
autoupdate@1.4.0
|
||||||
babel-compiler@7.7.0
|
babel-compiler@7.0.7
|
||||||
babel-runtime@1.5.0
|
babel-runtime@1.2.2
|
||||||
base64@1.0.12
|
barbatus:stars-rating@1.1.1
|
||||||
binary-heap@1.0.11
|
base64@1.0.11
|
||||||
blaze@2.5.0
|
binary-heap@1.0.10
|
||||||
blaze-tools@1.1.2
|
blaze@2.3.2
|
||||||
boilerplate-generator@1.7.1
|
blaze-tools@1.0.10
|
||||||
caching-compiler@1.2.2
|
boilerplate-generator@1.4.0
|
||||||
caching-html-compiler@1.2.1
|
browser-policy@1.1.0
|
||||||
callback-hook@1.4.0
|
browser-policy-common@1.0.11
|
||||||
|
browser-policy-content@1.1.0
|
||||||
|
browser-policy-framing@1.1.0
|
||||||
|
caching-compiler@1.1.11
|
||||||
|
caching-html-compiler@1.1.2
|
||||||
|
callback-hook@1.1.0
|
||||||
check@1.3.1
|
check@1.3.1
|
||||||
|
coffeescript@1.0.17
|
||||||
dburles:collection-helpers@1.1.0
|
dburles:collection-helpers@1.1.0
|
||||||
|
dburles:mongo-collection-instances@0.3.5
|
||||||
ddp@1.4.0
|
ddp@1.4.0
|
||||||
ddp-client@2.5.0
|
ddp-client@2.3.2
|
||||||
ddp-common@1.4.0
|
ddp-common@1.4.0
|
||||||
ddp-rate-limiter@1.1.0
|
ddp-rate-limiter@1.0.7
|
||||||
ddp-server@2.5.0
|
ddp-server@2.1.2
|
||||||
diff-sequence@1.1.1
|
deps@1.0.12
|
||||||
dynamic-import@0.7.2
|
diff-sequence@1.1.0
|
||||||
ecmascript@0.16.0
|
dynamic-import@0.3.0
|
||||||
ecmascript-runtime@0.8.0
|
ecmascript@0.10.7
|
||||||
ecmascript-runtime-client@0.12.1
|
ecmascript-runtime@0.5.0
|
||||||
ecmascript-runtime-server@0.11.0
|
ecmascript-runtime-client@0.6.2
|
||||||
ejson@1.1.1
|
ecmascript-runtime-server@0.5.0
|
||||||
email@2.2.0
|
ejson@1.1.0
|
||||||
es5-shim@4.8.0
|
email@1.2.3
|
||||||
facebook-oauth@1.10.0
|
es5-shim@4.7.3
|
||||||
|
facebook-oauth@1.4.0
|
||||||
facts@1.0.9
|
facts@1.0.9
|
||||||
fetch@0.1.1
|
|
||||||
flowkey:raven@1.1.0
|
flowkey:raven@1.1.0
|
||||||
fortawesome:fontawesome@4.7.0
|
fortawesome:fontawesome@4.7.0
|
||||||
fourseven:scss@4.14.1
|
fourseven:scss@4.5.4
|
||||||
gadicc:blaze-react-component@1.4.0
|
gadicc:blaze-react-component@1.4.0
|
||||||
gadicohen:robots-txt@0.0.10
|
gadicohen:robots-txt@0.0.10
|
||||||
gadicohen:sitemaps@0.0.26
|
gadicohen:sitemaps@0.0.26
|
||||||
geojson-utils@1.0.10
|
geojson-utils@1.0.10
|
||||||
github-oauth@1.3.1
|
github-oauth@1.2.0
|
||||||
google-oauth@1.4.1
|
google-oauth@1.2.5
|
||||||
hot-code-push@1.0.4
|
hot-code-push@1.0.4
|
||||||
html-tools@1.1.2
|
html-tools@1.0.11
|
||||||
htmljs@1.1.0
|
htmljs@1.0.11
|
||||||
http@2.0.0
|
http@1.4.0
|
||||||
id-map@1.1.1
|
id-map@1.1.0
|
||||||
inter-process-messaging@0.1.1
|
|
||||||
jquery@1.11.11
|
jquery@1.11.11
|
||||||
launch-screen@1.3.0
|
lai:collection-extensions@0.2.1_1
|
||||||
|
launch-screen@1.1.1
|
||||||
|
less@2.7.12
|
||||||
|
livedata@1.0.18
|
||||||
localstorage@1.2.0
|
localstorage@1.2.0
|
||||||
logging@1.3.1
|
logging@1.1.20
|
||||||
|
markdown@1.0.12
|
||||||
|
maximum:package-base@1.1.2
|
||||||
|
maximum:server-transform@0.5.0
|
||||||
mdg:geolocation@1.3.0
|
mdg:geolocation@1.3.0
|
||||||
meteor@1.10.0
|
meteor@1.8.6
|
||||||
meteor-base@1.5.1
|
meteor-base@1.3.0
|
||||||
minifier-css@1.6.0
|
meteorhacks:inject-initial@1.0.4
|
||||||
minifier-js@2.7.1
|
meteorhacks:zones@1.6.0
|
||||||
minimongo@1.7.0
|
meteortesting:browser-tests@0.2.0
|
||||||
mobile-experience@1.1.0
|
meteortesting:mocha@0.5.1
|
||||||
mobile-status-bar@1.1.0
|
minifier-css@1.3.1
|
||||||
modern-browsers@0.1.7
|
minifier-js@2.3.4
|
||||||
modules@0.17.0
|
minimongo@1.4.4
|
||||||
modules-runtime@0.12.0
|
mobile-experience@1.0.5
|
||||||
mongo@1.13.0
|
mobile-status-bar@1.0.14
|
||||||
mongo-decimal@0.1.2
|
modules@0.11.6
|
||||||
|
modules-runtime@0.9.2
|
||||||
|
mongo@1.4.7
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.8
|
mongo-id@1.0.7
|
||||||
mongo-livedata@1.0.12
|
mongo-livedata@1.0.12
|
||||||
mys:fonts@0.0.2
|
mys:fonts@0.0.2
|
||||||
natestrauser:publish-performant-counts@0.1.2
|
natestrauser:publish-performant-counts@0.1.2
|
||||||
nimble:restivus@0.8.12
|
nimble:restivus@0.8.12
|
||||||
npm-mongo@3.9.1
|
npm-bcrypt@0.9.3
|
||||||
|
npm-mongo@2.2.34
|
||||||
nspangler:autoreconnect@0.0.1
|
nspangler:autoreconnect@0.0.1
|
||||||
oauth@2.1.0
|
oauth@1.2.3
|
||||||
oauth2@1.3.1
|
oauth2@1.2.0
|
||||||
observe-sequence@1.0.16
|
observe-sequence@1.0.16
|
||||||
ordered-dict@1.1.0
|
ordered-dict@1.1.0
|
||||||
ostrio:cookies@2.7.0
|
ostrio:cookies@2.1.3
|
||||||
ostrio:mailer@2.1.0
|
ostrio:mailer@2.1.0
|
||||||
ostrio:meteor-root@1.0.7
|
ostrio:meteor-root@1.0.7
|
||||||
|
peerlibrary:assert@0.2.5
|
||||||
|
peerlibrary:fiber-utils@0.6.0
|
||||||
|
peerlibrary:reactive-mongo@0.1.1
|
||||||
|
peerlibrary:server-autorun@0.5.2
|
||||||
percolate:migrations@1.0.2
|
percolate:migrations@1.0.2
|
||||||
promise@0.12.0
|
practicalmeteor:chai@2.1.0_1
|
||||||
|
practicalmeteor:mocha-core@1.0.1
|
||||||
|
promise@0.10.2
|
||||||
raix:eventemitter@0.1.3
|
raix:eventemitter@0.1.3
|
||||||
random@1.2.0
|
random@1.1.0
|
||||||
rate-limit@1.0.9
|
rate-limit@1.0.9
|
||||||
react-fast-refresh@0.2.0
|
|
||||||
react-meteor-data@0.2.16
|
react-meteor-data@0.2.16
|
||||||
reactive-dict@1.3.0
|
reactive-dict@1.2.0
|
||||||
reactive-var@1.0.11
|
reactive-var@1.0.11
|
||||||
reload@1.3.1
|
reload@1.2.0
|
||||||
retry@1.1.0
|
retry@1.1.0
|
||||||
reywood:publish-composite@1.6.0
|
reywood:publish-composite@1.6.0
|
||||||
routepolicy@1.1.1
|
routepolicy@1.0.13
|
||||||
saucecode:timezoned-synced-cron@1.2.11
|
saucecode:timezoned-synced-cron@1.2.11
|
||||||
selaias:cookie-consent@0.4.0
|
selaias:cookie-consent@0.4.0
|
||||||
service-configuration@1.3.0
|
server-render@0.3.0
|
||||||
session@1.2.0
|
service-configuration@1.0.11
|
||||||
|
session@1.1.7
|
||||||
sha@1.0.9
|
sha@1.0.9
|
||||||
shell-server@0.5.0
|
shell-server@0.3.1
|
||||||
simple:json-routes@1.0.4
|
shim-common@0.1.0
|
||||||
socket-stream-client@0.4.0
|
simple:json-routes@2.1.0
|
||||||
spacebars@1.2.0
|
socket-stream-client@0.1.0
|
||||||
spacebars-compiler@1.3.0
|
spacebars@1.0.15
|
||||||
standard-minifier-css@1.7.4
|
spacebars-compiler@1.1.3
|
||||||
standard-minifier-js@2.7.1
|
srp@1.0.10
|
||||||
static-html@1.3.2
|
standard-minifier-css@1.4.1
|
||||||
templating@1.4.1
|
standard-minifier-js@2.3.3
|
||||||
templating-compiler@1.4.1
|
static-html@1.2.2
|
||||||
templating-runtime@1.5.0
|
templating@1.3.2
|
||||||
templating-tools@1.2.1
|
templating-compiler@1.3.3
|
||||||
|
templating-runtime@1.3.2
|
||||||
|
templating-tools@1.1.2
|
||||||
themeteorchef:bert@2.1.3
|
themeteorchef:bert@2.1.3
|
||||||
tmeasday:check-npm-versions@0.3.2
|
tmeasday:check-npm-versions@0.3.2
|
||||||
tracker@1.2.0
|
tracker@1.1.3
|
||||||
|
ui@1.0.13
|
||||||
underscore@1.0.10
|
underscore@1.0.10
|
||||||
url@1.3.2
|
url@1.2.0
|
||||||
vjrj:piwik@0.3.1
|
vjrj:piwik@0.3.1
|
||||||
webapp@1.13.0
|
webapp@1.5.0
|
||||||
webapp-hashing@1.1.0
|
webapp-hashing@1.0.9
|
||||||
|
xolvio:cleaner@0.3.3
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
# Don't reload meteor server when we modify or do test
|
# Don't reload meteor server when we modify or do test
|
||||||
cucumber
|
cucumber
|
||||||
output.json
|
output.json
|
||||||
# REST smoke-test harness runs standalone (Node), never as Meteor server code
|
|
||||||
smoke
|
|
||||||
|
|
|
||||||
278
UPGRADE.md
278
UPGRADE.md
|
|
@ -1,278 +0,0 @@
|
||||||
# Meteor upgrade — 1.6.1.1 → 2.x (→ 3.x blocked)
|
|
||||||
|
|
||||||
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:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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.1** — `node-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.
|
|
||||||
|
|
||||||
## 🚧 3.x — blocked (do not run `meteor update --release 3.x`)
|
|
||||||
|
|
||||||
Two independent blockers, both external/coordination:
|
|
||||||
1. **MongoDB 3.2 → ≥ 4.4.** Meteor 3's driver requires it. Shared Comunes infra
|
|
||||||
(rsmain) — coordinate the DB upgrade in a separate phase. This *also* gates
|
|
||||||
Meteor 2.5+.
|
|
||||||
2. **Notifications cutover.** The old notif code is already removed from this
|
|
||||||
branch; production must not run this build until `tcef-notifications` is
|
|
||||||
emitting in prod.
|
|
||||||
|
|
||||||
When Mongo is ≥ 4.4 and the notifications service is live in prod, the remaining
|
|
||||||
3.x work is: `meteor update --release 3.x`, finish the async/await migration of
|
|
||||||
server code (collections/methods/publications `*Async`, no Fibers), React 18
|
|
||||||
render root, and re-run the smoke test.
|
|
||||||
|
|
||||||
## 3.x groundwork — done on branch `meteor3-wip` (in progress, not deployable)
|
|
||||||
|
|
||||||
To de-risk the eventual 3.x jump, the migration was driven as far as it goes
|
|
||||||
against a **modern dockerized MongoDB** (the "mongo superior" decision: give
|
|
||||||
fuegos its own Mongo 7 instead of the shared rsmain 3.2, which is what actually
|
|
||||||
unblocks Meteor 3's driver). This lives on `meteor3-wip`; `meteor3-upgrade` stays
|
|
||||||
at the deployable Meteor 2.5.
|
|
||||||
|
|
||||||
**Modern Mongo (local, reproducible):**
|
|
||||||
```bash
|
|
||||||
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"}]})'
|
|
||||||
export MONGO_URL="mongodb://localhost:27019/fuegos?replicaSet=rs0"
|
|
||||||
# meteor-tool node 22 needs this to reach warehouse.meteor.com (else Happy-Eyeballs
|
|
||||||
# picks an unreachable IPv6):
|
|
||||||
export NODE_OPTIONS="--dns-result-order=ipv4first --no-network-family-autoselection"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reached `METEOR@3.1` with all package-version conflicts resolved.** Dead
|
|
||||||
packages swapped for Meteor-3 equivalents:
|
|
||||||
|
|
||||||
| Old (mongo@1.x / dead) | New |
|
|
||||||
|---|---|
|
|
||||||
| `mongo-livedata` | removed (deprecated alias) |
|
|
||||||
| `facts` | `facts-base` |
|
|
||||||
| `saucecode:timezoned-synced-cron` | `quave:synced-cron` |
|
|
||||||
| `aldeed:collection2-core` | `aldeed:collection2@4.0.4` |
|
|
||||||
| `mys:fonts` (pinned caching-compiler@1.0.0) | removed |
|
|
||||||
| `fourseven:scss@4.14.1` (node-sass, fails on node 22) | `@5.0.0` (Dart Sass) + `sass` npm |
|
|
||||||
| `nimble:restivus` (vendored) | accounts-password loosened to `2.0.0 \|\| 3.0.0` |
|
|
||||||
|
|
||||||
SCSS then compiles (Dart Sass; only `@import`/`lighten`/`darken` deprecation
|
|
||||||
warnings — cosmetic debt: migrate to `@use` + `color.adjust`).
|
|
||||||
|
|
||||||
**Blocked at the client-bundle linker:** `Runtime is not available, but it uses
|
|
||||||
features needing the runtime: underscore` — one of the old client atmosphere
|
|
||||||
packages (bert / blaze-react / cookie-consent / meteor-status / bootstrap-4 /
|
|
||||||
etc.) needs replacing for Meteor 3's linker. Beyond that, the **server
|
|
||||||
async/await migration is the main remaining effort** (unavoidable for 3.x):
|
|
||||||
|
|
||||||
- `Rest.js` + its helpers (`countRealFires`, `firesUnion`, `whichAreFalsePositives`,
|
|
||||||
`fireFromHash`, `subscriptionsInsert`, `upsertFalsePositive`) use sync Mongo
|
|
||||||
(`findOne`, `find().fetch()/.count()`, `insert/update/remove/upsert`) → must
|
|
||||||
become `*Async`.
|
|
||||||
- The vendored **restivus must be patched to `await`** async endpoint handlers
|
|
||||||
(its `_callEndpoint` uses the return value synchronously).
|
|
||||||
- `methods.js`/`publications.js` across the app, `subsUnion.js`, `migrations.js`,
|
|
||||||
the Comments feature, and startup files → async.
|
|
||||||
- `cron.js` `SyncedCron` import (quave export) and `facts.js` `Facts` import
|
|
||||||
(facts-base export).
|
|
||||||
- React 16 → 18 render root (`createRoot`) in `startup/client/index.js`.
|
|
||||||
|
|
||||||
This is a multi-hour migration; the smoke test (now `mongosh`-capable, see
|
|
||||||
`smoke/smoke.sh` on that branch) is the gate. Resume on `meteor3-wip`.
|
|
||||||
|
|
||||||
## Dependency debt (tracked, to resolve in the noted escala)
|
|
||||||
|
|
||||||
| Dep | From | Target | Status |
|
|
||||||
|---|---|---|---|
|
|
||||||
| React / react-dom | 16.0 | 18 | **debt** — deferred; ancient react-* libs (react-bootstrap 0.31, reactstrap 5-alpha, react-leaflet 1.8) break on 18. React 16 works through Meteor 2.x/3.x. Own front-end project. |
|
|
||||||
| Server async/await | Fibers | `*Async` APIs | **pending** — prep for 3.x; 2.x still runs on Fibers so not yet required. |
|
|
||||||
| raven / flowkey:raven | 2.4 | @sentry/node + @sentry/react | debt — DSN off until GlitchTip/Sentry back (sentry.comunes.org dead → harmless boot-log timeout noise) |
|
|
||||||
| nodemailer | 4 | 6+ | pending |
|
|
||||||
| Babel | 7 beta | 7 stable | ✅ done (escala 1) |
|
|
||||||
| i18next | 10 | current | debt — pin; not on the critical path |
|
|
||||||
| Leaflet | 1.3.1 | current | debt — pin; not on the critical path |
|
|
||||||
| 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) |
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
/* eslint-disable import/no-absolute-path */
|
|
||||||
import { Mongo } from 'meteor/mongo';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Comments collection (fire-page comments).
|
|
||||||
*
|
|
||||||
* Replaces the abandoned `arkham:comments-ui` Atmosphere package (no Meteor 2.x
|
|
||||||
* build) with a small React + methods implementation. The collection name
|
|
||||||
* ('comments') and the core document shape are kept so existing production
|
|
||||||
* documents keep rendering:
|
|
||||||
*
|
|
||||||
* { referenceId: 'fire-<id>', content, userId, username,
|
|
||||||
* media: { type, content }, likes: [userId], dislikes: [userId],
|
|
||||||
* status: 'approved', createdAt, updatedAt }
|
|
||||||
*
|
|
||||||
* Legacy documents may also carry replies[]/starRatings[]/ratingScore/
|
|
||||||
* isAnonymous — those features were disabled in this app's config and are
|
|
||||||
* ignored by the new UI.
|
|
||||||
*/
|
|
||||||
const Comments = new Mongo.Collection('comments');
|
|
||||||
|
|
||||||
// Writes only through the vetted server methods.
|
|
||||||
Comments.deny({
|
|
||||||
insert: () => true,
|
|
||||||
update: () => true,
|
|
||||||
remove: () => true
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Comments;
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
* Media analyzers ported from the old arkham:comments-ui package
|
|
||||||
* (lib/services/media-analyzers). Given a comment's text, detect an embeddable
|
|
||||||
* image or YouTube URL and return { type, content }. First match wins.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const imageAnalyzer = {
|
|
||||||
name: 'image',
|
|
||||||
getMediaFromContent(content) {
|
|
||||||
if (content) {
|
|
||||||
const urls = content.match(/(\S+\.[^/\s]+(\/\S+|\/|))(.jpg|.png|.gif)/g);
|
|
||||||
if (urls && urls[0]) return urls[0];
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const youtubeAnalyzer = {
|
|
||||||
name: 'youtube',
|
|
||||||
getMediaFromContent(content) {
|
|
||||||
const parts = (content || '').match(/(?:https?:\/\/)?(?:www\.youtube\.com|youtu\.?be)\/([\w=?]+)/);
|
|
||||||
let mediaContent = '';
|
|
||||||
if (parts && parts[1]) {
|
|
||||||
let id = parts[1];
|
|
||||||
if (id.indexOf('v=') > -1) {
|
|
||||||
const subParts = id.match(/v=([\w]+)/);
|
|
||||||
if (subParts && subParts[1]) id = subParts[1];
|
|
||||||
}
|
|
||||||
mediaContent = `https://www.youtube.com/embed/${id}`;
|
|
||||||
}
|
|
||||||
return mediaContent;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const analyzers = [imageAnalyzer, youtubeAnalyzer];
|
|
||||||
|
|
||||||
// Returns { type, content } or {}.
|
|
||||||
export default function getMediaFromContent(content) {
|
|
||||||
for (let i = 0; i < analyzers.length; i += 1) {
|
|
||||||
const mediaContent = analyzers[i].getMediaFromContent(content);
|
|
||||||
if (mediaContent) return { type: analyzers[i].name, content: mediaContent };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
// Server-side registration for the Comments feature.
|
|
||||||
import './methods';
|
|
||||||
import './publications';
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
/* eslint-disable import/no-absolute-path */
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import { check } from 'meteor/check';
|
|
||||||
import Comments from '/imports/api/Comments/Comments';
|
|
||||||
import getMediaFromContent from '/imports/api/Comments/mediaAnalyzers';
|
|
||||||
import rateLimit from '/imports/modules/rate-limit';
|
|
||||||
import onCommentAdd from '/imports/api/Comments/server/onCommentAdd';
|
|
||||||
|
|
||||||
const MAX_LEN = 5000;
|
|
||||||
|
|
||||||
function usernameOf(user) {
|
|
||||||
return (user && user.profile && user.profile.name && user.profile.name.first)
|
|
||||||
? user.profile.name.first
|
|
||||||
: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireOwner(commentId, userId) {
|
|
||||||
const comment = Comments.findOne(commentId);
|
|
||||||
if (!comment) throw new Meteor.Error('404', 'Comment not found');
|
|
||||||
if (comment.userId !== userId) throw new Meteor.Error('403', 'Not your comment');
|
|
||||||
return comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle(commentId, field, otherField, userId) {
|
|
||||||
check(commentId, String);
|
|
||||||
if (!userId) throw new Meteor.Error('403', 'Login required');
|
|
||||||
const comment = Comments.findOne(commentId);
|
|
||||||
if (!comment) throw new Meteor.Error('404', 'Comment not found');
|
|
||||||
const has = (comment[field] || []).includes(userId);
|
|
||||||
const modifier = has
|
|
||||||
? { $pull: { [field]: userId } }
|
|
||||||
: { $addToSet: { [field]: userId }, $pull: { [otherField]: userId } };
|
|
||||||
Comments.update(commentId, modifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
Meteor.methods({
|
|
||||||
'comments.insert': function commentsInsert(referenceId, content) {
|
|
||||||
check(referenceId, String);
|
|
||||||
check(content, String);
|
|
||||||
if (!this.userId) throw new Meteor.Error('403', 'Login required to comment');
|
|
||||||
const text = content.trim();
|
|
||||||
if (!text) throw new Meteor.Error('400', 'Empty comment');
|
|
||||||
if (text.length > MAX_LEN) throw new Meteor.Error('400', 'Comment too long');
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const doc = {
|
|
||||||
referenceId,
|
|
||||||
content: text,
|
|
||||||
userId: this.userId,
|
|
||||||
username: usernameOf(Meteor.users.findOne(this.userId)),
|
|
||||||
media: getMediaFromContent(text),
|
|
||||||
likes: [],
|
|
||||||
dislikes: [],
|
|
||||||
status: 'approved',
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now
|
|
||||||
};
|
|
||||||
const _id = Comments.insert(doc);
|
|
||||||
// Fire-and-forget: notify other commenters of this fire. Never let a mail
|
|
||||||
// failure break the insert.
|
|
||||||
try {
|
|
||||||
onCommentAdd({ ...doc, _id });
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`comments onCommentAdd failed: ${e}`);
|
|
||||||
}
|
|
||||||
return _id;
|
|
||||||
},
|
|
||||||
|
|
||||||
'comments.edit': function commentsEdit(commentId, content) {
|
|
||||||
check(commentId, String);
|
|
||||||
check(content, String);
|
|
||||||
if (!this.userId) throw new Meteor.Error('403', 'Login required');
|
|
||||||
requireOwner(commentId, this.userId);
|
|
||||||
const text = content.trim();
|
|
||||||
if (!text) throw new Meteor.Error('400', 'Empty comment');
|
|
||||||
if (text.length > MAX_LEN) throw new Meteor.Error('400', 'Comment too long');
|
|
||||||
Comments.update(commentId, {
|
|
||||||
$set: { content: text, media: getMediaFromContent(text), updatedAt: new Date() }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'comments.remove': function commentsRemove(commentId) {
|
|
||||||
check(commentId, String);
|
|
||||||
if (!this.userId) throw new Meteor.Error('403', 'Login required');
|
|
||||||
requireOwner(commentId, this.userId);
|
|
||||||
Comments.remove(commentId);
|
|
||||||
},
|
|
||||||
|
|
||||||
'comments.like': function commentsLike(commentId) {
|
|
||||||
toggle(commentId, 'likes', 'dislikes', this.userId);
|
|
||||||
},
|
|
||||||
|
|
||||||
'comments.dislike': function commentsDislike(commentId) {
|
|
||||||
toggle(commentId, 'dislikes', 'likes', this.userId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
rateLimit({
|
|
||||||
methods: ['comments.insert', 'comments.edit', 'comments.remove', 'comments.like', 'comments.dislike'],
|
|
||||||
limit: 5,
|
|
||||||
timeRange: 1000
|
|
||||||
});
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
/* eslint-disable import/no-absolute-path */
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import i18n from 'i18next';
|
|
||||||
import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email';
|
|
||||||
import getEmailOf from '/imports/modules/get-email-of-user';
|
|
||||||
import Comments from '/imports/api/Comments/Comments';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When a comment is added to a fire, email the other users who commented on the
|
|
||||||
* same fire (uniq, excluding the author). Ported from the old
|
|
||||||
* startup/server/comments.js `onEvent` handler.
|
|
||||||
*/
|
|
||||||
export default function onCommentAdd(payload) {
|
|
||||||
const { referenceId, userId } = payload;
|
|
||||||
const query = { referenceId, userId: { $ne: userId } };
|
|
||||||
|
|
||||||
Comments.rawCollection().distinct('userId', query).then((users) => {
|
|
||||||
const path = referenceId.replace(/fire-/, 'fire/archive/');
|
|
||||||
const fireUrl = Meteor.absoluteUrl(path);
|
|
||||||
Meteor.users.find({ _id: { $in: users } }).forEach((user) => {
|
|
||||||
const { firstName, emailAddress } = getEmailOf(user);
|
|
||||||
if (emailAddress) {
|
|
||||||
const emailOpts = {
|
|
||||||
to: emailAddress,
|
|
||||||
subject: subjectTruncate.apply(i18n.t('Hay más información sobre un fuego')),
|
|
||||||
lang: user.lang,
|
|
||||||
template: 'new-fire-comment',
|
|
||||||
templateVars: {
|
|
||||||
applicationName: i18n.t('AppName'),
|
|
||||||
firstName,
|
|
||||||
fireUrl
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sendEmail(emailOpts).catch((error) => {
|
|
||||||
console.warn(`comments new-fire-comment mail failed: ${error}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.warn(`comments distinct() failed: ${error}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
/* eslint-disable import/no-absolute-path */
|
|
||||||
/* eslint-disable prefer-arrow-callback */
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import { check } from 'meteor/check';
|
|
||||||
import Comments from '/imports/api/Comments/Comments';
|
|
||||||
|
|
||||||
// Comments for one reference (a fire page), oldest first.
|
|
||||||
Meteor.publish('comments.forReference', function commentsForReference(referenceId) {
|
|
||||||
check(referenceId, String);
|
|
||||||
return Comments.find(
|
|
||||||
{ referenceId },
|
|
||||||
{ sort: { createdAt: 1 } }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -84,7 +84,7 @@ const find = (collection, northEastLng, northEastLat, southWestLng, southWestLat
|
||||||
return fires;
|
return fires;
|
||||||
};
|
};
|
||||||
|
|
||||||
Meteor.publish('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
|
Meteor.publishTransformed('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
|
||||||
// latitude -90 and 90 and the longitude between -180 and 180
|
// latitude -90 and 90 and the longitude between -180 and 180
|
||||||
check(northEastLng, NumberBetween(-180, 180));
|
check(northEastLng, NumberBetween(-180, 180));
|
||||||
check(southWestLat, NumberBetween(-90, 90));
|
check(southWestLat, NumberBetween(-90, 90));
|
||||||
|
|
@ -94,7 +94,7 @@ Meteor.publish('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLn
|
||||||
return find(FalsePositives, northEastLng, northEastLat, southWestLng, southWestLat);
|
return find(FalsePositives, northEastLng, northEastLat, southWestLng, southWestLat);
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.publish('industriesMyloc', function industriesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
|
Meteor.publishTransformed('industriesMyloc', function industriesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
|
||||||
// latitude -90 and 90 and the longitude between -180 and 180
|
// latitude -90 and 90 and the longitude between -180 and 180
|
||||||
check(northEastLng, NumberBetween(-180, 180));
|
check(northEastLng, NumberBetween(-180, 180));
|
||||||
check(southWestLat, NumberBetween(-90, 90));
|
check(southWestLat, NumberBetween(-90, 90));
|
||||||
|
|
|
||||||
153
imports/modules/server/notificationsProcess.js
Normal file
153
imports/modules/server/notificationsProcess.js
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import Notifications from '/imports/api/Notifications/Notifications';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { dateLongFormat } from '/imports/api/Common/dates';
|
||||||
|
// import sendMail from '/imports/startup/server/email';
|
||||||
|
import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email';
|
||||||
|
import { isMailServerMaster } from '/imports/startup/server/email';
|
||||||
|
// import { hr } from '/imports/startup/server/email';
|
||||||
|
import getEmailOf from '/imports/modules/get-email-of-user';
|
||||||
|
import image from 'google-maps-image-api-url';
|
||||||
|
import gcm from 'node-gcm';
|
||||||
|
import { trim } from '/imports/ui/components/NotificationsObserver/util.js';
|
||||||
|
import ravenLogger from '/imports/startup/server/ravenLogger';
|
||||||
|
|
||||||
|
let validFcmSender = true;
|
||||||
|
|
||||||
|
if (!(Meteor.settings.private.fcmApiToken && Meteor.settings.private.fcmApiToken.length > 0)) {
|
||||||
|
console.warn('Missing settings.private.fcmApiToken key, mobile notifications will not work');
|
||||||
|
validFcmSender = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.npmjs.com/package/google-maps-image-api-url
|
||||||
|
// https://stackoverflow.com/questions/24355007/is-there-no-way-to-embed-a-google-map-into-an-html-email
|
||||||
|
// https://developers.google.com/maps/documentation/static-maps/intro
|
||||||
|
function imgUrl(lat, lng) {
|
||||||
|
return image({
|
||||||
|
key: Meteor.settings.gmaps.key,
|
||||||
|
type: 'staticmap',
|
||||||
|
center: `${lat},${lng}`,
|
||||||
|
size: '640x480',
|
||||||
|
zoom: 16,
|
||||||
|
maptype: 'hybrid',
|
||||||
|
language: 'es',
|
||||||
|
markers: `icon: ${Meteor.settings.private.fireIconUrl}|${lat},${lng}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
function imgEl(lat, lng) {
|
||||||
|
return `<img src="${imgUrl(lat, lng)}" width="640" height="480"/>`;
|
||||||
|
} */
|
||||||
|
|
||||||
|
// Cutover flag (fase 1a): channels listed in
|
||||||
|
// Meteor.settings.private.notifDisabledChannels (e.g. ['mobile','web']) are
|
||||||
|
// handled by the tcef-notifications microservice, so the old observer/cron must
|
||||||
|
// NOT send them. Mutual exclusion per channel — reversible by editing settings,
|
||||||
|
// no code deploy needed. ES5-compatible on purpose (Meteor 1.6 / Node 8).
|
||||||
|
const notifDisabledChannels = (Meteor.settings.private && Meteor.settings.private.notifDisabledChannels) || [];
|
||||||
|
|
||||||
|
const processNotif = (notif) => {
|
||||||
|
if (notifDisabledChannels.indexOf(notif.type) !== -1) {
|
||||||
|
// This channel was migrated to tcef-notifications; do nothing here.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isMailServerMaster && validFcmSender && notif.type === 'mobile' && notif.notified !== true) {
|
||||||
|
const fcmSender = new gcm.Sender(Meteor.settings.private.fcmApiToken);
|
||||||
|
const user = Meteor.users.findOne({ _id: notif.userId });
|
||||||
|
moment.locale(user.lang);
|
||||||
|
// duplicate code below
|
||||||
|
const body = `${trim(notif.content)}`;
|
||||||
|
|
||||||
|
// https://firebase.google.com/docs/cloud-messaging/concept-options
|
||||||
|
const msg = new gcm.Message();
|
||||||
|
|
||||||
|
msg.addNotification({
|
||||||
|
title: i18n.t('Alerta de fuego'),
|
||||||
|
body,
|
||||||
|
click_action: 'FLUTTER_NOTIFICATION_CLICK',
|
||||||
|
tag: notif._id, // prevent duplication of fire notifications
|
||||||
|
sound: 'default', // Indicates sound to be played. Supports only default currently.
|
||||||
|
icon: 'launch_image' // 'ic_launcher'
|
||||||
|
});
|
||||||
|
|
||||||
|
msg.addData('id', notif._id._str);
|
||||||
|
msg.addData('description', body);
|
||||||
|
msg.addData('lat', notif.geo.coordinates[1]);
|
||||||
|
msg.addData('lon', notif.geo.coordinates[0]);
|
||||||
|
msg.addData('when', notif.when);
|
||||||
|
msg.addData('subsId', notif.subsId._str);
|
||||||
|
msg.addData('sealed', notif.sealed);
|
||||||
|
|
||||||
|
const registrationTokens = [];
|
||||||
|
if (!user.fireBaseToken) {
|
||||||
|
console.warn('This mobile user doesn\'t have a firebase registration token');
|
||||||
|
} else {
|
||||||
|
registrationTokens.push(user.fireBaseToken);
|
||||||
|
// FIXME: better join users
|
||||||
|
if (validFcmSender) {
|
||||||
|
fcmSender.send(msg, { registrationTokens }, Meteor.bindEnvironment(function processResult(err, response) {
|
||||||
|
if (err) {
|
||||||
|
console.error(`FCM error: ${err}`);
|
||||||
|
// FIXME send to sentry
|
||||||
|
ravenLogger.log(err);
|
||||||
|
} else {
|
||||||
|
// console.log(`FCM response: ${response}`);
|
||||||
|
Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isMailServerMaster && notif.type === 'web' && notif.emailNotified !== true) {
|
||||||
|
const user = Meteor.users.findOne({ _id: notif.userId });
|
||||||
|
const { firstName, emailAddress } = getEmailOf(user);
|
||||||
|
|
||||||
|
if (emailAddress) {
|
||||||
|
const img = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
|
||||||
|
// const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
|
||||||
|
const fireUrl = `${Meteor.absoluteUrl('fire/')}${notif.sealed}`;
|
||||||
|
// const fireHtmlUrl = `<a href="${fireUrl}">${i18n.t('Más información sobre este fuego')}</a>`;
|
||||||
|
// TODO get _id of fire
|
||||||
|
// const fireTextUrl = `${i18n.t('Más información sobre este fuego')}:\n${fireUrl}`;
|
||||||
|
// FIXME use our map as url and static map as img
|
||||||
|
moment.locale(user.lang);
|
||||||
|
// moment user tz ?
|
||||||
|
const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`;
|
||||||
|
|
||||||
|
// TODO Comunes Address
|
||||||
|
|
||||||
|
const emailOpts = {
|
||||||
|
to: emailAddress,
|
||||||
|
// userName: firstName,
|
||||||
|
// sendAt: new Date(),
|
||||||
|
subject: subjectTruncate.apply(message),
|
||||||
|
// text: `${message}\n\n${fireTextUrl}\n\n`,
|
||||||
|
// template: '<body><h2>{{appName}}</h2>{{{html}}}</body>',
|
||||||
|
lang: user.lang,
|
||||||
|
template: 'new-fire',
|
||||||
|
templateVars: {
|
||||||
|
applicationName: i18n.t('AppName'),
|
||||||
|
firstName,
|
||||||
|
message,
|
||||||
|
fireUrl,
|
||||||
|
img,
|
||||||
|
subsUrl: Meteor.absoluteUrl('subscriptions')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sendEmail(emailOpts).catch((error) => {
|
||||||
|
throw new Meteor.Error('500', `${error}`);
|
||||||
|
});
|
||||||
|
// sendMail(emailOpts, true);
|
||||||
|
Notifications.update(notif._id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } });
|
||||||
|
} else {
|
||||||
|
// Not email or not verified -> remove notif so we don't retry to send it
|
||||||
|
Notifications.remove({ _id: notif._id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default processNotif;
|
||||||
42
imports/startup/client/comments.js
Normal file
42
imports/startup/client/comments.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* global Comments */
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
|
import i18n from '/imports/startup/client/i18n';
|
||||||
|
import '/imports/startup/common/comments';
|
||||||
|
|
||||||
|
import './comments.scss';
|
||||||
|
|
||||||
|
i18n.init((err, t) => {
|
||||||
|
Comments.ui.setContent({
|
||||||
|
title: ' ', // i18n.t('Comentarios'),
|
||||||
|
save: t('Guardar'),
|
||||||
|
reply: t('Responder'),
|
||||||
|
edit: t('Editar'),
|
||||||
|
remove: t('Borrar'),
|
||||||
|
'placeholder-textarea': t('Añadir un comentario'),
|
||||||
|
'add-button-reply': t('Añadir una respuesta'),
|
||||||
|
'add-button': t('Añadir comentario'),
|
||||||
|
'you-need-to-login': t('Necesitas iniciar sesión para'),
|
||||||
|
'add comments': t('añadir comentarios'),
|
||||||
|
'like comments': t('puntuar comentarios'),
|
||||||
|
'rate comments': t('puntuar comentarios'),
|
||||||
|
'add replies': t('responder'),
|
||||||
|
'load-more': t('Más comentarios')
|
||||||
|
});
|
||||||
|
|
||||||
|
/* This in client side */
|
||||||
|
Comments.ui.config({
|
||||||
|
limit: 20, // default 10
|
||||||
|
loadMoreCount: 20, // default 20
|
||||||
|
/* generateAvatar: function genAvatar(user, isAnonymous) {
|
||||||
|
if (isAnonymous) {
|
||||||
|
return i18n.t('Anónimo');
|
||||||
|
}
|
||||||
|
return user.profile && user.profile.name && user.profile.name.first ? user.profile.name.first : null;
|
||||||
|
}, */
|
||||||
|
template: 'bootstrap', // default 'semantic-ui'
|
||||||
|
// default 'http://s3.amazonaws.com/37assets/svn/765-default-avatar.png'
|
||||||
|
defaultAvatar: '/default-avatar.png',
|
||||||
|
markdown: true
|
||||||
|
});
|
||||||
|
});
|
||||||
29
imports/startup/client/comments.scss
Normal file
29
imports/startup/client/comments.scss
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
.comments-section {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-box {
|
||||||
|
max-width: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.comment-content {
|
||||||
|
/* white-space: pre-line; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* remove comment btn danger style */
|
||||||
|
|
||||||
|
div.media-body.comment> div> div> div.btn.btn-danger.remove-action {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid rgb(204, 204, 204);
|
||||||
|
color: rgb(51, 51, 51);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* text-area height */
|
||||||
|
.form-control .create-comment {
|
||||||
|
height: 9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-avatar {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
21
imports/startup/common/comments.js
Normal file
21
imports/startup/common/comments.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* global Comments */
|
||||||
|
|
||||||
|
// Client and Server
|
||||||
|
Comments.config({
|
||||||
|
rating: 'likes-and-dislikes',
|
||||||
|
allowAnonymous: () => false,
|
||||||
|
allowReplies: () => false, // disabled right now: I don't know hot to get referenceId of replies
|
||||||
|
anonymousSalt: 'klasddl3lala0l3lasdlas0ol3lasdlao3lasdoaslaldal3lasdclasdlal3lasdladlaq',
|
||||||
|
publishUserFields: {
|
||||||
|
profile: 1
|
||||||
|
},
|
||||||
|
mediaAnalyzers: [
|
||||||
|
Comments.analyzers.image,
|
||||||
|
Comments.analyzers.youtube
|
||||||
|
],
|
||||||
|
generateUsername: function genUser(user) {
|
||||||
|
// console.log(JSON.stringify(user));
|
||||||
|
// FIXME
|
||||||
|
return user.profile && user.profile.name && user.profile.name.first ? user.profile.name.first : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -21,16 +21,11 @@ function isPrivateIP(ip) {
|
||||||
const dbpath = '/usr/local/share/maxmind-geolite2/GeoLite2-City.mmdb';
|
const dbpath = '/usr/local/share/maxmind-geolite2/GeoLite2-City.mmdb';
|
||||||
// const dbpath = `${process.env.PWD}/private/GeoLite2-City.mmdb`;
|
// const dbpath = `${process.env.PWD}/private/GeoLite2-City.mmdb`;
|
||||||
|
|
||||||
let IPGeocoder;
|
if (!fs.existsSync(dbpath)) {
|
||||||
if (fs.existsSync(dbpath)) {
|
|
||||||
IPGeocoder = maxmind.openSync(dbpath);
|
|
||||||
} else {
|
|
||||||
// In production the DB is provisioned via cron; in local dev it may be
|
|
||||||
// absent. Degrade gracefully instead of crashing at boot: localize() below
|
|
||||||
// already falls back to a default location when no geo data is available.
|
|
||||||
console.error(`Maxmind db not found ${dbpath}, download via cron with https://www.npmjs.com/package/maxmind-geolite2-mirror`);
|
console.error(`Maxmind db not found ${dbpath}, download via cron with https://www.npmjs.com/package/maxmind-geolite2-mirror`);
|
||||||
IPGeocoder = { get() { return null; } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IPGeocoder = maxmind.openSync(dbpath);
|
||||||
export default IPGeocoder;
|
export default IPGeocoder;
|
||||||
|
|
||||||
// Warning: Meteor cannot access to this.connection with arrow functions
|
// Warning: Meteor cannot access to this.connection with arrow functions
|
||||||
|
|
@ -52,7 +47,7 @@ export function localize() {
|
||||||
// http://dev.maxmind.com/geoip/geoip2/geolite2/
|
// http://dev.maxmind.com/geoip/geoip2/geolite2/
|
||||||
const geo = IPGeocoder.get(clientIP);
|
const geo = IPGeocoder.get(clientIP);
|
||||||
// console.warn(geo);
|
// console.warn(geo);
|
||||||
if (geo && geo.location && geo.location.latitude && geo.location.longitude) {
|
if (geo.location && geo.location.latitude && geo.location.longitude) {
|
||||||
return geo;
|
return geo;
|
||||||
}
|
}
|
||||||
// geoIP fallback, Madrid
|
// geoIP fallback, Madrid
|
||||||
|
|
|
||||||
80
imports/startup/server/comments.js
Normal file
80
imports/startup/server/comments.js
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
/* global Comments */
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email';
|
||||||
|
import getEmailOf from '/imports/modules/get-email-of-user';
|
||||||
|
import '../common/comments';
|
||||||
|
|
||||||
|
/* import i18n from 'i18next';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { dateLongFormat } from '/imports/api/Common/dates';
|
||||||
|
import Notifications from '/imports/api/Notifications/Notifications';
|
||||||
|
// import sendMail from '/imports/startup/server/email';
|
||||||
|
// import { hr } from '/imports/startup/server/email';
|
||||||
|
import getOAuthProfile from '/imports/modules/get-oauth-profile';
|
||||||
|
import image from 'google-maps-image-api-url';
|
||||||
|
import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; */
|
||||||
|
|
||||||
|
Meteor.startup(() => {
|
||||||
|
// On Server or Client (preferably on Server)
|
||||||
|
Comments.config({
|
||||||
|
onEvent: (name, action, payload) => {
|
||||||
|
// e.g send a mail
|
||||||
|
// console.log(`name: ${name}, action: ${action}, payload: ${JSON.stringify(payload)}`);
|
||||||
|
|
||||||
|
// Samples:
|
||||||
|
|
||||||
|
// name: comment, action: add, payload: {"referenceId":"fire-56a9cc5a5586879eb9cc6dd7","content":"asdfasdf asd fasd fas","userId":"tdK4e43ikjDXct7Gj","isAnonymous":false,"createdAt":"2018-03-04T14:53:40.319Z","likes":[],"dislikes":[],"replies":[],"media":{},"status":"approved","starRatings":[],"ratingScore":0,"_id":"mfAe3HL9qNofhiAnh"}
|
||||||
|
// name: reply, action: add, payload: {"replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":[],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":0,"_id":"DyHt28rF6rTDnC5fP","rootUserId":"tdK4e43ikjDXct7Gj"}
|
||||||
|
// name: reply, action: like, payload: {"replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":["tdK4e43ikjDXct7Gj"],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":1,"_id":"DyHt28rF6rTDnC5fP","ratedUserId":"tdK4e43ikjDXct7Gj","rootUserId":"tdK4e43ikjDXct7Gj"}
|
||||||
|
// name: comment, action: like, payload: {"_id":"mfAe3HL9qNofhiAnh","ratedUserId":"tdK4e43ikjDXct7Gj"}
|
||||||
|
// name: reply, action: edit, payload: {"_id":"DyHt28rF6rTDnC5fP","replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf dasd fadsfa","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":[],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":0,"starRatings":[],"ratedUserId":"tdK4e43ikjDXct7Gj","rootUserId":"tdK4e43ikjDXct7Gj"}
|
||||||
|
// name: reply, action: remove, payload: {"_id":"DyHt28rF6rTDnC5fP","rootUserId":"tdK4e43ikjDXct7Gj"}
|
||||||
|
|
||||||
|
// name: (comment|reply)
|
||||||
|
// action: (add|like|edit|remove)
|
||||||
|
|
||||||
|
if (name === 'comment' && action.match(/add|like|edit/)) {
|
||||||
|
if (action === 'add') {
|
||||||
|
// Check for other users that did comments and send an email (uniq users, and not this user)
|
||||||
|
// console.log(payload.referenceId);
|
||||||
|
const query = payload.isAnonymous ?
|
||||||
|
{ referenceId: payload.referenceId } :
|
||||||
|
{ referenceId: payload.referenceId, userId: { $ne: payload.userId } };
|
||||||
|
Comments.getCollection().rawCollection().distinct('userId', query).then((users) => {
|
||||||
|
// console.log(users);
|
||||||
|
const path = payload.referenceId.replace(/fire-/, 'fire/archive/');
|
||||||
|
const fireUrl = Meteor.absoluteUrl(path);
|
||||||
|
// console.log(fireUrl);
|
||||||
|
Meteor.users.find({ _id: { $in: users } }).forEach((user) => {
|
||||||
|
const { firstName, emailAddress } = getEmailOf(user);
|
||||||
|
// console.log(JSON.stringify(user));
|
||||||
|
if (emailAddress) {
|
||||||
|
const emailOpts = {
|
||||||
|
to: emailAddress,
|
||||||
|
subject: subjectTruncate.apply(i18n.t('Hay más información sobre un fuego')),
|
||||||
|
lang: user.lang,
|
||||||
|
template: 'new-fire-comment',
|
||||||
|
templateVars: {
|
||||||
|
applicationName: i18n.t('AppName'),
|
||||||
|
firstName,
|
||||||
|
fireUrl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sendEmail(emailOpts).catch((error) => {
|
||||||
|
throw new Meteor.Error('500', `${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name === 'reply' && action.match(/add|like|edit/)) {
|
||||||
|
// Check for other previous users to this thread and send an email
|
||||||
|
// console.log('Reply op');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import { tweetIberiaFires, tweetEuropeFires } from '/imports/api/ActiveFires/server/tweetFiresInZone';
|
import { tweetIberiaFires, tweetEuropeFires } from '/imports/api/ActiveFires/server/tweetFiresInZone';
|
||||||
import { isMailServerMaster, sendEmailsFromQueue } from '/imports/startup/server/email';
|
import { isMailServerMaster, sendEmailsFromQueue } from '/imports/startup/server/email';
|
||||||
|
import Notifications from '/imports/api/Notifications/Notifications';
|
||||||
|
import processNotif from '/imports/modules/server/notificationsProcess.js';
|
||||||
|
|
||||||
// https://github.com/thesaucecode/meteor-synced-cron/
|
// https://github.com/thesaucecode/meteor-synced-cron/
|
||||||
|
|
||||||
|
|
@ -53,11 +55,29 @@ Meteor.startup(() => {
|
||||||
job: () => sendEmailsFromQueue()
|
job: () => sendEmailsFromQueue()
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: the "Process pending notif" job (push via node-gcm + notification
|
SyncedCron.add({
|
||||||
// emails) was removed here — that responsibility now lives in the
|
name: 'Process pending notif',
|
||||||
// tcef-notifications microservice (fases 1a-1c). See UPGRADE.md for the
|
timezone: 'Europe/Madrid',
|
||||||
// deploy-ordering dependency (the old code must not be shut off in
|
schedule: (parser) => {
|
||||||
// production until the new service is emitting).
|
// http://bunkat.github.io/later/
|
||||||
|
const sched = parser.text('every 15 min');
|
||||||
|
if (sched.error !== -1) {
|
||||||
|
console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`);
|
||||||
|
}
|
||||||
|
return sched;
|
||||||
|
},
|
||||||
|
job: () => {
|
||||||
|
const mobileNotif = Notifications.find({ nofitied: null, type: 'mobile' });
|
||||||
|
mobileNotif.forEach((notif) => {
|
||||||
|
processNotif(notif);
|
||||||
|
});
|
||||||
|
const emailNotif = Notifications.find({ emailNofitied: null, type: 'web' });
|
||||||
|
emailNotif.forEach((notif) => {
|
||||||
|
processNotif(notif);
|
||||||
|
});
|
||||||
|
return { mobile: mobileNotif.count(), web: emailNotif.count() };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const esEn = Meteor.settings.private.twitter.es.enabled;
|
const esEn = Meteor.settings.private.twitter.es.enabled;
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ import './accounts';
|
||||||
import './api';
|
import './api';
|
||||||
import './fixtures';
|
import './fixtures';
|
||||||
import './email';
|
import './email';
|
||||||
import '/imports/api/Comments/server';
|
|
||||||
import './IPGeocoder';
|
import './IPGeocoder';
|
||||||
import './migrations';
|
import './migrations';
|
||||||
|
import './notificationsObserver';
|
||||||
import './facts';
|
import './facts';
|
||||||
|
import './comments';
|
||||||
import './sitemaps';
|
import './sitemaps';
|
||||||
import './subsUnion';
|
import './subsUnion';
|
||||||
import './prerender';
|
import './prerender';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
/* global Migrations */
|
/* global Migrations, Comments */
|
||||||
/* eslint-disable import/no-absolute-path */
|
/* eslint-disable import/no-absolute-path */
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import Comments from '/imports/api/Comments/Comments';
|
|
||||||
import { Accounts } from 'meteor/accounts-base';
|
import { Accounts } from 'meteor/accounts-base';
|
||||||
import randomHex from 'crypto-random-hex';
|
import randomHex from 'crypto-random-hex';
|
||||||
import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions';
|
import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions';
|
||||||
|
|
@ -164,7 +163,7 @@ Meteor.startup(() => {
|
||||||
Migrations.add({
|
Migrations.add({
|
||||||
version: 11,
|
version: 11,
|
||||||
up: function noAnonComments() {
|
up: function noAnonComments() {
|
||||||
Comments.remove({ isAnonymous: true });
|
Comments.getCollection().remove({ isAnonymous: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
19
imports/startup/server/notificationsObserver.js
Normal file
19
imports/startup/server/notificationsObserver.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import Notifications from '/imports/api/Notifications/Notifications';
|
||||||
|
import processNotif from '/imports/modules/server/notificationsProcess.js';
|
||||||
|
import { isMailServerMaster } from '/imports/startup/server/email';
|
||||||
|
|
||||||
|
Meteor.startup(() => {
|
||||||
|
if (isMailServerMaster) {
|
||||||
|
Notifications.find().observe({
|
||||||
|
added: function notifAdded(notif) {
|
||||||
|
processNotif(notif);
|
||||||
|
},
|
||||||
|
changed: function notifChanged(updatedNotif) { // , oldNotif) {
|
||||||
|
processNotif(updatedNotif);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/* global sitemaps */
|
/* global sitemaps Comments */
|
||||||
/* eslint-disable import/no-absolute-path */
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
import Fires from '/imports/api/Fires/Fires';
|
import Fires from '/imports/api/Fires/Fires';
|
||||||
import Comments from '/imports/api/Comments/Comments';
|
// import Comments from '/imports/api/Comments/Comments';
|
||||||
|
|
||||||
const firesMapEnabled = false;
|
const firesMapEnabled = false;
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ sitemaps.add('/sitemap.xml', () => {
|
||||||
if (firesMapEnabled) {
|
if (firesMapEnabled) {
|
||||||
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
|
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
|
||||||
// Search the last comment of tha fire
|
// Search the last comment of tha fire
|
||||||
const lastComment = Comments.findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
|
const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
|
||||||
out.push({
|
out.push({
|
||||||
page: `/fire/archive/${fire._id._str}`,
|
page: `/fire/archive/${fire._id._str}`,
|
||||||
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
|
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
.comments-section {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments-box {
|
|
||||||
max-width: inherit;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment {
|
|
||||||
padding: 0.75em 0;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-author {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-date {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.comment-content {
|
|
||||||
white-space: pre-line;
|
|
||||||
margin: 0.4em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-media-image {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-media-youtube iframe {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 240px;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-actions .btn {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-actions .btn.active {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* keep the previous "remove" button neutral (not danger red) */
|
|
||||||
.comment-owner-actions .remove-action {
|
|
||||||
color: rgb(51, 51, 51);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-form .create-comment {
|
|
||||||
height: 9em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-login-hint {
|
|
||||||
color: #888;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-avatar {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
/* eslint-disable import/no-absolute-path */
|
|
||||||
/* eslint-disable jsx-a11y/media-has-caption */
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withTracker } from 'meteor/react-meteor-data';
|
|
||||||
import { translate } from 'react-i18next';
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import moment from 'moment';
|
|
||||||
import CommentsCollection from '/imports/api/Comments/Comments';
|
|
||||||
import './Comments.scss';
|
|
||||||
|
|
||||||
function MediaEmbed({ media }) {
|
|
||||||
if (!media || !media.content) return null;
|
|
||||||
if (media.type === 'image') {
|
|
||||||
return <img className="comment-media-image" src={media.content} alt="" />;
|
|
||||||
}
|
|
||||||
if (media.type === 'youtube') {
|
|
||||||
return (
|
|
||||||
<div className="comment-media-youtube">
|
|
||||||
<iframe title="youtube" src={media.content} frameBorder="0" allowFullScreen />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MediaEmbed.propTypes = { media: PropTypes.object };
|
|
||||||
MediaEmbed.defaultProps = { media: null };
|
|
||||||
|
|
||||||
class CommentsBox extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = { draft: '', editingId: null, editDraft: '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
call(method, ...args) {
|
|
||||||
Meteor.call(method, ...args, (err) => {
|
|
||||||
if (err) console.warn(`${method}: ${err.reason || err.message}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
submit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const content = this.state.draft.trim();
|
|
||||||
if (!content) return;
|
|
||||||
this.setState({ draft: '' });
|
|
||||||
this.call('comments.insert', this.props.referenceId, content);
|
|
||||||
};
|
|
||||||
|
|
||||||
saveEdit = (id) => {
|
|
||||||
const content = this.state.editDraft.trim();
|
|
||||||
if (!content) return;
|
|
||||||
this.setState({ editingId: null, editDraft: '' });
|
|
||||||
this.call('comments.edit', id, content);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderComment(c) {
|
|
||||||
const { t } = this.props;
|
|
||||||
const currentUserId = Meteor.userId();
|
|
||||||
const isOwner = currentUserId && c.userId === currentUserId;
|
|
||||||
const likes = (c.likes || []).length;
|
|
||||||
const dislikes = (c.dislikes || []).length;
|
|
||||||
const liked = currentUserId && (c.likes || []).includes(currentUserId);
|
|
||||||
const disliked = currentUserId && (c.dislikes || []).includes(currentUserId);
|
|
||||||
const author = c.username || t('Anónimo');
|
|
||||||
return (
|
|
||||||
<li key={c._id} className="comment">
|
|
||||||
<div className="comment-header">
|
|
||||||
<span className="comment-author">{author}</span>
|
|
||||||
<span className="comment-date">{moment(c.createdAt).format('LLL')}</span>
|
|
||||||
</div>
|
|
||||||
{this.state.editingId === c._id ? (
|
|
||||||
<div className="comment-edit">
|
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
value={this.state.editDraft}
|
|
||||||
onChange={ev => this.setState({ editDraft: ev.target.value })}
|
|
||||||
/>
|
|
||||||
<button type="button" className="btn btn-primary btn-sm" onClick={() => this.saveEdit(c._id)}>{t('Guardar')}</button>
|
|
||||||
<button type="button" className="btn btn-link btn-sm" onClick={() => this.setState({ editingId: null })}>{t('Borrar')}</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// Plain text (whitespace preserved via CSS). Rendered as text — not
|
|
||||||
// HTML — so user content can never inject markup (safer than the old
|
|
||||||
// markdown-to-HTML comments package).
|
|
||||||
<p className="comment-content">{c.content}</p>
|
|
||||||
)}
|
|
||||||
<MediaEmbed media={c.media} />
|
|
||||||
<div className="comment-actions">
|
|
||||||
<button type="button" className={`btn btn-link btn-sm${liked ? ' active' : ''}`} disabled={!currentUserId} onClick={() => this.call('comments.like', c._id)}>
|
|
||||||
{'👍'} {likes}
|
|
||||||
</button>
|
|
||||||
<button type="button" className={`btn btn-link btn-sm${disliked ? ' active' : ''}`} disabled={!currentUserId} onClick={() => this.call('comments.dislike', c._id)}>
|
|
||||||
{'👎'} {dislikes}
|
|
||||||
</button>
|
|
||||||
{isOwner && (
|
|
||||||
<span className="comment-owner-actions">
|
|
||||||
<button type="button" className="btn btn-link btn-sm" onClick={() => this.setState({ editingId: c._id, editDraft: c.content })}>{t('Editar')}</button>
|
|
||||||
<button type="button" className="btn btn-link btn-sm remove-action" onClick={() => this.call('comments.remove', c._id)}>{t('Borrar')}</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { t, comments, ready } = this.props;
|
|
||||||
const loggedIn = !!Meteor.userId();
|
|
||||||
return (
|
|
||||||
<div className="comments-box">
|
|
||||||
<ul className="comments-list">
|
|
||||||
{comments.map(c => this.renderComment(c))}
|
|
||||||
</ul>
|
|
||||||
{ready && comments.length === 0 && (
|
|
||||||
<p className="comments-empty">{t('Añadir un comentario')}</p>
|
|
||||||
)}
|
|
||||||
{loggedIn ? (
|
|
||||||
<form className="comment-form" onSubmit={this.submit}>
|
|
||||||
<textarea
|
|
||||||
className="form-control create-comment"
|
|
||||||
placeholder={t('Añadir un comentario')}
|
|
||||||
value={this.state.draft}
|
|
||||||
onChange={ev => this.setState({ draft: ev.target.value })}
|
|
||||||
/>
|
|
||||||
<button type="submit" className="btn btn-primary">{t('Añadir comentario')}</button>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<p className="comment-login-hint">
|
|
||||||
{t('Necesitas iniciar sesión para')} {t('añadir comentarios')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CommentsBox.propTypes = {
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
referenceId: PropTypes.string.isRequired,
|
|
||||||
comments: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
ready: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
const CommentsBoxContainer = withTracker(({ referenceId }) => {
|
|
||||||
const handle = Meteor.subscribe('comments.forReference', referenceId);
|
|
||||||
return {
|
|
||||||
ready: handle.ready(),
|
|
||||||
comments: CommentsCollection.find({ referenceId }, { sort: { createdAt: 1 } }).fetch()
|
|
||||||
};
|
|
||||||
})(CommentsBox);
|
|
||||||
|
|
||||||
export default translate([], { wait: true })(CommentsBoxContainer);
|
|
||||||
|
|
@ -12,13 +12,14 @@ import { Bert } from 'meteor/themeteorchef:bert';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Map, GeoJSON } from 'react-leaflet';
|
import { Map, GeoJSON } from 'react-leaflet';
|
||||||
import { rectangleAround } from 'map-common-utils';
|
import { rectangleAround } from 'map-common-utils';
|
||||||
|
import Blaze from 'meteor/gadicc:blaze-react-component';
|
||||||
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
|
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
|
||||||
import NotFound from '/imports/ui/pages/NotFound/NotFound';
|
import NotFound from '/imports/ui/pages/NotFound/NotFound';
|
||||||
import FiresCollection from '/imports/api/Fires/Fires';
|
import FiresCollection from '/imports/api/Fires/Fires';
|
||||||
import FireList from '/imports/ui/components/Maps/FireList';
|
import FireList from '/imports/ui/components/Maps/FireList';
|
||||||
import FromNow from '/imports/ui/components/FromNow/FromNow';
|
import FromNow from '/imports/ui/components/FromNow/FromNow';
|
||||||
import { dateLongFormat, dateYYYYMMDD } from '/imports/api/Common/dates';
|
import { dateLongFormat, dateYYYYMMDD } from '/imports/api/Common/dates';
|
||||||
import CommentsBox from '/imports/ui/components/Comments/CommentsBox';
|
import '/imports/startup/client/comments';
|
||||||
import FalsePositiveTypes from '/imports/api/FalsePositives/FalsePositiveTypes';
|
import FalsePositiveTypes from '/imports/api/FalsePositives/FalsePositiveTypes';
|
||||||
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
|
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
|
||||||
import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries';
|
import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries';
|
||||||
|
|
@ -232,7 +233,7 @@ class Fire extends React.Component {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="comments-section">
|
<div className="comments-section">
|
||||||
<CommentsBox referenceId={`fire-${fire._id}`} />
|
<Blaze template="commentsBox" id={`fire-${fire._id}`} />
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7635
package-lock.json
generated
7635
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,7 @@
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.29.7",
|
"@babel/runtime": "^7.0.0-beta.44",
|
||||||
"@cleverbeagle/dates": "^0.5.1",
|
"@cleverbeagle/dates": "^0.5.1",
|
||||||
"@cleverbeagle/seeder": "^1.3.1",
|
"@cleverbeagle/seeder": "^1.3.1",
|
||||||
"@turf/buffer": "^5.1.5",
|
"@turf/buffer": "^5.1.5",
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
"modernizr": "^3.6.0",
|
"modernizr": "^3.6.0",
|
||||||
"moment": "^2.19.1",
|
"moment": "^2.19.1",
|
||||||
"moment-timezone": "^0.5.14",
|
"moment-timezone": "^0.5.14",
|
||||||
|
"node-gcm": "^1.0.2",
|
||||||
"node-geocoder": "^3.21.1",
|
"node-geocoder": "^3.21.1",
|
||||||
"node-gyp": "^3.7.0",
|
"node-gyp": "^3.7.0",
|
||||||
"nodemailer": "^4.4.2",
|
"nodemailer": "^4.4.2",
|
||||||
|
|
|
||||||
1
packages/meteor-comments-ui
Submodule
1
packages/meteor-comments-ui
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 4761da921f467838a2c01b34e84a7703ee49b12c
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
// We need a function that treats thrown errors exactly like Iron Router would.
|
|
||||||
// This file is written in JavaScript to enable copy-pasting Iron Router code.
|
|
||||||
|
|
||||||
// Taken from: https://github.com/iron-meteor/iron-router/blob/9c369499c98af9fd12ef9e68338dee3b1b1276aa/lib/router_server.js#L3
|
|
||||||
var env = process.env.NODE_ENV || 'development';
|
|
||||||
|
|
||||||
// Taken from: https://github.com/iron-meteor/iron-router/blob/9c369499c98af9fd12ef9e68338dee3b1b1276aa/lib/router_server.js#L47
|
|
||||||
ironRouterSendErrorToResponse = function (err, req, res) {
|
|
||||||
if (res.statusCode < 400)
|
|
||||||
res.statusCode = 500;
|
|
||||||
|
|
||||||
if (err.status)
|
|
||||||
res.statusCode = err.status;
|
|
||||||
|
|
||||||
if (env === 'development')
|
|
||||||
msg = (err.stack || err.toString()) + '\n';
|
|
||||||
else
|
|
||||||
//XXX get this from standard dict of error messages?
|
|
||||||
msg = 'Server error.';
|
|
||||||
|
|
||||||
console.error(err.stack || err.toString());
|
|
||||||
|
|
||||||
if (res.headersSent)
|
|
||||||
return req.socket.destroy();
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/html');
|
|
||||||
res.setHeader('Content-Length', Buffer.byteLength(msg));
|
|
||||||
if (req.method === 'HEAD')
|
|
||||||
return res.end();
|
|
||||||
res.end(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,34 +0,0 @@
|
||||||
Package.describe({
|
|
||||||
name: 'nimble:restivus',
|
|
||||||
summary: 'Create authenticated REST APIs in Meteor via HTTP/HTTPS. (Vendored fork: precompiled JS, accounts-password 2.x compatible.)',
|
|
||||||
version: '0.8.12',
|
|
||||||
git: 'https://github.com/kahmali/meteor-restivus.git'
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Vendored fork of nimble:restivus@0.8.12.
|
|
||||||
*
|
|
||||||
* Why: the published Atmosphere package pins accounts-password@1.3.3, which
|
|
||||||
* conflicts with Meteor 2.x (accounts-password 2.0). The upstream source is
|
|
||||||
* CoffeeScript, and the coffeescript build plugin crashes this build machine
|
|
||||||
* (node_contextify assertion). So the .coffee sources were precompiled to plain
|
|
||||||
* JS (lib/restivus-all.js) and this package.js drops the coffeescript build
|
|
||||||
* dependency and loosens the accounts-password constraint to 2.x.
|
|
||||||
*
|
|
||||||
* The REST behavior is unchanged — verified by the REST smoke test.
|
|
||||||
*/
|
|
||||||
Package.onUse(function (api) {
|
|
||||||
api.versionsFrom('METEOR@2.3');
|
|
||||||
|
|
||||||
api.use('check');
|
|
||||||
api.use('underscore');
|
|
||||||
api.use('accounts-password@2.0.0');
|
|
||||||
api.use('simple:json-routes');
|
|
||||||
|
|
||||||
// iron-router error helper defines the `ironRouterSendErrorToResponse` global
|
|
||||||
// used by the routes, so it must load first.
|
|
||||||
api.addFiles('lib/iron-router-error-to-response.js', 'server');
|
|
||||||
api.addFiles('lib/restivus-all.js', 'server');
|
|
||||||
|
|
||||||
api.export('Restivus', 'server');
|
|
||||||
});
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
# REST smoke test
|
|
||||||
|
|
||||||
Regression harness for the REST API in `imports/api/Rest/Rest.js` — the API the
|
|
||||||
Flutter app (`fires_flutter/lib/models/fires_api.dart`) depends on.
|
|
||||||
|
|
||||||
It calls every Flutter-consumed endpoint against a locally running dev server
|
|
||||||
with deterministic seed data, normalizes the intrinsically volatile fields
|
|
||||||
(uptime, generated ids, insert timestamps) and compares the responses to
|
|
||||||
committed snapshots in `smoke/snapshots/`.
|
|
||||||
|
|
||||||
**It must stay green after every Meteor upgrade escala. The snapshots are the
|
|
||||||
contract: any drift is treated as an upgrade bug.**
|
|
||||||
|
|
||||||
## Reproducible local startup
|
|
||||||
|
|
||||||
1. **MongoDB** (prod parity = 3.2; a dev-only instance in Docker):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --name tcef-mongo32 -p 27018:27017 mongo:3.2
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Meteor dev server** (from the repo root):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export PATH="$HOME/.meteor:$PATH"
|
|
||||||
export MONGO_URL="mongodb://localhost:27018/fuegos"
|
|
||||||
meteor --settings settings-development.json --port 3100
|
|
||||||
```
|
|
||||||
|
|
||||||
`settings-development.json` carries a dev-only `private.internalApiToken`
|
|
||||||
(`dev-smoke-token`) which is what enables the REST API. Without it the API
|
|
||||||
routes are not even registered (see `Rest.js`).
|
|
||||||
|
|
||||||
> Note: `IPGeocoder.js` degrades gracefully when the MaxMind GeoLite2 DB is
|
|
||||||
> absent (it is provisioned by cron in production) so the server boots in a
|
|
||||||
> clean checkout.
|
|
||||||
|
|
||||||
## Running the smoke test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./smoke/smoke.sh # seed + compare against baseline snapshots
|
|
||||||
./smoke/smoke.sh --update # seed + regenerate the baseline snapshots
|
|
||||||
```
|
|
||||||
|
|
||||||
Env overrides: `MONGO_CONTAINER` (default `tcef-mongo32`), `MONGO_DB`
|
|
||||||
(`fuegos`), `BASE_URL` (`http://localhost:3100`).
|
|
||||||
|
|
||||||
## What is covered
|
|
||||||
|
|
||||||
| Snapshot | Endpoint | Flutter call |
|
|
||||||
|---|---|---|
|
|
||||||
| `status_uptime` | `GET status/uptime` | — |
|
|
||||||
| `status_active_fires_count` | `GET status/active-fires-count` | — |
|
|
||||||
| `status_last_fire_check` | `GET status/last-fire-check` | — |
|
|
||||||
| `status_last_fire_detected` | `GET status/last-fire-detected` | — |
|
|
||||||
| `status_subs_public_union` | `GET status/subs-public-union/:token` | `getMonitoredAreas` |
|
|
||||||
| `fires_in` | `GET fires-in/:token/:lat/:lng/:km` | — |
|
|
||||||
| `fires_in_full` | `GET fires-in-full/:token/:lat/:lng/:km` | `getFiresInLocation` |
|
|
||||||
| `mobile_users_post` | `POST mobile/users` | `createUser` |
|
|
||||||
| `mobile_subscriptions_post` | `POST mobile/subscriptions` | `subscribe` |
|
|
||||||
| `mobile_subscriptions_all` | `GET mobile/subscriptions/all/:token/:mobileToken` | `fetchYourLocations` |
|
|
||||||
| `mobile_subscriptions_delete` | `DELETE mobile/subscriptions/:token/:mobileToken/:subsId` | `unsubscribe` |
|
|
||||||
| `mobile_falsepositive_post` | `POST mobile/falsepositive` | `markFalsePositive` |
|
|
||||||
|
|
||||||
## Normalized (volatile) fields
|
|
||||||
|
|
||||||
Compared structurally but blanked to a placeholder because they cannot be
|
|
||||||
deterministic: `status/uptime` → `ms`; `mobile/users` → `userId`, `username`,
|
|
||||||
`upsertResult.insertedId`; `mobile/subscriptions/all` → `owner`, `createdAt`,
|
|
||||||
`updatedAt`; `mobile/falsepositive` → `upsert.insertedId`.
|
|
||||||
|
|
||||||
Everything else (ids of seeded docs, geometries, counts, jsend envelope, field
|
|
||||||
names and types) is asserted byte-for-byte.
|
|
||||||
172
smoke/run.js
172
smoke/run.js
|
|
@ -1,172 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
/*
|
|
||||||
* REST smoke test for the Flutter-consumed API of todos-contra-el-fuego-web.
|
|
||||||
*
|
|
||||||
* Calls every endpoint the Flutter app (fires_flutter/lib/models/fires_api.dart)
|
|
||||||
* depends on, normalizes the intrinsically volatile fields (uptime, generated
|
|
||||||
* ids, insert timestamps), and compares the result against committed snapshots.
|
|
||||||
*
|
|
||||||
* The responses MUST stay identical across every Meteor upgrade escala — the
|
|
||||||
* app parses them field by field.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* node smoke/run.js --update # (re)write baseline snapshots
|
|
||||||
* node smoke/run.js # compare against baseline, exit 1 on drift
|
|
||||||
*
|
|
||||||
* Env:
|
|
||||||
* BASE_URL default http://localhost:3100
|
|
||||||
* SETTINGS default settings-development.json (token + ironPassword)
|
|
||||||
*
|
|
||||||
* Requires the dev server running and the DB seeded (smoke/seed.sh does both).
|
|
||||||
*/
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const ROOT = path.resolve(__dirname, '..');
|
|
||||||
const SNAP_DIR = path.join(__dirname, 'snapshots');
|
|
||||||
const BASE = (process.env.BASE_URL || 'http://localhost:3100').replace(/\/$/, '');
|
|
||||||
const API = `${BASE}/api/v1`;
|
|
||||||
const UPDATE = process.argv.includes('--update');
|
|
||||||
|
|
||||||
const settings = JSON.parse(fs.readFileSync(path.join(ROOT, process.env.SETTINGS || 'settings-development.json'), 'utf8'));
|
|
||||||
const TOKEN = settings.private.internalApiToken;
|
|
||||||
const IRON_PASSWORD = settings.private.ironPassword;
|
|
||||||
|
|
||||||
// Fixed inputs — must match smoke/seed.js
|
|
||||||
const MOBILE_TOKEN = 'smoke-mobile-token';
|
|
||||||
const SUBS_ID = 'd0000000000000000000000d';
|
|
||||||
const LAT = 40.41;
|
|
||||||
const LON = -3.70;
|
|
||||||
|
|
||||||
// ---------- helpers ----------
|
|
||||||
function stableStringify(v) {
|
|
||||||
return JSON.stringify(sortKeys(v), null, 2);
|
|
||||||
}
|
|
||||||
function sortKeys(v) {
|
|
||||||
if (Array.isArray(v)) return v.map(sortKeys);
|
|
||||||
if (v && typeof v === 'object') {
|
|
||||||
return Object.keys(v).sort().reduce((acc, k) => { acc[k] = sortKeys(v[k]); return acc; }, {});
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
async function req(method, url, body) {
|
|
||||||
const opts = { method, headers: {} };
|
|
||||||
if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
|
|
||||||
const res = await fetch(url, opts);
|
|
||||||
const text = await res.text();
|
|
||||||
let json;
|
|
||||||
try { json = JSON.parse(text); } catch (e) { json = { __nonJson: text }; }
|
|
||||||
return { status: res.status, json };
|
|
||||||
}
|
|
||||||
// Replace volatile fields (by key name) with a stable placeholder, recursively.
|
|
||||||
function scrub(obj, keys) {
|
|
||||||
if (Array.isArray(obj)) return obj.map(o => scrub(o, keys));
|
|
||||||
if (obj && typeof obj === 'object') {
|
|
||||||
const out = {};
|
|
||||||
for (const k of Object.keys(obj)) {
|
|
||||||
out[k] = keys.includes(k) ? `<${k}>` : scrub(obj[k], keys);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- iron seal for mobile/falsepositive ----------
|
|
||||||
async function sealFire() {
|
|
||||||
const Iron = require('iron');
|
|
||||||
const fire = {
|
|
||||||
ourid: { type: 'Point', coordinates: [LON, LAT] },
|
|
||||||
lat: LAT, lon: LON, type: 'viirs',
|
|
||||||
when: '2024-01-01T00:00:00.000Z',
|
|
||||||
scan: 0.5, track: 0.4, address: 'Madrid, Spain'
|
|
||||||
};
|
|
||||||
// iron v5 seal returns a promise
|
|
||||||
return Iron.seal(fire, IRON_PASSWORD, Iron.defaults);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- test sequence ----------
|
|
||||||
async function collect() {
|
|
||||||
const snaps = {};
|
|
||||||
|
|
||||||
snaps['status_uptime'] = scrub((await req('GET', `${API}/status/uptime`)).json, ['ms']);
|
|
||||||
snaps['status_active_fires_count'] = (await req('GET', `${API}/status/active-fires-count`)).json;
|
|
||||||
snaps['status_last_fire_check'] = (await req('GET', `${API}/status/last-fire-check`)).json;
|
|
||||||
snaps['status_last_fire_detected'] = (await req('GET', `${API}/status/last-fire-detected`)).json;
|
|
||||||
snaps['status_subs_public_union'] = (await req('GET', `${API}/status/subs-public-union/${TOKEN}`)).json;
|
|
||||||
|
|
||||||
// Read-only fires query BEFORE any false-positive is inserted.
|
|
||||||
snaps['fires_in_full'] = (await req('GET', `${API}/fires-in-full/${TOKEN}/${LAT}/${LON}/100`)).json;
|
|
||||||
snaps['fires_in'] = (await req('GET', `${API}/fires-in/${TOKEN}/${LAT}/${LON}/100`)).json;
|
|
||||||
|
|
||||||
// Create mobile user
|
|
||||||
const users = await req('POST', `${API}/mobile/users`, { token: TOKEN, mobileToken: MOBILE_TOKEN, lang: 'es' });
|
|
||||||
snaps['mobile_users_post'] = scrub(users.json, ['userId', 'username', 'insertedId']);
|
|
||||||
|
|
||||||
// Subscribe
|
|
||||||
const sub = await req('POST', `${API}/mobile/subscriptions`, {
|
|
||||||
token: TOKEN, mobileToken: MOBILE_TOKEN, id: SUBS_ID, lat: LAT, lon: LON, distance: 40
|
|
||||||
});
|
|
||||||
snaps['mobile_subscriptions_post'] = sub.json;
|
|
||||||
|
|
||||||
// List subscriptions
|
|
||||||
const list = await req('GET', `${API}/mobile/subscriptions/all/${TOKEN}/${MOBILE_TOKEN}`);
|
|
||||||
snaps['mobile_subscriptions_all'] = scrub(list.json, ['owner', 'createdAt', 'updatedAt']);
|
|
||||||
|
|
||||||
// Delete the subscription
|
|
||||||
const del = await req('DELETE', `${API}/mobile/subscriptions/${TOKEN}/${MOBILE_TOKEN}/${SUBS_ID}`);
|
|
||||||
snaps['mobile_subscriptions_delete'] = del.json;
|
|
||||||
|
|
||||||
// False positive (LAST — mutates falsePositives collection)
|
|
||||||
const sealed = await sealFire();
|
|
||||||
const fp = await req('POST', `${API}/mobile/falsepositive`, {
|
|
||||||
token: TOKEN, mobileToken: MOBILE_TOKEN, sealed, type: 'falsealarm'
|
|
||||||
});
|
|
||||||
snaps['mobile_falsepositive_post'] = scrub(fp.json, ['insertedId']);
|
|
||||||
|
|
||||||
return snaps;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
if (!TOKEN) { console.error('No internalApiToken in settings'); process.exit(2); }
|
|
||||||
fs.mkdirSync(SNAP_DIR, { recursive: true });
|
|
||||||
const snaps = await collect();
|
|
||||||
|
|
||||||
let fail = 0;
|
|
||||||
for (const [name, value] of Object.entries(snaps)) {
|
|
||||||
const file = path.join(SNAP_DIR, `${name}.json`);
|
|
||||||
const got = stableStringify(value);
|
|
||||||
if (UPDATE) {
|
|
||||||
fs.writeFileSync(file, `${got}\n`);
|
|
||||||
console.log(` wrote ${name}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(file)) { console.error(` MISSING baseline: ${name}`); fail++; continue; }
|
|
||||||
const want = fs.readFileSync(file, 'utf8').trimEnd();
|
|
||||||
if (want === got.trimEnd()) {
|
|
||||||
console.log(` PASS ${name}`);
|
|
||||||
} else {
|
|
||||||
fail++;
|
|
||||||
console.error(` FAIL ${name}`);
|
|
||||||
console.error(diff(want, got));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (UPDATE) { console.log('baseline updated'); return; }
|
|
||||||
if (fail) { console.error(`\n${fail} snapshot(s) drifted`); process.exit(1); }
|
|
||||||
console.log('\nAll REST snapshots identical ✔');
|
|
||||||
}
|
|
||||||
|
|
||||||
function diff(a, b) {
|
|
||||||
const al = a.split('\n');
|
|
||||||
const bl = b.split('\n');
|
|
||||||
const out = [];
|
|
||||||
const n = Math.max(al.length, bl.length);
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
if (al[i] !== bl[i]) {
|
|
||||||
if (al[i] !== undefined) out.push(` - ${al[i]}`);
|
|
||||||
if (bl[i] !== undefined) out.push(` + ${bl[i]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out.slice(0, 40).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((e) => { console.error(e); process.exit(2); });
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
/*
|
|
||||||
* Deterministic seed for the REST smoke test.
|
|
||||||
* Run against the local Mongo used by the dev Meteor server, e.g.:
|
|
||||||
* docker exec -i tcef-mongo32 mongo fuegos < smoke/seed.js
|
|
||||||
*
|
|
||||||
* It resets the collections the REST API reads/writes to a fixed, known state
|
|
||||||
* so that endpoint responses are reproducible across Meteor upgrade escalas.
|
|
||||||
* Only test-owned documents are touched.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// --- Fixed identifiers (24-hex ObjectIds) -------------------------------
|
|
||||||
var FIRE_A = ObjectId('a0000000000000000000000a');
|
|
||||||
var FIRE_B = ObjectId('b0000000000000000000000b');
|
|
||||||
var ARCHIVE_FIRE = ObjectId('c0000000000000000000000c');
|
|
||||||
var MOBILE_TOKEN = 'smoke-mobile-token';
|
|
||||||
|
|
||||||
// Madrid-ish coordinates
|
|
||||||
var LAT = 40.41;
|
|
||||||
var LON = -3.70;
|
|
||||||
|
|
||||||
var WHEN = ISODate('2024-01-01T00:00:00.000Z');
|
|
||||||
var WHEN_B = ISODate('2024-01-02T00:00:00.000Z'); // later, so last-fire-detected is deterministic
|
|
||||||
var FIXED = ISODate('2024-01-01T00:00:00.000Z');
|
|
||||||
|
|
||||||
// --- activefires --------------------------------------------------------
|
|
||||||
db.activefires.deleteMany({});
|
|
||||||
db.activefires.insertMany([
|
|
||||||
{
|
|
||||||
_id: FIRE_A,
|
|
||||||
ourid: { type: 'Point', coordinates: [LON, LAT] },
|
|
||||||
lat: LAT, lon: LON, type: 'viirs', when: WHEN,
|
|
||||||
scan: 0.5, track: 0.4, acq_date: '2024-01-01', acq_time: '00:00',
|
|
||||||
satellite: 'N', confidence: 50, version: '1.0NRT', frp: 4.3,
|
|
||||||
daynight: 'N', bright_ti4: 330.2, bright_ti5: 291,
|
|
||||||
createdAt: FIXED, updatedAt: FIXED
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: FIRE_B,
|
|
||||||
ourid: { type: 'Point', coordinates: [LON + 0.02, LAT + 0.02] },
|
|
||||||
lat: LAT + 0.02, lon: LON + 0.02, type: 'viirs', when: WHEN_B,
|
|
||||||
scan: 0.5, track: 0.4, acq_date: '2024-01-01', acq_time: '00:05',
|
|
||||||
satellite: 'N', confidence: 60, version: '1.0NRT', frp: 6.1,
|
|
||||||
daynight: 'N', bright_ti4: 331.0, bright_ti5: 292,
|
|
||||||
createdAt: FIXED, updatedAt: FIXED
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
db.activefires.createIndex({ ourid: '2dsphere' });
|
|
||||||
|
|
||||||
// --- fires (archive) — must match the sealed fire used by mobile/falsepositive
|
|
||||||
db.fires.deleteMany({});
|
|
||||||
db.fires.insertOne({
|
|
||||||
_id: ARCHIVE_FIRE,
|
|
||||||
ourid: { type: 'Point', coordinates: [LON, LAT] },
|
|
||||||
lat: LAT, lon: LON, type: 'viirs', when: WHEN,
|
|
||||||
scan: 0.5, track: 0.4, address: 'Madrid, Spain',
|
|
||||||
createdAt: FIXED, updatedAt: FIXED
|
|
||||||
});
|
|
||||||
db.fires.createIndex({ ourid: '2dsphere' });
|
|
||||||
|
|
||||||
// --- falsePositives / industries: empty so "real fire" counting is stable
|
|
||||||
db.falsePositives.deleteMany({});
|
|
||||||
db.industries.deleteMany({});
|
|
||||||
db.industries.createIndex({ geo: '2dsphere' });
|
|
||||||
|
|
||||||
// --- subscriptions: clean; index for geo queries
|
|
||||||
db.subscriptions.deleteMany({});
|
|
||||||
db.subscriptions.createIndex({ geo: '2dsphere' });
|
|
||||||
|
|
||||||
// --- siteSettings consumed by status endpoints --------------------------
|
|
||||||
db.siteSettings.deleteMany({ name: { $in: ['last-fire-check', 'subs-public-union', 'subs-public-union-bounds'] } });
|
|
||||||
db.siteSettings.insertMany([
|
|
||||||
{ _id: ObjectId('51e00000000000000000000c'), name: 'last-fire-check', value: FIXED, createdAt: FIXED, updatedAt: FIXED },
|
|
||||||
{
|
|
||||||
_id: ObjectId('51e0000000000000000000a1'),
|
|
||||||
name: 'subs-public-union',
|
|
||||||
value: JSON.stringify({
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: {
|
|
||||||
type: 'MultiPolygon',
|
|
||||||
coordinates: [[[[LON - 1, LAT - 1], [LON + 1, LAT - 1], [LON + 1, LAT + 1], [LON - 1, LAT + 1], [LON - 1, LAT - 1]]]]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
createdAt: FIXED, updatedAt: FIXED
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: ObjectId('51e0000000000000000000b2'),
|
|
||||||
name: 'subs-public-union-bounds',
|
|
||||||
value: JSON.stringify({ ne: { lat: LAT + 1, lon: LON + 1 }, sw: { lat: LAT - 1, lon: LON - 1 } }),
|
|
||||||
createdAt: FIXED, updatedAt: FIXED
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// --- users: remove any previous smoke user so mobile/users creates fresh
|
|
||||||
db.users.deleteMany({ fireBaseToken: MOBILE_TOKEN });
|
|
||||||
|
|
||||||
print('seed OK: activefires=' + db.activefires.count() + ' fires=' + db.fires.count() + ' siteSettings=' + db.siteSettings.count({ name: /fire|union/ }));
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Seed the dev Mongo deterministically and run the REST smoke test.
|
|
||||||
#
|
|
||||||
# smoke/smoke.sh # compare against committed baseline snapshots
|
|
||||||
# smoke/smoke.sh --update # (re)generate the baseline snapshots
|
|
||||||
#
|
|
||||||
# Env overrides:
|
|
||||||
# MONGO_CONTAINER docker container running the dev Mongo (default tcef-mongo32)
|
|
||||||
# MONGO_DB database name (default fuegos)
|
|
||||||
# BASE_URL dev server URL (default http://localhost:3100)
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
MONGO_CONTAINER="${MONGO_CONTAINER:-tcef-mongo32}"
|
|
||||||
MONGO_DB="${MONGO_DB:-fuegos}"
|
|
||||||
|
|
||||||
echo "== Seeding ${MONGO_DB} in ${MONGO_CONTAINER} =="
|
|
||||||
# Copy the script in and run it as a whole file: the mongo shell evaluates a
|
|
||||||
# file argument atomically (piping via stdin is line-by-line and breaks on
|
|
||||||
# multi-line comments / var scope).
|
|
||||||
docker cp smoke/seed.js "${MONGO_CONTAINER}:/tmp/tcef-seed.js"
|
|
||||||
docker exec "${MONGO_CONTAINER}" mongo "${MONGO_DB}" /tmp/tcef-seed.js | tail -1
|
|
||||||
|
|
||||||
echo "== Running REST smoke test against ${BASE_URL:-http://localhost:3100} =="
|
|
||||||
node smoke/run.js "$@"
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"real": 2,
|
|
||||||
"total": 2
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"falsePos": [],
|
|
||||||
"fires": [
|
|
||||||
{
|
|
||||||
"_id": {
|
|
||||||
"_str": "a0000000000000000000000a"
|
|
||||||
},
|
|
||||||
"lat": 40.41,
|
|
||||||
"lon": -3.7,
|
|
||||||
"scan": 0.5,
|
|
||||||
"track": 0.4,
|
|
||||||
"type": "viirs",
|
|
||||||
"when": "2024-01-01T00:00:00.000Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_id": {
|
|
||||||
"_str": "b0000000000000000000000b"
|
|
||||||
},
|
|
||||||
"lat": 40.43,
|
|
||||||
"lon": -3.68,
|
|
||||||
"scan": 0.5,
|
|
||||||
"track": 0.4,
|
|
||||||
"type": "viirs",
|
|
||||||
"when": "2024-01-02T00:00:00.000Z"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"industries": [],
|
|
||||||
"real": 2,
|
|
||||||
"total": 2
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"upsert": {
|
|
||||||
"insertedId": "<insertedId>",
|
|
||||||
"numberAffected": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "success"
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"count": 1,
|
|
||||||
"subscriptions": [
|
|
||||||
{
|
|
||||||
"_id": {
|
|
||||||
"_str": "d0000000000000000000000d"
|
|
||||||
},
|
|
||||||
"createdAt": "<createdAt>",
|
|
||||||
"distance": 40,
|
|
||||||
"geo": {
|
|
||||||
"coordinates": [
|
|
||||||
-3.7,
|
|
||||||
40.41
|
|
||||||
],
|
|
||||||
"type": "Point"
|
|
||||||
},
|
|
||||||
"location": {
|
|
||||||
"lat": 40.41,
|
|
||||||
"lon": -3.7
|
|
||||||
},
|
|
||||||
"owner": "<owner>",
|
|
||||||
"type": "mobile",
|
|
||||||
"updatedAt": "<updatedAt>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"status": "success"
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"data": {},
|
|
||||||
"status": "success"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"subsId": "d0000000000000000000000d"
|
|
||||||
},
|
|
||||||
"status": "success"
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"lang": "es",
|
|
||||||
"mobileToken": "smoke-mobile-token",
|
|
||||||
"upsertResult": {
|
|
||||||
"insertedId": "<insertedId>",
|
|
||||||
"numberAffected": 1
|
|
||||||
},
|
|
||||||
"userId": "<userId>",
|
|
||||||
"username": "<username>"
|
|
||||||
},
|
|
||||||
"status": "success"
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"total": 2
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"_id": {
|
|
||||||
"_str": "51e00000000000000000000c"
|
|
||||||
},
|
|
||||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
|
||||||
"name": "last-fire-check",
|
|
||||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
|
||||||
"value": "2024-01-01T00:00:00.000Z"
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"_id": {
|
|
||||||
"_str": "b0000000000000000000000b"
|
|
||||||
},
|
|
||||||
"acq_date": "2024-01-01",
|
|
||||||
"acq_time": "00:05",
|
|
||||||
"bright_ti4": 331,
|
|
||||||
"bright_ti5": 292,
|
|
||||||
"confidence": 60,
|
|
||||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
|
||||||
"daynight": "N",
|
|
||||||
"frp": 6.1,
|
|
||||||
"lat": 40.43,
|
|
||||||
"lon": -3.68,
|
|
||||||
"ourid": {
|
|
||||||
"coordinates": [
|
|
||||||
-3.68,
|
|
||||||
40.43
|
|
||||||
],
|
|
||||||
"type": "Point"
|
|
||||||
},
|
|
||||||
"satellite": "N",
|
|
||||||
"scan": 0.5,
|
|
||||||
"track": 0.4,
|
|
||||||
"type": "viirs",
|
|
||||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
|
||||||
"version": "1.0NRT",
|
|
||||||
"when": "2024-01-02T00:00:00.000Z"
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"bounds": {
|
|
||||||
"_id": {
|
|
||||||
"_str": "51e0000000000000000000b2"
|
|
||||||
},
|
|
||||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
|
||||||
"name": "subs-public-union-bounds",
|
|
||||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
|
||||||
"value": "{\"ne\":{\"lat\":41.41,\"lon\":-2.7},\"sw\":{\"lat\":39.41,\"lon\":-4.7}}"
|
|
||||||
},
|
|
||||||
"union": {
|
|
||||||
"_id": {
|
|
||||||
"_str": "51e0000000000000000000a1"
|
|
||||||
},
|
|
||||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
|
||||||
"name": "subs-public-union",
|
|
||||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
|
||||||
"value": "{\"type\":\"Feature\",\"geometry\":{\"type\":\"MultiPolygon\",\"coordinates\":[[[[-4.7,39.41],[-2.7,39.41],[-2.7,41.41],[-4.7,41.41],[-4.7,39.41]]]]}}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": "success"
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"ms": "<ms>"
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue